Skip to content
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
23 changes: 16 additions & 7 deletions apps/agentic-chat/src/components/tools/useSwapExecution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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}`)
Expand Down
71 changes: 71 additions & 0 deletions apps/agentic-chat/src/utils/ensureAllowance.ts
Original file line number Diff line number Diff line change
@@ -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<string | undefined> {
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 })
}
Loading