Skip to content

Commit

Permalink
feat: migrate stacks generate txs
Browse files Browse the repository at this point in the history
  • Loading branch information
fbwoolf committed Nov 17, 2024
1 parent 314a880 commit f964de6
Show file tree
Hide file tree
Showing 34 changed files with 996 additions and 97 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 { useCallback } from 'react';
import { FieldValues } from 'react-hook-form';

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 { useNextNonce } from '@leather.io/query';
import {
GenerateUnsignedTransactionOptions,
TransactionTypes,
generateUnsignedTransaction,
} from '@leather.io/stacks';
import { stxToMicroStx } from '@leather.io/utils';

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

return useCallback(
async (values?: FieldValues) => {
if (!account || !stxSigner) return;

const options: GenerateUnsignedTransactionOptions = {
publicKey: bytesToHex(stxSigner.publicKey),
nonce: Number(values?.nonce) ?? nextNonce?.nonce,
fee: stxToMicroStx(values?.fee || 0).toNumber(),
txData: {
txType: TransactionTypes.STXTransfer,
// Using account address here as a fallback for a fee estimation
recipient: values?.recipient ?? stxAddress,
amount: values?.amount ? stxToMicroStx(values?.amount).toString(10) : '0',
memo: values?.memo || undefined,
network,
// Coercing type here as we don't have the public key
// as expected by STXTransferPayload type.
// This code will likely need to change soon with Ledger
// work, and coercion allows us to remove lots of type mangling
// and types are out of sync with @stacks/connect
} as any,
};
return generateUnsignedTransaction(options);
},
[account, stxSigner, nextNonce?.nonce, stxAddress, network]
);
}
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 onPress(data: z.infer<typeof schema>) {
onSubmit(data);
// 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(onPress)}
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,40 @@
import { useEffect } from 'react';
import { useFormContext } from 'react-hook-form';

import { useSendFormContext } from '@/features/send/send-form/send-form-context';
import { HasChildren } from '@/utils/types';
import BigNumber from 'bignumber.js';
import { z } from 'zod';

import { defaultStacksFees, useNextNonce } from '@leather.io/query';
import { useOnMount } from '@leather.io/ui/native';
import { microStxToStx } from '@leather.io/utils';

const defaultFeeFallback = 2500;

interface SendFormStacksSetterProps extends HasChildren {
address: string;
}
export function SendFormStacksSetter({ address, children }: SendFormStacksSetterProps) {
const { schema } = useSendFormContext();
const { setValue, formState, getValues } = useFormContext<z.infer<typeof schema>>();
const { data: nextNonce } = useNextNonce(address);

// Temporary default fee
const defaultFee =
defaultStacksFees.estimates[0]?.fee.amount ?? new BigNumber(defaultFeeFallback);

useOnMount(() => setValue('fee', microStxToStx(defaultFee).toString()));

useEffect(() => {
if (
nextNonce?.nonce &&
!formState.dirtyFields.nonce &&
getValues('nonce') !== nextNonce.nonce
) {
return setValue('nonce', nextNonce?.nonce);
}
}, [formState.dirtyFields.nonce, getValues, nextNonce?.nonce, setValue]);

return children;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useForm } from 'react-hook-form';

import { useGenerateStxTokenTransferUnsignedTx } from '@/common/transactions/stacks-transactions.hooks';
import { useGetStacksAddresses } from '@/features/balances/stacks/use-get-stacks-addresses';
import { useStxBalance } from '@/queries/balance/stacks-balance.query';
import { zodResolver } from '@hookform/resolvers/zod';
import { bytesToHex } from '@noble/hashes/utils';

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

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

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

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

const formMethods = useForm<SendFormStxSchema>({
mode: 'onChange',
defaultValues: defaultSendFormStxValues,
resolver: zodResolver(sendFormStxSchema),
});

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

return {
address: addresses[0] ?? '',
availableBalance,
fiatBalance,
formMethods,
onGoBack() {
navigation.navigate('send-select-asset', { account: route.params.account });
},
async onSubmit(data: SendFormStxSchema) {
// eslint-disable-next-line no-console, lingui/no-unlocalized-strings
console.log('Send form data:', data);
const tx = await generateTx(data);
// 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
Expand Up @@ -6,6 +6,7 @@ export const sendFormStxSchema = z.object({
}),
recipient: z.string(),
memo: z.string().optional(),
nonce: z.number(),
fee: z.string(),
});

Expand All @@ -15,5 +16,6 @@ export const defaultSendFormStxValues: SendFormStxSchema = {
amount: '',
recipient: '',
memo: '',
nonce: 0,
fee: '',
};
2 changes: 2 additions & 0 deletions apps/mobile/src/features/send/send-form/send-form-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { ZodTypeAny } from 'zod';
import { CryptoAssetProtocol, CryptoCurrency, Money } from '@leather.io/models';

export interface SendFormContext {
name: string;
protocol: CryptoAssetProtocol;
symbol: CryptoCurrency;
availableBalance: Money;
fiatBalance: Money;
defaultValues: FieldValues;
schema: ZodTypeAny;
onSubmit(data: any): void;
}

const sendFormContext = createContext<SendFormContext | null>(null);
Expand Down
Loading

0 comments on commit f964de6

Please sign in to comment.