diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 2fedd1bee..15e59c093 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -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", @@ -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", diff --git a/apps/mobile/src/common/transactions/stacks-transactions.hooks.ts b/apps/mobile/src/common/transactions/stacks-transactions.hooks.ts new file mode 100644 index 000000000..8806a2b02 --- /dev/null +++ b/apps/mobile/src/common/transactions/stacks-transactions.hooks.ts @@ -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] + ); +} diff --git a/apps/mobile/src/features/account-list/account-list.tsx b/apps/mobile/src/features/account-list/account-list.tsx index c19fa6235..ebfa41b38 100644 --- a/apps/mobile/src/features/account-list/account-list.tsx +++ b/apps/mobile/src/features/account-list/account-list.tsx @@ -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} /> )} diff --git a/apps/mobile/src/features/balances/bitcoin/bitcoin-balance.tsx b/apps/mobile/src/features/balances/bitcoin/bitcoin-balance.tsx index ffeb598d0..2dd33de19 100644 --- a/apps/mobile/src/features/balances/bitcoin/bitcoin-balance.tsx +++ b/apps/mobile/src/features/balances/bitcoin/bitcoin-balance.tsx @@ -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} diff --git a/apps/mobile/src/features/balances/stacks/stacks-balance.tsx b/apps/mobile/src/features/balances/stacks/stacks-balance.tsx index 4ca0b410a..cbf0dffe6 100644 --- a/apps/mobile/src/features/balances/stacks/stacks-balance.tsx +++ b/apps/mobile/src/features/balances/stacks/stacks-balance.tsx @@ -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} diff --git a/apps/mobile/src/features/balances/token-balance.tsx b/apps/mobile/src/features/balances/token-balance.tsx index f51d8f198..c040d0629 100644 --- a/apps/mobile/src/features/balances/token-balance.tsx +++ b/apps/mobile/src/features/balances/token-balance.tsx @@ -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; } @@ -19,7 +32,7 @@ export function TokenBalance({ icon, tokenName, availableBalance, - chain, + protocol, fiatBalance, onPress, }: TokenBalanceProps) { @@ -29,7 +42,7 @@ export function TokenBalance({ } - captionLeft={chain} + captionLeft={getChainLayerFromAssetProtocol(protocol)} captionRight={} /> diff --git a/apps/mobile/src/features/send/utils.ts b/apps/mobile/src/features/send/send-form.utils.ts similarity index 100% rename from apps/mobile/src/features/send/utils.ts rename to apps/mobile/src/features/send/send-form.utils.ts diff --git a/apps/mobile/src/features/send/send-form/components/send-form-amount-field.tsx b/apps/mobile/src/features/send/send-form/components/send-form-amount-field.tsx index f6b9581af..f6dff7194 100644 --- a/apps/mobile/src/features/send/send-form/components/send-form-amount-field.tsx +++ b/apps/mobile/src/features/send/send-form/components/send-form-amount-field.tsx @@ -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'; @@ -29,12 +28,6 @@ export function SendFormAmountField() { value={value} /> )} - rules={{ - required: t({ - id: 'send-form.amount-field.error.amount-required', - message: 'Amount is required', - }), - }} /> ); } diff --git a/apps/mobile/src/features/send/send-form/components/send-form-asset.tsx b/apps/mobile/src/features/send/send-form/components/send-form-asset.tsx index 37d3ddd6d..2ff0156c3 100644 --- a/apps/mobile/src/features/send/send-form/components/send-form-asset.tsx +++ b/apps/mobile/src/features/send/send-form/components/send-form-asset.tsx @@ -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 ( diff --git a/apps/mobile/src/features/send/send-form/components/send-form-button.tsx b/apps/mobile/src/features/send/send-form/components/send-form-button.tsx index 0c2652817..8e0b278c6 100644 --- a/apps/mobile/src/features/send/send-form/components/send-form-button.tsx +++ b/apps/mobile/src/features/send/send-form/components/send-form-button.tsx @@ -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>(); - function onSubmit(data: z.infer) { + function onPress(data: z.infer) { + 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 (