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

Merged
merged 1 commit into from
Nov 22, 2024
Merged
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
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) {
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
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
Loading