Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 55 additions & 6 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 All @@ -579,7 +623,7 @@ describe('KeyringRequestHandler', () => {

describe('sendTransfer', () => {
it('sends funds successfully', async () => {
const response = await snap.onKeyringRequest({
const response = snap.onKeyringRequest({
origin: ORIGIN,
method: submitRequestMethod,
params: {
Expand All @@ -590,23 +634,26 @@ describe('KeyringRequestHandler', () => {
request: {
method: AccountCapability.SendTransfer,
params: {
account: { address: account.address },
recipients: [
{
address: 'bcrt1qstku2y3pfh9av50lxj55arm8r5gj8tf2yv5nxz',
amount: '1000',
},
{
address: 'bcrt1q4gfcga7jfjmm02zpvrh4ttc5k7lmnq2re52z2y',
amount: '1000',
},
],
feeRate: 3,
},
},
} as KeyringRequest,
});

expect(response).toRespondWith({
const ui = await response.getInterface();
assertIsConfirmationDialog(ui);
await ui.ok();

const result = await response;

expect(result).toRespondWith({
pending: false,
result: {
txid: expect.any(String),
Expand All @@ -626,6 +673,7 @@ describe('KeyringRequestHandler', () => {
request: {
method: AccountCapability.SendTransfer,
params: {
account: { address: account.address },
recipients: [{ address: 'notAnAddress', amount: '1000' }],
},
},
Expand Down Expand Up @@ -654,6 +702,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": "o9ZJmt7WEmIOXnmPlqmqRbGWztcCkDTKkW2VaBza56E=",
"shasum": "Uc/YZtf+LHxVSweUBVFhmwohUjDJtJcacvsahFJsvds=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
19 changes: 18 additions & 1 deletion packages/snap/src/entities/confirmation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Network } from '@metamask/bitcoindevkit';
import type { Network, Psbt } from '@metamask/bitcoindevkit';

import type { BitcoinAccount } from './account';

Expand Down Expand Up @@ -33,4 +33,21 @@ export type ConfirmationRepository = {
message: string,
origin: string,
): Promise<void>;

/**
* Inserts a send transfer confirmation interface.
*
* @param account - The account sending the transfer.
* @param psbt - The PSBT of the transfer.
* @param recipient - The recipient of the transfer.
* @param recipient.address - The address of the recipient.
* @param recipient.amount - The amount to send to the recipient.
* @param origin - The origin of the request.
*/
insertSendTransfer(
account: BitcoinAccount,
psbt: Psbt,
recipient: { address: string; amount: string },
origin: string,
): Promise<void>;
};
1 change: 1 addition & 0 deletions packages/snap/src/entities/send-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type ConfirmSendFormContext = {
backgroundEventId?: string;
locale: string;
psbt: string;
origin?: string;
};

export type SendFormContext = {
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();
});
});
});
Loading
Loading