Skip to content

Commit

Permalink
validation
Browse files Browse the repository at this point in the history
  • Loading branch information
grod220 committed Mar 19, 2024
1 parent 81cad90 commit 6af9a2c
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 28 deletions.
40 changes: 13 additions & 27 deletions apps/minifront/src/components/ibc/ibc-out-form.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,12 @@
import { Button, Card, Input } from '@penumbra-zone/ui';
import { ChainSelector } from './chain-selector';
import { useMemo } from 'react';
import { useLoaderData } from 'react-router-dom';
import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb';
import { useStore } from '../../state';
import { ibcSelector } from '../../state/ibc';
import { filterBalancesPerChain, ibcSelector, ibcValidationErrors } from '../../state/ibc';
import InputToken from '../shared/input-token';
import { InputBlock } from '../shared/input-block';
import { validateAmount } from '../../state/send';
import { IbcLoaderResponse } from './ibc-loader';

interface IbcValidationFields {
recipientErr: boolean;
amountErr: boolean;
}

const ibcValidationErrors = (
asset: BalancesResponse | undefined,
amount: string,
recipient: string,
): IbcValidationFields => {
return {
recipientErr: !recipient, // TODO: validate recipient addr matches chain
amountErr: !asset ? false : validateAmount(asset, amount),
};
};

export const IbcOutForm = () => {
const assetBalances = useLoaderData() as IbcLoaderResponse;
const {
Expand All @@ -36,11 +17,10 @@ export const IbcOutForm = () => {
setAmount,
selection,
setSelection,
chain,
} = useStore(ibcSelector);

const validationErrors = useMemo(() => {
return ibcValidationErrors(selection, amount, destinationChainAddress);
}, [selection, amount, destinationChainAddress]);
const filteredBalances = filterBalancesPerChain(assetBalances, chain);
const validationErrors = useStore(ibcValidationErrors);

return (
<Card gradient className='md:p-5'>
Expand All @@ -51,6 +31,7 @@ export const IbcOutForm = () => {
void sendIbcWithdraw();
}}
>
<ChainSelector />
<InputToken
label='Amount to send'
placeholder='Enter an amount'
Expand All @@ -69,14 +50,19 @@ export const IbcOutForm = () => {
checkFn: () => validationErrors.amountErr,
},
]}
balances={assetBalances}
balances={filteredBalances}
/>
<ChainSelector />
<InputBlock
label='Recipient on destination chain'
className='mb-1'
value={destinationChainAddress}
validations={[]}
validations={[
{
type: 'error',
issue: 'address not valid',
checkFn: () => validationErrors.recipientErr,
},
]}
>
<Input
variant='transparent'
Expand Down
41 changes: 41 additions & 0 deletions apps/minifront/src/state/ibc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import { getAddressIndex } from '@penumbra-zone/getters/src/address-view';
import { typeRegistry } from '@penumbra-zone/types/src/registry';
import { toBaseUnit } from '@penumbra-zone/types/src/lo-hi';
import { planBuildBroadcast } from './helpers';
import { validateAmount } from './send';
import { IbcLoaderResponse } from '../components/ibc/ibc-loader';
import { getAssetId } from '@penumbra-zone/getters/src/metadata';
import { STAKING_TOKEN_METADATA } from '@penumbra-zone/constants/src/assets';
import { bech32IsValid } from '@penumbra-zone/bech32';

export interface IbcSendSlice {
selection: BalancesResponse | undefined;
Expand Down Expand Up @@ -141,3 +146,39 @@ const getPlanRequest = async ({
};

export const ibcSelector = (state: AllSlices) => state.ibc;

export const ibcValidationErrors = (state: AllSlices) => {
return {
recipientErr: !state.ibc.destinationChainAddress
? false
: !validateAddress(state.ibc.chain, state.ibc.destinationChainAddress),
amountErr: !state.ibc.selection ? false : validateAmount(state.ibc.selection, state.ibc.amount),
};
};

const validateAddress = (chain: Chain | undefined, address: string): boolean => {
if (!chain || address === '') return false;
return bech32IsValid(address, chain.addressPrefix);
};

/**
* Filters the given IBC loader response balances by checking if any of the assets
* in the balance view match the staking token's asset ID or any of the native assets
* of the specified chain.
*
* Until unwind support is implemented (https://github.com/penumbra-zone/web/issues/344),
* we need to ensure only native assets are sent out.
*/
export const filterBalancesPerChain = (
allBalances: IbcLoaderResponse,
chain: Chain | undefined,
): BalancesResponse[] => {
const penumbraAssetId = getAssetId(STAKING_TOKEN_METADATA);
const remoteChainNativeAssets = chain?.nativeAssets ?? [];
const assetIdsToCheck = [penumbraAssetId, ...remoteChainNativeAssets];

return allBalances.filter(({ balanceView }) => {
const metadata = getMetadata(balanceView);
return assetIdsToCheck.some(assetId => assetId.equals(metadata.penumbraAssetId));
});
};
1 change: 1 addition & 0 deletions packages/bech32/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './address';
export * from './asset';
export * from './identity-key';
export * from './penumbra-bech32';
export * from './validate';

/*
export const PENUMBRA_BECH32_ADDRESS = /^penumbra1[02-9ac-hj-np-z]{6,128}$/;
Expand Down
33 changes: 33 additions & 0 deletions packages/bech32/src/validate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { bech32 } from 'bech32';
import { bech32IsValid } from './validate';
import { describe, expect, test } from 'vitest';

describe('validate Bech32 address', () => {
test('should return true for a correct Bech32 address without prefix', () => {
const address = bech32.encode('osmo', bech32.toWords(Buffer.from('test', 'utf8')));
expect(bech32IsValid(address)).toBeTruthy();
});

test('should return true for a correct Bech32 address with prefix', () => {
const prefix = 'osmo';
const address = bech32.encode(prefix, bech32.toWords(Buffer.from('test', 'utf8')));
expect(bech32IsValid(address, prefix)).toBeTruthy();
});

test('should return false if the prefix does not match', () => {
const address = bech32.encode('osmo', bech32.toWords(Buffer.from('test', 'utf8')));
const wrongPrefix = 'noble';
expect(bech32IsValid(address, wrongPrefix)).toBeFalsy();
});

test('should return false for invalid Bech32 address', () => {
const address = 'invalidaddress';
expect(bech32IsValid(address)).toBeFalsy();
});

test('should return false for Bech32 address with unexpected prefix when prefix is provided', () => {
const prefix = 'osmo';
const address = bech32.encode('noble', bech32.toWords(Buffer.from('test', 'utf8')));
expect(bech32IsValid(address, prefix)).toBeFalsy();
});
});
15 changes: 15 additions & 0 deletions packages/bech32/src/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { bech32 } from 'bech32';

/**
* Validates a Bech32 encoded address. If a prefix is provided, it also checks that the address's
* prefix matches the expected prefix.
*/
export const bech32IsValid = (bech32Address: string, prefix?: string): boolean => {
try {
const { prefix: decodedPrefix } = bech32.decode(bech32Address);
return prefix ? prefix === decodedPrefix : true;
} catch (error) {
// If there's an error in decoding, it means the address is not valid Bech32
return false;
}
};
17 changes: 16 additions & 1 deletion packages/constants/src/chains.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
// Canonical data source: https://github.com/cosmos/chain-registry/tree/master
import { AssetId } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';

export interface Chain {
displayName: string;
chainId: string;
ibcChannel: string;
iconUrl: string;
nativeAssets: AssetId[];
addressPrefix: string;
}

// Canonical data source: https://github.com/cosmos/chain-registry/tree/master
export const testnetIbcChains: Chain[] = [
{
displayName: 'Osmosis',
chainId: 'osmo-test-5',
ibcChannel: 'channel-0',
iconUrl:
'https://raw.githubusercontent.com/cosmos/chain-registry/f1348793beb994c6cc0256ed7ebdb48c7aa70003/osmosis/images/osmo.svg',
nativeAssets: [],
addressPrefix: 'osmo',
},
{
displayName: 'Noble',
chainId: 'grand-1',
ibcChannel: 'channel-2',
iconUrl:
'https://raw.githubusercontent.com/cosmos/chain-registry/2ca39d0e4eaf3431cca13991948e099801f02e46/noble/images/stake.svg',
nativeAssets: [],
addressPrefix: 'noble',
},
];

0 comments on commit 6af9a2c

Please sign in to comment.