Skip to content

Commit

Permalink
feat: migrate bitcoin generate txs, closes LEA-1735
Browse files Browse the repository at this point in the history
  • Loading branch information
fbwoolf committed Nov 28, 2024
1 parent 0cad7aa commit 1bbdff0
Show file tree
Hide file tree
Showing 41 changed files with 771 additions and 190 deletions.
75 changes: 75 additions & 0 deletions apps/mobile/src/common/transactions/bitcoin-transactions.hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useCallback } from 'react';

import { useNetworkPreferenceBitcoinScureLibNetworkConfig } from '@/store/settings/settings.read';
import { t } from '@lingui/macro';

import {
CoinSelectionRecipient,
CoinSelectionUtxo,
determineUtxosForSpend,
determineUtxosForSpendAll,
generateUnsignedTransactionNativeSegwit,
} from '@leather.io/bitcoin';
import type { Money } from '@leather.io/models';

export interface BitcoinTransactionValues {
amount: Money;
recipients: CoinSelectionRecipient[];
}

export function useBtcPayerDetails(address: string, publicKey: string) {
const network = useNetworkPreferenceBitcoinScureLibNetworkConfig();

return {
network,
payerAddress: address,
payerPublicKey: publicKey,
};
}

export interface GenerateBtcUnsignedTransactionCallbackArgs {
feeRate: number;
isSendingMax?: boolean;
utxos: CoinSelectionUtxo[];
values: BitcoinTransactionValues;
}

export function useGenerateBtcUnsignedTransactionNativeSegwit(
btcPayerDetails: ReturnType<typeof useBtcPayerDetails>
) {
return useCallback(
async ({
feeRate,
isSendingMax,
utxos,
values,
}: GenerateBtcUnsignedTransactionCallbackArgs) => {
try {
const determineUtxosArgs = {
feeRate,
recipients: values.recipients,
utxos,
};

const { inputs, outputs, fee } = isSendingMax
? determineUtxosForSpendAll(determineUtxosArgs)
: determineUtxosForSpend(determineUtxosArgs);

if (!inputs.length) throw new Error('No inputs to sign');
if (!outputs.length) throw new Error('No outputs to sign');

return generateUnsignedTransactionNativeSegwit({
...btcPayerDetails,
inputs,
outputs,
fee,
});
} catch (e) {
// eslint-disable-next-line no-console
console.log(t`Error signing bitcoin transaction`, e);
return null;
}
},
[btcPayerDetails]
);
}
2 changes: 1 addition & 1 deletion apps/mobile/src/features/send/send-form.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
export interface SendSheetNavigatorParamList {
'send-select-account': undefined;
'send-select-asset': { account: Account };
'send-form-btc': { account: Account };
'send-form-btc': { account: Account; address: string; publicKey: string };
'send-form-stx': { account: Account; address: string; publicKey: string };
'sign-psbt': { psbtHex: string };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import { Controller, useFormContext } from 'react-hook-form';
import { TextInput } from '@/components/text-input';
import { z } from 'zod';

import { useSendFormContext } from '../send-form-context';
import { SendFormBaseContext, useSendFormContext } from '../send-form-context';

export function SendFormAmountField() {
const { schema } = useSendFormContext();
export function SendFormAmountField<T extends SendFormBaseContext<T>>() {
const { formData } = useSendFormContext<T>();
const {
control,
// TODO: Handle errors
// formState: { errors },
} = useFormContext<z.infer<typeof schema>>();
} = useFormContext<z.infer<typeof formData.schema>>();

return (
<Controller
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ import { TokenBalance } from '@/features/balances/token-balance';

import { Box, Pressable } from '@leather.io/ui/native';

import { useSendFormContext } from '../send-form-context';
import { SendFormBaseContext, useSendFormContext } from '../send-form-context';

interface SendFormAssetProps {
icon: React.ReactNode;
onPress(): void;
}
export function SendFormAsset({ icon, onPress }: SendFormAssetProps) {
const { name, protocol, availableBalance, fiatBalance, symbol } = useSendFormContext();
export function SendFormAsset<T extends SendFormBaseContext<T>>({
icon,
onPress,
}: SendFormAssetProps) {
const { formData } = useSendFormContext<T>();
const { availableBalance, fiatBalance, protocol, symbol, name } = formData;

return (
<Pressable onPress={onPress}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@ import { z } from 'zod';

import { Button } from '@leather.io/ui/native';

import { useSendFormContext } from '../send-form-context';
import { SendFormBaseContext, useSendFormContext } from '../send-form-context';

export function SendFormButton() {
export function SendFormButton<T extends SendFormBaseContext<T>>() {
const { displayToast } = useToastContext();
const { schema, onInitSendTransfer } = useSendFormContext();
const { formData } = useSendFormContext<T>();
const { onInitSendTransfer, schema } = formData;
const {
formState: { isDirty, isValid },
handleSubmit,
} = useFormContext<z.infer<typeof schema>>();

function onSubmitForm(values: z.infer<typeof schema>) {
onInitSendTransfer(values);
onInitSendTransfer(formData, values);
// Temporary toast for testing
displayToast({
title: t`Form submitted`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { t } from '@lingui/macro';

import { NoteEmptyIcon, SheetRef, Text, TouchableOpacity } from '@leather.io/ui/native';

import { SendFormBaseContext } from '../send-form-context';
import { MemoSheet } from '../sheets/memo-sheet';

export function SendFormMemo() {
export function SendFormMemo<T extends SendFormBaseContext<T>>() {
const memoSheetRef = useRef<SheetRef>(null);
return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { z } from 'zod';

import { Box, Numpad } from '@leather.io/ui/native';

import { useSendFormContext } from '../send-form-context';
import { SendFormBaseContext, useSendFormContext } from '../send-form-context';

export function SendFormNumpad() {
const { schema } = useSendFormContext();
const { setValue, watch } = useFormContext<z.infer<typeof schema>>();
export function SendFormNumpad<T extends SendFormBaseContext<T>>() {
const { formData } = useSendFormContext<T>();
const { setValue, watch } = useFormContext<z.infer<typeof formData.schema>>();
const amount = watch('amount');

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ import {
UserIcon,
} from '@leather.io/ui/native';

import { useSendFormContext } from '../send-form-context';
import { SendFormBaseContext, useSendFormContext } from '../send-form-context';
import { RecipientSheet } from '../sheets/recipient-sheet';

export function SendFormRecipient() {
const { schema } = useSendFormContext();
const { watch } = useFormContext<z.infer<typeof schema>>();
export function SendFormRecipient<T extends SendFormBaseContext<T>>() {
const { formData } = useSendFormContext<T>();
const { watch } = useFormContext<z.infer<typeof formData.schema>>();
const recipientSheetRef = useRef<SheetRef>(null);

const recipient = watch('recipient');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,95 @@
import { useCallback } from 'react';

import {
useBtcPayerDetails,
useGenerateBtcUnsignedTransactionNativeSegwit,
} from '@/common/transactions/bitcoin-transactions.hooks';
import BigNumber from 'bignumber.js';

import { CoinSelectionRecipient, CoinSelectionUtxo, getBitcoinFees } from '@leather.io/bitcoin';
import { AverageBitcoinFeeRates } from '@leather.io/models';
import { Utxo } from '@leather.io/query';
import { createMoneyFromDecimal } from '@leather.io/utils';

import {
CreateCurrentSendRoute,
useSendSheetNavigation,
useSendSheetRoute,
} from '../../send-form.utils';
import { SendFormStxSchema } from '../schemas/send-form-stx.schema';
import { SendFormBtcContext } from '../providers/send-form-btc-provider';
import { SendFormBtcSchema } from '../schemas/send-form-btc.schema';

type CurrentRoute = CreateCurrentSendRoute<'send-form-btc'>;

function parseSendFormValues(values: SendFormBtcSchema) {
return {
amount: createMoneyFromDecimal(new BigNumber(values.amount), 'BTC'),
recipients: [
{
address: values.recipient,
amount: createMoneyFromDecimal(new BigNumber(values.amount), 'BTC'),
},
],
};
}

function createCoinSelectionUtxos(utxos: Utxo[]): CoinSelectionUtxo[] {
return utxos.map(utxo => ({
address: utxo.address,
txid: utxo.txid,
value: Number(utxo.value),
vout: utxo.vout,
}));
}

export function useSendFormBtc() {
const route = useSendSheetRoute<CurrentRoute>();
const navigation = useSendSheetNavigation<CurrentRoute>();
const btcPayerDetails = useBtcPayerDetails(route.params.address, route.params.publicKey);

const getTransactionFees = useCallback(
(
feeRates: AverageBitcoinFeeRates,
recipients: CoinSelectionRecipient[],
utxos: CoinSelectionUtxo[]
) => getBitcoinFees({ feeRates, isSendingMax: false, recipients, utxos }),
[]
);

const generateTx = useGenerateBtcUnsignedTransactionNativeSegwit(btcPayerDetails);

return {
onGoBack() {
navigation.navigate('send-select-asset', { account: route.params.account });
},
// Temporary logs until we can hook up to approver flow
async onInitSendTransfer(values: SendFormStxSchema) {
async onInitSendTransfer(data: SendFormBtcContext, values: SendFormBtcSchema) {
// eslint-disable-next-line no-console, lingui/no-unlocalized-strings
console.log('Send form data:', parseSendFormValues(values));

const parsedSendFormValues = parseSendFormValues(values);
const coinSelectionUtxos = createCoinSelectionUtxos(data.utxos);

const tx = await generateTx({
feeRate: Number(values.feeRate),
values: parsedSendFormValues,
utxos: coinSelectionUtxos,
});

const fees = getTransactionFees(
data.feeRates,
parsedSendFormValues.recipients,
coinSelectionUtxos
);

// Show an error toast here?
if (!tx) throw new Error('Attempted to generate raw tx, but no tx exists');
// eslint-disable-next-line no-console, lingui/no-unlocalized-strings
console.log('tx hex:', tx.hex);
// eslint-disable-next-line no-console, lingui/no-unlocalized-strings
console.log('psbt:', tx.psbt);
// eslint-disable-next-line no-console, lingui/no-unlocalized-strings
console.log('Send form data:', values);
console.log('fees:', fees);
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
useSendSheetNavigation,
useSendSheetRoute,
} from '../../send-form.utils';
import { SendFormStxContext } from '../providers/send-form-stx-provider';
import { SendFormStxSchema } from '../schemas/send-form-stx.schema';

export type CurrentRoute = CreateCurrentSendRoute<'send-form-stx'>;
Expand Down Expand Up @@ -41,7 +42,7 @@ export function useSendFormStx() {
navigation.navigate('send-select-asset', { account: route.params.account });
},
// Temporary logs until we can hook up to approver flow
async onInitSendTransfer(values: SendFormStxSchema) {
async onInitSendTransfer(data: SendFormStxContext, values: SendFormStxSchema) {
// eslint-disable-next-line no-console, lingui/no-unlocalized-strings
console.log('Send form data:', values);
const tx = await generateTx(parseSendFormValues(values));
Expand All @@ -52,6 +53,8 @@ export function useSendFormStx() {
const txHex = bytesToHex(tx.serialize());
// eslint-disable-next-line no-console, lingui/no-unlocalized-strings
console.log('tx hex:', txHex);
// eslint-disable-next-line no-console, lingui/no-unlocalized-strings
console.log('fees:', data.fees);
},
};
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
import { AccountId } from '@/models/domain.model';
import { useBitcoinAccountTotalBitcoinBalance } from '@/queries/balance/bitcoin-balance.query';
import BigNumber from 'bignumber.js';
import {
useBitcoinAccountTotalBitcoinBalance,
useBitcoinAccountUtxos,
} from '@/queries/balance/bitcoin-balance.query';

import { Money } from '@leather.io/models';
import { useAverageBitcoinFeeRates } from '@leather.io/query';
import { AverageBitcoinFeeRates, Money } from '@leather.io/models';
import { Utxo, useAverageBitcoinFeeRates } from '@leather.io/query';

interface SendFormBtcData {
availableBalance: Money;
fiatBalance: Money;
feeRates: Record<string, BigNumber>;
feeRates: AverageBitcoinFeeRates;
utxos: Utxo[];
}

interface SendFormBtcLoaderProps {
account: AccountId;
children({ availableBalance, fiatBalance, feeRates }: SendFormBtcData): React.ReactNode;
children({ availableBalance, fiatBalance, feeRates, utxos }: SendFormBtcData): React.ReactNode;
}
export function SendFormBtcLoader({ account, children }: SendFormBtcLoaderProps) {
// Not sure if we need to load feeRates here?
const accountId = { fingerprint: account.fingerprint, accountIndex: account.accountIndex };
const { data: feeRates } = useAverageBitcoinFeeRates();
const { availableBalance, fiatBalance } = useBitcoinAccountTotalBitcoinBalance({
accountIndex: account.accountIndex,
fingerprint: account.fingerprint,
});
const { data: utxos = [] } = useBitcoinAccountUtxos(accountId);
const { availableBalance, fiatBalance } = useBitcoinAccountTotalBitcoinBalance(accountId);

if (!feeRates) return null;
// Handle loading and error states
if (!utxos.length || !feeRates) return null;

return children({ availableBalance, fiatBalance, feeRates });
return children({ availableBalance, fiatBalance, feeRates, utxos });
}
Loading

0 comments on commit 1bbdff0

Please sign in to comment.