From cef57635e961f627b482d71f2ee3430c23882500 Mon Sep 17 00:00:00 2001 From: jr-kenny Date: Mon, 15 Jun 2026 10:58:52 +0100 Subject: [PATCH] fix(wallet-sdk): keep the envelope id when bridge re-sends after ready bridge() in waitForReady mode returned one id to the caller but put a fresh uuid on the wire, so envelope-id correlation never matched. Thread the original id through to.send instead of regenerating it. --- .changeset/wallet-sdk-bridge-deferred-id.md | 5 +++ packages/wallet-sdk/src/core/Messenger.ts | 22 +++++----- .../wallet-sdk/test/src/Messenger.test.ts | 44 ++++++++++++++++++- 3 files changed, 58 insertions(+), 13 deletions(-) create mode 100644 .changeset/wallet-sdk-bridge-deferred-id.md diff --git a/.changeset/wallet-sdk-bridge-deferred-id.md b/.changeset/wallet-sdk-bridge-deferred-id.md new file mode 100644 index 0000000..623c117 --- /dev/null +++ b/.changeset/wallet-sdk-bridge-deferred-id.md @@ -0,0 +1,5 @@ +--- +"@abstract-foundation/wallet-sdk": patch +--- + +Fix `Messenger.bridge` losing the envelope id for messages sent before the wallet host's `ready` handshake. The deferred resend was minting a new id instead of reusing the one already handed back to the caller, so correlating a response by envelope id never matched for those early requests. diff --git a/packages/wallet-sdk/src/core/Messenger.ts b/packages/wallet-sdk/src/core/Messenger.ts index a6b3ead..bbfc607 100644 --- a/packages/wallet-sdk/src/core/Messenger.ts +++ b/packages/wallet-sdk/src/core/Messenger.ts @@ -143,12 +143,15 @@ export type Messenger = { id?: string | undefined, ): () => void; /** - * Send a message to the peer with a generated id. Returns the envelope. + * Send a message to the peer. Returns the envelope. An `id` may be supplied + * to reuse a previously generated id (e.g. when a deferred send must keep the + * id it already handed back to its caller); otherwise one is generated. */ send( topic: T, payload: Payload, target?: string | undefined, + id?: string | undefined, ): { id: string; topic: T; payload: Payload }; /** * Tear down all listeners. @@ -255,8 +258,8 @@ export function fromWindow( topic: T, payload: Payload, target?: string | undefined, + id: string = uuid(), ) { - const id = uuid(); const envelope: Envelope = { abs: ABS_BRAND, id, topic, payload }; // Never default to `*` — refuse to send if no origin is specified. const dest = target ?? targetOrigin; @@ -297,30 +300,25 @@ export function bridge(parameters: { resolveReady?.(options); }); - const send: Bridge["send"] = ((topic, payload, target) => { + const send: Bridge["send"] = ((topic, payload, target, id) => { if (waitForReady && topic !== "ready") { // Fire-and-forget — we still return the envelope id synchronously so // callers can correlate. Requests issued before ready will queue // implicitly: the listener registered in fromWindow stays attached and // will fire when the peer eventually answers. - const id = uuid(); + const envelopeId = id ?? uuid(); void readyPromise.then( () => { // Re-send with the same id so request/response correlation holds. - const dest = target; - if (dest) { - (to as Messenger).send(topic, payload, dest); - } else { - (to as Messenger).send(topic, payload); - } + (to as Messenger).send(topic, payload, target, envelopeId); }, () => { /* destroyed */ }, ); - return { id, topic, payload }; + return { id: envelopeId, topic, payload }; } - return (to as Messenger).send(topic, payload, target); + return (to as Messenger).send(topic, payload, target, id); }) as Bridge["send"]; return { diff --git a/packages/wallet-sdk/test/src/Messenger.test.ts b/packages/wallet-sdk/test/src/Messenger.test.ts index 93a41db..fe22ea6 100644 --- a/packages/wallet-sdk/test/src/Messenger.test.ts +++ b/packages/wallet-sdk/test/src/Messenger.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { fromWindow } from "../../src/core/Messenger.js"; +import { bridge, fromWindow } from "../../src/core/Messenger.js"; describe("fromWindow", () => { it("rejects messages from a different origin", () => { @@ -86,3 +86,45 @@ describe("fromWindow", () => { messenger.destroy(); }); }); + +describe("bridge", () => { + it("preserves the envelope id for sends queued before ready", async () => { + const targetOrigin = "https://wallet.test"; + const from = fromWindow(window, { targetOrigin }); + const postMessage = vi.fn(); + const peer = { postMessage } as unknown as Window; + const to = fromWindow(peer, { targetOrigin }); + + const b = bridge({ from, to, waitForReady: true }); + + // Sent before the peer announces ready — must be held until then. + const sent = b.send("rpc-request", { + id: 1, + jsonrpc: "2.0", + method: "eth_chainId", + }); + expect(postMessage).not.toHaveBeenCalled(); + + // Peer announces ready, flushing the queued send. + window.dispatchEvent( + new MessageEvent("message", { + origin: targetOrigin, + data: { + abs: 1, + id: "ready-1", + topic: "ready", + payload: { chainIds: [] }, + }, + }), + ); + + await vi.waitFor(() => expect(postMessage).toHaveBeenCalledTimes(1)); + + // The id handed back to the caller must equal the id that went on the + // wire, or request/response correlation by envelope id is impossible. + const envelope = postMessage.mock.calls[0]?.[0] as { id: string }; + expect(envelope.id).toBe(sent.id); + + b.destroy(); + }); +});