From ce8c4296e44b7497d56c142ff2b4c1c0268f8c48 Mon Sep 17 00:00:00 2001 From: ffmcgee <51971598+ffmcgee725@users.noreply.github.com> Date: Tue, 11 Nov 2025 17:09:10 +0100 Subject: [PATCH 01/10] fix: use randomly generated request id in `windowPostMessageTransport` and `externallyConnectableTransport` (#81) * fix: use randomly generated request id in windowPostMessageTransport and externallyConnectableTransport --- CHANGELOG.md | 4 +++ src/helpers/utils.ts | 17 +++++++++++ .../externallyConnectableTransport.test.ts | 18 ++++++++---- .../externallyConnectableTransport.ts | 4 +-- .../windowPostMessageTransport.test.ts | 28 ++++++++++++------- src/transports/windowPostMessageTransport.ts | 4 +-- 6 files changed, 56 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b80b24..2ee0240 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- fix: use randomly generated request id on `windowPostMessageTransport` and `externallyConnectableTransport` to avoid conflicts across disconnect/reconnect cycles in firefox ([#81](https://github.com/MetaMask/multichain-api-client/pull/81)) + ## [0.8.0] ### Added diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts index 1c74515..3c797bf 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -1,6 +1,23 @@ // chrome is a global injected by browser extensions declare const chrome: any; +// uint32 (two's complement) max +// more conservative than Number.MAX_SAFE_INTEGER +const MAX = 4_294_967_295; +let idCounter = Math.floor(Math.random() * MAX); + +/** + * Gets an ID that is guaranteed to be unique so long as no more than + * 4_294_967_295 (uint32 max) IDs are created, or the IDs are rapidly turned + * over. + * + * @returns The unique ID. + */ +export const getUniqueId = (): number => { + idCounter = (idCounter + 1) % MAX; + return idCounter; +}; + /** * Detects if we're in a Chrome-like environment with extension support */ diff --git a/src/transports/externallyConnectableTransport.test.ts b/src/transports/externallyConnectableTransport.test.ts index 74ef97c..f756997 100644 --- a/src/transports/externallyConnectableTransport.test.ts +++ b/src/transports/externallyConnectableTransport.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { type MockPort, mockSession } from '../../tests/mocks'; import * as metamaskExtensionId from '../helpers/metamaskExtensionId'; +import * as utils from '../helpers/utils'; import { TransportError } from '../types/errors'; import { getExternallyConnectableTransport } from './externallyConnectableTransport'; @@ -19,11 +20,18 @@ describe('ExternallyConnectableTransport', () => { let transport: ReturnType; let messageHandler: (msg: any) => void; let disconnectHandler: () => void; + const MOCK_INITIAL_REQUEST_ID = 1000; let mockPort: MockPort; beforeEach(() => { vi.clearAllMocks(); + // Mock getUniqueId() to return sequential values starting from MOCK_INITIAL_REQUEST_ID + let requestIdCounter = MOCK_INITIAL_REQUEST_ID; + vi.spyOn(utils, 'getUniqueId').mockImplementation(() => { + return requestIdCounter++; + }); + // Setup mock port mockPort = { postMessage: vi.fn(), @@ -80,7 +88,7 @@ describe('ExternallyConnectableTransport', () => { messageHandler({ type: 'caip-348', data: { - id: 1, + id: MOCK_INITIAL_REQUEST_ID, jsonrpc: '2.0', result: mockSession, }, @@ -91,7 +99,7 @@ describe('ExternallyConnectableTransport', () => { expect(mockPort.postMessage).toHaveBeenCalledWith({ type: 'caip-348', data: { - id: 1, + id: MOCK_INITIAL_REQUEST_ID, jsonrpc: '2.0', method: 'wallet_getSession', }, @@ -156,19 +164,19 @@ describe('ExternallyConnectableTransport', () => { 'Transport request timed out', ); - // Second request should work (id 2) + // Second request should work (id MOCK_INITIAL_REQUEST_ID + 1) const secondPromise = transport.request({ method: 'wallet_getSession' }); messageHandler({ type: 'caip-348', data: { - id: 2, + id: MOCK_INITIAL_REQUEST_ID + 1, jsonrpc: '2.0', result: mockSession, }, }); const response = await secondPromise; - expect(response).toEqual({ id: 2, jsonrpc: '2.0', result: mockSession }); + expect(response).toEqual({ id: MOCK_INITIAL_REQUEST_ID + 1, jsonrpc: '2.0', result: mockSession }); }); }); diff --git a/src/transports/externallyConnectableTransport.ts b/src/transports/externallyConnectableTransport.ts index 1c9158f..7adbc9d 100644 --- a/src/transports/externallyConnectableTransport.ts +++ b/src/transports/externallyConnectableTransport.ts @@ -1,5 +1,5 @@ import { detectMetamaskExtensionId } from '../helpers/metamaskExtensionId'; -import { withTimeout } from '../helpers/utils'; +import { getUniqueId, withTimeout } from '../helpers/utils'; import { TransportError, TransportTimeoutError } from '../types/errors'; import type { Transport, TransportResponse } from '../types/transport'; import { DEFAULT_REQUEST_TIMEOUT, REQUEST_CAIP } from './constants'; @@ -28,7 +28,7 @@ export function getExternallyConnectableTransport( let { extensionId } = params; const { defaultTimeout = DEFAULT_REQUEST_TIMEOUT } = params; let chromePort: chrome.runtime.Port | undefined; - let requestId = 1; + let requestId = getUniqueId(); const pendingRequests = new Map void>(); /** diff --git a/src/transports/windowPostMessageTransport.test.ts b/src/transports/windowPostMessageTransport.test.ts index 5e43c39..68d062c 100644 --- a/src/transports/windowPostMessageTransport.test.ts +++ b/src/transports/windowPostMessageTransport.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { mockSession } from '../../tests/mocks'; +import * as utils from '../helpers/utils'; import { TransportError } from '../types/errors'; import { CONTENT_SCRIPT, INPAGE, MULTICHAIN_SUBSTREAM_NAME } from './constants'; import { getWindowPostMessageTransport } from './windowPostMessageTransport'; @@ -24,11 +25,18 @@ global.location = mockLocation as any; describe('WindowPostMessageTransport', () => { let transport: ReturnType; let messageHandler: (event: MessageEvent) => void; + const MOCK_INITIAL_REQUEST_ID = 1000; beforeEach(() => { // Reset mocks vi.clearAllMocks(); + // Mock getUniqueId() to return sequential values starting from MOCK_INITIAL_REQUEST_ID + let requestIdCounter = MOCK_INITIAL_REQUEST_ID; + vi.spyOn(utils, 'getUniqueId').mockImplementation(() => { + return requestIdCounter++; + }); + // Setup addEventListener mock to capture the message handler mockWindow.addEventListener.mockImplementation((event: string, handler: (event: MessageEvent) => void) => { if (event === 'message') { @@ -62,7 +70,7 @@ describe('WindowPostMessageTransport', () => { name: MULTICHAIN_SUBSTREAM_NAME, data: { jsonrpc: '2.0', - id: 1, + id: MOCK_INITIAL_REQUEST_ID, method: 'wallet_getSession', }, }, @@ -77,7 +85,7 @@ describe('WindowPostMessageTransport', () => { data: { name: MULTICHAIN_SUBSTREAM_NAME, data: { - id: 1, + id: MOCK_INITIAL_REQUEST_ID, result: mockSession, }, }, @@ -87,7 +95,7 @@ describe('WindowPostMessageTransport', () => { const response = await requestPromise; expect(response).toEqual({ - id: 1, + id: MOCK_INITIAL_REQUEST_ID, result: mockSession, }); }); @@ -260,7 +268,7 @@ describe('WindowPostMessageTransport', () => { data: { name: MULTICHAIN_SUBSTREAM_NAME, data: { - id: 2, + id: MOCK_INITIAL_REQUEST_ID + 1, result: { success: true }, }, }, @@ -274,7 +282,7 @@ describe('WindowPostMessageTransport', () => { data: { name: MULTICHAIN_SUBSTREAM_NAME, data: { - id: 1, + id: MOCK_INITIAL_REQUEST_ID, result: mockSession, }, }, @@ -284,11 +292,11 @@ describe('WindowPostMessageTransport', () => { const [response1, response2] = await Promise.all([request1Promise, request2Promise]); expect(response1).toEqual({ - id: 1, + id: MOCK_INITIAL_REQUEST_ID, result: mockSession, }); expect(response2).toEqual({ - id: 2, + id: MOCK_INITIAL_REQUEST_ID + 1, result: { success: true }, }); }); @@ -324,14 +332,14 @@ describe('WindowPostMessageTransport', () => { // Second request should still work (simulate response) const secondPromise = transport.request({ method: 'wallet_getSession' }); - // Simulate response for id 2 (because first timed out with id 1, second increments to 2) + // Simulate response for id MOCK_INITIAL_REQUEST_ID + 1 (because first timed out with id MOCK_INITIAL_REQUEST_ID, second increments to MOCK_INITIAL_REQUEST_ID + 1) messageHandler({ data: { target: INPAGE, data: { name: MULTICHAIN_SUBSTREAM_NAME, data: { - id: 2, + id: MOCK_INITIAL_REQUEST_ID + 1, result: mockSession, }, }, @@ -340,6 +348,6 @@ describe('WindowPostMessageTransport', () => { } as MessageEvent); const result = await secondPromise; - expect(result).toEqual({ id: 2, result: mockSession }); + expect(result).toEqual({ id: MOCK_INITIAL_REQUEST_ID + 1, result: mockSession }); }); }); diff --git a/src/transports/windowPostMessageTransport.ts b/src/transports/windowPostMessageTransport.ts index 603f839..8c74fd3 100644 --- a/src/transports/windowPostMessageTransport.ts +++ b/src/transports/windowPostMessageTransport.ts @@ -1,4 +1,4 @@ -import { withTimeout } from '../helpers/utils'; +import { getUniqueId, withTimeout } from '../helpers/utils'; import { TransportError, TransportTimeoutError } from '../types/errors'; import type { Transport, TransportResponse } from '../types/transport'; import { CONTENT_SCRIPT, DEFAULT_REQUEST_TIMEOUT, INPAGE, MULTICHAIN_SUBSTREAM_NAME } from './constants'; @@ -20,7 +20,7 @@ export function getWindowPostMessageTransport(params: { defaultTimeout?: number const { defaultTimeout = DEFAULT_REQUEST_TIMEOUT } = params; let messageListener: ((event: MessageEvent) => void) | null = null; const pendingRequests: Map void> = new Map(); - let requestId = 1; + let requestId = getUniqueId(); /** * Storing notification callbacks. * If we detect a "notification" (a message without an id) coming from the extension, we'll call each callback in here. From 8c4b0f0dab8e07652d5f6629475c316f55e5e616 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 18:14:28 +0100 Subject: [PATCH 02/10] 0.8.1 (#82) Co-authored-by: github-actions --- CHANGELOG.md | 5 ++++- package.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ee0240..e4a7c39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.8.1] + ### Fixed - fix: use randomly generated request id on `windowPostMessageTransport` and `externallyConnectableTransport` to avoid conflicts across disconnect/reconnect cycles in firefox ([#81](https://github.com/MetaMask/multichain-api-client/pull/81)) @@ -106,7 +108,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release -[Unreleased]: https://github.com/MetaMask/multichain-api-client/compare/v0.8.0...HEAD +[Unreleased]: https://github.com/MetaMask/multichain-api-client/compare/v0.8.1...HEAD +[0.8.1]: https://github.com/MetaMask/multichain-api-client/compare/v0.8.0...v0.8.1 [0.8.0]: https://github.com/MetaMask/multichain-api-client/compare/v0.7.0...v0.8.0 [0.7.0]: https://github.com/MetaMask/multichain-api-client/compare/v0.6.5...v0.7.0 [0.6.5]: https://github.com/MetaMask/multichain-api-client/compare/v0.6.4...v0.6.5 diff --git a/package.json b/package.json index 9650709..e439753 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/multichain-api-client", - "version": "0.8.0", + "version": "0.8.1", "license": "ISC", "description": "MetaMask Multichain Api Client", "homepage": "https://github.com/MetaMask/multichain-api-client#readme", From 143046944fe8b2e0b8865b971d24660dda4216e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 04:03:46 +0000 Subject: [PATCH 03/10] build(deps-dev): bump vite in the npm_and_yarn group across 1 directory Bumps the npm_and_yarn group with 1 update in the / directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). Updates `vite` from 6.3.6 to 6.4.1 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/create-vite@6.4.1/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 6.4.1 dependency-type: direct:development dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 1934a21..cf93cf5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5270,8 +5270,8 @@ __metadata: linkType: hard "vite@npm:^5.0.0 || ^6.0.0, vite@npm:^6.2.5": - version: 6.3.6 - resolution: "vite@npm:6.3.6" + version: 6.4.1 + resolution: "vite@npm:6.4.1" dependencies: esbuild: "npm:^0.25.0" fdir: "npm:^6.4.4" @@ -5320,7 +5320,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10/8b8b6fe12318ca457396bf2053df7056cf4810f1d4a43b36b6afe59860e32b749c0685a290fe8a973b0d3da179ceec4c30cebbd3c91d0c47fbcf6436b17bdeef + checksum: 10/ea2083b6b1d1c9e85a13d6797ae989aa1dbc27a5c054319c71141934bf3f8dba8d54b510618040f95751148da63787f28f043df7458a194c81f8b6d8a2d32844 languageName: node linkType: hard From 3e9662b8df505213ce06385ca6bde182766fa8ba Mon Sep 17 00:00:00 2001 From: Baptiste Marchand <75846779+baptiste-marchand@users.noreply.github.com> Date: Wed, 26 Nov 2025 11:05:45 +0100 Subject: [PATCH 04/10] feat: add Bitcoin support (#80) * feat: add Bitcoin support * refactor: rename BitcoinRpc to Bip122Rpc for consistency * refactor: remove Commitment type from bip122.types.ts --- src/types/scopes/bip122.types.ts | 50 ++++++++++++++++++++++++++++++++ src/types/scopes/index.ts | 2 ++ 2 files changed, 52 insertions(+) create mode 100644 src/types/scopes/bip122.types.ts diff --git a/src/types/scopes/bip122.types.ts b/src/types/scopes/bip122.types.ts new file mode 100644 index 0000000..1809c85 --- /dev/null +++ b/src/types/scopes/bip122.types.ts @@ -0,0 +1,50 @@ +import type { RpcMethod } from '.'; + +type Utxo = { + // Outpoint of the utxo in the format : + outpoint: string; + // Value of output in satoshis + value: string; + derivationIndex: number; + // scriptPubley in ASM format + scriptPubkey: string; + scriptPubkeyHex: string; + // If the script can be represented as an address, omitted otherwise + address?: string; +}; + +export type Bip122Rpc = { + methods: { + signMessage: RpcMethod< + { + account: { address: string }; + message: string; + }, + { signature: string } + >; + sendTransfer: RpcMethod< + { + account: { address: string }; + recipients: { address: string; amount: string }[]; + feeRate?: number; + }, + { txid: string } + >; + signPsbt: RpcMethod< + { + account: { address: string }; + options: { + fill: boolean; + broadcast: boolean; + }; + psbt: string; + feeRate?: number | undefined; + }, + { psbt: string; txid: string | null } + >; + fillPsbt: RpcMethod<{ account: { address: string }; psbt: string }, { psbt: string }>; + broadcastPsbt: RpcMethod<{ account: { address: string }; psbt: string }, { txid: string }>; + computeFee: RpcMethod<{ account: { address: string }; psbt: string }, { fee: string }>; + getUtxo: RpcMethod<{ account: { address: string }; outpoint: string }, Utxo>; + }; +}; diff --git a/src/types/scopes/index.ts b/src/types/scopes/index.ts index c9dba34..c2b7932 100644 --- a/src/types/scopes/index.ts +++ b/src/types/scopes/index.ts @@ -1,3 +1,4 @@ +import type { Bip122Rpc } from './bip122.types'; import type { Eip155Rpc } from './eip155.types'; import type { SolanaRpc } from './solana.types'; @@ -32,4 +33,5 @@ export type MethodReturn, M extends MethodN export type DefaultRpcApi = { eip155: Eip155Rpc; solana: SolanaRpc; + bip122: Bip122Rpc; }; From c917af5da6c9519cc29cfc349b63982b469e48ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 11:05:48 +0000 Subject: [PATCH 05/10] build(deps): bump the npm_and_yarn group across 1 directory with 2 updates Bumps the npm_and_yarn group with 2 updates in the / directory: [glob](https://github.com/isaacs/node-glob) and [js-yaml](https://github.com/nodeca/js-yaml). Updates `glob` from 10.4.5 to 10.5.0 - [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md) - [Commits](https://github.com/isaacs/node-glob/compare/v10.4.5...v10.5.0) Updates `js-yaml` from 3.14.1 to 3.14.2 - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/3.14.1...3.14.2) --- updated-dependencies: - dependency-name: glob dependency-version: 10.5.0 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: js-yaml dependency-version: 3.14.2 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index cf93cf5..ccf0472 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2665,8 +2665,8 @@ __metadata: linkType: hard "glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.7, glob@npm:^10.4.1": - version: 10.4.5 - resolution: "glob@npm:10.4.5" + version: 10.5.0 + resolution: "glob@npm:10.5.0" dependencies: foreground-child: "npm:^3.1.0" jackspeak: "npm:^3.1.2" @@ -2676,7 +2676,7 @@ __metadata: path-scurry: "npm:^1.11.1" bin: glob: dist/esm/bin.mjs - checksum: 10/698dfe11828b7efd0514cd11e573eaed26b2dff611f0400907281ce3eab0c1e56143ef9b35adc7c77ecc71fba74717b510c7c223d34ca8a98ec81777b293d4ac + checksum: 10/ab3bccfefcc0afaedbd1f480cd0c4a2c0e322eb3f0aa7ceaa31b3f00b825069f17cf0f1fc8b6f256795074b903f37c0ade37ddda6a176aa57f1c2bbfe7240653 languageName: node linkType: hard @@ -3130,14 +3130,14 @@ __metadata: linkType: hard "js-yaml@npm:^3.14.1": - version: 3.14.1 - resolution: "js-yaml@npm:3.14.1" + version: 3.14.2 + resolution: "js-yaml@npm:3.14.2" dependencies: argparse: "npm:^1.0.7" esprima: "npm:^4.0.0" bin: js-yaml: bin/js-yaml.js - checksum: 10/9e22d80b4d0105b9899135365f746d47466ed53ef4223c529b3c0f7a39907743fdbd3c4379f94f1106f02755b5e90b2faaf84801a891135544e1ea475d1a1379 + checksum: 10/172e0b6007b0bf0fc8d2469c94424f7dd765c64a047d2b790831fecef2204a4054eabf4d911eb73ab8c9a3256ab8ba1ee8d655b789bf24bf059c772acc2075a1 languageName: node linkType: hard From b1949a60ffcce640e290afd63b0a8813d16f2234 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:39:14 +0100 Subject: [PATCH 06/10] 0.9.0 (#85) * 0.9.0 * chore: update changelog --------- Co-authored-by: github-actions Co-authored-by: Baptiste Marchand <75846779+baptiste-marchand@users.noreply.github.com> --- CHANGELOG.md | 9 ++++++++- package.json | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4a7c39..cf2eaab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.9.0] + +### Added + +- feat: add Bitcoin support ([#80](https://github.com/MetaMask/multichain-api-client/pull/80)) + ## [0.8.1] ### Fixed @@ -108,7 +114,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release -[Unreleased]: https://github.com/MetaMask/multichain-api-client/compare/v0.8.1...HEAD +[Unreleased]: https://github.com/MetaMask/multichain-api-client/compare/v0.9.0...HEAD +[0.9.0]: https://github.com/MetaMask/multichain-api-client/compare/v0.8.1...v0.9.0 [0.8.1]: https://github.com/MetaMask/multichain-api-client/compare/v0.8.0...v0.8.1 [0.8.0]: https://github.com/MetaMask/multichain-api-client/compare/v0.7.0...v0.8.0 [0.7.0]: https://github.com/MetaMask/multichain-api-client/compare/v0.6.5...v0.7.0 diff --git a/package.json b/package.json index e439753..6055c0c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/multichain-api-client", - "version": "0.8.1", + "version": "0.9.0", "license": "ISC", "description": "MetaMask Multichain Api Client", "homepage": "https://github.com/MetaMask/multichain-api-client#readme", From 5e52837bca4e105d003658b68f4a68a53a650175 Mon Sep 17 00:00:00 2001 From: Edouard Bougon <15703023+EdouardBougon@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:51:41 +0100 Subject: [PATCH 07/10] fix: warmup timeout (#87) * fix: warmup timeout * fix: warmup timeout --- README.md | 76 ++++++++++++++++--- src/helpers/utils.test.ts | 13 ++++ src/helpers/utils.ts | 7 +- src/index.ts | 7 +- src/multichainClient.test.ts | 15 ++-- src/multichainClient.ts | 4 +- src/transports/constants.ts | 3 +- .../externallyConnectableTransport.test.ts | 20 +++++ .../externallyConnectableTransport.ts | 7 +- .../windowPostMessageTransport.test.ts | 14 ++++ src/transports/windowPostMessageTransport.ts | 15 +++- src/types/transport.ts | 12 +++ 12 files changed, 165 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 78b58a8..8d2131a 100644 --- a/README.md +++ b/README.md @@ -32,15 +32,49 @@ const result = await client.invokeMethod({ await client.revokeSession(); ``` -### Configuring Transport Timeout +### Configuring Transport Timeouts -You can configure a default timeout (in milliseconds) for all requests made through the transport by passing the `defaultTimeout` option to `getDefaultTransport`: +#### Default Request Timeout + +By default, the transport has **no timeout** (`-1`) for requests. This is because most operations require user interaction (e.g., confirming transactions in the MetaMask extension), and we don't want to prematurely cancel requests while the user is reviewing them. + +However, you can configure a default timeout (in milliseconds) for all requests by passing the `defaultTimeout` option: + +```typescript +const transport = getDefaultTransport({ defaultTimeout: 30000 }); // 30 seconds timeout for all requests +const client = getMultichainClient({ transport }); +``` + +To explicitly disable timeouts (wait indefinitely), set the timeout to `-1`: + +```typescript +const transport = getDefaultTransport({ defaultTimeout: -1 }); // No timeout (default behavior) +``` + +#### Warmup Timeout + +The `warmupTimeout` is a special timeout used specifically for the **first request** sent immediately after the transport establishes its connection. This is useful because: + +- Some transports need a brief moment to fully initialize before they can reliably process requests +- The initial "warmup" request is typically a lightweight check (e.g., `wallet_getSession`) that doesn't require user interaction +- This timeout is usually much shorter than the regular request timeout ```typescript -const transport = getDefaultTransport({ defaultTimeout: 5000 }); // 5 seconds timeout for all requests +const transport = getDefaultTransport({ + warmupTimeout: 200, // 200 ms for the initial warmup request + defaultTimeout: -1 // No timeout for subsequent requests (user interactions) +}); const client = getMultichainClient({ transport }); ``` +**Key differences between `warmupTimeout` and `defaultTimeout`:** + +| Property | Purpose | Typical Value | When Applied | +| ---------------- | ----------------------------- | ----------------- | ---------------------------------------- | +| `warmupTimeout` | Initial connection validation | 200 ms | Only the first request after `connect()` | +| `defaultTimeout` | Regular request operations | `-1` (no timeout) | All subsequent requests | +``` + ## Extending RPC Types The client's RPC requests are strongly typed, enforcing the RPC methods and params to be defined ahead of usage. The client supports extending @@ -103,6 +137,7 @@ A transport must implement the following interface: ```typescript type Transport = { + warmupTimeout?: number; // Optional timeout for the initial warmup request connect: () => Promise; disconnect: () => Promise; isConnected: () => boolean; @@ -121,31 +156,50 @@ import { TransportError, TransportTimeoutError } from '@metamask/multichain-api- import type { Transport, TransportRequest, TransportResponse } from '@metamask/multichain-api-client'; type CustomTransportOptions = { - defaultTimeout?: number; // ms + defaultTimeout?: number; // Default timeout for all requests (use -1 for no timeout) + warmupTimeout?: number; // Optional timeout for the initial warmup request }; export function getCustomTransport(options: CustomTransportOptions = {}): Transport { - const { defaultTimeout = 5000 } = options; + const { defaultTimeout = -1, warmupTimeout } = options; // Default: no timeout return { + warmupTimeout, // Expose warmupTimeout for the client to use connect: async () => { ... }, disconnect: async () => { ... }, isConnected: () => { ...}, - request: async ( request: TRequest, { timeout }: { timeout?: number } = {}): Promise => { ... }, + request: async ( + request: TRequest, + { timeout = defaultTimeout }: { timeout?: number } = {} + ): Promise => { + // If timeout is -1, don't apply any timeout + if (timeout === -1) { + return performRequest(request); // Your actual request logic + } + + // Otherwise, wrap the request with a timeout + return withTimeout( + performRequest(request), + timeout, + () => new TransportTimeoutError() + ); + }, onNotification: (callback: (data: unknown) => void) => { ... }, }; } -// Usage -const transport = getCustomTransport({ defaultTimeout: 8000 }); +// Usage examples +const transport = getCustomTransport({ + warmupTimeout: 500, // 500 ms for initial connection check + defaultTimeout: -1 // No timeout for user interactions (default) +}); const client = getMultichainClient({ transport }); -// Per-request override +// Per-request timeout override await client.invokeMethod({ scope: 'eip155:1', request: { method: 'eth_chainId', params: [] }, - // The transport's request implementation can expose a timeout override - { timeout: 10000 // 10 seconds timeout for this request only + { timeout: 10000 } // 10 seconds timeout for this specific request }); ``` diff --git a/src/helpers/utils.test.ts b/src/helpers/utils.test.ts index 559fc47..6cfb3ad 100644 --- a/src/helpers/utils.test.ts +++ b/src/helpers/utils.test.ts @@ -106,5 +106,18 @@ describe('utils', () => { 'custom', ); }); + + it('should not apply timeout when timeoutMs is -1', async () => { + const slowPromise = new Promise((resolve) => { + setTimeout(() => resolve('completed'), 100); + }); + const result = await withTimeout(slowPromise, -1); + expect(result).toBe('completed'); + }); + + it('should handle rejection when timeoutMs is -1', async () => { + const failingPromise = Promise.reject(new Error('failed')); + await expect(withTimeout(failingPromise, -1)).rejects.toThrow('failed'); + }); }); }); diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts index 3c797bf..6f6d82b 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -68,10 +68,15 @@ export async function withRetry( /** * Returns a promise that resolves or rejects like the given promise, but fails if the timeout is exceeded. * @param promise - The promise to monitor - * @param timeoutMs - Maximum duration in ms + * @param timeoutMs - Maximum duration in ms. Use -1 to disable timeout. * @param errorFactory - Optional callback to generate a custom error on timeout */ export function withTimeout(promise: Promise, timeoutMs: number, errorFactory?: () => Error): Promise { + // If timeout is -1, return the promise without timeout + if (timeoutMs === -1) { + return promise; + } + return new Promise((resolve, reject) => { const timer = setTimeout(() => { if (errorFactory) { diff --git a/src/index.ts b/src/index.ts index ce7e447..3dcf448 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,11 +23,12 @@ import type { Transport } from './types/transport'; function getDefaultTransport({ extensionId, defaultTimeout, -}: { extensionId?: string; defaultTimeout?: number } = {}): Transport { + warmupTimeout, +}: { extensionId?: string; defaultTimeout?: number; warmupTimeout?: number } = {}): Transport { const isChrome = isChromeRuntime(); return isChrome - ? getExternallyConnectableTransport({ extensionId, defaultTimeout }) - : getWindowPostMessageTransport({ defaultTimeout }); + ? getExternallyConnectableTransport({ extensionId, defaultTimeout, warmupTimeout }) + : getWindowPostMessageTransport({ defaultTimeout, warmupTimeout }); } export { getMultichainClient, getDefaultTransport, getExternallyConnectableTransport, getWindowPostMessageTransport }; diff --git a/src/multichainClient.test.ts b/src/multichainClient.test.ts index 3e17b2d..24fca85 100644 --- a/src/multichainClient.test.ts +++ b/src/multichainClient.test.ts @@ -27,7 +27,7 @@ describe('getMultichainClient', () => { expect(result).toEqual(mockSession); // First call from initialization - expect(mockTransport.request).toHaveBeenNthCalledWith(1, { method: 'wallet_getSession' }); + expect(mockTransport.request).toHaveBeenNthCalledWith(1, { method: 'wallet_getSession' }, { timeout: 1_000 }); // Second call is the createSession request including options object expect(mockTransport.request).toHaveBeenNthCalledWith( 2, @@ -44,9 +44,14 @@ describe('getMultichainClient', () => { const result = await client.getSession(); expect(result).toEqual(mockSession); - expect(mockTransport.request).toHaveBeenCalledWith({ - method: 'wallet_getSession', - }); + // First call from initialization with warmupTimeout + expect(mockTransport.request).toHaveBeenNthCalledWith(1, { method: 'wallet_getSession' }, { timeout: 1_000 }); + // Second call from explicit getSession() + expect(mockTransport.request).toHaveBeenNthCalledWith( + 2, + { method: 'wallet_getSession', params: undefined }, + { timeout: undefined }, + ); }); describe('revokeSession', () => { @@ -85,7 +90,7 @@ describe('getMultichainClient', () => { }, }); expect(signAndSendResult).toEqual({ signature: 'mock-signature' }); - expect(mockTransport.request).toHaveBeenNthCalledWith(1, { method: 'wallet_getSession' }); + expect(mockTransport.request).toHaveBeenNthCalledWith(1, { method: 'wallet_getSession' }, { timeout: 1_000 }); expect(mockTransport.request).toHaveBeenNthCalledWith( 2, { diff --git a/src/multichainClient.ts b/src/multichainClient.ts index 66c06f7..eb2421e 100644 --- a/src/multichainClient.ts +++ b/src/multichainClient.ts @@ -70,7 +70,9 @@ export function getMultichainClient({ await ensureConnected(); // Use withRetry to handle the case where the Multichain API requests don't resolve on page load (cf. https://github.com/MetaMask/metamask-mobile/issues/16550) - await withRetry(() => transport.request({ method: 'wallet_getSession' })); + await withRetry(() => + transport.request({ method: 'wallet_getSession' }, { timeout: transport.warmupTimeout ?? 1_000 }), + ); })(); return await initializationPromise; diff --git a/src/transports/constants.ts b/src/transports/constants.ts index abb2f69..ae4dd46 100644 --- a/src/transports/constants.ts +++ b/src/transports/constants.ts @@ -5,4 +5,5 @@ export const INPAGE = 'metamask-inpage'; export const MULTICHAIN_SUBSTREAM_NAME = 'metamask-multichain-provider'; export const METAMASK_PROVIDER_STREAM_NAME = 'metamask-provider'; export const METAMASK_EXTENSION_CONNECT_CAN_RETRY = 'METAMASK_EXTENSION_CONNECT_CAN_RETRY'; -export const DEFAULT_REQUEST_TIMEOUT = 200; // 200ms +export const DEFAULT_REQUEST_TIMEOUT = -1; // No timeout by default +export const DEFAULT_WARMUP_TIMEOUT = 200; // 200 ms for initial warmup request diff --git a/src/transports/externallyConnectableTransport.test.ts b/src/transports/externallyConnectableTransport.test.ts index f756997..c7e3def 100644 --- a/src/transports/externallyConnectableTransport.test.ts +++ b/src/transports/externallyConnectableTransport.test.ts @@ -179,4 +179,24 @@ describe('ExternallyConnectableTransport', () => { const response = await secondPromise; expect(response).toEqual({ id: MOCK_INITIAL_REQUEST_ID + 1, jsonrpc: '2.0', result: mockSession }); }); + + it('should expose warmupTimeout when provided', () => { + const transportWithWarmup = getExternallyConnectableTransport({ + extensionId: testExtensionId, + warmupTimeout: 500, + }); + expect(transportWithWarmup.warmupTimeout).toBe(500); + }); + + it('should have default warmupTimeout of 200ms when not provided', () => { + expect(transport.warmupTimeout).toBe(200); + }); + + it('should support -1 as warmupTimeout to disable timeout', () => { + const transportWithNoTimeout = getExternallyConnectableTransport({ + extensionId: testExtensionId, + warmupTimeout: -1, + }); + expect(transportWithNoTimeout.warmupTimeout).toBe(-1); + }); }); diff --git a/src/transports/externallyConnectableTransport.ts b/src/transports/externallyConnectableTransport.ts index 7adbc9d..c4f9471 100644 --- a/src/transports/externallyConnectableTransport.ts +++ b/src/transports/externallyConnectableTransport.ts @@ -2,7 +2,7 @@ import { detectMetamaskExtensionId } from '../helpers/metamaskExtensionId'; import { getUniqueId, withTimeout } from '../helpers/utils'; import { TransportError, TransportTimeoutError } from '../types/errors'; import type { Transport, TransportResponse } from '../types/transport'; -import { DEFAULT_REQUEST_TIMEOUT, REQUEST_CAIP } from './constants'; +import { DEFAULT_REQUEST_TIMEOUT, DEFAULT_WARMUP_TIMEOUT, REQUEST_CAIP } from './constants'; /** * Creates a transport that communicates with the MetaMask extension via Chrome's externally_connectable API @@ -23,10 +23,10 @@ import { DEFAULT_REQUEST_TIMEOUT, REQUEST_CAIP } from './constants'; * ``` */ export function getExternallyConnectableTransport( - params: { extensionId?: string; defaultTimeout?: number } = {}, + params: { extensionId?: string; defaultTimeout?: number; warmupTimeout?: number } = {}, ): Transport { let { extensionId } = params; - const { defaultTimeout = DEFAULT_REQUEST_TIMEOUT } = params; + const { defaultTimeout = DEFAULT_REQUEST_TIMEOUT, warmupTimeout = DEFAULT_WARMUP_TIMEOUT } = params; let chromePort: chrome.runtime.Port | undefined; let requestId = getUniqueId(); const pendingRequests = new Map void>(); @@ -74,6 +74,7 @@ export function getExternallyConnectableTransport( } return { + warmupTimeout, connect: async () => { try { if (!extensionId) { diff --git a/src/transports/windowPostMessageTransport.test.ts b/src/transports/windowPostMessageTransport.test.ts index 68d062c..b0650d4 100644 --- a/src/transports/windowPostMessageTransport.test.ts +++ b/src/transports/windowPostMessageTransport.test.ts @@ -350,4 +350,18 @@ describe('WindowPostMessageTransport', () => { const result = await secondPromise; expect(result).toEqual({ id: MOCK_INITIAL_REQUEST_ID + 1, result: mockSession }); }); + + it('should expose warmupTimeout when provided', () => { + const transportWithWarmup = getWindowPostMessageTransport({ warmupTimeout: 500 }); + expect(transportWithWarmup.warmupTimeout).toBe(500); + }); + + it('should have default warmupTimeout of 200ms when not provided', () => { + expect(transport.warmupTimeout).toBe(200); + }); + + it('should support -1 as warmupTimeout to disable timeout', () => { + const transportWithNoTimeout = getWindowPostMessageTransport({ warmupTimeout: -1 }); + expect(transportWithNoTimeout.warmupTimeout).toBe(-1); + }); }); diff --git a/src/transports/windowPostMessageTransport.ts b/src/transports/windowPostMessageTransport.ts index 8c74fd3..472e877 100644 --- a/src/transports/windowPostMessageTransport.ts +++ b/src/transports/windowPostMessageTransport.ts @@ -1,7 +1,13 @@ import { getUniqueId, withTimeout } from '../helpers/utils'; import { TransportError, TransportTimeoutError } from '../types/errors'; import type { Transport, TransportResponse } from '../types/transport'; -import { CONTENT_SCRIPT, DEFAULT_REQUEST_TIMEOUT, INPAGE, MULTICHAIN_SUBSTREAM_NAME } from './constants'; +import { + CONTENT_SCRIPT, + DEFAULT_REQUEST_TIMEOUT, + DEFAULT_WARMUP_TIMEOUT, + INPAGE, + MULTICHAIN_SUBSTREAM_NAME, +} from './constants'; /** * Creates a transport that communicates with the MetaMask extension via window.postMessage @@ -16,8 +22,10 @@ import { CONTENT_SCRIPT, DEFAULT_REQUEST_TIMEOUT, INPAGE, MULTICHAIN_SUBSTREAM_N * const result = await transport.request({ method: 'eth_getBalance', params: ['0x123', 'latest'] }); * ``` */ -export function getWindowPostMessageTransport(params: { defaultTimeout?: number } = {}): Transport { - const { defaultTimeout = DEFAULT_REQUEST_TIMEOUT } = params; +export function getWindowPostMessageTransport( + params: { defaultTimeout?: number; warmupTimeout?: number } = {}, +): Transport { + const { defaultTimeout = DEFAULT_REQUEST_TIMEOUT, warmupTimeout = DEFAULT_WARMUP_TIMEOUT } = params; let messageListener: ((event: MessageEvent) => void) | null = null; const pendingRequests: Map void> = new Map(); let requestId = getUniqueId(); @@ -77,6 +85,7 @@ export function getWindowPostMessageTransport(params: { defaultTimeout?: number const isConnected = () => Boolean(messageListener); return { + warmupTimeout, connect: async () => { // If we're already connected, reconnect if (isConnected()) { diff --git a/src/types/transport.ts b/src/types/transport.ts index 2d9614c..ad18192 100644 --- a/src/types/transport.ts +++ b/src/types/transport.ts @@ -8,6 +8,18 @@ * - Handling notifications from the wallet */ export type Transport = { + /** + * Timeout used for the initial request sent right after the transport + * establishes its connection. + * + * This value represents the maximum time allowed for the first lightweight + * "warm-up" request to complete (e.g., a readiness or session check). It is + * typically shorter or different from the regular request timeout, as some + * transports require a distinct delay before they can reliably process the + * initial request. + */ + warmupTimeout?: number; + /** * Establishes a connection to the wallet * From 37a18d7d29e3772bf7bf549c7c0b50ff0bdb77da Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:55:13 +0100 Subject: [PATCH 08/10] build(deps): bump mdast-util-to-hast (#88) Bumps the npm_and_yarn group with 1 update in the / directory: [mdast-util-to-hast](https://github.com/syntax-tree/mdast-util-to-hast). Updates `mdast-util-to-hast` from 13.2.0 to 13.2.1 - [Release notes](https://github.com/syntax-tree/mdast-util-to-hast/releases) - [Commits](https://github.com/syntax-tree/mdast-util-to-hast/compare/13.2.0...13.2.1) --- updated-dependencies: - dependency-name: mdast-util-to-hast dependency-version: 13.2.1 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index ccf0472..09461e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3400,8 +3400,8 @@ __metadata: linkType: hard "mdast-util-to-hast@npm:^13.0.0": - version: 13.2.0 - resolution: "mdast-util-to-hast@npm:13.2.0" + version: 13.2.1 + resolution: "mdast-util-to-hast@npm:13.2.1" dependencies: "@types/hast": "npm:^3.0.0" "@types/mdast": "npm:^4.0.0" @@ -3412,7 +3412,7 @@ __metadata: unist-util-position: "npm:^5.0.0" unist-util-visit: "npm:^5.0.0" vfile: "npm:^6.0.0" - checksum: 10/b17ee338f843af31a1c7a2ebf0df6f0b41c9380b7119a63ab521d271df665456578e1234bb7617883e8d860fe878038dcf2b76ab2f21e0f7451215a096d26cce + checksum: 10/8fddf5e66ea24dc85c8fe1cc2acd8fbe36e9d4f21b06322e156431fd71385eab9d2d767646f50276ca4ce3684cb967c4e226c60c3fff3428feb687ccb598fa39 languageName: node linkType: hard From 92b0fdbe8baf36fbbe25df68ba213d300ff381cc Mon Sep 17 00:00:00 2001 From: Edouard Bougon <15703023+EdouardBougon@users.noreply.github.com> Date: Tue, 2 Dec 2025 12:00:29 +0100 Subject: [PATCH 09/10] feat: add tron scope types (#83) * feat: export all types * feat: add tron scope types --- src/index.ts | 1 + src/types/scopes/eip155.types.ts | 44 +++++++++++----------- src/types/scopes/index.ts | 2 + src/types/scopes/tron.types.ts | 64 ++++++++++++++++++++++++++++++++ src/types/session.ts | 21 +++++++++-- tests/index.test-d.ts | 15 ++++++++ 6 files changed, 122 insertions(+), 25 deletions(-) create mode 100644 src/types/scopes/tron.types.ts diff --git a/src/index.ts b/src/index.ts index 3dcf448..7476e29 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,4 +36,5 @@ export { getMultichainClient, getDefaultTransport, getExternallyConnectableTrans export type * from './types/transport'; export type * from './types/session'; export type * from './types/multichainApi'; +export type * from './types/scopes'; export * from './types/errors'; diff --git a/src/types/scopes/eip155.types.ts b/src/types/scopes/eip155.types.ts index 659bf22..798cd1e 100644 --- a/src/types/scopes/eip155.types.ts +++ b/src/types/scopes/eip155.types.ts @@ -1,15 +1,15 @@ import type { RpcMethod } from '.'; // Base types -type HexString = `0x${string}`; -type Address = `0x${string}`; -type Hash32 = `0x${string}`; -type BlockTag = 'earliest' | 'finalized' | 'safe' | 'latest' | 'pending'; -type BlockNumberOrTag = HexString | BlockTag; -type BlockNumberOrTagOrHash = HexString | BlockTag | Hash32; +export type HexString = `0x${string}`; +export type Address = `0x${string}`; +export type Hash32 = `0x${string}`; +export type BlockTag = 'earliest' | 'finalized' | 'safe' | 'latest' | 'pending'; +export type BlockNumberOrTag = HexString | BlockTag; +export type BlockNumberOrTagOrHash = HexString | BlockTag | Hash32; // Complex types for method parameters and responses -interface AddEthereumChainParameter { +export interface AddEthereumChainParameter { chainId: HexString; chainName: string; nativeCurrency: { @@ -22,7 +22,7 @@ interface AddEthereumChainParameter { iconUrls?: string[]; } -interface TypedData { +export interface TypedData { types: { EIP712Domain: Array<{ name: string; @@ -38,7 +38,7 @@ interface TypedData { message: Record; } -interface WatchAssetOptions { +export interface WatchAssetOptions { address: string; symbol?: string; decimals?: number; @@ -46,14 +46,14 @@ interface WatchAssetOptions { tokenId?: string; } -interface Call { +export interface Call { to?: Address; data?: HexString; value?: HexString; capabilities?: Record; } -interface SendCallsParameter { +export interface SendCallsParameter { version: string; id?: string; from: Address; @@ -63,12 +63,12 @@ interface SendCallsParameter { capabilities?: Record; } -interface BatchResult { +export interface BatchResult { id: string; capabilities?: Record; } -interface BatchStatus { +export interface BatchStatus { version: string; id: string; chainId: HexString; @@ -90,7 +90,7 @@ interface BatchStatus { capabilities?: Record; } -interface Transaction { +export interface Transaction { from: Address; to?: Address; gas?: HexString; @@ -108,14 +108,14 @@ interface Transaction { chainId?: HexString; } -interface Filter { +export interface Filter { fromBlock?: HexString; toBlock?: HexString; address?: Address | Address[]; topics?: Array; } -interface Log { +export interface Log { removed?: boolean; logIndex?: HexString; transactionIndex?: HexString; @@ -127,7 +127,7 @@ interface Log { topics: HexString[]; } -interface Block { +export interface Block { number: HexString; hash: Hash32; parentHash: Hash32; @@ -161,7 +161,7 @@ interface Block { mixHash?: Hash32; } -interface TransactionInfo { +export interface TransactionInfo { blockHash: Hash32; blockNumber: HexString; from: Address; @@ -187,7 +187,7 @@ interface TransactionInfo { yParity?: HexString; } -interface TransactionReceipt { +export interface TransactionReceipt { transactionHash: Hash32; transactionIndex: HexString; blockHash: Hash32; @@ -206,7 +206,7 @@ interface TransactionReceipt { blobGasPrice?: HexString; } -interface FeeHistory { +export interface FeeHistory { oldestBlock: HexString; baseFeePerGas: HexString[]; baseFeePerBlobGas?: HexString[]; @@ -215,7 +215,7 @@ interface FeeHistory { reward?: HexString[][]; } -interface AccountProof { +export interface AccountProof { address: Address; accountProof: HexString[]; balance: HexString; @@ -229,7 +229,7 @@ interface AccountProof { }>; } -type SyncingStatus = +export type SyncingStatus = | boolean | { startingBlock: HexString; diff --git a/src/types/scopes/index.ts b/src/types/scopes/index.ts index c2b7932..06dc114 100644 --- a/src/types/scopes/index.ts +++ b/src/types/scopes/index.ts @@ -1,6 +1,7 @@ import type { Bip122Rpc } from './bip122.types'; import type { Eip155Rpc } from './eip155.types'; import type { SolanaRpc } from './solana.types'; +import type { TronRpc } from './tron.types'; export type RpcApi = Record< string, @@ -34,4 +35,5 @@ export type DefaultRpcApi = { eip155: Eip155Rpc; solana: SolanaRpc; bip122: Bip122Rpc; + tron: TronRpc; }; diff --git a/src/types/scopes/tron.types.ts b/src/types/scopes/tron.types.ts new file mode 100644 index 0000000..bc4cb67 --- /dev/null +++ b/src/types/scopes/tron.types.ts @@ -0,0 +1,64 @@ +import type { RpcMethod } from '.'; + +/** + * A Base64-encoded message string. + * @example + * ```typescript + * const message = "Hello, Tron!"; + * const base64Message = Buffer.from(message).toString('base64'); + * ``` + */ +export type Base64Message = string; + +/** + * A Tron address in Base58Check format, starting with 'T'. + * @example "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8" + */ +export type TronAddress = `T${string}`; + +/** + * A signature. + */ +export type Signature = `0x${string}`; + +/** + * Signs a plain text message. + * The signature can be used to verify ownership of the account. + * + * @param address - The Tron address that will sign the message + * @param message - The message string in Base64 format to be signed + * @returns An object containing the hexadecimal signature of the message + */ +export type SignMessageMethod = RpcMethod< + { + address: TronAddress; + message: Base64Message; + }, + { signature: Signature } +>; + +/** + * Signs a Tron transaction. + * + * @param address - The Tron address that will sign the transaction + * @param transaction - The Tron transaction object containing `raw_data_hex` and `type` + * @returns An object containing the hexadecimal signature of the transaction + */ +export type SignTransactionMethod = RpcMethod< + { + address: TronAddress; + transaction: { + rawDataHex: string; + type: string; + }; + }, + { signature: Signature } +>; + +export type TronRpc = { + methods: { + signMessage: SignMessageMethod; + signTransaction: SignTransactionMethod; + }; + events: []; +}; diff --git a/src/types/session.ts b/src/types/session.ts index 1e94359..c3255f3 100644 --- a/src/types/session.ts +++ b/src/types/session.ts @@ -1,6 +1,21 @@ -type CaipAccountId = `${string}:${string}:${string}`; -type CaipChainId = `${string}:${string}`; -type Json = +/** + * CAIP-10 Account ID type. + * Format: `::
` + * Example: `eip155:1:0xabc123...` + */ +export type CaipAccountId = `${string}:${string}:${string}`; + +/** + * CAIP Chain ID type. + * Format: `:` + * Example: + * - `eip155:1` + * - `solana:mainnet` + * - `tron:728126428` + */ +export type CaipChainId = `${string}:${string}`; + +export type Json = | string | number | boolean diff --git a/tests/index.test-d.ts b/tests/index.test-d.ts index ce94dad..1dbbb9d 100644 --- a/tests/index.test-d.ts +++ b/tests/index.test-d.ts @@ -1,5 +1,6 @@ import { expectError, expectType } from 'tsd'; import { getMultichainClient } from '../src/index'; +import type { Signature } from '../src/types/scopes/tron.types'; import { getMockTransport } from './mocks'; const client = getMultichainClient({ transport: getMockTransport() }); @@ -46,6 +47,20 @@ expectType<`0x${string}`>( }), ); +// Basic tron signMessage call with correct scope and parameters +expectType<{ signature: Signature }>( + await client.invokeMethod({ + scope: 'tron:728126428', + request: { + method: 'signMessage', + params: { + address: 'TJRabPrwbZy45sbavfcjinPJC18kjpRTv8', + message: 'aGVsbG8gd29ybGQ=', + }, + }, + }), +); + // ========================================== // Test error cases for invalid inputs // ========================================== From aeb6e05d13e6c3e6854afea034b890b9484836f5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 12:08:54 +0100 Subject: [PATCH 10/10] 0.10.0 (#89) * 0.10.0 * chore: update changelog --------- Co-authored-by: github-actions Co-authored-by: Baptiste Marchand <75846779+baptiste-marchand@users.noreply.github.com> --- CHANGELOG.md | 13 ++++++++++++- package.json | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf2eaab..ccee932 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.10.0] + +### Added + +- feat: add tron scope types ([#83](https://github.com/MetaMask/multichain-api-client/pull/83)) + +### Fixed + +- fix: warmup timeout ([#87](https://github.com/MetaMask/multichain-api-client/pull/87)) + ## [0.9.0] ### Added @@ -114,7 +124,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release -[Unreleased]: https://github.com/MetaMask/multichain-api-client/compare/v0.9.0...HEAD +[Unreleased]: https://github.com/MetaMask/multichain-api-client/compare/v0.10.0...HEAD +[0.10.0]: https://github.com/MetaMask/multichain-api-client/compare/v0.9.0...v0.10.0 [0.9.0]: https://github.com/MetaMask/multichain-api-client/compare/v0.8.1...v0.9.0 [0.8.1]: https://github.com/MetaMask/multichain-api-client/compare/v0.8.0...v0.8.1 [0.8.0]: https://github.com/MetaMask/multichain-api-client/compare/v0.7.0...v0.8.0 diff --git a/package.json b/package.json index 6055c0c..b4a416c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/multichain-api-client", - "version": "0.9.0", + "version": "0.10.0", "license": "ISC", "description": "MetaMask Multichain Api Client", "homepage": "https://github.com/MetaMask/multichain-api-client#readme",