Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: migrate stacks generate txs, closes LEA-1732 #627

Draft
wants to merge 1 commit into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
49 changes: 49 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,49 @@
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 { TransactionTypes, generateUnsignedTransaction } from '@leather.io/stacks';
import { createMoney } from '@leather.io/utils';

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: createMoney(0, 'STX'),
anchorMode: AnchorMode.Any,
fee: createMoney(0, 'STX'),
};

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

return (values: Record<string, any>) => {
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) {
pete-watters marked this conversation as resolved.
Show resolved Hide resolved
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
@@ -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, 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,52 @@
import { useGenerateStxTokenTransferUnsignedTransaction } 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,
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
Loading