Skip to content

Commit d869a60

Browse files
committed
feat: migrate stacks generate txs, closes LEA-1732
1 parent ab26689 commit d869a60

36 files changed

+596
-149
lines changed

apps/mobile/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"@segment/sovran-react-native": "1.1.2",
6969
"@shopify/restyle": "2.4.2",
7070
"@stacks/common": "6.13.0",
71+
"@stacks/network": "6.13.0",
7172
"@stacks/stacks-blockchain-api-types": "7.8.2",
7273
"@stacks/transactions": "6.17.0",
7374
"@stacks/wallet-sdk": "6.15.0",
@@ -107,6 +108,7 @@
107108
"metro-resolver": "0.80.5",
108109
"prism-react-renderer": "2.4.0",
109110
"react": "18.2.0",
111+
"react-async-hook": "4.0.0",
110112
"react-dom": "18.2.0",
111113
"react-hook-form": "7.53.2",
112114
"react-native": "0.74.1",
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { AccountId } from '@/models/domain.model';
2+
import { useAccountByIndex } from '@/store/accounts/accounts.read';
3+
import { useStacksSigners } from '@/store/keychains/stacks/stacks-keychains.read';
4+
import { useNetworkPreferenceStacksNetwork } from '@/store/settings/settings.read';
5+
import { bytesToHex } from '@noble/hashes/utils';
6+
import { AnchorMode } from '@stacks/transactions';
7+
8+
import { useNextNonce } from '@leather.io/query';
9+
import { TransactionTypes, generateUnsignedTransaction } from '@leather.io/stacks';
10+
import { createMoney } from '@leather.io/utils';
11+
12+
export function useStxTokenTransferDetails({ fingerprint, accountIndex }: AccountId) {
13+
const account = useAccountByIndex(fingerprint, accountIndex);
14+
const network = useNetworkPreferenceStacksNetwork();
15+
const stxSigner = useStacksSigners().fromAccountIndex(fingerprint, accountIndex)[0];
16+
const stxAddress = stxSigner?.address ?? '';
17+
const { data: nextNonce } = useNextNonce(stxAddress);
18+
19+
if (!account || !stxSigner) return;
20+
21+
return {
22+
network,
23+
nonce: nextNonce?.nonce,
24+
publicKey: bytesToHex(stxSigner.publicKey),
25+
// stxAddress as fallback for fee estimation
26+
recipient: stxAddress,
27+
};
28+
}
29+
30+
const defaultRequiredStxTokenTransferOptions = {
31+
amount: createMoney(0, 'STX'),
32+
anchorMode: AnchorMode.Any,
33+
fee: createMoney(0, 'STX'),
34+
};
35+
36+
export function useGenerateStxTokenTransferUnsignedTransaction(account: AccountId) {
37+
const stxAccountSignerDetails = useStxTokenTransferDetails(account);
38+
39+
return (values: Record<string, any>) => {
40+
if (!stxAccountSignerDetails) return;
41+
42+
return generateUnsignedTransaction({
43+
txType: TransactionTypes.StxTokenTransfer,
44+
...defaultRequiredStxTokenTransferOptions,
45+
...stxAccountSignerDetails,
46+
...values,
47+
});
48+
};
49+
}

apps/mobile/src/features/account-list/account-list.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function AccountList({ accounts, onPress, showWalletInfo }: AccountListPr
3434
iconTestID={defaultIconTestId(account.icon)}
3535
onPress={() => onPress(account)}
3636
testID={TestId.walletListAccountCard}
37-
walletName={showWalletInfo ? wallet.name : ' '}
37+
walletName={showWalletInfo ? wallet.name : undefined}
3838
/>
3939
)}
4040
</WalletLoader>

apps/mobile/src/features/balances/bitcoin/bitcoin-balance.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,7 @@ export function BitcoinTokenBalance({
2727
id: 'asset_name.bitcoin',
2828
message: 'Bitcoin',
2929
})}
30-
chain={t({
31-
id: 'asset_name.layer_1',
32-
message: 'Layer 1',
33-
})}
30+
protocol="nativeBtc"
3431
fiatBalance={fiatBalance}
3532
availableBalance={availableBalance}
3633
onPress={onPress}

apps/mobile/src/features/balances/stacks/stacks-balance.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,7 @@ export function StacksTokenBalance({
2525
id: 'asset_name.stacks',
2626
message: 'Stacks',
2727
})}
28-
chain={t({
29-
id: 'asset_name.layer_1',
30-
message: 'Layer 1',
31-
})}
28+
protocol="nativeStx"
3229
fiatBalance={fiatBalance}
3330
availableBalance={availableBalance}
3431
onPress={onPress}

apps/mobile/src/features/balances/token-balance.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,29 @@
11
import { ReactNode } from 'react';
22

33
import { Balance } from '@/components/balance/balance';
4+
import { t } from '@lingui/macro';
45

5-
import { Money } from '@leather.io/models';
6+
import { CryptoAssetProtocol, Money } from '@leather.io/models';
67
import { Flag, ItemLayout, Pressable } from '@leather.io/ui/native';
78

9+
export function getChainLayerFromAssetProtocol(protocol: CryptoAssetProtocol) {
10+
switch (protocol) {
11+
case 'nativeBtc':
12+
case 'nativeStx':
13+
return t({ id: 'account_balance.caption_left.native', message: 'Layer 1' });
14+
case 'sip10':
15+
return t({ id: 'account_balance.caption_left.sip10', message: 'Layer 2 · Stacks' });
16+
default:
17+
return '';
18+
}
19+
}
20+
821
interface TokenBalanceProps {
922
ticker: string;
1023
icon: ReactNode;
1124
tokenName: string;
1225
availableBalance?: Money;
13-
chain: string;
26+
protocol: CryptoAssetProtocol;
1427
fiatBalance: Money;
1528
onPress?(): void;
1629
}
@@ -19,7 +32,7 @@ export function TokenBalance({
1932
icon,
2033
tokenName,
2134
availableBalance,
22-
chain,
35+
protocol,
2336
fiatBalance,
2437
onPress,
2538
}: TokenBalanceProps) {
@@ -29,7 +42,7 @@ export function TokenBalance({
2942
<ItemLayout
3043
titleLeft={tokenName}
3144
titleRight={availableBalance && <Balance balance={availableBalance} variant="label02" />}
32-
captionLeft={chain}
45+
captionLeft={getChainLayerFromAssetProtocol(protocol)}
3346
captionRight={
3447
<Balance balance={fiatBalance} variant="label02" color="ink.text-subdued" />
3548
}

apps/mobile/src/features/send/send-form/components/send-form-amount-field.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Controller, useFormContext } from 'react-hook-form';
22

33
import { TextInput } from '@/components/text-input';
4-
import { t } from '@lingui/macro';
54
import { z } from 'zod';
65

76
import { useSendFormContext } from '../send-form-context';
@@ -29,12 +28,6 @@ export function SendFormAmountField() {
2928
value={value}
3029
/>
3130
)}
32-
rules={{
33-
required: t({
34-
id: 'send-form.amount-field.error.amount-required',
35-
message: 'Amount is required',
36-
}),
37-
}}
3831
/>
3932
);
4033
}

apps/mobile/src/features/send/send-form/components/send-form-asset.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,22 @@ import { Box, Pressable } from '@leather.io/ui/native';
55
import { useSendFormContext } from '../send-form-context';
66

77
interface SendFormAssetProps {
8-
assetName: string;
9-
chain: string;
108
icon: React.ReactNode;
119
onPress(): void;
1210
}
13-
export function SendFormAsset({ assetName, chain, icon, onPress }: SendFormAssetProps) {
14-
const { availableBalance, fiatBalance, symbol } = useSendFormContext();
11+
export function SendFormAsset({ icon, onPress }: SendFormAssetProps) {
12+
const { name, protocol, availableBalance, fiatBalance, symbol } = useSendFormContext();
1513

1614
return (
1715
<Pressable onPress={onPress}>
1816
<Box borderColor="ink.border-default" borderRadius="sm" borderWidth={1}>
1917
<TokenBalance
2018
availableBalance={availableBalance}
21-
chain={chain}
19+
protocol={protocol}
2220
fiatBalance={fiatBalance}
2321
icon={icon}
2422
ticker={symbol}
25-
tokenName={assetName}
23+
tokenName={name}
2624
/>
2725
</Box>
2826
</Pressable>

apps/mobile/src/features/send/send-form/components/send-form-button.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,26 @@ import { useSendFormContext } from '../send-form-context';
1010

1111
export function SendFormButton() {
1212
const { displayToast } = useToastContext();
13-
const { schema } = useSendFormContext();
13+
const { schema, onSubmit } = useSendFormContext();
1414
const {
1515
formState: { isDirty, isValid },
1616
handleSubmit,
1717
} = useFormContext<z.infer<typeof schema>>();
1818

19-
function onSubmit(data: z.infer<typeof schema>) {
19+
function onSubmitForm(values: z.infer<typeof schema>) {
20+
onSubmit(values);
2021
// Temporary toast for testing
2122
displayToast({
2223
title: t`Form submitted`,
2324
type: 'success',
2425
});
25-
// eslint-disable-next-line no-console
26-
console.log(t`submit data:`, data);
2726
}
2827

2928
return (
3029
<Button
3130
mt="3"
3231
buttonState={isDirty && isValid ? 'default' : 'disabled'}
33-
onPress={handleSubmit(onSubmit)}
32+
onPress={handleSubmit(onSubmitForm)}
3433
title={t({
3534
id: 'send_form.review_button',
3635
message: 'Review',

apps/mobile/src/features/send/send-form/components/send-form-memo.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function SendFormMemo() {
2121
<NoteEmptyIcon />
2222
<Text variant="label02">
2323
{t({
24-
id: 'send-form.memo.input.label',
24+
id: 'send_form.memo.input.label',
2525
message: 'Add memo',
2626
})}
2727
</Text>

apps/mobile/src/features/send/send-form/components/send-form-recipient.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export function SendFormRecipient() {
4949
) : (
5050
<Text color="ink.text-subdued" variant="label02">
5151
{t({
52-
id: 'send-form.recipient.input.label',
52+
id: 'send_form.recipient.input.label',
5353
message: 'Enter recipient',
5454
})}
5555
</Text>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import {
2+
CreateCurrentSendRoute,
3+
useSendSheetNavigation,
4+
useSendSheetRoute,
5+
} from '../../send-form.utils';
6+
import { SendFormStxSchema } from '../schemas/send-form-stx.schema';
7+
8+
export type CurrentRoute = CreateCurrentSendRoute<'send-form-stx'>;
9+
10+
export function useSendFormBtc() {
11+
const route = useSendSheetRoute<CurrentRoute>();
12+
const navigation = useSendSheetNavigation<CurrentRoute>();
13+
14+
return {
15+
onGoBack() {
16+
navigation.navigate('send-select-asset', { account: route.params.account });
17+
},
18+
// Temporary logs until we can hook up to approver flow
19+
async onSubmit(values: SendFormStxSchema) {
20+
// eslint-disable-next-line no-console, lingui/no-unlocalized-strings
21+
console.log('Send form data:', values);
22+
},
23+
};
24+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { useGenerateStxTokenTransferUnsignedTransaction } from '@/common/transactions/stacks-transactions.hooks';
2+
import { bytesToHex } from '@noble/hashes/utils';
3+
import BigNumber from 'bignumber.js';
4+
5+
import { createMoneyFromDecimal } from '@leather.io/utils';
6+
7+
import {
8+
CreateCurrentSendRoute,
9+
useSendSheetNavigation,
10+
useSendSheetRoute,
11+
} from '../../send-form.utils';
12+
import { SendFormStxSchema } from '../schemas/send-form-stx.schema';
13+
14+
export type CurrentRoute = CreateCurrentSendRoute<'send-form-stx'>;
15+
16+
function parseSendFormValues(values: SendFormStxSchema) {
17+
return {
18+
amount: createMoneyFromDecimal(new BigNumber(values.amount), 'STX'),
19+
fee: createMoneyFromDecimal(new BigNumber(values.fee), 'STX'),
20+
memo: values.memo,
21+
recipient: values.recipient,
22+
};
23+
}
24+
25+
export function useSendFormStx() {
26+
const route = useSendSheetRoute<CurrentRoute>();
27+
const navigation = useSendSheetNavigation<CurrentRoute>();
28+
29+
const generateTx = useGenerateStxTokenTransferUnsignedTransaction({
30+
accountIndex: route.params.account.accountIndex,
31+
fingerprint: route.params.account.fingerprint,
32+
});
33+
34+
return {
35+
onGoBack() {
36+
navigation.navigate('send-select-asset', { account: route.params.account });
37+
},
38+
// Temporary logs until we can hook up to approver flow
39+
async onSubmit(values: SendFormStxSchema) {
40+
// eslint-disable-next-line no-console, lingui/no-unlocalized-strings
41+
console.log('Send form data:', values);
42+
const tx = await generateTx(parseSendFormValues(values));
43+
// eslint-disable-next-line no-console, lingui/no-unlocalized-strings
44+
console.log('Unsigned tx:', tx);
45+
// Show an error toast here?
46+
if (!tx) throw new Error('Attempted to generate unsigned tx, but tx is undefined');
47+
const txHex = bytesToHex(tx.serialize());
48+
// eslint-disable-next-line no-console, lingui/no-unlocalized-strings
49+
console.log('tx hex:', txHex);
50+
},
51+
};
52+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useBitcoinAccountTotalBitcoinBalance } from '@/queries/balance/bitcoin-balance.query';
2+
import { HasChildren } from '@/utils/types';
3+
import { t } from '@lingui/macro';
4+
5+
import { useSendSheetRoute } from '../../send-form.utils';
6+
import { useSendFormBtc } from '../hooks/use-send-form-btc';
7+
import { CurrentRoute } from '../hooks/use-send-form-stx';
8+
import { defaultSendFormBtcValues, sendFormBtcSchema } from '../schemas/send-form-btc.schema';
9+
import { SendFormProvider } from '../send-form-context';
10+
11+
export function SendFormBitcoinProvider({ children }: HasChildren) {
12+
const route = useSendSheetRoute<CurrentRoute>();
13+
14+
const { onSubmit } = useSendFormBtc();
15+
16+
const { availableBalance, fiatBalance } = useBitcoinAccountTotalBitcoinBalance({
17+
accountIndex: route.params.account.accountIndex,
18+
fingerprint: route.params.account.fingerprint,
19+
});
20+
21+
return (
22+
<SendFormProvider
23+
value={{
24+
name: t({
25+
id: 'asset_name.bitcoin',
26+
message: 'Bitcoin',
27+
}),
28+
protocol: 'nativeBtc',
29+
symbol: 'BTC',
30+
availableBalance,
31+
fiatBalance,
32+
defaultValues: defaultSendFormBtcValues,
33+
schema: sendFormBtcSchema,
34+
onSubmit,
35+
}}
36+
>
37+
{children}
38+
</SendFormProvider>
39+
);
40+
}

0 commit comments

Comments
 (0)