Skip to content

Commit

Permalink
feat: support multiple recipients in rpc send transfer method, closes #…
Browse files Browse the repository at this point in the history
  • Loading branch information
alter-eggo committed Apr 9, 2024
1 parent 1cd2625 commit 38cdc52
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -1,27 +1,39 @@
import { HStack, Stack, styled } from 'leather-styles/jsx';

import type { RpcSendTransferRecipient } from '@shared/rpc/methods/send-transfer';

import { truncateMiddle } from '@app/ui/utils/truncate-middle';

interface SendTransferDetailsProps {
address: string;
amount: string;
recipients: RpcSendTransferRecipient[];
currentAddress: string;
}
export function SendTransferDetails({ address, amount, currentAddress }: SendTransferDetailsProps) {
export function SendTransferDetails({ recipients, currentAddress }: SendTransferDetailsProps) {
return (
<Stack border="active" borderRadius="sm" gap="space.04" p="space.05" width="100%">
<HStack alignItems="center" gap="space.04" justifyContent="space-between">
<styled.span textStyle="caption.01">From</styled.span>
<styled.span textStyle="label.01">{truncateMiddle(currentAddress)}</styled.span>
</HStack>
<HStack alignItems="center" gap="space.04" justifyContent="space-between">
<styled.span textStyle="caption.01">To</styled.span>
<styled.span textStyle="label.01">{truncateMiddle(address)}</styled.span>
</HStack>
<HStack alignItems="center" gap="space.04" justifyContent="space-between">
<styled.span textStyle="caption.01">Amount</styled.span>
<styled.span textStyle="label.01">{amount}</styled.span>
</HStack>
<Stack width="100%">
{recipients.map(({ address, amount }, index) => (
<Stack
key={address + index}
border="active"
borderRadius="sm"
gap="space.04"
p="space.05"
width="100%"
>
<HStack alignItems="center" gap="space.04" justifyContent="space-between">
<styled.span textStyle="caption.01">From</styled.span>
<styled.span textStyle="label.01">{truncateMiddle(currentAddress)}</styled.span>
</HStack>
<HStack alignItems="center" gap="space.04" justifyContent="space-between">
<styled.span textStyle="caption.01">To</styled.span>
<styled.span textStyle="label.01">{truncateMiddle(address)}</styled.span>
</HStack>
<HStack alignItems="center" gap="space.04" justifyContent="space-between">
<styled.span textStyle="caption.01">Amount</styled.span>
<styled.span textStyle="label.01">{amount}</styled.span>
</HStack>
</Stack>
))}
</Stack>
);
}
13 changes: 5 additions & 8 deletions src/app/pages/rpc-send-transfer/rpc-send-transfer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,17 @@ import { useRpcSendTransfer } from './use-rpc-send-transfer';

export function RpcSendTransfer() {
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
const { address, amount, onChooseTransferFee, origin } = useRpcSendTransfer();
const amountAsMoney = createMoney(new BigNumber(amount), 'BTC');
const { recipients, recipientAddresses, totalAmount, onChooseTransferFee, origin } =
useRpcSendTransfer();
const amountAsMoney = createMoney(new BigNumber(totalAmount), 'BTC');
const formattedMoney = formatMoneyPadded(amountAsMoney);

useBreakOnNonCompliantEntity(address);
useBreakOnNonCompliantEntity(recipientAddresses);

return (
<>
<SendTransferHeader amount={formattedMoney} origin={origin} />
<SendTransferDetails
address={address}
amount={formattedMoney}
currentAddress={nativeSegwitSigner.address}
/>
<SendTransferDetails recipients={recipients} currentAddress={nativeSegwitSigner.address} />
<InfoCardFooter>
<Button borderRadius="sm" flexGrow={1} onClick={onChooseTransferFee}>
Continue
Expand Down
17 changes: 11 additions & 6 deletions src/app/pages/rpc-send-transfer/use-rpc-send-transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,25 @@ import { RouteUrls } from '@shared/route-urls';
import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-search-params';
import { useOnMount } from '@app/common/hooks/use-on-mount';
import { initialSearchParams } from '@app/common/initial-search-params';
import { sumNumbers } from '@app/common/math/helpers';
import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks';

export function useRpcSendTransferRequestParams() {
const defaultParams = useDefaultRequestParams();
return useMemo(
() => ({
...defaultParams,
address: initialSearchParams.get('address') ?? '',
amount: initialSearchParams.get('amount') ?? '',
requestId: initialSearchParams.get('requestId') ?? '',
recipients: initialSearchParams.getAll('recipient') ?? [],
amounts: initialSearchParams.getAll('amount') ?? [],
}),
[defaultParams]
);
}

export function useRpcSendTransfer() {
const navigate = useNavigate();
const { address, amount, origin } = useRpcSendTransferRequestParams();
const { origin, recipients, amounts } = useRpcSendTransferRequestParams();
const { data: utxos = [], refetch } = useCurrentNativeSegwitUtxos();

// Forcing a refetch to ensure UTXOs are fresh
Expand All @@ -32,13 +33,17 @@ export function useRpcSendTransfer() {
if (!origin) throw new Error('Invalid params');

return {
address,
amount,
recipients: recipients.map((address, index) => ({
address,
amount: amounts[index],
})),
origin,
utxos,
totalAmount: sumNumbers(amounts.map(Number)),
recipientAddresses: recipients,
async onChooseTransferFee() {
navigate(RouteUrls.RpcSendTransferChooseFee, {
state: { address, amount, utxos },
state: { recipients, utxos },
});
},
};
Expand Down
38 changes: 28 additions & 10 deletions src/background/messaging/rpc-methods/send-transfer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { RpcErrorCode, SendTransferRequest } from '@btckit/types';
import { RpcErrorCode, type RpcRequest, type SendTransferRequestParams } from '@btckit/types';

import { RouteUrls } from '@shared/route-urls';
import {
type RpcSendTransferParams,
type RpcSendTransferParamsLegacy,
convertRpcSendTransferLegacyParamsToNew,
getRpcSendTransferParamErrors,
validateRpcSendTransferLegacyParams,
validateRpcSendTransferParams,
} from '@shared/rpc/methods/send-transfer';
import { makeRpcErrorResponse } from '@shared/rpc/rpc-methods';
import { isDefined, isUndefined } from '@shared/utils';
import { isUndefined } from '@shared/utils';

import {
RequestParams,
Expand All @@ -16,7 +20,10 @@ import {
triggerRequestWindowOpen,
} from '../messaging-utils';

export async function rpcSendTransfer(message: SendTransferRequest, port: chrome.runtime.Port) {
export async function rpcSendTransfer(
message: RpcRequest<'sendTransfer', RpcSendTransferParams | SendTransferRequestParams>,
port: chrome.runtime.Port
) {
if (isUndefined(message.params)) {
chrome.tabs.sendMessage(
getTabIdFromPort(port),
Expand All @@ -28,29 +35,40 @@ export async function rpcSendTransfer(message: SendTransferRequest, port: chrome
return;
}

if (!validateRpcSendTransferParams(message.params)) {
// Legacy params support for backward compatibility
const params = validateRpcSendTransferLegacyParams(message.params)
? convertRpcSendTransferLegacyParamsToNew(message.params as RpcSendTransferParamsLegacy)
: (message.params as RpcSendTransferParams);

if (!validateRpcSendTransferParams(params)) {
chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('sendTransfer', {
id: message.id,
error: {
code: RpcErrorCode.INVALID_PARAMS,
message: getRpcSendTransferParamErrors(message.params),
message: getRpcSendTransferParamErrors(params),
},
})
);
return;
}

const recipients: [string, string][] = params.recipients.map(({ address }) => [
'recipient',
address,
]);
const amounts: [string, string][] = params.recipients.map(({ amount }) => ['amount', amount]);

const requestParams: RequestParams = [
['address', message.params.address],
['amount', message.params.amount],
['network', (message.params as any).network ?? 'mainnet'],
...recipients,
...amounts,
['network', params.network ?? 'mainnet'],
['requestId', message.id],
];

if (isDefined((message.params as any).account)) {
requestParams.push(['accountIndex', (message.params as any).account.toString()]);
if (params.account) {
requestParams.push(['accountIndex', params.account.toString()]);
}

const { urlParams, tabId } = makeSearchParamsWithDefaults(port, requestParams);
Expand Down
70 changes: 70 additions & 0 deletions src/shared/rpc/methods/send-transfer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {
convertRpcSendTransferLegacyParamsToNew,
rpcSendTransferParamsSchema,
rpcSendTransferParamsSchemaLegacy,
} from './send-transfer';

describe('`sendTransfer` method', () => {
describe('schema validation', () => {
test('that it validates single recipient sends', () => {
const params = {
network: 'mainnet',
account: 0,
address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq',
amount: '0.0001',
};

expect(rpcSendTransferParamsSchemaLegacy.isValidSync(params)).toBeTruthy();
});

test('that it validates multiple recipient sends', () => {
const params = {
network: 'mainnet',
account: 0,
recipients: [
{
address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq',
amount: '0.0001',
},
{
address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq',
amount: '0.0001',
},
],
};

expect(rpcSendTransferParamsSchema.isValidSync(params)).toBeTruthy();
});

test('that it fails validation for missing required fields', () => {
const params = {
network: 'mainnet',
account: 0,
};

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

test('that it converts legacy params to new params', () => {
const legacyParams = {
network: 'mainnet',
account: 0,
address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq',
amount: '0.0001',
};

const newParams = {
network: 'mainnet',
account: 0,
recipients: [
{
address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq',
amount: '0.0001',
},
],
};

expect(convertRpcSendTransferLegacyParamsToNew(legacyParams)).toEqual(newParams);
});
});
});
40 changes: 38 additions & 2 deletions src/shared/rpc/methods/send-transfer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SendTransferRequestParams } from '@btckit/types';
import * as yup from 'yup';

import { WalletDefaultNetworkConfigurationIds } from '@shared/constants';
Expand All @@ -9,14 +10,49 @@ import {
validateRpcParams,
} from './validation.utils';

const rpcSendTransferParamsSchema = yup.object().shape({
export const rpcSendTransferParamsSchemaLegacy = yup.object().shape({
account: accountSchema,
address: yup.string().required(),
amount: yup.string().required(),
network: yup.string().oneOf(Object.values(WalletDefaultNetworkConfigurationIds)),
});

// TODO: Import param types from btckit when updated
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)),
});

export interface RpcSendTransferParamsLegacy extends SendTransferRequestParams {
network: string;
}

export interface RpcSendTransferRecipient {
address: string;
amount: string;
}

export interface RpcSendTransferParams {
account?: number;
recipients: RpcSendTransferRecipient[];
network: string;
}

export function convertRpcSendTransferLegacyParamsToNew(params: RpcSendTransferParamsLegacy) {
return {
recipients: [{ address: params.address, amount: params.amount }],
network: params.network,
account: params.account,
};
}

export function validateRpcSendTransferLegacyParams(obj: unknown) {
return validateRpcParams(obj, rpcSendTransferParamsSchemaLegacy);
}

export function validateRpcSendTransferParams(obj: unknown) {
return validateRpcParams(obj, rpcSendTransferParamsSchema);
}
Expand Down
27 changes: 27 additions & 0 deletions test-app/src/components/bitcoin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,33 @@ export const Bitcoin = () => {
>
Send transfer
</styled.button>
<styled.button
mt={3}
onClick={() => {
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',
},
],
network: 'testnet',
})
.then((resp: any) => {
console.log({ sucesss: resp });
})
.catch((error: Error) => {
console.log({ error });
});
}}
>
Send transfer to multiple addresses
</styled.button>
</Box>
);
};

0 comments on commit 38cdc52

Please sign in to comment.