Skip to content

Commit d08415f

Browse files
author
Execution Coordinator
committed
fix(nip46): initialize RPC subscription in fromPayload() (#332)
When restoring an NDKNip46Signer via fromPayload(), the RPC subscription was never initialized because startListening() was not called. This caused sign(), encrypt(), and decrypt() to hang indefinitely since no subscription was listening for NIP-46 responses. The fix adds `await signer.startListening()` to fromPayload() before returning the signer, matching the behavior of blockUntilReady() and other initialization paths. Tests: - Added test verifying fromPayload() initializes the RPC subscription - Added test verifying sign() works on a restored signer - Updated mock NDK to support subscribe() for startListening() tests - Updated serialization test to mock startListening() for isolation Closes #332
1 parent d995f36 commit d08415f

File tree

3 files changed

+64
-0
lines changed

3 files changed

+64
-0
lines changed

core/src/signers/nip46/index.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ function createMockNDK() {
1515
debug: debugMock,
1616
getUser: vi.fn((opts: { pubkey: string }) => new NDKUser({ pubkey: opts.pubkey })),
1717
pools: [],
18+
// Mock subscribe: returns a subscription-like object and immediately triggers onEose
19+
subscribe: vi.fn((_filter: any, opts: any) => {
20+
const sub = { stop: vi.fn(), on: vi.fn(), off: vi.fn() };
21+
// Trigger onEose asynchronously so the RPC subscribe promise resolves
22+
if (opts?.onEose) queueMicrotask(() => opts.onEose());
23+
return sub;
24+
}),
1825
} as unknown as NDK;
1926
}
2027

@@ -249,6 +256,52 @@ describe("NDKNip46Signer", () => {
249256
expect(parsed.payload.bunkerPubkey).toBe("bunkerpubkey");
250257
expect(parsed.payload.localSignerPayload).toBeDefined();
251258
});
259+
260+
it("fromPayload initializes the RPC subscription (issue #332)", async () => {
261+
// Create and serialize a signer
262+
const token = `bunker://${bob.pubkey}?pubkey=${alice.pubkey}&relay=wss://relay.nsec.app`;
263+
const signer = new NDKNip46Signer(ndk, token, localSigner);
264+
const payload = signer.toPayload();
265+
266+
// Deserialize — fromPayload should call startListening()
267+
const deserialized = await NDKNip46Signer.fromPayload(payload, ndk);
268+
269+
// The subscription should have been initialized
270+
expect((deserialized as any).subscription).toBeDefined();
271+
});
272+
273+
it("restored signer can sign events via RPC (issue #332)", async () => {
274+
// Create and serialize a signer
275+
const token = `bunker://${bob.pubkey}?pubkey=${alice.pubkey}&relay=wss://relay.nsec.app`;
276+
const signer = new NDKNip46Signer(ndk, token, localSigner);
277+
const payload = signer.toPayload();
278+
279+
// Deserialize
280+
const deserialized = await NDKNip46Signer.fromPayload(payload, ndk);
281+
282+
// Replace the RPC with a mock that responds to sign requests
283+
const mockRpc = createMockRpc();
284+
(deserialized as any).rpc = mockRpc;
285+
286+
(mockRpc.sendRequest as any).mockImplementation(
287+
(_bunkerPubkey: string, _method: string, _params: any[], _kind: number, cb: Function) => {
288+
cb({ result: JSON.stringify({ sig: "restored-sig" }) });
289+
},
290+
);
291+
292+
const event: NostrEvent = {
293+
kind: 1,
294+
pubkey: alice.pubkey,
295+
created_at: Math.floor(Date.now() / 1000),
296+
tags: [],
297+
content: "test",
298+
id: "eventid",
299+
sig: "signature",
300+
};
301+
302+
const sig = await deserialized.sign(event);
303+
expect(sig).toBe("restored-sig");
304+
});
252305
});
253306

254307
describe("error handling", () => {

core/src/signers/nip46/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,12 @@ export class NDKNip46Signer extends EventEmitter implements NDKSigner {
600600
signer._user = new NDKUser({ pubkey: payload.userPubkey });
601601
if (signer._user) signer._user.ndk = ndk;
602602
}
603+
604+
// Initialize the RPC subscription so that sign/encrypt/decrypt
605+
// requests receive responses. Without this, operations hang
606+
// indefinitely because no subscription is listening.
607+
await signer.startListening();
608+
603609
return signer;
604610
}
605611
}

core/src/signers/serialization.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ describe("Signer Serialization/Deserialization", () => {
116116
payload: localSigner.privateKey,
117117
});
118118

119+
// Mock startListening to avoid real relay subscriptions during deserialization
120+
const mockStartListening = vi
121+
.spyOn(NDKNip46Signer.prototype as any, "startListening")
122+
.mockResolvedValue(undefined);
123+
119124
// Mock blockUntilReady for the deserialized instance to avoid network calls
120125
// and simulate successful connection/user retrieval.
121126
const mockBlockUntilReady = vi

0 commit comments

Comments
 (0)