Skip to content

Commit

Permalink
fix: recipient validation with bns field
Browse files Browse the repository at this point in the history
  • Loading branch information
fbwoolf committed Mar 5, 2023
1 parent 95885a2 commit da22011
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 81 deletions.
30 changes: 20 additions & 10 deletions src/app/common/validation/forms/address-validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@ function notCurrentAddressValidatorFactory(currentAddress: string) {

export function notCurrentAddressValidator(currentAddress: string) {
return yup.string().test({
test: notCurrentAddressValidatorFactory(currentAddress),
message: FormErrorMessages.SameAddress,
test: notCurrentAddressValidatorFactory(currentAddress),
});
}

export function btcAddressValidator() {
return yup
.string()
.defined('Enter a bitcoin address')
.defined('Enter a Bitcoin address')
.test((input, context) => {
if (!input) return false;
if (!validate(input))
return context.createError({
message: 'Invalid bitcoin address',
message: 'Invalid Bitcoin address',
});
return true;
});
Expand Down Expand Up @@ -56,19 +56,29 @@ export function btcAddressNetworkValidator(network: NetworkModes) {
});
}

export function stxAddressNetworkValidatorFactory(currentNetwork: NetworkConfiguration) {
function stxAddressNetworkValidatorFactory(currentNetwork: NetworkConfiguration) {
return (value: unknown) => {
if (!isString(value)) return false;
return validateAddressChain(value, currentNetwork);
};
}

export function stxAddressValidator(errorMsg: string) {
export function stxAddressNetworkValidator(network: NetworkConfiguration) {
return yup.string().test({
message: errorMsg,
test(value: unknown) {
if (!isString(value)) return false;
return validateStacksAddress(value);
},
message: FormErrorMessages.IncorrectNetworkAddress,
test: stxAddressNetworkValidatorFactory(network),
});
}

export function stxAddressValidator(errorMsg: string) {
return yup
.string()
.defined('Enter a Stacks address')
.test({
message: errorMsg,
test(value: unknown) {
if (!isString(value)) return false;
return validateStacksAddress(value);
},
});
}
91 changes: 70 additions & 21 deletions src/app/common/validation/forms/recipient-validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,17 @@ import { StacksClient } from '@app/query/stacks/stacks-client';

import {
notCurrentAddressValidator,
stxAddressNetworkValidatorFactory,
stxAddressNetworkValidator,
stxAddressValidator,
} from './address-validators';

// ts-unused-exports:disable-next-line
export function stxRecipientValidator(
currentAddress: string,
currentNetwork: NetworkConfiguration
) {
return stxAddressValidator(FormErrorMessages.InvalidAddress)
.test({
message: FormErrorMessages.IncorrectNetworkAddress,
test: stxAddressNetworkValidatorFactory(currentNetwork),
})
.concat(stxAddressNetworkValidator(currentNetwork))
.concat(notCurrentAddressValidator(currentAddress || ''));
}

Expand All @@ -30,25 +28,76 @@ interface StxRecipientAddressOrBnsNameValidatorArgs {
currentAddress: string;
currentNetwork: NetworkConfiguration;
}
// TODO: This validation is messy, but each individual test is needed
// to ensure the error message returned is correct here, otherwise it
// just returns the invalid address error
export function stxRecipientAddressOrBnsNameValidator({
client,
currentAddress,
currentNetwork,
}: StxRecipientAddressOrBnsNameValidatorArgs) {
return yup.string().test({
message: FormErrorMessages.InvalidAddress,
test: async value => {
try {
await stxRecipientValidator(currentAddress, currentNetwork).validate(value);
return true;
} catch (e) {}
try {
const isTestnet = currentNetwork.chain.stacks.chainId === ChainID.Testnet;
const owner = await fetchNameOwner(client, value ?? '', isTestnet);
return owner !== null;
} catch (e) {
return false;
}
},
});
return (
yup
.string()
.defined('Enter a Stacks address')
// BNS name lookup validation
.test({
message: FormErrorMessages.InvalidAddress,
test: async value => {
if (value?.includes('.')) {
try {
const isTestnet = currentNetwork.chain.stacks.chainId === ChainID.Testnet;
const owner = await fetchNameOwner(client, value ?? '', isTestnet);
return owner !== null;
} catch (e) {
return false;
}
}
return true;
},
})
// Stacks address validations
.test({
message: FormErrorMessages.InvalidAddress,
test: async value => {
if (!value?.includes('.')) {
try {
await stxAddressValidator(currentAddress).validate(value);
return true;
} catch (e) {
return false;
}
}
return true;
},
})
.test({
message: FormErrorMessages.IncorrectNetworkAddress,
test: async value => {
if (!value?.includes('.')) {
try {
await stxAddressNetworkValidator(currentNetwork).validate(value);
return true;
} catch (e) {
return false;
}
}
return true;
},
})
.test({
message: FormErrorMessages.SameAddress,
test: async value => {
if (!value?.includes('.')) {
try {
await notCurrentAddressValidator(currentAddress || '').validate(value);
return true;
} catch (e) {
return false;
}
}
return true;
},
})
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,11 @@ export function FormErrors() {
const [firstError] =
Object.entries(form.errors).filter(omitAmountErrorsAsDisplayedElsewhere) ?? [];

const [message] = firstError ?? [];
const [field, message] = firstError ?? [];

// TODO: This doesn't currently work with how we use two fields
// to handle bns name fetching and validation. The field here
// with the error is not the `firstError` so the input highlights
// as having an error, but doesn't show the message. Replacing with
// checking the entire form for `dirty`.
// const isFirstErrorFieldTouched = (form.touched as any)[field];
const isFirstErrorFieldTouched = (form.touched as any)[field];

return message && form.dirty && shouldDisplayErrors(form) ? (
return message && isFirstErrorFieldTouched && shouldDisplayErrors(form) ? (
<AnimateHeight duration={400} easing="ease-out" height={showHide}>
<Flex height={openHeight + 'px'}>
<ErrorLabel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@ import { useStacksRecipientBnsName } from '../hooks/use-stacks-recipient-bns-nam
import { RecipientFieldBnsAddress } from './recipient-field-bns-address';

export function StacksRecipientField() {
const { getBnsAddress, bnsAddress } = useStacksRecipientBnsName();
const { bnsAddress, getBnsAddress, setBnsAddress } = useStacksRecipientBnsName();
const navigate = useNavigate();

const onClickLabel = () => {
setBnsAddress('');
navigate(RouteUrls.SendCryptoAssetFormRecipientAccounts);
};

return (
<RecipientField
labelAction="Choose account"
name="recipientAddressOrBnsName"
onBlur={getBnsAddress}
onClickLabelAction={() => navigate(RouteUrls.SendCryptoAssetFormRecipientAccounts)}
onClickLabelAction={onClickLabel}
placeholder="Address or name"
topInputOverlay={
!!bnsAddress ? <RecipientFieldBnsAddress bnsAddress={bnsAddress} /> : undefined
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,37 @@
import { useCallback, useState } from 'react';

import { useField } from 'formik';
import { useFormikContext } from 'formik';

import { StacksSendFormValues } from '@shared/models/form.model';

import { fetchNameOwner } from '@app/query/stacks/bns/bns.utils';
import { useStacksClientUnanchored } from '@app/store/common/api-clients.hooks';
import { useCurrentNetworkState } from '@app/store/networks/networks.hooks';

export function useStacksRecipientBnsName() {
const [, _, recipientFieldHelpers] = useField('recipient');
const [recipientAddressOrBnsNameField] = useField('recipientAddressOrBnsName');
const { setFieldValue, validateField, values } = useFormikContext<StacksSendFormValues>();
const [bnsAddress, setBnsAddress] = useState('');
const client = useStacksClientUnanchored();
const { isTestnet } = useCurrentNetworkState();

const getBnsAddress = useCallback(async () => {
setBnsAddress('');
try {
const owner = await fetchNameOwner(
client,
recipientAddressOrBnsNameField.value ?? '',
isTestnet
);
if (owner) {
recipientFieldHelpers.setValue(owner);
setBnsAddress(owner);
} else {
recipientFieldHelpers.setValue(recipientAddressOrBnsNameField.value);
}
return;
} catch {
recipientFieldHelpers.setValue(recipientAddressOrBnsNameField.value);
if (values.recipientAddressOrBnsName.includes('.')) {
try {
const owner = await fetchNameOwner(
client,
values.recipientAddressOrBnsName ?? '',
isTestnet
);
if (owner) {
setBnsAddress(owner);
setFieldValue('resolvedRecipient', owner);
// Only validate if the address is found/set
validateField('resolvedRecipient');
}
} catch (e) {}
}
}, [recipientAddressOrBnsNameField.value, recipientFieldHelpers, isTestnet, client]);
}, [values.recipientAddressOrBnsName, client, isTestnet, setFieldValue, validateField]);

return { getBnsAddress, bnsAddress };
return { bnsAddress, getBnsAddress, setBnsAddress };
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@ import { useWalletType } from '@app/common/use-wallet-type';
import { stacksFungibleTokenAmountValidator } from '@app/common/validation/forms/amount-validators';
import { stxFeeValidator } from '@app/common/validation/forms/fee-validators';
import { stxMemoValidator } from '@app/common/validation/forms/memo-validators';
import {
stxRecipientAddressOrBnsNameValidator,
stxRecipientValidator,
} from '@app/common/validation/forms/recipient-validators';
import { stxRecipientAddressOrBnsNameValidator } from '@app/common/validation/forms/recipient-validators';
import { nonceValidator } from '@app/common/validation/nonce-validators';
import { StxAvatar } from '@app/components/crypto-assets/stacks/components/stx-avatar';
import { EditNonceButton } from '@app/components/edit-nonce-button';
Expand Down Expand Up @@ -96,13 +93,13 @@ export function StacksSip10FungibleTokenSendForm({}) {
memo: '',
nonce: nextNonce?.nonce,
recipientAddressOrBnsName: '',
resolvedRecipient: '',
symbol,
...routeState,
});

const validationSchema = yup.object({
amount: stacksFungibleTokenAmountValidator(availableTokenBalance),
recipient: stxRecipientValidator(currentAccountStxAddress, currentNetwork),
recipientAddressOrBnsName: stxRecipientAddressOrBnsNameValidator({
client,
currentAddress: currentAccountStxAddress,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ import {
} from '@app/common/validation/forms/amount-validators';
import { stxFeeValidator } from '@app/common/validation/forms/fee-validators';
import { stxMemoValidator } from '@app/common/validation/forms/memo-validators';
import {
stxRecipientAddressOrBnsNameValidator,
stxRecipientValidator,
} from '@app/common/validation/forms/recipient-validators';
import { stxRecipientAddressOrBnsNameValidator } from '@app/common/validation/forms/recipient-validators';
import { nonceValidator } from '@app/common/validation/nonce-validators';
import { StxAvatar } from '@app/components/crypto-assets/stacks/components/stx-avatar';
import { EditNonceButton } from '@app/components/edit-nonce-button';
Expand Down Expand Up @@ -100,14 +97,13 @@ export function StxSendForm() {
feeType: FeeTypes[FeeTypes.Unknown],
memo: '',
nonce: nextNonce?.nonce,
recipientAddress: '',
recipientAddressOrBnsName: '',
resolvedRecipient: '',
...routeState,
});

const validationSchema = yup.object({
amount: stxAmountValidator().concat(stxAvailableBalanceValidator(availableStxBalance)),
recipient: stxRecipientValidator(currentAccountStxAddress, currentNetwork),
recipientAddressOrBnsName: stxRecipientAddressOrBnsNameValidator({
client,
currentAddress: currentAccountStxAddress,
Expand Down
1 change: 1 addition & 0 deletions src/shared/models/form.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface StacksSendFormValues {
nonce?: number | string;
recipient: string;
recipientAddressOrBnsName: string;
resolvedRecipient: string;
symbol?: string;
}

Expand Down
17 changes: 8 additions & 9 deletions tests/specs/send/send-stx.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
TEST_BNS_RESOLVED_ADDRESS,
} from '@tests/mocks/constants';
import { FeesSelectors } from '@tests/selectors/fees.selectors';
import { SendCryptoAssetSelectors } from '@tests/selectors/send.selectors';

import { FormErrorMessages } from '@app/common/error-messages';

Expand All @@ -21,15 +20,16 @@ test.describe('send stx', () => {

test.describe('send form input fields', () => {
test('that send max button sets available balance minus fee', async ({ sendPage }) => {
await sendPage.amountInput.fill('1');
await sendPage.amountInput.fill('.0001');
await sendPage.amountInput.clear();
await sendPage.amountInput.blur();
await sendPage.sendMaxButton.click();
await sendPage.recipientInput.fill(TEST_ACCOUNT_2_STX_ADDRESS);
await sendPage.amountInput.blur();
test.expect(await sendPage.amountInput.inputValue()).toBeTruthy();
});

test('that recipient address matches bns name', async ({ page, sendPage }) => {
await sendPage.amountInput.fill('1');
await sendPage.amountInput.fill('.0001');
await sendPage.recipientInput.fill(TEST_BNS_NAME);
await sendPage.recipientInput.blur();
await sendPage.resolvedBnsAddressLabel.waitFor();
Expand Down Expand Up @@ -81,13 +81,12 @@ test.describe('send stx', () => {
test.expect(errorMsg).toContain('Insufficient balance');
});

test('that valid addresses are accepted', async ({ page, sendPage }) => {
await sendPage.amountInput.fill('0.0001');
test('that valid addresses are accepted', async ({ sendPage }) => {
await sendPage.amountInput.fill('0.000001');
await sendPage.recipientInput.fill(TEST_ACCOUNT_2_STX_ADDRESS);
await sendPage.recipientInput.blur();
await sendPage.previewSendTxButton.click();
const detail = page.getByTestId(SendCryptoAssetSelectors.ConfirmationDetailsAmountAndSymbol);
test.expect(await detail.innerText()).toContain('STX');
const details = await sendPage.confirmationDetails.allInnerTexts();
test.expect(details).toBeTruthy();
});

test('that the address must be valid', async ({ sendPage }) => {
Expand Down

0 comments on commit da22011

Please sign in to comment.