Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wallet-sdk-bridge-deferred-id.md
Original file line number Diff line number Diff line change
@@ -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.
22 changes: 10 additions & 12 deletions packages/wallet-sdk/src/core/Messenger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends Topic>(
topic: T,
payload: Payload<T>,
target?: string | undefined,
id?: string | undefined,
): { id: string; topic: T; payload: Payload<T> };
/**
* Tear down all listeners.
Expand Down Expand Up @@ -255,8 +258,8 @@ export function fromWindow(
topic: T,
payload: Payload<T>,
target?: string | undefined,
id: string = uuid(),
) {
const id = uuid();
const envelope: Envelope<T> = { abs: ABS_BRAND, id, topic, payload };
// Never default to `*` — refuse to send if no origin is specified.
const dest = target ?? targetOrigin;
Expand Down Expand Up @@ -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 {
Expand Down
44 changes: 43 additions & 1 deletion packages/wallet-sdk/test/src/Messenger.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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();
});
});