From 53eb27dd5f60813b56b12e9dfa416f3279a926c1 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:57:32 -0600 Subject: [PATCH 1/3] feat(swap-widget): broadcast api-supplied approval txs Use quote.approval.approvalTxs (sequential broadcast, wait for each receipt) instead of hand-encoding a single approve. Inherits the API's USDT-style reset handling - the previous single approve reverted on-chain for tokens requiring a zero-allowance reset whenever a prior exact approval was left unspent. Local encoding remains as fallback for API versions predating approvalTxs. Co-Authored-By: Claude Fable 5 --- .../swap-widget/src/hooks/useSwapApproval.ts | 56 ++++++++++++------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/packages/swap-widget/src/hooks/useSwapApproval.ts b/packages/swap-widget/src/hooks/useSwapApproval.ts index 7f3c9be7abb..6d60443bb95 100644 --- a/packages/swap-widget/src/hooks/useSwapApproval.ts +++ b/packages/swap-widget/src/hooks/useSwapApproval.ts @@ -45,6 +45,28 @@ export const useSwapApproval = () => { return } + if (!sellAmountBaseUnit || sellAmountBaseUnit === '0') { + actorRef.send({ type: 'APPROVAL_ERROR', error: 'No sell amount specified' }) + return + } + + // The API supplies ready-to-sign exact approvals in broadcast order, including a + // preceding approve(spender, 0) for tokens that require resetting a non-zero allowance + // (e.g. USDT); the local encode covers API versions predating approvalTxs + const approvalTxs = quote.approval.approvalTxs?.length + ? quote.approval.approvalTxs + : [ + { + to: sellAssetAddress, + data: encodeFunctionData({ + abi: erc20Abi, + functionName: 'approve', + args: [quote.approval.spender as `0x${string}`, BigInt(sellAmountBaseUnit)], + }), + value: '0', + }, + ] + const requiredChainId = getEvmNetworkId(sellAsset.chainId) const client = walletClient as WalletClient @@ -66,31 +88,25 @@ export const useSwapApproval = () => { rpcUrls: { default: { http: [] } }, } - if (!sellAmountBaseUnit || sellAmountBaseUnit === '0') { - actorRef.send({ type: 'APPROVAL_ERROR', error: 'No sell amount specified' }) - return - } - - const approvalData = encodeFunctionData({ - abi: erc20Abi, - functionName: 'approve', - args: [quote.approval.spender as `0x${string}`, BigInt(sellAmountBaseUnit)], - }) - - const approvalHash = await client.sendTransaction({ - to: sellAssetAddress as `0x${string}`, - data: approvalData, - value: BigInt(0), - chain, - account: walletAddress as `0x${string}`, - }) - const rpcUrl = chain.rpcUrls?.default?.http?.[0] const publicClient = createPublicClient({ chain, transport: rpcUrl ? http(rpcUrl) : http(), }) - await publicClient.waitForTransactionReceipt({ hash: approvalHash }) + + let approvalHash: `0x${string}` | undefined + for (const approvalTx of approvalTxs) { + approvalHash = await client.sendTransaction({ + to: approvalTx.to as `0x${string}`, + data: approvalTx.data as `0x${string}`, + value: BigInt(approvalTx.value), + chain, + account: walletAddress as `0x${string}`, + }) + await publicClient.waitForTransactionReceipt({ hash: approvalHash }) + } + + if (!approvalHash) throw new Error('No approval transactions to broadcast') actorRef.send({ type: 'APPROVAL_SUCCESS', txHash: approvalHash }) } catch (error) { From ca88dd1d0173babdedcb9f98d916f085ff4be830 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:25:36 -0600 Subject: [PATCH 2/3] fix(swap-widget): trust api approvalTxs, check receipt status, bump 0.7.0 The machine only enters approving when the api set isRequired, which contractually implies non-empty approvalTxs - the local encode fallback was unreachable on current api versions and silently reproduced the legacy exact-approve behavior (no USDT reset) against stale ones. Replace it with an explicit error. Throw when an approval receipt reverts: in the reset-then-approve flow an unchecked reverted reset would cascade into a second guaranteed revert and a false APPROVAL_SUCCESS. Co-Authored-By: Claude Fable 5 --- packages/swap-widget/package.json | 2 +- .../swap-widget/src/hooks/useSwapApproval.ts | 27 +++++++------------ 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/packages/swap-widget/package.json b/packages/swap-widget/package.json index cff327e709d..d3e1b74ed31 100644 --- a/packages/swap-widget/package.json +++ b/packages/swap-widget/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/swap-widget", - "version": "0.6.0", + "version": "0.7.0", "description": "Embeddable swap widget using ShapeShift API", "repository": "https://github.com/shapeshift/web", "license": "MIT", diff --git a/packages/swap-widget/src/hooks/useSwapApproval.ts b/packages/swap-widget/src/hooks/useSwapApproval.ts index 6d60443bb95..d667fcd9674 100644 --- a/packages/swap-widget/src/hooks/useSwapApproval.ts +++ b/packages/swap-widget/src/hooks/useSwapApproval.ts @@ -1,6 +1,6 @@ import { useEffect, useRef } from 'react' import type { WalletClient } from 'viem' -import { createPublicClient, encodeFunctionData, erc20Abi, http } from 'viem' +import { createPublicClient, http } from 'viem' import { getBaseAsset } from '../constants/chains' import { switchOrAddChain, VIEM_CHAINS_BY_ID } from '../constants/viemChains' @@ -50,22 +50,12 @@ export const useSwapApproval = () => { return } - // The API supplies ready-to-sign exact approvals in broadcast order, including a - // preceding approve(spender, 0) for tokens that require resetting a non-zero allowance - // (e.g. USDT); the local encode covers API versions predating approvalTxs - const approvalTxs = quote.approval.approvalTxs?.length - ? quote.approval.approvalTxs - : [ - { - to: sellAssetAddress, - data: encodeFunctionData({ - abi: erc20Abi, - functionName: 'approve', - args: [quote.approval.spender as `0x${string}`, BigInt(sellAmountBaseUnit)], - }), - value: '0', - }, - ] + // api-supplied approvals are exact and in broadcast order (reset-then-approve for USDT-likes) + const approvalTxs = quote.approval.approvalTxs + if (!approvalTxs?.length) { + actorRef.send({ type: 'APPROVAL_ERROR', error: 'No approval transactions in quote' }) + return + } const requiredChainId = getEvmNetworkId(sellAsset.chainId) const client = walletClient as WalletClient @@ -103,7 +93,8 @@ export const useSwapApproval = () => { chain, account: walletAddress as `0x${string}`, }) - await publicClient.waitForTransactionReceipt({ hash: approvalHash }) + const receipt = await publicClient.waitForTransactionReceipt({ hash: approvalHash }) + if (receipt.status !== 'success') throw new Error('Approval transaction reverted') } if (!approvalHash) throw new Error('No approval transactions to broadcast') From 252aec0888836a06ca0ea301353cdda0d586eb51 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:27:47 -0600 Subject: [PATCH 3/3] fix(swap-widget): accurate invariant message on post-broadcast hash check Co-Authored-By: Claude Fable 5 --- packages/swap-widget/src/hooks/useSwapApproval.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/swap-widget/src/hooks/useSwapApproval.ts b/packages/swap-widget/src/hooks/useSwapApproval.ts index d667fcd9674..e13d54d934d 100644 --- a/packages/swap-widget/src/hooks/useSwapApproval.ts +++ b/packages/swap-widget/src/hooks/useSwapApproval.ts @@ -97,7 +97,7 @@ export const useSwapApproval = () => { if (receipt.status !== 'success') throw new Error('Approval transaction reverted') } - if (!approvalHash) throw new Error('No approval transactions to broadcast') + if (!approvalHash) throw new Error('Expected at least one approval transaction') actorRef.send({ type: 'APPROVAL_SUCCESS', txHash: approvalHash }) } catch (error) {