From e387351a1a171a2296dc9cf707f29c9bacdf40d0 Mon Sep 17 00:00:00 2001 From: alter-eggo Date: Tue, 16 Apr 2024 18:37:19 +0400 Subject: [PATCH] feat: add send transfer validation --- src/app/common/error-formatters.ts | 2 +- .../bitcoin/use-generate-bitcoin-tx.ts | 4 +- .../validation/forms/address-validators.ts | 2 +- .../validation/forms/amount-validators.ts | 2 +- .../validation/forms/currency-validators.ts | 2 +- .../validation/forms/recipient-validators.ts | 2 +- .../rpc-send-transfer-confirmation.tsx | 2 +- .../hooks/use-send-inscription-form.tsx | 2 +- .../hooks/use-recipient-bns-name.tsx | 2 +- .../stacks/use-stacks-common-send-form.tsx | 2 +- src/app/pages/swap/hooks/use-swap-form.tsx | 2 +- src/{app/common => shared}/error-messages.ts | 0 src/shared/forms/amount-validators.ts | 3 ++ .../forms/bitcoin-address-validators.ts | 53 +++++++++++++++++++ src/shared/rpc/methods/send-transfer.spec.ts | 8 +-- src/shared/rpc/methods/send-transfer.ts | 34 +++++++++--- test-app/src/components/bitcoin.tsx | 28 +++++++++- tests/specs/send/send-stx.spec.ts | 2 +- 18 files changed, 128 insertions(+), 24 deletions(-) rename src/{app/common => shared}/error-messages.ts (100%) create mode 100644 src/shared/forms/amount-validators.ts create mode 100644 src/shared/forms/bitcoin-address-validators.ts diff --git a/src/app/common/error-formatters.ts b/src/app/common/error-formatters.ts index 0ab7b6b8c80..c7d3ef3991c 100644 --- a/src/app/common/error-formatters.ts +++ b/src/app/common/error-formatters.ts @@ -1,7 +1,7 @@ import { Money } from '@shared/models/money.model'; import { isFunction } from '@shared/utils'; -import { FormErrorMessages } from '@app/common/error-messages'; +import { FormErrorMessages } from '@shared/error-messages'; export function formatPrecisionError(num?: Money) { if (!num) return FormErrorMessages.CannotDeterminePrecision; diff --git a/src/app/common/transactions/bitcoin/use-generate-bitcoin-tx.ts b/src/app/common/transactions/bitcoin/use-generate-bitcoin-tx.ts index 0464646633b..0b4a68eacc6 100644 --- a/src/app/common/transactions/bitcoin/use-generate-bitcoin-tx.ts +++ b/src/app/common/transactions/bitcoin/use-generate-bitcoin-tx.ts @@ -156,14 +156,14 @@ export function useGenerateUnsignedNativeSegwitMultipleRecipientsTx() { }); } - outputs.forEach((output, index) => { + outputs.forEach(output => { // When coin selection returns output with no address we assume it is // a change output if (!output.address) { tx.addOutputAddress(signer.address, BigInt(output.value), networkMode); return; } - tx.addOutputAddress(values.recipients[index].address, BigInt(output.value), networkMode); + tx.addOutputAddress(output.address, BigInt(output.value), networkMode); }); return { hex: tx.hex, fee, psbt: tx.toPSBT(), inputs }; diff --git a/src/app/common/validation/forms/address-validators.ts b/src/app/common/validation/forms/address-validators.ts index 6df6b4a8931..f434ae4ad64 100644 --- a/src/app/common/validation/forms/address-validators.ts +++ b/src/app/common/validation/forms/address-validators.ts @@ -4,7 +4,7 @@ import * as yup from 'yup'; import { BitcoinNetworkModes, NetworkConfiguration } from '@shared/constants'; import { isString } from '@shared/utils'; -import { FormErrorMessages } from '@app/common/error-messages'; +import { FormErrorMessages } from '@shared/error-messages'; import { validateAddressChain, validateStacksAddress } from '@app/common/stacks-utils'; function notCurrentAddressValidatorFactory(currentAddress: string) { diff --git a/src/app/common/validation/forms/amount-validators.ts b/src/app/common/validation/forms/amount-validators.ts index 7b5007f94e5..9b897ac9e5c 100644 --- a/src/app/common/validation/forms/amount-validators.ts +++ b/src/app/common/validation/forms/amount-validators.ts @@ -14,7 +14,7 @@ import { import { UtxoResponseItem } from '@app/query/bitcoin/bitcoin-client'; import { formatInsufficientBalanceError, formatPrecisionError } from '../../error-formatters'; -import { FormErrorMessages } from '../../error-messages'; +import { FormErrorMessages } from '../../../../shared/error-messages'; import { currencyAmountValidator, stxAmountPrecisionValidator } from './currency-validators'; const minSpendAmountInSats = 6000; diff --git a/src/app/common/validation/forms/currency-validators.ts b/src/app/common/validation/forms/currency-validators.ts index 9c20bd40e87..f9749737adf 100644 --- a/src/app/common/validation/forms/currency-validators.ts +++ b/src/app/common/validation/forms/currency-validators.ts @@ -3,7 +3,7 @@ import * as yup from 'yup'; import { BTC_DECIMALS, STX_DECIMALS } from '@shared/constants'; import { isNumber } from '@shared/utils'; -import { FormErrorMessages } from '@app/common/error-messages'; +import { FormErrorMessages } from '@shared/error-messages'; import { countDecimals } from '@app/common/math/helpers'; export function currencyAmountValidator() { diff --git a/src/app/common/validation/forms/recipient-validators.ts b/src/app/common/validation/forms/recipient-validators.ts index 04f5074ce53..cdba90a9f57 100644 --- a/src/app/common/validation/forms/recipient-validators.ts +++ b/src/app/common/validation/forms/recipient-validators.ts @@ -1,7 +1,7 @@ import { NetworkConfiguration } from '@shared/constants'; import { stacksChainIdToCoreNetworkMode } from '@shared/crypto/stacks/stacks.utils'; -import { FormErrorMessages } from '@app/common/error-messages'; +import { FormErrorMessages } from '@shared/error-messages'; import { notCurrentAddressValidator, diff --git a/src/app/pages/rpc-send-transfer/rpc-send-transfer-confirmation.tsx b/src/app/pages/rpc-send-transfer/rpc-send-transfer-confirmation.tsx index cf534c2cebe..0943694a3e9 100644 --- a/src/app/pages/rpc-send-transfer/rpc-send-transfer-confirmation.tsx +++ b/src/app/pages/rpc-send-transfer/rpc-send-transfer-confirmation.tsx @@ -121,7 +121,7 @@ export function RpcSendTransferConfirmation() { return ( <> - + {recipients.map((recipient, index) => ( { + if (!input) return false; + if (!validate(input)) + return context.createError({ + message: FormErrorMessages.InvalidAddress, + }); + return true; + }); +} + +// ts-unused-exports:disable-next-line +export function btcTaprootAddressValidator() { + return yup.string().test((input, context) => { + if (!input || !validate(input)) return false; + if (getAddressInfo(input).type !== AddressType.p2tr) + return context.createError({ + message: 'Only taproot addresses are supported', + }); + return true; + }); +} + +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 (!isString(value)) return false; + return validate(value, getAddressNetworkType(network)); + }; +} + +export function btcAddressNetworkValidator(network: BitcoinNetworkModes) { + return yup.string().test({ + test: btcAddressNetworkValidatorFactory(network), + message: FormErrorMessages.IncorrectNetworkAddress, + }); +} diff --git a/src/shared/rpc/methods/send-transfer.spec.ts b/src/shared/rpc/methods/send-transfer.spec.ts index 48e41d0f54a..19c5883c474 100644 --- a/src/shared/rpc/methods/send-transfer.spec.ts +++ b/src/shared/rpc/methods/send-transfer.spec.ts @@ -24,11 +24,11 @@ describe('`sendTransfer` method', () => { recipients: [ { address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', - amount: '0.0001', + amount: '10000', }, { address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', - amount: '0.0001', + amount: '800', }, ], }; @@ -50,7 +50,7 @@ describe('`sendTransfer` method', () => { network: 'mainnet', account: 0, address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', - amount: '0.0001', + amount: '10000', }; const newParams = { @@ -59,7 +59,7 @@ describe('`sendTransfer` method', () => { recipients: [ { address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', - amount: '0.0001', + amount: '10000', }, ], }; diff --git a/src/shared/rpc/methods/send-transfer.ts b/src/shared/rpc/methods/send-transfer.ts index af50c7df5de..21d94b30430 100644 --- a/src/shared/rpc/methods/send-transfer.ts +++ b/src/shared/rpc/methods/send-transfer.ts @@ -1,7 +1,13 @@ import type { SendTransferRequestParams } from '@btckit/types'; import * as yup from 'yup'; -import { WalletDefaultNetworkConfigurationIds } from '@shared/constants'; +import { type BitcoinNetworkModes, WalletDefaultNetworkConfigurationIds } from '@shared/constants'; +import { FormErrorMessages } from '@shared/error-messages'; +import { checkIfDigitsOnly } from '@shared/forms/amount-validators'; +import { + btcAddressNetworkValidator, + btcAddressValidator, +} from '@shared/forms/bitcoin-address-validators'; import { accountSchema, @@ -19,11 +25,27 @@ export const rpcSendTransferParamsSchemaLegacy = yup.object().shape({ export const rpcSendTransferParamsSchema = yup.object().shape({ account: accountSchema, - recipients: yup - .array() - .of(yup.object().shape({ address: yup.string().required(), amount: yup.string().required() })) - .required(), - network: yup.string().oneOf(Object.values(WalletDefaultNetworkConfigurationIds)), + network: yup.string().required().oneOf(Object.values(WalletDefaultNetworkConfigurationIds)), + recipients: yup.array().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; + return btcAddressNetworkValidator(network).isValidSync(value); + } + ), + amount: yup + .string() + .required() + .test('amount-validation', 'Sat denominated amounts only', value => { + return checkIfDigitsOnly(value); + }), + }) + ), }); export interface RpcSendTransferParamsLegacy extends SendTransferRequestParams { diff --git a/test-app/src/components/bitcoin.tsx b/test-app/src/components/bitcoin.tsx index c0230cf617b..4d0a8aef8fd 100644 --- a/test-app/src/components/bitcoin.tsx +++ b/test-app/src/components/bitcoin.tsx @@ -417,7 +417,7 @@ export const Bitcoin = () => { }, { address: TEST_TESTNET_ACCOUNT_2_BTC_ADDRESS, - amount: '10000', + amount: '0.010000', }, ], network: 'testnet', @@ -432,6 +432,32 @@ export const Bitcoin = () => { > Send transfer to multiple addresses + { + console.log('requesting'); + (window as any).LeatherProvider?.request('sendTransfer', { + recipients: [ + { + address: TEST_TESTNET_ACCOUNT_2_BTC_ADDRESS, + amount: '10000', + }, + { + address: TEST_TESTNET_ACCOUNT_2_BTC_ADDRESS, + amount: '10000', + }, + ], + }) + .then((resp: any) => { + console.log({ sucesss: resp }); + }) + .catch((error: Error) => { + console.log({ error }); + }); + }} + > + Send transfer validate error + ); }; diff --git a/tests/specs/send/send-stx.spec.ts b/tests/specs/send/send-stx.spec.ts index 8b2fd1673ec..823ad847a49 100644 --- a/tests/specs/send/send-stx.spec.ts +++ b/tests/specs/send/send-stx.spec.ts @@ -9,7 +9,7 @@ import { getDisplayerAddress } from '@tests/utils'; import { STX_DECIMALS } from '@shared/constants'; -import { FormErrorMessages } from '@app/common/error-messages'; +import { FormErrorMessages } from '@shared/error-messages'; import { test } from '../../fixtures/fixtures';