this repo has no description
1import { useState, useEffect } from 'react';
2import { getTransaction, formatToshis } from '../lib/api';
3import type { Transaction } from '../lib/api';
4import { Stack, Divider } from './ui/Layout';
5import {
6 Terminal,
7 Section,
8 LabelValue,
9 DataTable,
10 DataRow,
11 ActionBar,
12 Message,
13} from './ui/Terminal';
14
15interface Props {
16 txId: string;
17}
18
19const handleCache = new Map<string, string>();
20
21async function resolveHandle(did: string): Promise<string> {
22 if (handleCache.has(did)) {
23 return handleCache.get(did)!;
24 }
25
26 try {
27 // For did:web, extract the domain directly
28 if (did.startsWith('did:web:')) {
29 const domain = did.replace('did:web:', '');
30 handleCache.set(did, domain);
31 return domain;
32 }
33
34 // For did:plc, use PLC directory
35 if (did.startsWith('did:plc:')) {
36 const didRes = await fetch(`https://plc.directory/${did}`);
37 if (didRes.ok) {
38 const doc = await didRes.json();
39 const handle = doc.alsoKnownAs?.[0]?.replace('at://', '') || did;
40 handleCache.set(did, handle);
41 return handle;
42 }
43 }
44 } catch {
45 // Fall back to DID
46 }
47
48 handleCache.set(did, did);
49 return did;
50}
51
52export default function TransactionDetail({ txId }: Props) {
53 const [tx, setTx] = useState<Transaction | null>(null);
54 const [loading, setLoading] = useState(true);
55 const [error, setError] = useState<string | null>(null);
56 const [resolvedHandles, setResolvedHandles] = useState<Map<string, string>>(new Map());
57
58 useEffect(() => {
59 async function fetchTx() {
60 try {
61 const result = await getTransaction(txId);
62 setTx(result.transaction);
63
64 const dids = new Set<string>();
65 result.transaction.outputs.forEach(o => dids.add(o.owner));
66 // Handle both trigger types
67 const trigger = result.transaction.trigger;
68 if (trigger) {
69 if ('followerDid' in trigger) {
70 dids.add(trigger.followerDid);
71 } else if ('senderDid' in trigger) {
72 dids.add(trigger.senderDid);
73 }
74 }
75
76 const handles = new Map<string, string>();
77 await Promise.all(
78 Array.from(dids).map(async did => {
79 const handle = await resolveHandle(did);
80 handles.set(did, handle);
81 })
82 );
83 setResolvedHandles(handles);
84 } catch (e) {
85 setError(e instanceof Error ? e.message : 'Failed to fetch transaction');
86 } finally {
87 setLoading(false);
88 }
89 }
90 fetchTx();
91 }, [txId]);
92
93 if (loading) {
94 return (
95 <Terminal title="TRANSACTION">
96 <Section>
97 <LabelValue label="STATUS" value="LOADING..." status="warning" />
98 </Section>
99 </Terminal>
100 );
101 }
102
103 if (error || !tx) {
104 return (
105 <Terminal title="TRANSACTION NOT FOUND">
106 <Section>
107 <Stack gap="sm">
108 <LabelValue label="TXID" value={txId} />
109 <Message type="error">{error || 'Transaction not found'}</Message>
110 </Stack>
111 </Section>
112 <Divider />
113 <ActionBar>
114 <a href="/wallet" className="btn secondary">[BACK TO WALLET]</a>
115 <a href="/" className="btn secondary">[HOME]</a>
116 </ActionBar>
117 </Terminal>
118 );
119 }
120
121 const formatDid = (did: string) => {
122 const handle = resolvedHandles.get(did);
123 if (handle && handle !== did) {
124 return `@${handle}`;
125 }
126 return did.length > 32 ? `${did.slice(0, 32)}...` : did;
127 };
128
129 const totalOutput = tx.outputs.reduce((sum, o) => sum + o.amount, 0);
130
131 return (
132 <Terminal title="TRANSACTION DETAILS">
133 <Section title="INFO">
134 <Stack gap="xs">
135 <LabelValue label="TXID" value={txId} />
136 <LabelValue label="TYPE" value={tx.type.toUpperCase()} />
137 <LabelValue label="DATE" value={new Date(tx.createdAt).toLocaleString()} />
138 {tx.trigger && 'followerDid' in tx.trigger && (
139 <>
140 <LabelValue label="TRIGGER" value="Follow event" />
141 <LabelValue label="FROM" value={formatDid((tx.trigger as { followerDid: string }).followerDid)} />
142 </>
143 )}
144 {tx.trigger && 'type' in tx.trigger && (tx.trigger as { type: string }).type === 'cashtag' && (
145 <>
146 <LabelValue label="TRIGGER" value="$attoshi cashtag" />
147 <LabelValue label="FROM" value={formatDid((tx.trigger as { senderDid: string }).senderDid)} />
148 </>
149 )}
150 </Stack>
151 </Section>
152
153 <Divider />
154
155 <Section title="RECIPIENTS">
156 <DataTable
157 headers={['#', 'AMOUNT', 'TO']}
158 widths={['2rem', '8rem', '1fr']}
159 >
160 {tx.outputs.map((output, i) => (
161 <DataRow
162 key={i}
163 cells={[
164 String(i + 1),
165 `${formatToshis(output.amount)} @toshis`,
166 formatDid(output.owner),
167 ]}
168 widths={['2rem', '8rem', '1fr']}
169 />
170 ))}
171 </DataTable>
172 <LabelValue
173 label="TOTAL"
174 value={`${formatToshis(totalOutput)} @toshis`}
175 />
176 </Section>
177
178 {tx.type === 'transfer' && tx.inputs.length > 0 && (
179 <>
180 <Divider />
181 <Section title="SPENT COINS">
182 <DataTable
183 headers={['#', 'SOURCE TX']}
184 widths={['2rem', '1fr']}
185 >
186 {tx.inputs.map((input, i) => (
187 <DataRow
188 key={i}
189 cells={[
190 String(i + 1),
191 `${input.txId}:${input.index}`,
192 ]}
193 widths={['2rem', '1fr']}
194 />
195 ))}
196 </DataTable>
197 </Section>
198 </>
199 )}
200
201 <Divider />
202
203 <ActionBar>
204 <a href="/wallet" className="btn secondary">[BACK TO WALLET]</a>
205 <a href="/" className="btn secondary">[HOME]</a>
206 </ActionBar>
207 </Terminal>
208 );
209}