Skip to content

Commit

Permalink
feat: migrate stacks generate txs, closes LEA-1732
Browse files Browse the repository at this point in the history
  • Loading branch information
fbwoolf committed Nov 19, 2024
1 parent 314a880 commit 1f9ea83
Show file tree
Hide file tree
Showing 36 changed files with 619 additions and 150 deletions.
2 changes: 2 additions & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"@segment/sovran-react-native": "1.1.2",
"@shopify/restyle": "2.4.2",
"@stacks/common": "6.13.0",
"@stacks/network": "6.13.0",
"@stacks/stacks-blockchain-api-types": "7.8.2",
"@stacks/transactions": "6.17.0",
"@stacks/wallet-sdk": "6.15.0",
Expand Down Expand Up @@ -106,6 +107,7 @@
"metro-resolver": "0.80.5",
"prism-react-renderer": "2.4.0",
"react": "18.2.0",
"react-async-hook": "4.0.0",
"react-dom": "18.2.0",
"react-hook-form": "7.53.2",
"react-native": "0.74.1",
Expand Down
51 changes: 51 additions & 0 deletions apps/mobile/src/common/transactions/stacks-transactions.hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { AccountId } from '@/models/domain.model';
import { useAccountByIndex } from '@/store/accounts/accounts.read';
import { useStacksSigners } from '@/store/keychains/stacks/stacks-keychains.read';
import { useNetworkPreferenceStacksNetwork } from '@/store/settings/settings.read';
import { bytesToHex } from '@noble/hashes/utils';
import { AnchorMode } from '@stacks/transactions';

import { useNextNonce } from '@leather.io/query';
import {
GenerateUnsignedStxTokenTransferArgs,
TransactionTypes,
generateUnsignedTransaction,
} from '@leather.io/stacks';

export function useStxTokenTransferDetails({ fingerprint, accountIndex }: AccountId) {
const account = useAccountByIndex(fingerprint, accountIndex);
const network = useNetworkPreferenceStacksNetwork();
const stxSigner = useStacksSigners().fromAccountIndex(fingerprint, accountIndex)[0];
const stxAddress = stxSigner?.address ?? '';
const { data: nextNonce } = useNextNonce(stxAddress);

if (!account || !stxSigner) return;

return {
network,
nonce: nextNonce?.nonce,
publicKey: bytesToHex(stxSigner.publicKey),
// stxAddress as fallback for fee estimation
recipient: stxAddress,
};
}

const defaultRequiredStxTokenTransferOptions = {
amount: 0,
anchorMode: AnchorMode.Any,
};

export function useGenerateStxTokenTransferUnsignedTransaction(account: AccountId) {
const stxAccountSignerDetails = useStxTokenTransferDetails(account);

return (values: Partial<GenerateUnsignedStxTokenTransferArgs>) => {
if (!stxAccountSignerDetails) return;

return generateUnsignedTransaction({
txType: TransactionTypes.StxTokenTransfer,
...defaultRequiredStxTokenTransferOptions,
...stxAccountSignerDetails,
...values,
});
};
}
2 changes: 1 addition & 1 deletion apps/mobile/src/features/account-list/account-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function AccountList({ accounts, onPress, showWalletInfo }: AccountListPr
iconTestID={defaultIconTestId(account.icon)}
onPress={() => onPress(account)}
testID={TestId.walletListAccountCard}
walletName={showWalletInfo ? wallet.name : ' '}
walletName={showWalletInfo ? wallet.name : undefined}
/>
)}
</WalletLoader>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ export function BitcoinTokenBalance({
id: 'asset_name.bitcoin',
message: 'Bitcoin',
})}
chain={t({
id: 'asset_name.layer_1',
message: 'Layer 1',
})}
protocol="nativeBtc"
fiatBalance={fiatBalance}
availableBalance={availableBalance}
onPress={onPress}
Expand Down
5 changes: 1 addition & 4 deletions apps/mobile/src/features/balances/stacks/stacks-balance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@ export function StacksTokenBalance({
id: 'asset_name.stacks',
message: 'Stacks',
})}
chain={t({
id: 'asset_name.layer_1',
message: 'Layer 1',
})}
protocol="nativeStx"
fiatBalance={fiatBalance}
availableBalance={availableBalance}
onPress={onPress}
Expand Down
21 changes: 17 additions & 4 deletions apps/mobile/src/features/balances/token-balance.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
import { ReactNode } from 'react';

import { Balance } from '@/components/balance/balance';
import { t } from '@lingui/macro';

import { Money } from '@leather.io/models';
import { CryptoAssetProtocol, Money } from '@leather.io/models';
import { Flag, ItemLayout, Pressable } from '@leather.io/ui/native';

export function getChainLayerFromAssetProtocol(protocol: CryptoAssetProtocol) {
switch (protocol) {
case 'nativeBtc':
case 'nativeStx':
return t({ id: 'account_balance.caption_left.native', message: 'Layer 1' });
case 'sip10':
return t({ id: 'account_balance.caption_left.sip10', message: 'Layer 2 · Stacks' });
default:
return '';
}
}

interface TokenBalanceProps {
ticker: string;
icon: ReactNode;
tokenName: string;
availableBalance?: Money;
chain: string;
protocol: CryptoAssetProtocol;
fiatBalance: Money;
onPress?(): void;
}
Expand All @@ -19,7 +32,7 @@ export function TokenBalance({
icon,
tokenName,
availableBalance,
chain,
protocol,
fiatBalance,
onPress,
}: TokenBalanceProps) {
Expand All @@ -29,7 +42,7 @@ export function TokenBalance({
<ItemLayout
titleLeft={tokenName}
titleRight={availableBalance && <Balance balance={availableBalance} />}
captionLeft={chain}
captionLeft={getChainLayerFromAssetProtocol(protocol)}
captionRight={<Balance balance={fiatBalance} color="ink.text-subdued" />}
/>
</Flag>
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Controller, useFormContext } from 'react-hook-form';

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

import { useSendFormContext } from '../send-form-context';
Expand Down Expand Up @@ -29,12 +28,6 @@ export function SendFormAmountField() {
value={value}
/>
)}
rules={{
required: t({
id: 'send-form.amount-field.error.amount-required',
message: 'Amount is required',
}),
}}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,22 @@ import { Box, Pressable } from '@leather.io/ui/native';
import { useSendFormContext } from '../send-form-context';

interface SendFormAssetProps {
assetName: string;
chain: string;
icon: React.ReactNode;
onPress(): void;
}
export function SendFormAsset({ assetName, chain, icon, onPress }: SendFormAssetProps) {
const { availableBalance, fiatBalance, symbol } = useSendFormContext();
export function SendFormAsset({ icon, onPress }: SendFormAssetProps) {
const { name, protocol, availableBalance, fiatBalance, symbol } = useSendFormContext();

return (
<Pressable onPress={onPress}>
<Box borderColor="ink.border-default" borderRadius="sm" borderWidth={1} p="4" mb="0">
<TokenBalance
availableBalance={availableBalance}
chain={chain}
protocol={protocol}
fiatBalance={fiatBalance}
icon={icon}
ticker={symbol}
tokenName={assetName}
tokenName={name}
/>
</Box>
</Pressable>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,26 @@ import { useSendFormContext } from '../send-form-context';

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

function onSubmit(data: z.infer<typeof schema>) {
function onSubmitForm(values: z.infer<typeof schema>) {
onSubmit(values);
// Temporary toast for testing
displayToast({
title: t`Form submitted`,
type: 'success',
});
// eslint-disable-next-line no-console
console.log(t`submit data:`, data);
}

return (
<Button
mt="3"
buttonState={isDirty && isValid ? 'default' : 'disabled'}
onPress={handleSubmit(onSubmit)}
onPress={handleSubmit(onSubmitForm)}
title={t({
id: 'send_form.review_button',
message: 'Review',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function SendFormMemo() {
<NoteEmptyIcon />
<Text variant="label02">
{t({
id: 'send-form.memo.input.label',
id: 'send_form.memo.input.label',
message: 'Add memo',
})}
</Text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function SendFormRecipient() {
) : (
<Text color="ink.text-subdued" variant="label02">
{t({
id: 'send-form.recipient.input.label',
id: 'send_form.recipient.input.label',
message: 'Enter recipient',
})}
</Text>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
CreateCurrentSendRoute,
useSendSheetNavigation,
useSendSheetRoute,
} from '../../send-form.utils';
import { SendFormStxSchema } from '../schemas/send-form-stx.schema';

export type CurrentRoute = CreateCurrentSendRoute<'send-form-stx'>;

export function useSendFormBtc() {
const route = useSendSheetRoute<CurrentRoute>();
const navigation = useSendSheetNavigation<CurrentRoute>();

return {
onGoBack() {
navigation.navigate('send-select-asset', { account: route.params.account });
},
// Temporary logs until we can hook up to approver flow
async onSubmit(values: SendFormStxSchema) {
// eslint-disable-next-line no-console, lingui/no-unlocalized-strings
console.log('Send form data:', values);
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useGenerateStxTokenTransferUnsignedTransaction } from '@/common/transactions/stacks-transactions.hooks';
import { bytesToHex } from '@noble/hashes/utils';

import { stxToMicroStx } from '@leather.io/utils';

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

export type CurrentRoute = CreateCurrentSendRoute<'send-form-stx'>;

function parseSendFormValues(values: SendFormStxSchema) {
return {
amount: values.amount ? stxToMicroStx(values.amount).toString(10) : '0',
fee: stxToMicroStx(values.fee || 0).toNumber(),
memo: values.memo,
recipient: values.recipient,
};
}

export function useSendFormStx() {
const route = useSendSheetRoute<CurrentRoute>();
const navigation = useSendSheetNavigation<CurrentRoute>();

const generateTx = useGenerateStxTokenTransferUnsignedTransaction({
accountIndex: route.params.account.accountIndex,
fingerprint: route.params.account.fingerprint,
});

return {
onGoBack() {
navigation.navigate('send-select-asset', { account: route.params.account });
},
// Temporary logs until we can hook up to approver flow
async onSubmit(values: SendFormStxSchema) {
// eslint-disable-next-line no-console, lingui/no-unlocalized-strings
console.log('Send form data:', values);
const tx = await generateTx(parseSendFormValues(values));
// eslint-disable-next-line no-console, lingui/no-unlocalized-strings
console.log('Unsigned tx:', tx);
// Show an error toast here?
if (!tx) throw new Error('Attempted to generate unsigned tx, but tx is undefined');
const txHex = bytesToHex(tx.serialize());
// eslint-disable-next-line no-console, lingui/no-unlocalized-strings
console.log('tx hex:', txHex);
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useBitcoinAccountTotalBitcoinBalance } from '@/queries/balance/bitcoin-balance.query';
import { HasChildren } from '@/utils/types';
import { t } from '@lingui/macro';

import { useSendSheetRoute } from '../../send-form.utils';
import { useSendFormBtc } from '../hooks/use-send-form-btc';
import { CurrentRoute } from '../hooks/use-send-form-stx';
import { defaultSendFormBtcValues, sendFormBtcSchema } from '../schemas/send-form-btc.schema';
import { SendFormProvider } from '../send-form-context';

export function SendFormBitcoinProvider({ children }: HasChildren) {
const route = useSendSheetRoute<CurrentRoute>();

const { onSubmit } = useSendFormBtc();

const { availableBalance, fiatBalance } = useBitcoinAccountTotalBitcoinBalance({
accountIndex: route.params.account.accountIndex,
fingerprint: route.params.account.fingerprint,
});

return (
<SendFormProvider
value={{
name: t({
id: 'asset_name.bitcoin',
message: 'Bitcoin',
}),
protocol: 'nativeBtc',
symbol: 'BTC',
availableBalance,
fiatBalance,
defaultValues: defaultSendFormBtcValues,
schema: sendFormBtcSchema,
onSubmit,
}}
>
{children}
</SendFormProvider>
);
}
Loading

0 comments on commit 1f9ea83

Please sign in to comment.