diff --git a/apps/agentic-chat/src/components/tools/useSwapExecution.tsx b/apps/agentic-chat/src/components/tools/useSwapExecution.tsx index 5c60632a..446a883d 100644 --- a/apps/agentic-chat/src/components/tools/useSwapExecution.tsx +++ b/apps/agentic-chat/src/components/tools/useSwapExecution.tsx @@ -13,7 +13,8 @@ import { switchNetworkStep } from '@/lib/steps/switchNetworkStep' import type { StepStatus } from '@/lib/stepUtils' import { withWalletLock } from '@/lib/walletMutex' import type { SolanaWalletSigner } from '@/utils/chains/types' -import { executeApproval, executeSwap } from '@/utils/swapExecutor' +import { ensureAllowance } from '@/utils/ensureAllowance' +import { executeSwap } from '@/utils/swapExecutor' import { waitForConfirmedReceipt } from '@/utils/waitForConfirmedReceipt' export const SWAP_STEPS = { QUOTE: 0, NETWORK: 1, APPROVE: 2, SWAP: 3 } as const @@ -44,7 +45,7 @@ export const useSwapExecution = ( useExecuteOnce(ctx, swapData, async (data, ctx) => { await withWalletLock(async () => { try { - const { needsApproval, approvalTx, swapTx } = data + const { swapTx } = data if (!swapTx?.from) throw new Error('Invalid swap output: missing swapTx.from') if (!swapTx?.chainId) throw new Error('Invalid swap output: missing swapTx.chainId') @@ -76,12 +77,20 @@ export const useSwapExecution = ( // Step 1: Network switch await switchNetworkStep(ctx, sellAssetChainId) - // Step 2: Approve (skip if not needed) - if (needsApproval && approvalTx) { - ctx.setSubstatus('Requesting approval signature...') - const approvalTxHash = await executeApproval(approvalTx, { solanaSigner }) - ctx.setMeta({ approvalTxHash }) + // Step 2: Approve — re-check on-chain allowance to handle parallel swaps + ctx.setSubstatus('Checking allowance...') + const approvalTxHash = await ensureAllowance({ + sellAssetId: data.swapData.sellAsset.assetId, + sellAssetChainId: sellAssetChainId, + sellAssetPrecision: data.swapData.sellAsset.precision, + approvalTarget: data.swapData.approvalTarget, + sellAmountCryptoPrecision: data.swapData.sellAmountCryptoPrecision, + sellAccount: data.swapData.sellAccount, + solanaSigner, + }) + if (approvalTxHash) { + ctx.setMeta({ approvalTxHash }) if (chainNamespace === CHAIN_NAMESPACE.Evm) { ctx.setSubstatus('Waiting for confirmation...') await waitForConfirmedReceipt(Number(chainReference), approvalTxHash as `0x${string}`) diff --git a/apps/agentic-chat/src/utils/ensureAllowance.ts b/apps/agentic-chat/src/utils/ensureAllowance.ts new file mode 100644 index 00000000..3297c707 --- /dev/null +++ b/apps/agentic-chat/src/utils/ensureAllowance.ts @@ -0,0 +1,71 @@ +import { ASSET_NAMESPACE, fromAssetId, fromChainId } from '@shapeshiftoss/caip' +import { toBaseUnit } from '@shapeshiftoss/utils' +import { getPublicClient } from '@wagmi/core' +import { erc20Abi, encodeFunctionData } from 'viem' + +import { wagmiConfig } from '@/lib/wagmi-config' +import type { SolanaWalletSigner } from '@/utils/chains/types' +import { executeApproval } from '@/utils/swapExecutor' + +interface EnsureAllowanceParams { + sellAssetId: string + sellAssetChainId: string + sellAssetPrecision: number + approvalTarget: string + sellAmountCryptoPrecision: string + sellAccount: string + solanaSigner?: SolanaWalletSigner +} + +// Re-checks on-chain allowance and executes approval if needed. +// Returns the approval tx hash if an approval was sent, undefined otherwise. +export async function ensureAllowance(params: EnsureAllowanceParams): Promise { + const { + sellAssetId, + sellAssetChainId, + sellAssetPrecision, + approvalTarget, + sellAmountCryptoPrecision, + sellAccount, + solanaSigner, + } = params + + const { assetNamespace, assetReference } = fromAssetId(sellAssetId) + + // Native assets (slip44) don't need approval + if (assetNamespace !== ASSET_NAMESPACE.erc20) return undefined + + const tokenAddress = assetReference as `0x${string}` + const { chainReference } = fromChainId(sellAssetChainId) + const chainId = Number(chainReference) + + const publicClient = getPublicClient(wagmiConfig, { chainId }) + if (!publicClient) throw new Error(`No public client for chain ${chainId}`) + + const requiredAmount = BigInt(toBaseUnit(sellAmountCryptoPrecision, sellAssetPrecision)) + + const currentAllowance = await publicClient.readContract({ + address: tokenAddress, + abi: erc20Abi, + functionName: 'allowance', + args: [sellAccount as `0x${string}`, approvalTarget as `0x${string}`], + }) + + if (currentAllowance >= requiredAmount) return undefined + + const approveData = encodeFunctionData({ + abi: erc20Abi, + functionName: 'approve', + args: [approvalTarget as `0x${string}`, requiredAmount], + }) + + const approvalTx = { + chainId: sellAssetChainId, + data: approveData, + from: sellAccount, + to: tokenAddress, + value: '0', + } + + return executeApproval(approvalTx, { solanaSigner }) +}