Skip to content

Commit

Permalink
Merge pull request #5970 from leather-io/refactor/yup-zod
Browse files Browse the repository at this point in the history
refactor: use zod for rpc validation
  • Loading branch information
kyranjamie authored Nov 18, 2024
2 parents 490d5c7 + c38725a commit e017e56
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 101 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@
"webextension-polyfill": "0.12.0",
"yup": "1.4.0",
"zod": "3.23.8",
"zod-validation-error": "3.4.0",
"zxcvbn": "4.4.2"
},
"devDependencies": {
Expand Down
44 changes: 38 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions src/shared/forms/address-validators.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Network, validate } from 'bitcoin-address-validation';
import { Network, getAddressInfo, validate } from 'bitcoin-address-validation';
import * as yup from 'yup';

import type { BitcoinNetworkModes } from '@leather.io/models';
Expand All @@ -23,14 +23,17 @@ export function btcAddressValidator() {
});
}

export function getNetworkTypeFromAddress(address: string) {
return getAddressInfo(address).network as BitcoinNetworkModes;
}

function btcAddressNetworkValidatorFactory(network: BitcoinNetworkModes) {
function getAddressNetworkType(network: BitcoinNetworkModes): Network {
// Signet uses testnet address format, this parsing is to please the
// validation library
if (network === 'signet') return Network.testnet;
return network as Network;
}

return (value?: string) => {
if (isUndefined(value) || isEmptyString(value)) return true;
return validate(value, getAddressNetworkType(network));
Expand Down
42 changes: 39 additions & 3 deletions src/shared/rpc/methods/send-transfer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('`sendTransfer` method', () => {
amount: '10000',
};

expect(rpcSendTransferParamsSchemaLegacy.isValidSync(params)).toBeTruthy();
expect(rpcSendTransferParamsSchemaLegacy.safeParse(params).success).toEqual(true);
});

test('that it validates multiple recipient sends', () => {
Expand All @@ -33,7 +33,7 @@ describe('`sendTransfer` method', () => {
],
};

expect(rpcSendTransferParamsSchema.isValidSync(params)).toBeTruthy();
expect(rpcSendTransferParamsSchema.safeParse(params).success).toEqual(true);
});

test('that it fails validation for missing required fields', () => {
Expand All @@ -42,7 +42,7 @@ describe('`sendTransfer` method', () => {
account: 0,
};

expect(() => rpcSendTransferParamsSchema.validateSync(params)).toThrow();
expect(() => rpcSendTransferParamsSchema.parse(params)).toThrow();
});

test('that it converts legacy params to new params', () => {
Expand All @@ -67,4 +67,40 @@ describe('`sendTransfer` method', () => {
expect(convertRpcSendTransferLegacyParamsToNew(legacyParams)).toEqual(newParams);
});
});

test('that it fails if addresses do not match the defined network', () => {
const result = rpcSendTransferParamsSchema.safeParse({
network: 'mainnet',
account: 0,
recipients: [
{
// Expected to fail because we've passed a testnet address
address: 'tb1pjqhc5xw5xuhhg3zvk5p545q055e4farx37pvwnj2plt9t8lezvys0n58yz',
amount: '10000',
},
],
});
expect(result.success).toEqual(false);
});

test('that it fails when addresses of different networks are passed', () => {
const result = rpcSendTransferParamsSchema.safeParse({
network: 'mainnet',
account: 0,
recipients: [
{
address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq',
amount: '100',
},
{
address: 'tb1pjqhc5xw5xuhhg3zvk5p545q055e4farx37pvwnj2plt9t8lezvys0n58yz',
amount: '100',
},
],
});
expect(result.error?.issues.map(issue => issue.message)).toContain(
'Cannot tranfer to addresses of different networks'
);
expect(result.success).toEqual(false);
});
});
83 changes: 48 additions & 35 deletions src/shared/rpc/methods/send-transfer.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import type { SendTransferRequestParams } from '@btckit/types';
import * as yup from 'yup';
import { z } from 'zod';

import { type BitcoinNetworkModes, WalletDefaultNetworkConfigurationIds } from '@leather.io/models';
import { uniqueArray } from '@leather.io/utils';

import { FormErrorMessages } from '@shared/error-messages';
import { btcAddressNetworkValidator, btcAddressValidator } from '@shared/forms/address-validators';
import {
btcAddressNetworkValidator,
getNetworkTypeFromAddress,
} from '@shared/forms/address-validators';
import { checkIfDigitsOnly } from '@shared/forms/amount-validators';

import {
Expand All @@ -16,42 +20,51 @@ import {

export const defaultRpcSendTransferNetwork = 'mainnet';

export const rpcSendTransferParamsSchemaLegacy = yup.object().shape({
account: accountSchema,
address: yup.string().required(),
amount: yup.string().required(),
network: yup.string().oneOf(Object.values(WalletDefaultNetworkConfigurationIds)),
export const rpcSendTransferParamsSchemaLegacy = z.object({
account: accountSchema.optional(),
address: z.string(),
amount: z.string(),
network: z
.enum(Object.values(WalletDefaultNetworkConfigurationIds) as [string, ...string[]])
.optional(),
});

export const rpcSendTransferParamsSchema = yup.object().shape({
account: accountSchema,
network: yup.string().oneOf(Object.values(WalletDefaultNetworkConfigurationIds)),
recipients: yup
.array()
.required()
.of(
yup.object().shape({
// check network is valid for address
address: btcAddressValidator().test(
'address-network-validation',
FormErrorMessages.IncorrectNetworkAddress,
(value, context) => {
const contextOptions = context.options as any;
const network =
(contextOptions.from[1].value.network as BitcoinNetworkModes) ||
defaultRpcSendTransferNetwork;
return btcAddressNetworkValidator(network).isValidSync(value);
}
),
amount: yup
.string()
.required()
.test('amount-validation', 'Sat denominated amounts only', value => {
return checkIfDigitsOnly(value);
export const rpcSendTransferParamsSchema = z
.object({
account: accountSchema.optional(),
network: z
.enum(Object.values(WalletDefaultNetworkConfigurationIds) as [string, ...string[]])
.optional(),
recipients: z
.array(
z.object({
address: z.string(),
amount: z.string().refine(value => checkIfDigitsOnly(value), {
message: 'Sat denominated amounts only',
}),
})
),
});
})
)
.nonempty()
.refine(
recipients => {
const inferredNetworksByAddress = recipients.map(({ address }) =>
getNetworkTypeFromAddress(address)
);
return uniqueArray(inferredNetworksByAddress).length === 1;
},
{ message: 'Cannot tranfer to addresses of different networks', path: ['recipients'] }
),
})
.refine(
({ network, recipients }) => {
const addressNetworks = recipients.map(recipient =>
btcAddressNetworkValidator(network as BitcoinNetworkModes).isValidSync(recipient.address)
);

return !addressNetworks.some(val => val === false);
},
{ message: FormErrorMessages.IncorrectNetworkAddress, path: ['recipients'] }
);

export interface RpcSendTransferParamsLegacy extends SendTransferRequestParams {
network: string;
Expand Down
22 changes: 10 additions & 12 deletions src/shared/rpc/methods/sign-message.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PaymentTypes } from '@btckit/types';
import * as yup from 'yup';
import { z } from 'zod';

import { WalletDefaultNetworkConfigurationIds } from '@leather.io/models';
import type { PaymentTypes } from '@leather.io/rpc';

import {
accountSchema,
Expand All @@ -10,18 +10,16 @@ import {
validateRpcParams,
} from './validation.utils';

// TODO: Import Bip322MessageTypes from btckit when updated
type SupportedBip322MessageTypes = 'bip322';

const rpcSignMessageParamsSchema = yup.object().shape({
type: yup.string<SupportedBip322MessageTypes>(),
account: accountSchema,
message: yup.string().required(),
network: yup.string().oneOf(Object.values(WalletDefaultNetworkConfigurationIds)),
paymentType: yup.string<PaymentTypes>(),
const rpcSignMessageParamsSchema = z.object({
type: z.enum(['bip322']).optional(),
account: accountSchema.optional(),
message: z.string(),
network: z
.enum(Object.values(WalletDefaultNetworkConfigurationIds) as [string, ...string[]])
.optional(),
paymentType: z.enum(['p2tr', 'p2wpkh'] as [PaymentTypes, PaymentTypes]).optional(),
});

// TODO: Import param types from btckit when updated
export function validateRpcSignMessageParams(obj: unknown) {
return validateRpcParams(obj, rpcSignMessageParamsSchema);
}
Expand Down
Loading

0 comments on commit e017e56

Please sign in to comment.