Skip to content
Draft
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
4 changes: 4 additions & 0 deletions packages/bitcoin-wallet-snap/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- Use the shared unified send flow router, method names, error codes, and validation response type from `@metamask/snap-networks-utils` for `onAddressInput`, `onAmountInput`, `confirmSend`, and `computeFee` ([#556](https://github.com/MetaMask/internal-snaps/pull/556))

## [3.3.0]

### Changed
Expand Down
8 changes: 4 additions & 4 deletions packages/bitcoin-wallet-snap/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ module.exports = {
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 77.33,
functions: 66.76,
lines: 84.81,
statements: 84.58,
branches: 78.49,
functions: 68.83,
lines: 85.49,
statements: 85.24,
},
},
};
2 changes: 1 addition & 1 deletion packages/bitcoin-wallet-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/internal-snaps.git"
},
"source": {
"shasum": "gflKiopn6Pqmvn4qBx7ernV56ESlo6TUhNwwQJfCbOI=",
"shasum": "iG0ppVQP9bp12wraD+yPov6ofWJYGx0Q0K5Y3B5qNzM=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { InvalidInputError } from '@metamask/snaps-sdk';
import type { GetPreferencesResult } from '@metamask/snaps-sdk';
import { create, object, string } from '@metamask/superstruct';
import { mock } from 'jest-mock-extended';

import { BaseError, ExternalServiceError } from '../entities';
Expand Down Expand Up @@ -103,6 +105,20 @@ describe('HandlerMiddleware', () => {
expect(mockTrackError).toHaveBeenCalledWith(thrown);
});

it('wraps a @metamask/superstruct StructError as an invalid input error', async () => {
let error: Error | undefined;
try {
create({}, object({ value: string() }));
} catch (caught) {
error = caught as Error;
}

expect(error).toBeDefined();
await expect(
middleware.handle(jest.fn().mockRejectedValue(error)),
).rejects.toThrow(InvalidInputError);
});

it('handles error successfully if instance of BaseError', async () => {
const error = new BaseError('Test error', 1);
const mockFn = jest.fn().mockRejectedValue(error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
SnapError,
getErrorMessage,
} from '@metamask/snaps-sdk';
import { StructError as MetamaskStructError } from '@metamask/superstruct';
import { StructError } from 'superstruct';

import type { Translator, Logger, SnapClient } from '../entities';
Expand Down Expand Up @@ -107,7 +108,10 @@ export class HandlerMiddleware {
throw new InternalError(errMsg, error.data);
}
} else {
if (error instanceof StructError) {
if (
error instanceof StructError ||
error instanceof MetamaskStructError
) {
const errMsg = messages['error.0']?.message ?? 'Invalid format';
throw new InvalidInputError(
`${errMsg}: ${error.message}`,
Expand Down
120 changes: 84 additions & 36 deletions packages/bitcoin-wallet-snap/src/handlers/RpcHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,22 @@ import {
DEFAULT_METAMASK_ORIGIN,
normalizeError,
resolveFlowTransactionType,
SendFlowRequestRouter,
UuidStruct,
createSendFlowStructs,
} from '@metamask/snap-networks-utils';
import type {
ProofOfOwnershipBatchRequestItem,
ProofOfOwnershipBatchResponse,
} from '@metamask/snap-networks-utils';
import type { Json, JsonRpcRequest } from '@metamask/snaps-sdk';
import {
enums as enumsV3,
object as objectV3,
refine as refineV3,
string as stringV3,
} from '@metamask/superstruct';
import { CaipAssetTypeStruct, parseCaipAssetType } from '@metamask/utils';
import type { CaipAssetType } from '@metamask/utils';
import { Verifier } from 'bip322-js';
import {
Expand Down Expand Up @@ -39,18 +49,10 @@ import { scopeToNetwork } from './caip';
import type { TransactionFee } from './mappings';
import { mapPsbtToTransaction, mapToTransactionFees } from './mappings';
import { parsePsbt } from './parsers';
import type {
ConfirmSendRequest,
OnAddressInputRequest,
OnAmountInputRequest,
} from './types';
import type { ValidationResponse } from './validation';
import {
NO_ERRORS_RESPONSE,
INVALID_RESPONSE,
ConfirmSendRequestStruct,
OnAddressInputRequestStruct,
OnAmountInputRequestStruct,
RpcMethod,
SendErrorCodes,
validateAmount,
Expand Down Expand Up @@ -139,13 +141,58 @@ function isSignProofOfOwnershipMessageBatchError(
return Object.hasOwn(signedMessage, 'error');
}

const BitcoinCaipAssetTypeStruct = refineV3(
CaipAssetTypeStruct,
'bitcoin-caip-asset-id',
(assetId) => parseCaipAssetType(assetId).chainId.startsWith('bip122:'),
);

const bitcoinSendFlowStructs = createSendFlowStructs({
// Bitcoin validates amounts and addresses inside the handlers so it can
// return an invalid response instead of an invalid params error.
address: stringV3(),
amount: stringV3(),
assetId: BitcoinCaipAssetTypeStruct,
overrides: {
OnAddressInputParams: objectV3({
value: stringV3(),
accountId: UuidStruct,
}),
ComputeFeeParams: objectV3({
accountId: stringV3(),
transaction: stringV3(),
scope: enumsV3(Object.values(BtcScope)),
}),
},
});

type OnAddressInputParams = { value: string; accountId: string };
type OnAmountInputParams = {
value: string;
accountId: string;
assetId: CaipAssetType;
};
type ConfirmSendParams = {
fromAccountId: string;
toAddress: string;
amount: string;
assetId: CaipAssetType;
};
type ComputeFeeParams = {
accountId: string;
transaction: string;
scope: BtcScope;
};

export class RpcHandler {
readonly #logger: Logger;

readonly #sendFlowUseCases: SendFlowUseCases;

readonly #accountUseCases: AccountUseCases;

readonly #sendFlowRouter: SendFlowRequestRouter;

constructor(
sendFlow: SendFlowUseCases,
accounts: AccountUseCases,
Expand All @@ -154,6 +201,22 @@ export class RpcHandler {
this.#logger = logger;
this.#sendFlowUseCases = sendFlow;
this.#accountUseCases = accounts;
this.#sendFlowRouter = new SendFlowRequestRouter({
structs: bitcoinSendFlowStructs,
// Preserve the existing Bitcoin behaviour of surfacing the raw request
// validation error with its format details.
createInvalidParamsError: (error): Error => error as Error,
handlers: {
onAddressInput: async (params): Promise<Json> =>
this.#onAddressInput(params as OnAddressInputParams),
onAmountInput: async (params): Promise<Json> =>
this.#onAmountInput(params as OnAmountInputParams),
confirmSend: async (params): Promise<Json> =>
this.#confirmSend(params as ConfirmSendParams),
computeFee: async (params): Promise<Json> =>
this.#computeFee(params as ComputeFeeParams),
},
});
}

async route(origin: string, request: JsonRpcRequest): Promise<Json> {
Expand All @@ -162,6 +225,11 @@ export class RpcHandler {
throw new FormatError('Missing params');
}

const sendFlowResponse = await this.#sendFlowRouter.handle(request);
if (sendFlowResponse !== undefined) {
return sendFlowResponse;
}

switch (method as RpcMethod) {
case RpcMethod.StartSendTransactionFlow: {
assert(params, CreateSendFormRequest);
Expand All @@ -176,26 +244,6 @@ export class RpcHandler {
params.options,
);
}
case RpcMethod.ComputeFee: {
assert(params, ComputeFeeRequest);
return this.#computeFee(
params.accountId,
params.transaction,
params.scope,
);
}
case RpcMethod.OnAddressInput: {
assert(params, OnAddressInputRequestStruct);
return this.#onAddressInput(params);
}
case RpcMethod.OnAmountInput: {
assert(params, OnAmountInputRequestStruct);
return this.#onAmountInput(params);
}
case RpcMethod.ConfirmSend: {
assert(params, ConfirmSendRequestStruct);
return await this.#confirmSend(params);
}
case RpcMethod.VerifyMessage: {
assert(params, VerifyMessageRequest);
return this.#verifyMessage(
Expand Down Expand Up @@ -299,19 +347,19 @@ export class RpcHandler {
return { transactionId: txid.toString(), canBeMalleable };
}

async #computeFee(
accountId: string,
transaction: string,
scope: BtcScope,
): Promise<TransactionFee[]> {
async #computeFee({
accountId,
transaction,
scope,
}: ComputeFeeParams): Promise<TransactionFee[]> {
const psbt = parsePsbt(transaction);
const amount = await this.#accountUseCases.computeFee(accountId, psbt);

return [mapToTransactionFees(amount, scopeToNetwork[scope])];
}

async #onAddressInput(
request: OnAddressInputRequest,
request: OnAddressInputParams,
): Promise<ValidationResponse> {
const { value, accountId } = request;

Expand All @@ -334,7 +382,7 @@ export class RpcHandler {
}

async #onAmountInput(
request: OnAmountInputRequest,
request: OnAmountInputParams,
): Promise<ValidationResponse> {
const { value, accountId } = request;

Expand Down Expand Up @@ -380,7 +428,7 @@ export class RpcHandler {
}
}

async #confirmSend(request: ConfirmSendRequest): Promise<Json> {
async #confirmSend(request: ConfirmSendParams): Promise<Json> {
try {
const account = await this.#accountUseCases.get(request.fromAccountId);

Expand Down
17 changes: 7 additions & 10 deletions packages/bitcoin-wallet-snap/src/handlers/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Address, Amount } from '@metamask/bitcoindevkit';
import { BtcMethod } from '@metamask/keyring-api';
import {
parseProofOfOwnershipMessage as parseSharedProofOfOwnershipMessage,
SendFlowErrorCodes,
SendFlowMethod,
UuidStruct,
} from '@metamask/snap-networks-utils';
import type { ProofOfOwnershipMessage } from '@metamask/snap-networks-utils';
Expand All @@ -28,13 +30,13 @@ import type { BitcoinAccount, CodifiedError, Logger } from '../entities';
import { ValidationError } from '../entities';

export const RpcMethod = {
OnAddressInput: SendFlowMethod.OnAddressInput,
OnAmountInput: SendFlowMethod.OnAmountInput,
ConfirmSend: SendFlowMethod.ConfirmSend,
ComputeFee: SendFlowMethod.ComputeFee,
StartSendTransactionFlow: 'startSendTransactionFlow',
SignAndSendTransaction: 'signAndSendTransaction',
ComputeFee: 'computeFee',
VerifyMessage: 'verifyMessage',
OnAddressInput: 'onAddressInput',
OnAmountInput: 'onAmountInput',
ConfirmSend: 'confirmSend',
SignRewardsMessage: 'signRewardsMessage',
SignProofOfOwnership: 'signProofOfOwnership',
/**
Expand All @@ -46,12 +48,7 @@ export const RpcMethod = {

export type RpcMethod = (typeof RpcMethod)[keyof typeof RpcMethod];

export const SendErrorCodes = {
Required: 'Required',
Invalid: 'Invalid',
InsufficientBalance: 'InsufficientBalance',
InsufficientBalanceToCoverFee: 'InsufficientBalanceToCoverFee',
} as const;
export const SendErrorCodes = SendFlowErrorCodes;

export type SendErrorCodes =
(typeof SendErrorCodes)[keyof typeof SendErrorCodes];
Expand Down
4 changes: 4 additions & 0 deletions packages/snap-networks-utils/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add the shared unified send flow helpers: `SendFlowMethod`, `SendFlowErrorCodes`, the validation and max-sendable response structs, `createSendFlowStructs`, and `SendFlowRequestRouter`. The router owns method dispatch, request validation, and validation-response validation while each network snap supplies chain-specific CAIP-19 asset, address, amount, and transaction structs ([#556](https://github.com/MetaMask/internal-snaps/pull/556))

## [3.2.0]

### Added
Expand Down
28 changes: 28 additions & 0 deletions packages/snap-networks-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,31 @@ export type {
TrackErrorCapableProvider,
TrackErrorFn,
} from './utils/errors';
export {
PositiveNumberStringStruct,
Base64Struct,
SendFlowValidationResponseStruct,
SendFlowGetMaxSendableAmountResponseStruct,
createSendFlowStructs,
} from './utils/sendFlow/structs';
export type {
SendFlowStructs,
SendFlowParamStructOverrides,
CreateSendFlowStructsOptions,
} from './utils/sendFlow/structs';
export {
SendFlowRequestRouter,
isSendFlowMethod,
} from './utils/sendFlow/router';
export { SendFlowMethod, SendFlowErrorCodes } from './utils/sendFlow/types';
export type {
SendFlowErrorCode,
SendFlowValidationResponse,
SendFlowGetMaxSendableAmountResponse,
SendFlowHandlers,
SendFlowOnAddressInputParams,
SendFlowOnAmountInputParams,
SendFlowGetMaxSendableAmountParams,
SendFlowConfirmSendParams,
SendFlowComputeFeeParams,
} from './utils/sendFlow/types';
Loading
Loading