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 22, 2024
1 parent 5abb340 commit 95a00fd
Show file tree
Hide file tree
Showing 46 changed files with 1,128 additions and 337 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 @@ -107,6 +108,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
39 changes: 39 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,39 @@
import { useNetworkPreferenceStacksNetwork } from '@/store/settings/settings.read';
import { AnchorMode } from '@stacks/transactions';

import {
StacksUnsignedTokenTransferOptions,
TransactionTypes,
generateUnsignedTransaction,
} from '@leather.io/stacks';
import { createMoney } from '@leather.io/utils';

export function useStxAccountTransferDetails(address: string, publicKey: string) {
const network = useNetworkPreferenceStacksNetwork();

return {
network,
publicKey,
// Fallback for fee estimation
recipient: address,
};
}

const defaultRequiredStxTokenTransferOptions = {
amount: createMoney(0, 'STX'),
anchorMode: AnchorMode.Any,
fee: createMoney(0, 'STX'),
nonce: '',
};

export function useGenerateStxTokenTransferUnsignedTransaction(
stxAccountDetails: ReturnType<typeof useStxAccountTransferDetails>
) {
return (values: Partial<StacksUnsignedTokenTransferOptions>) =>
generateUnsignedTransaction({
txType: TransactionTypes.StxTokenTransfer,
...defaultRequiredStxTokenTransferOptions,
...stxAccountDetails,
...values,
});
}
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 @@ -28,10 +28,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} variant="label02" />}
captionLeft={chain}
captionLeft={getChainLayerFromAssetProtocol(protocol)}
captionRight={
<Balance balance={fiatBalance} variant="label02" color="ink.text-subdued" />
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface SendSheetNavigatorParamList {
'send-select-account': undefined;
'send-select-asset': { account: Account };
'send-form-btc': { account: Account };
'send-form-stx': { account: Account };
'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
@@ -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}>
<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, onInitSendTransfer } = 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>) {
onInitSendTransfer(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';

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

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 onInitSendTransfer(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,57 @@
import {
useGenerateStxTokenTransferUnsignedTransaction,
useStxAccountTransferDetails,
} from '@/common/transactions/stacks-transactions.hooks';
import { bytesToHex } from '@noble/hashes/utils';
import BigNumber from 'bignumber.js';

import { createMoneyFromDecimal } 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: createMoneyFromDecimal(new BigNumber(values.amount), 'STX'),
fee: createMoneyFromDecimal(new BigNumber(values.fee), 'STX'),
memo: values.memo,
nonce: values.nonce,
recipient: values.recipient,
};
}

export function useSendFormStx() {
const route = useSendSheetRoute<CurrentRoute>();
const navigation = useSendSheetNavigation<CurrentRoute>();
const stxAccountDetails = useStxAccountTransferDetails(
route.params.address,
route.params.publicKey
);

const generateTx = useGenerateStxTokenTransferUnsignedTransaction(stxAccountDetails);

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) {
// 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,29 @@
import { AccountId } from '@/models/domain.model';
import { useBitcoinAccountTotalBitcoinBalance } from '@/queries/balance/bitcoin-balance.query';
import BigNumber from 'bignumber.js';

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

interface SendFormBtcData {
availableBalance: Money;
fiatBalance: Money;
feeRates: Record<string, BigNumber>;
}

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

if (!feeRates) return null;

return children({ availableBalance, fiatBalance, feeRates });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { AccountId } from '@/models/domain.model';
import { useStxBalance } from '@/queries/balance/stacks-balance.query';
import { useStacksSignerAddressFromAccountIndex } from '@/store/keychains/stacks/stacks-keychains.read';

import { Money } from '@leather.io/models';
import { useNextNonce } from '@leather.io/query';

interface SendFormStxData {
availableBalance: Money;
fiatBalance: Money;
nonce: number | string;
}

interface SendFormStxLoaderProps {
account: AccountId;
children({ availableBalance, fiatBalance, nonce }: SendFormStxData): React.ReactNode;
}
export function SendFormStxLoader({ account, children }: SendFormStxLoaderProps) {
const address =
useStacksSignerAddressFromAccountIndex(account.fingerprint, account.accountIndex) ?? '';
const { availableBalance, fiatBalance } = useStxBalance([address]);

const { data: nextNonce } = useNextNonce(address);

if (!address || !nextNonce) return null;

return children({
availableBalance,
fiatBalance,
nonce: nextNonce?.nonce ?? '',
});
}
Loading

0 comments on commit 95a00fd

Please sign in to comment.