Skip to content

Commit c240538

Browse files
committed
render signed typed data nicely, tested e2e
1 parent 7a81354 commit c240538

File tree

3 files changed

+190
-31
lines changed

3 files changed

+190
-31
lines changed

src/App.tsx

Lines changed: 163 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@ import {
1111
} from "viem";
1212
import { waitForTransactionReceipt } from "viem/actions";
1313

14-
import { api, applyChainId, isOk, renderJSON } from "./utils/helpers.ts";
14+
import { api, applyChainId, isOk, renderJSON, renderMaybeParsedJSON } from "./utils/helpers.ts";
1515
import type {
1616
ApiErr,
1717
ApiOk,
1818
EIP1193,
1919
EIP6963AnnounceProviderEvent,
2020
EIP6963ProviderInfo,
2121
PendingAny,
22+
PendingSigning,
2223
} from "./utils/types.ts";
2324

2425
export function App() {
@@ -33,17 +34,21 @@ export function App() {
3334
);
3435

3536
const [confirmed, setConfirmed] = useState<boolean>(false);
36-
const [pending, setPending] = useState<PendingAny | null>(null);
37+
const [pendingTx, setPendingTx] = useState<PendingAny | null>(null);
38+
const [pendingSigning, setPendingSigning] = useState<PendingSigning | null>(null);
3739
const [selectedUuid, setSelectedUuid] = useState<string | null>(null);
3840
const selected = providers.find((p) => p.info.uuid === selectedUuid) ?? null;
3941

4042
const [account, setAccount] = useState<Address>();
4143
const [chainId, setChainId] = useState<number>();
4244
const [chain, setChain] = useState<Chain>();
45+
4346
const [lastTxReceipt, setLastTxReceipt] = useState<TransactionReceipt | null>(null);
4447
const [lastTxHash, setLastTxHash] = useState<string | null>(null);
48+
const [lastSignature, setLastSignature] = useState<string | null>(null);
4549

46-
const pollIntervalRef = useRef<number | null>(null);
50+
const pollTxRef = useRef<number | null>(null);
51+
const pollSigningRef = useRef<number | null>(null);
4752
const prevSelectedUuidRef = useRef<string | null>(null);
4853

4954
const connect = async () => {
@@ -79,9 +84,70 @@ export function App() {
7984
setConfirmed(true);
8085
};
8186

87+
const signCurrentMessage = async () => {
88+
if (!selected || !pendingSigning) return;
89+
90+
const { id, signType, request } = pendingSigning;
91+
const signer = request.address;
92+
const msg = request.message;
93+
94+
try {
95+
let signature: string;
96+
97+
switch (signType) {
98+
case "PersonalSign":
99+
// Standard message signing
100+
signature = (await selected.provider.request({
101+
method: "personal_sign",
102+
params: [msg, signer],
103+
})) as string;
104+
break;
105+
106+
case "SignTypedDataV4":
107+
// EIP-712 typed data signing
108+
signature = (await selected.provider.request({
109+
method: "eth_signTypedData_v4",
110+
params: [signer, msg],
111+
})) as string;
112+
break;
113+
114+
default:
115+
throw new Error(`Unsupported signType: ${signType}`);
116+
}
117+
118+
await api("/api/signing/response", "POST", {
119+
id,
120+
signature,
121+
error: null,
122+
});
123+
124+
setLastSignature(signature);
125+
setPendingSigning(null);
126+
} catch (e: unknown) {
127+
const errMsg =
128+
typeof e === "object" &&
129+
e &&
130+
"message" in e &&
131+
typeof (e as { message?: unknown }).message === "string"
132+
? (e as { message: string }).message
133+
: String(e);
134+
135+
try {
136+
await api("/api/signing/response", "POST", {
137+
id,
138+
signature: null,
139+
error: errMsg,
140+
});
141+
} catch {}
142+
143+
setLastSignature(null);
144+
setPendingSigning(null);
145+
}
146+
};
147+
82148
// Sign and send the current pending transaction.
83-
const signAndSendCurrent = async () => {
84-
if (!selected || !pending?.request) return;
149+
const signAndSendCurrentTx = async () => {
150+
if (!selected || !pendingTx?.request) return;
85151

86152
const walletClient = createWalletClient({
87153
transport: custom(selected.provider),
@@ -91,18 +157,14 @@ export function App() {
91157
try {
92158
const hash = (await selected.provider.request({
93159
method: "eth_sendTransaction",
94-
params: [pending.request],
160+
params: [pendingTx.request],
95161
})) as `0x${string}`;
96162
setLastTxHash(hash);
97163

98-
await api("/api/transaction/response", "POST", { id: pending.id, hash, error: null });
99-
100-
console.log("sent tx:", hash);
164+
await api("/api/transaction/response", "POST", { id: pendingTx.id, hash, error: null });
101165

102166
const receipt = await waitForTransactionReceipt(walletClient, { hash });
103167
setLastTxReceipt(receipt);
104-
105-
console.log("tx receipt:", receipt);
106168
} catch (e: unknown) {
107169
const msg =
108170
typeof e === "object" &&
@@ -116,7 +178,7 @@ export function App() {
116178

117179
try {
118180
await api("/api/transaction/response", "POST", {
119-
id: pending.id,
181+
id: pendingTx.id,
120182
hash: null,
121183
error: msg,
122184
});
@@ -126,12 +188,18 @@ export function App() {
126188

127189
// Reset all client state.
128190
const resetClientState = useCallback(() => {
129-
if (pollIntervalRef.current) {
130-
window.clearInterval(pollIntervalRef.current);
131-
pollIntervalRef.current = null;
191+
if (pollTxRef.current) {
192+
window.clearInterval(pollTxRef.current);
193+
pollTxRef.current = null;
194+
}
195+
196+
if (pollSigningRef.current) {
197+
window.clearInterval(pollSigningRef.current);
198+
pollSigningRef.current = null;
132199
}
133200

134-
setPending(null);
201+
setPendingTx(null);
202+
setPendingSigning(null);
135203
setLastTxHash(null);
136204
setLastTxReceipt(null);
137205

@@ -205,9 +273,9 @@ export function App() {
205273
}, [selected, confirmed]);
206274

207275
// Poll for pending transaction requests.
208-
// Stops when one is found.
276+
// Stops when one is found or when a pending signing request is found.
209277
useEffect(() => {
210-
if (!confirmed || pending) return;
278+
if (!confirmed || pendingTx || pendingSigning) return;
211279

212280
let active = true;
213281

@@ -218,27 +286,60 @@ export function App() {
218286
if (isOk(resp)) {
219287
window.clearInterval(id);
220288
if (active) {
221-
setPending(resp.data);
289+
setPendingTx(resp.data);
222290
}
223291
}
224292
} catch {}
225293
}, 1000);
226294

227-
pollIntervalRef.current = id;
295+
pollTxRef.current = id;
228296

229297
return () => {
230298
active = false;
231299
window.clearInterval(id);
232-
if (pollIntervalRef.current === id) {
233-
pollIntervalRef.current = null;
300+
if (pollTxRef.current === id) {
301+
pollTxRef.current = null;
234302
}
235303
};
236-
}, [confirmed, pending]);
304+
}, [confirmed, pendingTx, pendingSigning]);
305+
306+
// Poll for pending signing requests.
307+
// Stops when one is found or when a pending transaction request is found.
308+
useEffect(() => {
309+
if (!confirmed || pendingSigning || pendingTx) return;
310+
311+
let active = true;
312+
313+
const id = window.setInterval(async () => {
314+
if (!active) return;
315+
try {
316+
const resp = await api<ApiOk<PendingSigning> | ApiErr>("/api/signing/request");
317+
if (isOk(resp)) {
318+
window.clearInterval(id);
319+
if (active) {
320+
setPendingSigning(resp.data);
321+
}
322+
}
323+
} catch {}
324+
}, 1000);
325+
326+
pollSigningRef.current = id;
327+
328+
return () => {
329+
active = false;
330+
window.clearInterval(id);
331+
if (pollSigningRef.current === id) {
332+
pollSigningRef.current = null;
333+
}
334+
};
335+
}, [confirmed, pendingSigning, pendingTx]);
237336

238337
return (
239338
<div className="wrapper">
240339
<div className="container">
241-
<div className="notice">Browser wallet is still in early development. Use with caution!</div>
340+
<div className="notice">
341+
Browser wallet is still in early development. Use with caution!
342+
</div>
242343

243344
<img className="banner" src="banner.png" alt="Foundry Browser Wallet" />
244345

@@ -294,12 +395,42 @@ rpc: ${chain?.rpcUrls?.default?.http?.[0] ?? chain?.rpcUrls?.public?.http?.[
294395
</>
295396
)}
296397

297-
{selected && account && confirmed && !lastTxHash && (
398+
{selected &&
399+
account &&
400+
confirmed &&
401+
!pendingTx &&
402+
!pendingSigning &&
403+
!lastTxHash &&
404+
!lastSignature && (
405+
<>
406+
<div className="section-title">Transaction To Sign</div>
407+
<div className="box">
408+
<pre>No pending transaction or signing request</pre>
409+
</div>
410+
</>
411+
)}
412+
413+
{selected && account && confirmed && !lastTxHash && pendingTx && (
414+
<>
415+
<div className="section-title">Transaction to Sign & Send</div>
416+
<div className="box">
417+
<pre>{renderJSON(pendingTx.request)}</pre>
418+
</div>
419+
<button type="button" className="wallet-send" onClick={signAndSendCurrentTx}>
420+
Sign &amp; Send
421+
</button>
422+
</>
423+
)}
424+
425+
{selected && account && confirmed && !pendingTx && pendingSigning && (
298426
<>
299-
<div className="section-title">To Sign</div>
427+
<div className="section-title">Message / Data to Sign</div>
300428
<div className="box">
301-
<pre>{pending ? renderJSON(pending) : "No pending transaction"}</pre>
429+
<pre>{renderMaybeParsedJSON(pendingSigning.request)}</pre>
302430
</div>
431+
<button type="button" className="wallet-send" onClick={signCurrentMessage}>
432+
Sign
433+
</button>
303434
</>
304435
)}
305436

@@ -317,10 +448,11 @@ rpc: ${chain?.rpcUrls?.default?.http?.[0] ?? chain?.rpcUrls?.public?.http?.[
317448
</>
318449
)}
319450

320-
{selected && account && pending && confirmed && !lastTxHash && (
321-
<button type="button" className="wallet-send" onClick={signAndSendCurrent}>
322-
Sign & Send
323-
</button>
451+
{selected && account && confirmed && lastSignature && (
452+
<>
453+
<div className="section-title">Signature Result</div>
454+
<pre className="box">{lastSignature}</pre>
455+
</>
324456
)}
325457
</div>
326458
</div>

src/utils/helpers.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,24 @@ export const api = async <T = unknown>(
7171
export const renderJSON = (obj: unknown) =>
7272
JSON.stringify(obj, (_k, v) => (typeof v === "bigint" ? v.toString() : v), 2);
7373

74+
export const renderMaybeParsedJSON = (value: unknown): string => {
75+
if (value == null) return renderJSON(value);
76+
77+
if (typeof value === "object" && "message" in value && typeof (value as any).message === "string") {
78+
const obj = value as { message: string };
79+
80+
try {
81+
const parsed = JSON.parse(obj.message);
82+
83+
return renderJSON({ ...value, message: parsed });
84+
} catch {
85+
return renderJSON(value);
86+
}
87+
}
88+
89+
return renderJSON(value);
90+
};
91+
7492
export const isOk = <T>(r: ApiOk<T> | ApiErr | null | undefined): r is ApiOk<T> => {
7593
return !!r && (r as ApiOk<T>).status === "ok";
7694
};

src/utils/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ declare global {
1212
}
1313
}
1414

15+
export type PendingSigning = {
16+
id: string;
17+
signType: "PersonalSign" | "SignTypedDataV4";
18+
request: {
19+
message: string;
20+
address: string;
21+
};
22+
};
23+
1524
export type EIP6963ProviderInfo = {
1625
uuid: string;
1726
name: string;

0 commit comments

Comments
 (0)