Skip to content

Commit a0331b5

Browse files
committed
Add template transaction status components
1 parent 2dcc853 commit a0331b5

4 files changed

Lines changed: 201 additions & 0 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,8 @@ coverage
1111
.env
1212
.env.local
1313
.env.*.local
14+
15+
# Local scratch apps / planning notes
16+
apps/new-app/
17+
apps/test/
18+
docs/tickets/
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use client';
2+
3+
import React, { useEffect, useState } from 'react';
4+
5+
import { explorerAddressUrl } from '../lib/chains';
6+
import { fetchManifest, getPrimaryDeployment } from '../lib/manifest';
7+
8+
function shortAddress(addr: string): string {
9+
if (!addr || addr.length < 12) return addr;
10+
return `${addr.slice(0, 8)}${addr.slice(-6)}`;
11+
}
12+
13+
export default function FooterDeploymentMeta() {
14+
const [chainId, setChainId] = useState<number | null>(null);
15+
const [address, setAddress] = useState<string | null>(null);
16+
17+
useEffect(() => {
18+
let cancelled = false;
19+
async function load() {
20+
try {
21+
const manifest = await fetchManifest();
22+
const deployment = getPrimaryDeployment(manifest);
23+
if (cancelled || !deployment) return;
24+
const parsedChainId = Number(deployment.chainId);
25+
const parsedAddress = String(deployment.deploymentEntrypointAddress ?? '');
26+
if (Number.isFinite(parsedChainId)) setChainId(parsedChainId);
27+
if (parsedAddress && parsedAddress !== '0x0000000000000000000000000000000000000000') {
28+
setAddress(parsedAddress);
29+
}
30+
} catch {
31+
// Footer metadata is best-effort and should never fail rendering.
32+
}
33+
}
34+
void load();
35+
return () => {
36+
cancelled = true;
37+
};
38+
}, []);
39+
40+
const link = address && chainId !== null ? explorerAddressUrl(chainId, address) : null;
41+
42+
return (
43+
<div className="footerMeta">
44+
<span>Powered by Token Host</span>
45+
{chainId !== null ? <span className="badge">chain {String(chainId)}</span> : null}
46+
{address ? (
47+
link ? (
48+
<a className="footerLink" href={link} target="_blank" rel="noreferrer">
49+
<span className="badge">{shortAddress(address)}</span>
50+
</a>
51+
) : (
52+
<span className="badge">{shortAddress(address)}</span>
53+
)
54+
) : null}
55+
</div>
56+
);
57+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React from 'react';
2+
3+
import { explorerTxUrl } from '../lib/chains';
4+
5+
export type TxPhase = 'idle' | 'submitting' | 'submitted' | 'confirming' | 'confirmed' | 'failed';
6+
7+
function shortHash(hash: string): string {
8+
if (!hash || hash.length < 14) return hash;
9+
return `${hash.slice(0, 10)}${hash.slice(-6)}`;
10+
}
11+
12+
function phaseLabel(phase: TxPhase): string {
13+
if (phase === 'submitting') return 'Submitting transaction…';
14+
if (phase === 'submitted') return 'Transaction submitted.';
15+
if (phase === 'confirming') return 'Waiting for confirmation…';
16+
if (phase === 'confirmed') return 'Transaction confirmed.';
17+
if (phase === 'failed') return 'Transaction failed.';
18+
return '';
19+
}
20+
21+
export default function TxStatus(props: {
22+
phase: TxPhase;
23+
hash?: string | null;
24+
chainId?: number | null;
25+
error?: string | null;
26+
}) {
27+
const { phase, hash, chainId, error } = props;
28+
if (phase === 'idle' && !error) return null;
29+
30+
const toneClass = phase === 'failed' || error ? 'txStatus fail' : 'txStatus';
31+
const txUrl = hash && Number.isFinite(chainId) ? explorerTxUrl(chainId as number, hash) : null;
32+
33+
return (
34+
<div className={toneClass}>
35+
<div className="txStatusHead">{phaseLabel(phase)}</div>
36+
{hash ? (
37+
<div className="txStatusRow">
38+
<span className="badge">{shortHash(hash)}</span>
39+
{txUrl ? (
40+
<a className="txStatusLink" href={txUrl} target="_blank" rel="noreferrer">
41+
View tx
42+
</a>
43+
) : null}
44+
</div>
45+
) : null}
46+
{error ? <div className="pre" style={{ marginTop: 10 }}>{error}</div> : null}
47+
</div>
48+
);
49+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { encodeFunctionData, type Address } from 'viem';
2+
3+
import { makeWalletClient, requestWalletAddress } from './clients';
4+
import { getRelayBaseUrl, getTxMode } from './manifest';
5+
import type { TxPhase } from '../components/TxStatus';
6+
7+
export type SubmitWriteTxResult = {
8+
hash: `0x${string}`;
9+
receipt: any;
10+
};
11+
12+
export async function submitWriteTx(args: {
13+
manifest: any;
14+
deployment: any;
15+
chain: any;
16+
publicClient: any;
17+
address: `0x${string}`;
18+
abi: any[];
19+
functionName: string;
20+
contractArgs: any[];
21+
value?: bigint;
22+
setStatus?: (s: string | null) => void;
23+
onPhase?: (phase: TxPhase) => void;
24+
onHash?: (hash: `0x${string}`) => void;
25+
}): Promise<SubmitWriteTxResult> {
26+
const mode = getTxMode(args.manifest);
27+
28+
if (mode === 'sponsored') {
29+
args.onPhase?.('submitting');
30+
args.setStatus?.('Submitting sponsored transaction…');
31+
const data = encodeFunctionData({
32+
abi: args.abi,
33+
functionName: args.functionName,
34+
args: args.contractArgs
35+
});
36+
37+
const relayBaseUrl = getRelayBaseUrl(args.manifest).replace(/\/+$/, '');
38+
const relayUrl = relayBaseUrl.endsWith('/__tokenhost/relay') ? relayBaseUrl : `${relayBaseUrl}/__tokenhost/relay`;
39+
const res = await fetch(relayUrl, {
40+
method: 'POST',
41+
headers: { 'content-type': 'application/json' },
42+
body: JSON.stringify({
43+
to: args.address,
44+
data,
45+
value: args.value ? `0x${args.value.toString(16)}` : undefined
46+
})
47+
});
48+
49+
const body = await res.json().catch(() => null);
50+
if (!res.ok || !body?.ok || !body?.txHash) {
51+
const msg = String(body?.error ?? `Relay request failed (HTTP ${res.status}).`);
52+
throw new Error(msg);
53+
}
54+
55+
const hash = String(body.txHash) as `0x${string}`;
56+
args.onHash?.(hash);
57+
args.onPhase?.('submitted');
58+
args.setStatus?.(`Submitted ${hash.slice(0, 10)}…`);
59+
args.onPhase?.('confirming');
60+
args.setStatus?.('Waiting for confirmation…');
61+
const receipt = await args.publicClient.waitForTransactionReceipt({ hash });
62+
args.onPhase?.('confirmed');
63+
args.setStatus?.(`Confirmed ${hash.slice(0, 10)}…`);
64+
return { hash, receipt };
65+
}
66+
67+
args.onPhase?.('submitting');
68+
args.setStatus?.('Connecting wallet…');
69+
const account = await requestWalletAddress(args.chain);
70+
const walletClient = makeWalletClient(args.chain);
71+
args.setStatus?.('Sending transaction…');
72+
const hash = (await walletClient.writeContract({
73+
address: args.address as Address,
74+
abi: args.abi,
75+
functionName: args.functionName,
76+
args: args.contractArgs,
77+
account,
78+
value: args.value,
79+
chain: args.chain
80+
})) as `0x${string}`;
81+
args.onHash?.(hash);
82+
args.onPhase?.('submitted');
83+
args.setStatus?.(`Submitted ${hash.slice(0, 10)}…`);
84+
args.onPhase?.('confirming');
85+
args.setStatus?.('Waiting for confirmation…');
86+
const receipt = await args.publicClient.waitForTransactionReceipt({ hash });
87+
args.onPhase?.('confirmed');
88+
args.setStatus?.(`Confirmed ${hash.slice(0, 10)}…`);
89+
return { hash, receipt };
90+
}

0 commit comments

Comments
 (0)