diff --git a/src/app/pages/send/send-crypto-asset-form/form/sip10/sip10-token-send-form.tsx b/src/app/pages/send/send-crypto-asset-form/form/sip10/sip10-token-send-form.tsx index 34fdd76a48b..4cf413e2691 100644 --- a/src/app/pages/send/send-crypto-asset-form/form/sip10/sip10-token-send-form.tsx +++ b/src/app/pages/send/send-crypto-asset-form/form/sip10/sip10-token-send-form.tsx @@ -1,4 +1,4 @@ -import { Navigate, useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import type { CryptoAssetBalance, MarketData, Sip10CryptoAssetInfo } from '@leather-wallet/models'; @@ -22,12 +22,14 @@ function Sip10TokenSendFormLoader({ children }: Sip10TokenSendFormLoaderProps) { const token = useSip10Token(contractId ?? ''); const priceAsMarketData = useAlexCurrencyPriceAsMarketData(); const toast = useToast(); + const navigate = useNavigate(); if (!contractId) return; if (!token) { toast.error('Token not found'); - return ; + navigate(RouteUrls.SendCryptoAsset); + return; } return children({ diff --git a/src/app/query/stacks/fees/fees.query.ts b/src/app/query/stacks/fees/fees.query.ts index 6ede33dfddc..1db27ff4a52 100644 --- a/src/app/query/stacks/fees/fees.query.ts +++ b/src/app/query/stacks/fees/fees.query.ts @@ -23,6 +23,7 @@ function fetchTransactionFeeEstimation(currentNetwork: any, limiter: PQueue) { } ), { + priority: 2, throwOnTimeout: true, } ); diff --git a/src/app/store/transactions/token-transfer.hooks.ts b/src/app/store/transactions/token-transfer.hooks.ts index 9eff1ed34c6..bd179df2880 100644 --- a/src/app/store/transactions/token-transfer.hooks.ts +++ b/src/app/store/transactions/token-transfer.hooks.ts @@ -17,6 +17,7 @@ import { uintCV, } from '@stacks/transactions'; +import { logger } from '@shared/logger'; import type { StacksSendFormValues, StacksTransactionFormValues } from '@shared/models/form.model'; import { stxToMicroStx } from '@app/common/money/unit-conversion'; @@ -86,65 +87,70 @@ export function useGenerateFtTokenTransferUnsignedTx(info: Sip10CryptoAssetInfo) return useCallback( async (values?: StacksSendFormValues | StacksTransactionFormValues) => { - if (!account) return; - - const functionName = 'transfer'; - const recipient = - values && 'recipient' in values - ? createAddress(values.recipient || '') - : createEmptyAddress(); - const amount = values && 'amount' in values ? values.amount : 0; - const memo = - values && 'memo' in values && values.memo !== '' - ? someCV(bufferCVFromString(values.memo || '')) - : noneCV(); - - const amountAsFractionalUnit = ftUnshiftDecimals(amount, info.decimals || 0); - const { - address: contractAddress, - contractName, - assetName, - } = getAssetStringParts(info.contractId); - - const postConditionOptions = { - amount: amountAsFractionalUnit, - contractAddress, - contractAssetName: assetName, - contractName, - stxAddress: account.address, - }; - - const postConditions = [makePostCondition(postConditionOptions)]; - - // (transfer (uint principal principal) (response bool uint)) - const functionArgs: ClarityValue[] = [ - uintCV(amountAsFractionalUnit), - standardPrincipalCVFromAddress(createAddress(account.address)), - standardPrincipalCVFromAddress(recipient), - ]; - - if (info.hasMemo) { - functionArgs.push(memo); - } + try { + if (!account) return; + + const functionName = 'transfer'; + const recipient = + values && 'recipient' in values + ? createAddress(values.recipient || '') + : createEmptyAddress(); + const amount = values && 'amount' in values ? values.amount : 0; + const memo = + values && 'memo' in values && values.memo !== '' + ? someCV(bufferCVFromString(values.memo || '')) + : noneCV(); + + const amountAsFractionalUnit = ftUnshiftDecimals(amount, info.decimals || 0); + const { + address: contractAddress, + contractName, + assetName, + } = getAssetStringParts(info.contractId); - const options = { - txData: { - txType: TransactionTypes.ContractCall, + const postConditionOptions = { + amount: amountAsFractionalUnit, contractAddress, + contractAssetName: assetName, contractName, - functionName, - functionArgs: functionArgs.map(serializeCV).map(arg => bytesToHex(arg)), - postConditions, - postConditionMode: PostConditionMode.Deny, - network, + stxAddress: account.address, + }; + + const postConditions = [makePostCondition(postConditionOptions)]; + + // (transfer (uint principal principal) (response bool uint)) + const functionArgs: ClarityValue[] = [ + uintCV(amountAsFractionalUnit), + standardPrincipalCVFromAddress(createAddress(account.address)), + standardPrincipalCVFromAddress(recipient), + ]; + + if (info.hasMemo) { + functionArgs.push(memo); + } + + const options = { + txData: { + txType: TransactionTypes.ContractCall, + contractAddress, + contractName, + functionName, + functionArgs: functionArgs.map(serializeCV).map(arg => bytesToHex(arg)), + postConditions, + postConditionMode: PostConditionMode.Deny, + network, + publicKey: account.stxPublicKey, + }, + fee: stxToMicroStx(values?.fee || 0).toNumber(), publicKey: account.stxPublicKey, - }, - fee: stxToMicroStx(values?.fee || 0).toNumber(), - publicKey: account.stxPublicKey, - nonce: Number(values?.nonce) ?? nextNonce?.nonce, - } as const; + nonce: Number(values?.nonce) ?? nextNonce?.nonce, + } as const; - return generateUnsignedTransaction(options); + return generateUnsignedTransaction(options); + } catch (error) { + logger.error('Failed to generate unsigned transaction', error); + return; + } }, [account, info.contractId, info.decimals, info.hasMemo, network, nextNonce?.nonce] ); diff --git a/src/app/ui/utils/get-asset-contract-address.ts b/src/app/ui/utils/get-asset-contract-address.ts new file mode 100644 index 00000000000..0f917b2f8e2 --- /dev/null +++ b/src/app/ui/utils/get-asset-contract-address.ts @@ -0,0 +1,14 @@ +/** + * getAssetContractAddress + * + * Gets the assets contract address of a string: contract_id or fully qualified asset name. + * + * @param value - the source string: [principal].[contract-name] or [principal].[contract-name]::[asset-name] + */ +export function getAssetContractAddress(value: string): string { + if (value.includes('.')) { + return value.split('.')[0]; + } + + return value; +} diff --git a/src/app/ui/utils/get-asset-string-parts.ts b/src/app/ui/utils/get-asset-string-parts.ts index a82b44ed3fb..9727837bf5b 100644 --- a/src/app/ui/utils/get-asset-string-parts.ts +++ b/src/app/ui/utils/get-asset-string-parts.ts @@ -1,3 +1,4 @@ +import { getAssetContractAddress } from './get-asset-contract-address'; import { getAssetName } from './get-asset-name'; import { getContractName } from './get-contract-name'; @@ -23,13 +24,13 @@ export const getAssetStringParts = ( // } // ); return { - address: fullyQualifiedName, + address: getAssetContractAddress(fullyQualifiedName), contractName: fullyQualifiedName, assetName: fullyQualifiedName, }; } - const address = fullyQualifiedName.split('.')[0]; + const address = getAssetContractAddress(fullyQualifiedName); const contractName = getContractName(fullyQualifiedName); const assetName = getAssetName(fullyQualifiedName); diff --git a/src/app/ui/utils/stacks-ft-utils.spec.ts b/src/app/ui/utils/stacks-ft-utils.spec.ts new file mode 100644 index 00000000000..99a461ea2a7 --- /dev/null +++ b/src/app/ui/utils/stacks-ft-utils.spec.ts @@ -0,0 +1,24 @@ +import { getAssetContractAddress } from './get-asset-contract-address'; + +const fullyQualifiedName = 'SP2KAF9RF86PVX3NEE27DFV1CQX0T4WGR41X3S45C.btc-monkeys-bananas::BANANA'; +const contractId = 'SP2KAF9RF86PVX3NEE27DFV1CQX0T4WGR41X3S45C.btc-monkeys-bananas'; + +const contractAddress = 'SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQW2S7D0'; + +describe('getAssetContractAddress', () => { + test('should return the contract address from a fully qualified asset name', () => { + const assetContractAddress = getAssetContractAddress(fullyQualifiedName); + + expect(assetContractAddress).toBe(contractAddress); + }); + + test('should return the contract address from a contract id', () => { + const assetContractAddress = getAssetContractAddress(contractId); + return expect(assetContractAddress).toBe(contractAddress); + }); + + test('should return the contract address from a contract address', () => { + const assetContractAddress = getAssetContractAddress(contractAddress); + expect(assetContractAddress).toBe(contractAddress); + }); +});