Skip to content
47 changes: 47 additions & 0 deletions packages/snap/integration-test/keyring-request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ describe('KeyringRequestHandler', () => {
request: {
method: AccountCapability.GetUtxo,
params: {
account: { address: account.address },
outpoint: utxos[0]?.outpoint,
},
},
Expand Down Expand Up @@ -221,6 +222,7 @@ describe('KeyringRequestHandler', () => {
request: {
method: AccountCapability.SignPsbt,
params: {
account: { address: account.address },
psbt: TEMPLATE_PSBT,
feeRate: 3,
options: {
Expand Down Expand Up @@ -253,6 +255,7 @@ describe('KeyringRequestHandler', () => {
request: {
method: AccountCapability.SignPsbt,
params: {
account: { address: account.address },
psbt: TEMPLATE_PSBT,
feeRate: 3,
options: {
Expand Down Expand Up @@ -285,6 +288,7 @@ describe('KeyringRequestHandler', () => {
request: {
method: AccountCapability.SignPsbt,
params: {
account: { address: account.address },
psbt: TEMPLATE_PSBT,
feeRate: 3,
options: {
Expand Down Expand Up @@ -317,6 +321,7 @@ describe('KeyringRequestHandler', () => {
request: {
method: AccountCapability.SignPsbt,
params: {
account: { address: account.address },
psbt: 'notAPsbt',
options: {
fill: true,
Expand Down Expand Up @@ -350,6 +355,7 @@ describe('KeyringRequestHandler', () => {
request: {
method: AccountCapability.SignPsbt,
params: {
account: { address: account.address },
psbt: TEMPLATE_PSBT,
},
},
Expand All @@ -363,6 +369,37 @@ describe('KeyringRequestHandler', () => {
stack: expect.anything(),
});
});

it('fails if missing account', async () => {
const response = await snap.onKeyringRequest({
origin: ORIGIN,
method: submitRequestMethod,
params: {
id: account.id,
origin,
scope: BtcScope.Regtest,
account: account.id,
request: {
method: AccountCapability.SignPsbt,
params: {
psbt: TEMPLATE_PSBT,
feeRate: 3,
options: {
fill: true,
broadcast: true,
},
},
},
} as KeyringRequest,
});

expect(response).toRespondWithError({
code: -32000,
message:
'Invalid format: At path: account -- Expected an object, but received: undefined',
stack: expect.anything(),
});
});
});

describe('fillPsbt', () => {
Expand All @@ -382,6 +419,7 @@ describe('KeyringRequestHandler', () => {
request: {
method: AccountCapability.FillPsbt,
params: {
account: { address: account.address },
psbt: TEMPLATE_PSBT,
feeRate: 3,
},
Expand Down Expand Up @@ -409,6 +447,7 @@ describe('KeyringRequestHandler', () => {
request: {
method: AccountCapability.FillPsbt,
params: {
account: { address: account.address },
psbt: 'notAPsbt',
},
},
Expand Down Expand Up @@ -444,6 +483,7 @@ describe('KeyringRequestHandler', () => {
request: {
method: AccountCapability.ComputeFee,
params: {
account: { address: account.address },
psbt: TEMPLATE_PSBT,
feeRate: 3,
},
Expand Down Expand Up @@ -471,6 +511,7 @@ describe('KeyringRequestHandler', () => {
request: {
method: AccountCapability.ComputeFee,
params: {
account: { address: account.address },
psbt: 'notAPsbt',
},
},
Expand Down Expand Up @@ -507,6 +548,7 @@ describe('KeyringRequestHandler', () => {
request: {
method: AccountCapability.SignPsbt,
params: {
account: { address: account.address },
psbt: TEMPLATE_PSBT,
feeRate: 3,
options: {
Expand All @@ -533,6 +575,7 @@ describe('KeyringRequestHandler', () => {
request: {
method: AccountCapability.BroadcastPsbt,
params: {
account: { address: account.address },
psbt: result.psbt,
},
},
Expand All @@ -559,6 +602,7 @@ describe('KeyringRequestHandler', () => {
request: {
method: AccountCapability.BroadcastPsbt,
params: {
account: { address: account.address },
psbt: 'notAPsbt',
},
},
Expand Down Expand Up @@ -590,6 +634,7 @@ describe('KeyringRequestHandler', () => {
request: {
method: AccountCapability.SendTransfer,
params: {
account: { address: account.address },
recipients: [
{
address: 'bcrt1qstku2y3pfh9av50lxj55arm8r5gj8tf2yv5nxz',
Expand Down Expand Up @@ -626,6 +671,7 @@ describe('KeyringRequestHandler', () => {
request: {
method: AccountCapability.SendTransfer,
params: {
account: { address: account.address },
recipients: [{ address: 'notAnAddress', amount: '1000' }],
},
},
Expand Down Expand Up @@ -654,6 +700,7 @@ describe('KeyringRequestHandler', () => {
request: {
method: AccountCapability.SignMessage,
params: {
account: { address: account.address },
message: 'Hello, world!',
},
},
Expand Down
2 changes: 1 addition & 1 deletion packages/snap/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/snap-bitcoin-wallet.git"
},
"source": {
"shasum": "j9W3ulkpAGrL0lkkChX7wavGEY3qq6nCf4lQ0KmmF78=",
"shasum": "mEW269SxMuGEGDIqlOjdUfQNye0DonqYArJaDJhyMrI=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
111 changes: 110 additions & 1 deletion packages/snap/src/handlers/KeyringHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import type {
KeyringResponse,
Transaction as KeyringTransaction,
KeyringRequest,
KeyringAccount,
} from '@metamask/keyring-api';
import { BtcAccountType, BtcScope } from '@metamask/keyring-api';
import { BtcAccountType, BtcMethod, BtcScope } from '@metamask/keyring-api';
import { mock } from 'jest-mock-extended';
import { assert } from 'superstruct';

Expand Down Expand Up @@ -844,4 +845,112 @@ describe('KeyringHandler', () => {
expect(result).toStrictEqual(expectedResponse);
});
});

describe('resolveAccountAddress', () => {
const mockKeyringAccount1 = mock<KeyringAccount>({
id: 'account-1',
address: 'test123',
scopes: [BtcScope.Regtest],
});
const mockKeyringAccount2 = mock<KeyringAccount>({
id: 'account-2',
address: 'test456',
scopes: [BtcScope.Regtest],
});

beforeEach(() => {
mockAccounts.list.mockResolvedValue([mockAccount]);
});

it('resolves account address successfully', async () => {
const request = {
id: '1',
jsonrpc: '2.0' as const,
method: BtcMethod.SignPsbt,
params: {
account: { address: 'test123' },
psbt: 'psbt',
},
};
const requestWithoutCommonHeader = {
method: request.method,
params: request.params,
};

// mockAccounts.list.mockResolvedValue([mockAccount]);
jest
.spyOn(handler, 'listAccounts')
.mockResolvedValueOnce([mockKeyringAccount1, mockKeyringAccount2]);
mockKeyringRequest.resolveAccountAddress.mockReturnValue(
'bip122:000000000019d6689c085ae165831e93:test123',
);

const result = await handler.resolveAccountAddress(
BtcScope.Regtest,
request,
);

expect(handler.listAccounts).toHaveBeenCalled();
expect(mockKeyringRequest.resolveAccountAddress).toHaveBeenCalledWith(
[mockKeyringAccount1, mockKeyringAccount2],
BtcScope.Regtest,
requestWithoutCommonHeader,
);
expect(result).toStrictEqual({
address: 'bip122:000000000019d6689c085ae165831e93:test123',
});
});

it('returns null on error', async () => {
const request = {
id: '1',
jsonrpc: '2.0' as const,
method: BtcMethod.SignPsbt,
params: {
account: { address: 'notfound' },
psbt: 'psbt',
},
};

jest
.spyOn(handler, 'listAccounts')
.mockImplementation()
.mockResolvedValue([mockKeyringAccount1, mockKeyringAccount2]);
mockKeyringRequest.resolveAccountAddress.mockImplementation(() => {
throw new Error('Account not found');
});

const result = await handler.resolveAccountAddress(
BtcScope.Regtest,
request,
);

expect(result).toBeNull();
});

it('returns null when request validation fails', async () => {
const invalidRequest = {
id: '1',
jsonrpc: '2.0' as const,
method: 'invalid',
params: {},
};

jest
.spyOn(handler, 'listAccounts')
.mockImplementation()
.mockResolvedValue([mockKeyringAccount1, mockKeyringAccount2]);

jest.mocked(assert).mockImplementationOnce(() => {
throw new Error('Invalid request');
});

const result = await handler.resolveAccountAddress(
BtcScope.Regtest,
invalidRequest,
);

expect(result).toBeNull();
});
});
});
55 changes: 53 additions & 2 deletions packages/snap/src/handlers/KeyringHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ListAccountsRequestStruct,
ListAccountTransactionsRequestStruct,
MetaMaskOptionsStruct,
ResolveAccountAddressRequestStruct,
SetSelectedAccountsRequestStruct,
SubmitRequestRequestStruct,
} from '@metamask/keyring-api';
Expand All @@ -29,8 +30,9 @@ import type {
MetaMaskOptions,
DiscoveredAccount,
KeyringRequest,
ResolvedAccountAddress,
} from '@metamask/keyring-api';
import type { Json, JsonRpcRequest } from '@metamask/utils';
import type { CaipChainId, Json, JsonRpcRequest } from '@metamask/utils';
import {
assert,
boolean,
Expand All @@ -54,9 +56,13 @@ import {
caipToAddressType,
scopeToNetwork,
networkToScope,
NetworkStruct,
} from './caip';
import { CronMethod } from './CronHandler';
import type { KeyringRequestHandler } from './KeyringRequestHandler';
import {
BtcWalletRequestStruct,
type KeyringRequestHandler,
} from './KeyringRequestHandler';
import {
mapToDiscoveredAccount,
mapToKeyringAccount,
Expand Down Expand Up @@ -160,6 +166,13 @@ export class KeyringHandler implements Keyring {
await this.setSelectedAccounts(request.params.accounts);
return null;
}
case `${KeyringRpcMethod.ResolveAccountAddress}`: {
assert(request, ResolveAccountAddressRequestStruct);
return this.resolveAccountAddress(
request.params.scope,
request.params.request,
);
}

default: {
throw new InexistentMethodError('Keyring method not supported', {
Expand Down Expand Up @@ -386,6 +399,44 @@ export class KeyringHandler implements Keyring {
});
}

/**
* Resolves the address of an account from a signing request.
*
* This is required by the routing system of MetaMask to dispatch
* incoming non-EVM dapp signing requests.
*
* @param scope - Request's scope (CAIP-2).
* @param request - Signing request object.
* @returns A Promise that resolves to the account address that must
* be used to process this signing request, or null if none candidates
* could be found.
*/
async resolveAccountAddress(
scope: CaipChainId,
request: JsonRpcRequest,
): Promise<ResolvedAccountAddress | null> {
try {
assert(scope, NetworkStruct);
const { method, params } = request;

const requestWithoutCommonHeader = { method, params };
assert(requestWithoutCommonHeader, BtcWalletRequestStruct);

const allAccounts = await this.listAccounts();

const caip10Address = this.#keyringRequest.resolveAccountAddress(
allAccounts,
scope,
requestWithoutCommonHeader,
);

return { address: caip10Address };
} catch (error: unknown) {
this.#logger.error({ error }, 'Error resolving account address');
return null;
}
}

#extractAddressType(path: string): AddressType {
const segments = path.split('/');
if (segments.length < 4) {
Expand Down
Loading