Skip to content

Commit 3619298

Browse files
authored
fix: re-introduce resolveAccountAddress on v2 KeyringSnapRpc (#585)
<!-- CURSOR_SUMMARY --> > [!NOTE] > **Medium Risk** > Touches signing-request routing contracts across API, client, and Snap handler; incorrect behavior could mis-route dapp signing, though changes are additive and optional for Snaps. > > **Overview** > Restores **`keyring_resolveAccountAddress`** on the v2 snap keyring stack so MetaMask can ask a Snap which account address should handle a signing request for a given CAIP-2 scope and JSON-RPC call. > > **keyring-api (v2)** adds the RPC method constant, request/response structs (nullable `ResolvedAccountAddress`), the optional `resolveAccountAddress` on `KeyringSnapRpc`, and a **`snap.resolveAccountAddress`** capability flag. > > **keyring-snap-client** and **keyring-snap-sdk** wire the method through: the client sends validated RPC calls; the SDK handler dispatches to `keyring.resolveAccountAddress` or throws if unsupported. Tests cover success, `null` responses, and missing implementation. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 5951828. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent d1d8459 commit 3619298

9 files changed

Lines changed: 208 additions & 6 deletions

File tree

packages/keyring-api/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add `resolveAccountAddress` to `KeyringSnapRpc` (v2) ([#585](https://github.com/MetaMask/accounts/pull/585))
13+
- Add `snap.resolveAccountAddress` boolean flag to `KeyringCapabilities` to declare support for this method.
14+
1015
## [23.4.0]
1116

1217
### Added

packages/keyring-api/src/v2/api/keyring-capabilities.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ export const KeyringCapabilitiesStruct = object({
108108
* (`keyring_setSelectedAccounts`).
109109
*/
110110
selectedAccounts: exactOptional(boolean()),
111+
/**
112+
* Whether the keyring supports resolving the account address to use for
113+
* routing a signing request (`keyring_resolveAccountAddress`).
114+
*/
115+
resolveAccountAddress: exactOptional(boolean()),
111116
}),
112117
),
113118
});

packages/keyring-api/src/v2/api/keyring-snap-rpc.ts

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,31 @@
1-
import type { AccountId } from '@metamask/keyring-utils';
2-
import { UuidStruct } from '@metamask/keyring-utils';
1+
import type { AccountId, JsonRpcRequest } from '@metamask/keyring-utils';
2+
import { UuidStruct, JsonRpcRequestStruct } from '@metamask/keyring-utils';
33
import type { Infer } from '@metamask/superstruct';
44
import {
55
array,
66
literal,
7+
nullable,
78
number,
89
object,
910
record,
1011
string,
1112
union,
1213
} from '@metamask/superstruct';
1314

15+
import { ResolvedAccountAddressStruct } from '../../api/address';
16+
import type { ResolvedAccountAddress } from '../../api/address';
1417
import { BalanceStruct } from '../../api/balance';
1518
import type { Balance } from '../../api/balance';
16-
import { CaipAssetTypeOrIdStruct, CaipAssetTypeStruct } from '../../api/caip';
17-
import type { CaipAssetType, CaipAssetTypeOrId } from '../../api/caip';
19+
import {
20+
CaipAssetTypeOrIdStruct,
21+
CaipAssetTypeStruct,
22+
CaipChainIdStruct,
23+
} from '../../api/caip';
24+
import type {
25+
CaipAssetType,
26+
CaipAssetTypeOrId,
27+
CaipChainId,
28+
} from '../../api/caip';
1829
import { PaginationStruct } from '../../api/pagination';
1930
import type { Pagination } from '../../api/pagination';
2031
import { TransactionsPageStruct } from '../../api/transaction';
@@ -32,6 +43,7 @@ export const KeyringSnapRpcMethod = {
3243
GetAccountTransactions: 'keyring_getAccountTransactions',
3344
GetAccountAssets: 'keyring_getAccountAssets',
3445
GetAccountBalances: 'keyring_getAccountBalances',
46+
ResolveAccountAddress: 'keyring_resolveAccountAddress',
3547
} as const;
3648

3749
/**
@@ -150,6 +162,30 @@ export type GetAccountBalancesResponse = Infer<
150162
typeof GetAccountBalancesResponseStruct
151163
>;
152164

165+
// ----------------------------------------------------------------------------
166+
// Resolve account address
167+
168+
export const ResolveAccountAddressRequestStruct = object({
169+
...CommonHeader,
170+
method: literal(`${KeyringSnapRpcMethod.ResolveAccountAddress}`),
171+
params: object({
172+
scope: CaipChainIdStruct,
173+
request: JsonRpcRequestStruct,
174+
}),
175+
});
176+
177+
export type ResolveAccountAddressRequest = Infer<
178+
typeof ResolveAccountAddressRequestStruct
179+
>;
180+
181+
export const ResolveAccountAddressResponseStruct = nullable(
182+
ResolvedAccountAddressStruct,
183+
);
184+
185+
export type ResolveAccountAddressResponse = Infer<
186+
typeof ResolveAccountAddressResponseStruct
187+
>;
188+
153189
// ----------------------------------------------------------------------------
154190

155191
/**
@@ -161,7 +197,8 @@ export type KeyringSnapRpcRequests =
161197
| SetSelectedAccountsRequest
162198
| GetAccountTransactionsRequest
163199
| GetAccountAssetsRequest
164-
| GetAccountBalancesRequest;
200+
| GetAccountBalancesRequest
201+
| ResolveAccountAddressRequest;
165202

166203
/**
167204
* Extract the proper request type for a given {@link KeyringSnapRpcMethod}.
@@ -205,4 +242,13 @@ export type KeyringSnapRpc = KeyringRpc & {
205242
id: AccountId,
206243
assets: CaipAssetType[],
207244
) => Promise<Record<CaipAssetType, Balance>>;
245+
246+
/**
247+
* Resolve the account address to use for routing a signing request.
248+
* Maps to `keyring_resolveAccountAddress`.
249+
*/
250+
resolveAccountAddress?: (
251+
scope: CaipChainId,
252+
request: JsonRpcRequest,
253+
) => Promise<ResolvedAccountAddress | null>;
208254
};

packages/keyring-snap-client/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add `resolveAccountAddress` to v2 `KeyringClient` ([#585](https://github.com/MetaMask/accounts/pull/585))
13+
1014
## [9.1.0]
1115

1216
### Added

packages/keyring-snap-client/src/v2/KeyringClient.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,5 +272,45 @@ describe('KeyringClient', () => {
272272
expect(response).toStrictEqual(expectedResponse);
273273
});
274274
});
275+
276+
describe('resolveAccountAddress', () => {
277+
it('sends a request to resolve an account address and returns the response', async () => {
278+
const scope = 'bip122:000000000019d6689c085ae165831e93';
279+
const request = {
280+
jsonrpc: '2.0' as const,
281+
id: '1',
282+
method: 'signPsbt',
283+
params: {},
284+
};
285+
const expectedResponse = {
286+
address:
287+
'bip122:000000000019d6689c085ae165831e93:1A1zP1eP5QGefi2DMPTfTL5SLmv7Divf',
288+
};
289+
290+
mockSender.send.mockResolvedValue(expectedResponse);
291+
const response = await client.resolveAccountAddress(scope, request);
292+
expect(mockSender.send).toHaveBeenCalledWith({
293+
jsonrpc: '2.0',
294+
id: expect.any(String),
295+
method: `${KeyringSnapRpcMethod.ResolveAccountAddress}`,
296+
params: { scope, request },
297+
});
298+
expect(response).toStrictEqual(expectedResponse);
299+
});
300+
301+
it('returns null when the snap cannot resolve an address', async () => {
302+
const scope = 'bip122:000000000019d6689c085ae165831e93';
303+
const request = {
304+
jsonrpc: '2.0' as const,
305+
id: '1',
306+
method: 'signPsbt',
307+
params: {},
308+
};
309+
310+
mockSender.send.mockResolvedValue(null);
311+
const response = await client.resolveAccountAddress(scope, request);
312+
expect(response).toBeNull();
313+
});
314+
});
275315
});
276316
});

packages/keyring-snap-client/src/v2/KeyringClient.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import type { KeyringAccount, KeyringRequest } from '@metamask/keyring-api';
1+
import type {
2+
CaipChainId,
3+
KeyringAccount,
4+
KeyringRequest,
5+
ResolvedAccountAddress,
6+
} from '@metamask/keyring-api';
27
import type {
38
Balance,
49
CaipAssetType,
@@ -19,6 +24,7 @@ import {
1924
GetAccountTransactionsResponseStruct,
2025
GetAccountAssetsResponseStruct,
2126
GetAccountBalancesResponseStruct,
27+
ResolveAccountAddressResponseStruct,
2228
} from '@metamask/keyring-api/v2';
2329
import type {
2430
CreateAccountOptions,
@@ -27,6 +33,7 @@ import type {
2733
KeyringSnapRpc,
2834
KeyringRpcRequest,
2935
} from '@metamask/keyring-api/v2';
36+
import type { JsonRpcRequest } from '@metamask/keyring-utils';
3037
import type { AccountId } from '@metamask/keyring-utils';
3138
import { strictMask } from '@metamask/keyring-utils';
3239
import { assert } from '@metamask/superstruct';
@@ -252,4 +259,27 @@ export class KeyringClient implements KeyringSnapRpc {
252259
GetAccountBalancesResponseStruct,
253260
);
254261
}
262+
263+
/**
264+
* Resolves the account address to use for routing a signing request.
265+
*
266+
* @param scope - CAIP-2 chain ID of the signing request.
267+
* @param request - The signing JSON-RPC request.
268+
* @returns A promise that resolves to the resolved address, or `null` if
269+
* the Snap cannot determine an address for this request.
270+
*/
271+
async resolveAccountAddress(
272+
scope: CaipChainId,
273+
request: JsonRpcRequest,
274+
): Promise<ResolvedAccountAddress | null> {
275+
return strictMask(
276+
await this.#sender.send({
277+
jsonrpc: '2.0',
278+
id: uuid(),
279+
method: KeyringSnapRpcMethod.ResolveAccountAddress,
280+
params: { scope, request },
281+
}),
282+
ResolveAccountAddressResponseStruct,
283+
);
284+
}
255285
}

packages/keyring-snap-sdk/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add `keyring_resolveAccountAddress` dispatch to v2 `handleKeyringRequest` ([#585](https://github.com/MetaMask/accounts/pull/585))
13+
1014
## [9.1.0]
1115

1216
### Added

packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type {
1919
GetAccountTransactionsRequest,
2020
GetAccountAssetsRequest,
2121
GetAccountBalancesRequest,
22+
ResolveAccountAddressRequest,
2223
KeyringRpc,
2324
KeyringSnapRpc,
2425
} from '@metamask/keyring-api/v2';
@@ -38,6 +39,7 @@ describe('handleKeyringRequest', () => {
3839
getAccountTransactions: jest.fn(),
3940
getAccountAssets: jest.fn(),
4041
getAccountBalances: jest.fn(),
42+
resolveAccountAddress: jest.fn(),
4143
};
4244

4345
afterEach(() => {
@@ -472,6 +474,60 @@ describe('handleKeyringRequest', () => {
472474
);
473475
});
474476

477+
it('calls `keyring_resolveAccountAddress`', async () => {
478+
const request: ResolveAccountAddressRequest = {
479+
jsonrpc: '2.0',
480+
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4',
481+
method: `${KeyringSnapRpcMethod.ResolveAccountAddress}`,
482+
params: {
483+
scope: 'bip122:000000000019d6689c085ae165831e93',
484+
request: {
485+
jsonrpc: '2.0',
486+
id: '1',
487+
method: 'signPsbt',
488+
params: {},
489+
},
490+
},
491+
};
492+
493+
const mockedResult = {
494+
address:
495+
'bip122:000000000019d6689c085ae165831e93:1A1zP1eP5QGefi2DMPTfTL5SLmv7Divf',
496+
};
497+
keyring.resolveAccountAddress.mockResolvedValue(mockedResult);
498+
const result = await handleKeyringRequest(keyring, request);
499+
500+
expect(keyring.resolveAccountAddress).toHaveBeenCalledWith(
501+
request.params.scope,
502+
request.params.request,
503+
);
504+
expect(result).toStrictEqual(mockedResult);
505+
});
506+
507+
it('throws an error if `keyring_resolveAccountAddress` is not implemented', async () => {
508+
const request: ResolveAccountAddressRequest = {
509+
jsonrpc: '2.0',
510+
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4',
511+
method: `${KeyringSnapRpcMethod.ResolveAccountAddress}`,
512+
params: {
513+
scope: 'bip122:000000000019d6689c085ae165831e93',
514+
request: {
515+
jsonrpc: '2.0',
516+
id: '1',
517+
method: 'signPsbt',
518+
params: {},
519+
},
520+
},
521+
};
522+
523+
const partialKeyring: KeyringSnapRpc = { ...keyring };
524+
delete partialKeyring.resolveAccountAddress;
525+
526+
await expect(handleKeyringRequest(partialKeyring, request)).rejects.toThrow(
527+
`Method not supported: ${KeyringSnapRpcMethod.ResolveAccountAddress}`,
528+
);
529+
});
530+
475531
it('throws an error if an unknown method is called', async () => {
476532
const request: JsonRpcRequest = {
477533
jsonrpc: '2.0',

packages/keyring-snap-sdk/src/v2/rpc-handler.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
GetAccountTransactionsRequestStruct,
1717
GetAccountAssetsRequestStruct,
1818
GetAccountBalancesRequestStruct,
19+
ResolveAccountAddressRequestStruct,
1920
} from '@metamask/keyring-api/v2';
2021
import type { KeyringSnapRpc } from '@metamask/keyring-api/v2';
2122
import type { JsonRpcRequest } from '@metamask/keyring-utils';
@@ -138,6 +139,17 @@ async function dispatchKeyringRequest(
138139
);
139140
}
140141

142+
case `${KeyringSnapRpcMethod.ResolveAccountAddress}`: {
143+
if (keyring.resolveAccountAddress === undefined) {
144+
throw new MethodNotSupportedError(request.method);
145+
}
146+
assert(request, ResolveAccountAddressRequestStruct);
147+
return keyring.resolveAccountAddress(
148+
request.params.scope,
149+
request.params.request,
150+
);
151+
}
152+
141153
default: {
142154
throw new MethodNotSupportedError(request.method);
143155
}

0 commit comments

Comments
 (0)