From 08fefa3ec549c7632dfbc50027886a9b03eb4668 Mon Sep 17 00:00:00 2001 From: Ty J Everett Date: Mon, 8 Jun 2026 21:00:50 -0700 Subject: [PATCH] Bound wallet HTTP bridge requests --- package-lock.json | 6 --- package.json | 5 +++ src/onWalletReady.ts | 84 ++++++++++++++++++++++++++++++++++++-- test/onWalletReady.test.ts | 39 ++++++++++++++++++ 4 files changed, 125 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index e2f29c7..734e839 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1160,12 +1160,6 @@ "idb": "^8.0.2" } }, - "node_modules/@bsv/wallet-toolbox-client/node_modules/@bsv/sdk": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@bsv/sdk/-/sdk-2.1.2.tgz", - "integrity": "sha512-648RLJEkbH+vFt5Q65orm3jdmZDnmLdt8YUpyQcctxVYwHvhEN/7RxMXjA+OnNLHZV1IOstqwXQ+4ar89KUs7Q==", - "license": "SEE LICENSE IN LICENSE.txt" - }, "node_modules/@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", diff --git a/package.json b/package.json index a41976b..1f83a3a 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,11 @@ "vite-plugin-electron": "^0.29.0", "vitest": "^3.0.0" }, + "overrides": { + "@bsv/wallet-toolbox-client": { + "@bsv/sdk": "2.1.3" + } + }, "build": { "appId": "com.bsvblockchain.bsvdesktop", "productName": "BSV-Desktop", diff --git a/src/onWalletReady.ts b/src/onWalletReady.ts index ac98b46..d991a3b 100644 --- a/src/onWalletReady.ts +++ b/src/onWalletReady.ts @@ -45,6 +45,83 @@ interface HttpResponseEvent { body: string; } +const WALLET_HTTP_REQUEST_TIMEOUT_MS = 60000; +const WALLET_TIMEOUT_PREFIX = 'WALLET_REQUEST_TIMEOUT'; + +class WalletRequestTimeoutError extends Error { + constructor( + public readonly method: string, + public readonly timeoutMs: number + ) { + super(`${WALLET_TIMEOUT_PREFIX}: ${method} did not complete within ${timeoutMs}ms`); + this.name = 'WalletRequestTimeoutError'; + } +} + +function withTimeout(method: string, operation: Promise): Promise { + let timeout: ReturnType | undefined; + + const timeoutPromise = new Promise((_, reject) => { + timeout = setTimeout(() => { + reject(new WalletRequestTimeoutError(method, WALLET_HTTP_REQUEST_TIMEOUT_MS)); + }, WALLET_HTTP_REQUEST_TIMEOUT_MS); + }); + + return Promise.race([operation, timeoutPromise]).finally(() => { + if (timeout) clearTimeout(timeout); + }); +} + +function withWalletRequestTimeout(wallet: WalletInterface): WalletInterface { + return new Proxy(wallet, { + get(target, property, receiver) { + const value = Reflect.get(target, property, receiver); + if (typeof value !== 'function') { + return value; + } + + return (...args: unknown[]) => withTimeout(String(property), value.apply(target, args)); + } + }); +} + +function timeoutPayloadFromBody(body: string): { message: string; method: string; timeoutMs: number } | undefined { + let parsed: unknown = body; + try { + parsed = JSON.parse(body); + } catch { + parsed = { message: body }; + } + + const message = typeof parsed === 'object' && parsed !== null && 'message' in parsed + ? String((parsed as { message?: unknown }).message) + : typeof parsed === 'string' + ? parsed + : ''; + const match = message.match(/^WALLET_REQUEST_TIMEOUT: (.+) did not complete within (\d+)ms$/); + if (!match) return undefined; + + return { + message, + method: match[1], + timeoutMs: Number(match[2]) + }; +} + +function normalizeWalletTimeoutResponse(response: HttpResponseEvent): HttpResponseEvent { + const timeoutPayload = timeoutPayloadFromBody(response.body); + if (!timeoutPayload) return response; + + return { + ...response, + status: 504, + body: JSON.stringify({ + code: WALLET_TIMEOUT_PREFIX, + ...timeoutPayload + }) + }; +} + /** * Duck-type check for WERR_REVIEW_ACTIONS-shaped errors. We don't use * `error?.constructor.name === 'WERR_REVIEW_ACTIONS'` because Vite's @@ -163,8 +240,8 @@ export const onWalletReady = async (wallet: WalletInterface): Promise<(() => voi window.electronAPI.onHttpRequest(async (req: HttpRequestEvent) => { let response: HttpResponseEvent; - const wallet = _currentWallet; - if (!wallet) { + const rawWallet = _currentWallet; + if (!rawWallet) { response = { request_id: req.request_id, status: 503, @@ -173,6 +250,7 @@ export const onWalletReady = async (wallet: WalletInterface): Promise<(() => voi window.electronAPI.sendHttpResponse(response); return; } + const wallet = withWalletRequestTimeout(rawWallet); try { const origin = parseOrigin(req.headers); @@ -868,7 +946,7 @@ export const onWalletReady = async (wallet: WalletInterface): Promise<(() => voi } // Send response back to main process - window.electronAPI.sendHttpResponse(response); + window.electronAPI.sendHttpResponse(normalizeWalletTimeoutResponse(response)); } catch (e) { console.error("Error handling http-request event:", e); response = { diff --git a/test/onWalletReady.test.ts b/test/onWalletReady.test.ts index 39cec57..3066f6d 100644 --- a/test/onWalletReady.test.ts +++ b/test/onWalletReady.test.ts @@ -276,6 +276,45 @@ describe('onWalletReady', () => { ) }) + it('returns 504 when a wallet call does not complete', async () => { + vi.useFakeTimers() + try { + const wallet = makeMockWallet({ + getPublicKey: vi.fn(() => new Promise(() => {})), + }) + await onWalletReady(wallet) + const handler = mockOnHttpRequest.mock.calls[0][0] + + const pending = handler({ + request_id: 8, + path: '/getPublicKey', + headers: { origin: 'https://example.com' }, + body: JSON.stringify({ identityKey: true, seekPermission: false }), + method: 'POST', + }) + + await vi.advanceTimersByTimeAsync(60000) + await pending + + const response = mockSendHttpResponse.mock.calls[0][0] + expect(response).toEqual( + expect.objectContaining({ + request_id: 8, + status: 504, + }) + ) + expect(JSON.parse(response.body)).toEqual( + expect.objectContaining({ + code: 'WALLET_REQUEST_TIMEOUT', + method: 'getPublicKey', + timeoutMs: 60000, + }) + ) + } finally { + vi.useRealTimers() + } + }) + it('preserves WERR_REVIEW_ACTIONS fields when the class name is minified', async () => { const reviewActionResults = [{ txid: '00'.repeat(32), status: 'serviceError' }] const sendWithResults = [{ txid: '00'.repeat(32), status: 'failed' }]