fix(elastic-legacy): decode position fees by index, not named keys#3147
Draft
viet-nv wants to merge 77 commits into
Draft
fix(elastic-legacy): decode position fees by index, not named keys#3147viet-nv wants to merge 77 commits into
viet-nv wants to merge 77 commits into
Conversation
Set up the foundation for migrating off ethers.js: a central 'utils/viem.ts' re-exporting the viem primitives the codebase will lean on, a 'utils/migration.ts' bridge with 'bigNumberToBigIns't, 'bigIntToBigNumber', and a 'hashToTxResponse' shim for legacy 'TransactionResponse' callers, and a 'no-restricted-imports' ESLint rule (at 'warn') covering 'ethers', 'ethers/lib/utils', and the '@ethersproject/*' packages with per-module redirect messages.
Replace parseUnits, formatUnits, parseEther, formatEther, isAddress, namehash, keccak256/id, hexlify, arrayify, parseBytes32String, formatBytes32String, hexZeroPad, and getCreate2Address calls with their viem counterparts across hooks, services, state, and pages. BigNumber inputs are bridged through bigNumberToBigInt; viem bigint outputs are wrapped back via bigIntToBigNumber where downstream code still expects ethers types. Extends the central viem re-exports with getContractAddress, hexToString, namehash, and stringToHex.
Replace inline 'new Interface(...)' instances and 'Interface.encode/decode' calls with viem 'encodeFunctionData'/'decodeFunctionResult' across token, balance, approve, wrap, permit, claim, and elastic-legacy paths. Swap 'defaultAbiCoder.encode' for 'encodeAbiParameters(parseAbiParameters(...))', '@ethersproject/solidity keccak256' and 'solidityPack' for viem 'keccak256(encodePacked(...))', ethers 'Interface.parseLog' for 'decodeEventLog', and 'splitSignature' for 'parseSignature'. Extends the viem re-exports with 'decodeEventLog', 'encodePacked', 'parseAbiItem', 'parseSignature', and 'toEventSelector'.
Replace 'useTokenBalance' ethers Contract reads with 'usePublicClient' from wagmi and viem 'readContract' / 'getBalance'; 'balanceOf' and 'decimals' now fetch in parallel via 'Promise.all'. Replace 'MaxUint256' and 'AddressZero' from '@ethersproject/constants' with viem 'maxUint256' and 'zeroAddress'. Extends the viem re-exports with 'maxUint256', 'zeroAddress', and 'zeroHash'.
Reimplement , , and on top of wagmi / . The Redux multicall slice (reducer, polling updater, action creators) is removed; reads now go through TanStack Query's cache and viem's batched multicall. now takes an instead of an ethers ; and are replaced by their raw-ABI exports (, ). token-balance batching now calls viem via . Bigint values returned by viem are wrapped back to in the result so existing callers (, , JSBI passthrough) continue to work.
Replace the ethers subscription in the application updater with wagmi ; the local chainId/blockNumber state machine and 100ms debounce are no longer needed since wagmi keys its query by chainId and fires at most once per block. Migrate to read the native balance via , dropping the dependency on this path.
Replace in and the transactions updater with wagmi . viem / throw / when the tx isn't yet seen; convert those to to preserve the old ethers null-on-missing contract while letting genuine RPC errors propagate to the outer logger. For , which is a third-party helper typed against ethers, wrap the public client with (now exported) so the block-range scan runs over the wagmi transport instead of the wallet RPC. Map viem's field back to when feeding the fallback into .
Switch `useCurrentBlockTimestamp` and `useTransactionDeadline` to return
`bigint` and drop their ethers `BigNumber` dependency. Migrate the
remaining-balance check in `ProAmmPoolInfo`, the vote-power sort and
formatter in `Participants`, and the faucet reward state in `FaucetModal`
to native bigint arithmetic. Cascade the deadline shape change into
`TokenPair` and `ZapOut`: `deadline.toNumber()` → `Number(deadline)` and
`deadline.toHexString()` → `\`0x${deadline.toString(16)}\``.
Route transaction sending through a unified sendEVMTransaction that uses viem walletClient/publicClient via wagmi. The paymaster branch keeps ethers because @holdstation/paymaster-helper is typed against it. Inline ensureNotBlacklisted at the entry point (and in the ZkMe delegateTransaction) so the blacklist gate that the legacy useWeb3React proxy enforced still runs for every send. BlacklistedWalletError moves to utils/transactionError so both call sites can share it. Resolve effectiveChainId before the BASE_BUILDER_CODE check so callers that omit chainId still get the suffix on Base. Migrate the direct callers — ClaimButton, SelectTreasuryGrant, useSendToken, FarmLegacy, ProAmmFee, and the Earns submitTransaction helper — to the new entry point. submitTransaction now accepts isSmartConnector so Safe/AA wallets on Base produce the correct calldata; all ten Earns callers thread it through from useWeb3React.
Change the value parameter on sendEVMTransaction from ethers BigNumber to native bigint and update all callers to pass bigint literals or BigInt(...) conversions. The paymaster branch still forwards to ethers BigNumber for the SignerPaymaster SDK, but now derives it from the normalised txValue so the zero-handling logic is consistent with the non-paymaster path. In state/transactions/updater.tsx the viem TransactionReceipt already exposes gasUsed and effectiveGasPrice as bigint, so the BigNumber.from(0) fallback becomes 0n.
…Client Introduce and in src/utils/walletClient.ts. The first wraps 's and overrides so the Blackjack compliance check fires once at the EIP-1193 boundary for any signing method — mirroring the SIGNING_METHODS set previously enforced by the legacy Proxy. The second uses the gated client and strips from , which viem rejects when present. Migrate the remaining ethers signer / sites onto these helpers: useElasticLegacy remove-liquidity and collect-fee, RemoveLiquidityProAmm burn, both Elastic snapshot claim modals, the limit-order create/cancel and Smart Exit signing paths, the three Earns zap widget signTypedData callbacks, the three Campaign signMessage flows, usePermitNft V3 and V4, SelectTreasuryGrant's ZkMe delegate and Opt-Out flows, and useEstimateGasTxs. sendEVMTransaction's non-paymaster branch now uses the gated client; the inline call survives only in the paymaster branch, which routes through ethers Signer and bypasses viem. Add a entry forbidding raw from outside the wrapper file.
…ndEVMTransaction Migrate the ethers `Contract.method(args, overrides)` write paths still in app code to `sendEVMTransaction` + `encodeFunctionData`. The new entry point already handles estimateGas, gas margin, Base builder-code suffix, and the Blackjack gate, so each call site collapses to a single `sendEVMTransaction` invocation with the ABI-encoded calldata. Covered flows: token approvals (useApproveCallback), WETH wrap/unwrap (useWrapCallback), KyberDAO stake/unstake/migrate/delegate/undelegate and rewards claim/vote (kyberdao/index.tsx), Elastic legacy farm emergencyWithdraw (FarmLegacy.tsx), Elastic farm burnFromFarm with V1 /V2/V21 ABI selection (RemoveLiquidityProAmm), classic AMM removeLiquidity with the per-chain method ladder (TokenPair.tsx) and zapOut (ZapOut.tsx). The zap calculation view functions also move from \`zapContract.calculate*(...)\` to \`readContract\` against the same ABI, with the bigint result bridged back to BigNumber to keep the hook's public surface. Add \`paymasterGasMultiplier?: number\` on sendEVMTransaction: when set, the paymaster branch multiplies the raw \`estimateGas\` result by that value instead of running it through \`calculateGasMargin\`. The approval flow passes \`2\` here, preserving the historical 2x bump that the SignerPaymaster-helper integration needs for ERC20 approvals. TotalSupply.ts uses the now-bigint multicall result via a 0n-aware check; useRewards exposes \`claim()\` as \`Promise<string | undefined>\` to match the honest tx-hash return.
…leaves
Migrate the remaining non-foundation BigNumber and ethers-type usages to
native bigint and viem-aligned shapes. The bridge layer (useContract,
getContract, useEthersProvider, sendTransaction's paymaster branch, the
useWeb3React proxy in hooks/index.ts) is intentionally kept for now —
those depend on third-party ethers contracts that need their own pass.
\`utils/formatBalance.ts\` and \`utils/numbers.ts\` lose their BigNumber
imports: \`getFullDisplayBalance\` / \`formatUnitsToFixed\` accept \`bigint |
string\`, and \`formatDisplayNumber\`'s \`FormatValue\` union drops BigNumber
(the bigint branch already covers integer math). All callers updated
accordingly (KyberDAO inputs, vote page, position list).
\`types/position.ts\` switches PositionDetails to bigint, and the
multicall bridge in \`state/multicall/hooks.ts\` tightens its \`MethodArg\`
union to \`string | number | bigint\` so the type matches what
\`isMethodArg\` actually accepts at runtime. The Elastic-legacy /
RemoveLiquidityProAmm / ProAmmFee flows consume position fields as
bigint, and the parent \`tokenId\` extraction in RemoveLiquidityProAmm
parses via \`BigInt(tokenId)\` and forwards \`tokenId\` directly to the
multicall args.
\`useZap.calculateZapOutAmount\` and \`useZapOutAmount\` move to bigint
end-to-end, eliminating the BigNumber bridge that \`state/burn/hooks.ts\`
was using for \`lpQty\`. \`usePermitNft\` exposes its nonce as bigint;
\`useApproveCallback\`, \`useClaimReward\`, \`useSwapCallbackV3\`,
\`useTokenBalance\`, \`useEstimateGasTxs\`, and the \`kyberdao\` write
helpers drop their leftover BigNumber/TransactionResponse parameter and
return types in favour of bigint or \`{ hash: string }\` shapes. The
useClaimReward subtraction wraps the on-chain \`res[0]\` BigNumber with
\`.toString()\` before \`BigInt()\` to mirror the existing kyberdao
precedent.
The state/transactions adder narrows its tx pick to a small \`LegacyTx\`
shape so it no longer pulls from \`@ethersproject/abstract-provider\`,
and coerces the viem \`to: string | null\` field to \`undefined\` for the
Redux action; the Earns \`submitTransaction\` helper and
\`navigateToPositionAfterZap\` accept structural \`LegacyWeb3Provider\` /
\`ReceiptProvider\` shapes built locally instead of importing the ethers
Web3Provider type.
…k scan Drop the find-replacement-tx package and the ethers Provider shim that wrapped the viem public client just to feed that helper. Replacement detection now runs entirely against the viem public client: when a pending tx's nonce has been overtaken on chain, scan the last 256 blocks since sentAtBlock for a transaction from the same sender with the same nonce. A self-transfer with empty data is treated as a wallet-side cancellation (matches the legacy \`TxValidationError\` "Transaction canceled." path); a different hash is treated as a speed- up/replace and dispatched through replaceTx. If no candidate is found, fall through to the existing nonce-advancement checkRemoveTxs path. Block scan is bounded to keep RPC pressure low on long-pending transactions. The dependency is removed from package.json.
…viem readContract
Migrate the remaining \`contract.method(args)\` call sites that hit ethers
Contract instances directly: 17 reads across 8 files, plus one consumer
cleanup that drops a now-redundant BigNumber bridge.
The pattern is uniform — \`await contract.fn(args)\` becomes
\`await readContract(wagmiConfig, { address, abi, functionName, args, chainId })\`
(or the imperative \`multicall(...)\` equivalent for batched lookups).
Coverage:
- pages/Earns/utils/fees.ts: 6 sites covering NFT manager \`ownerOf\` /
\`positionInfo\` and StateView \`getPositionInfo\` / \`getFeeGrowthInside\`,
plus the static \`collect(...)\` simulation. Switched from
\`getNftManagerContract\` / \`getReadingContractWithCustomChain\` to the
bare address + ABI from \`EARN_DEXES[dex]\`. The univ4 fee math drops
the BigNumber bridge in favour of native bigint arithmetic.
- pages/Earns/PositionDetail/index.tsx: NFT manager \`ownerOf\` against
the position's own chainId.
- hooks/kyberdao/index.tsx: 4 read sites (\`isValidClaim\`,
\`totalSupply\`, \`getMerkleData\`, \`getClaimedAmounts\`) against the
rewards distributor and KNC contracts on mainnet.
- hooks/useClaimReward.ts: 2 reads (\`getClaimedAmounts\`,
\`isValidClaim\`) plus the \`claim()\` write routed through
\`sendEVMTransaction\` + \`encodeFunctionData\`. The promise chain
collapses to async/await.
- hooks/useApproveCallback.ts: ERC20 \`allowance\` read.
- hooks/Tokens.ts: ERC20 \`name\`/\`symbol\`/\`decimals\` lookup migrated
from a hand-rolled \`tryBlockAndAggregate\` to viem
\`multicall(..., { allowFailure: true })\`, with a guard that drops the
token entirely if any leg fails so a malformed Token never reaches
the SDK constructor.
- hooks/useElasticLegacy.ts: kept the custom \`tryBlockAndAggregate\`
pattern (heterogeneous pre-encoded calls) but routed it through
\`readContract\` against \`MULTICALL_ABI\`.
- pages/ElasticSnapshot/components/InstantClaimModal.tsx: parallel
\`claimed(...)\` reads across Mainnet / Optimism / Polygon / Avalanche;
each call routes to its own \`chainId\` via wagmi's transport, and the
merkle leaf index is wrapped in \`BigInt(...)\` for ABI consistency.
- pages/KyberDAO/StakeKNC/index.tsx: drops the \`bigNumberToBigInt\`
wrapper since \`totalMigratedKNC\` is now native bigint.
…to feat/wagmi-migration
…ulticall bridge
Replace the ethers \`Contract\` instance produced by \`useReadingContract\` /
\`useSigningContract\` with a lightweight \`ContractRef = { address: Address,
abi: Abi }\`. The multicall bridge in \`state/multicall/hooks.ts\` now
consumes the ref directly — no more \`interface.format('json')\` round-trip
to extract the ABI — and the type is imported from \`hooks/useContract\` so
there is a single source of truth.
\`useSigningContract\` keeps its \`account\` gate (returns \`null\` when
disconnected) so existing \`if (!contract) return\` call sites still behave
the same. \`useReadingContract\`'s third \`customChainId\` argument is a
no-op carried for source compatibility; every cross-chain caller already
routes through \`readContract({ chainId })\` directly.
Migrate the last three React-side \`contract.method(args)\` sites still
hitting ethers:
- pages/ElasticSnapshot/components/Vesting.tsx: parallel reads of
\`claimed\`, \`vestingStartTime\`, and \`vestingEndTime\` move to viem's
\`readContract\`, pinned to \`ChainId.MATIC\`. The decoded \`bigint\`
results are coerced via \`Number(...)\` for the unix-timestamp state,
safe since the values fit well under \`Number.MAX_SAFE_INTEGER\`.
- pages/RemoveLiquidity/TokenPair.tsx / ZapOut.tsx:
\`pairContract.nonces(account)\` moves to \`readContract\` against the
existing pair ABI. The returned bigint is hex-encoded inline
(\`\`\`0x\${nonce.toString(16)}\`\`\`) for the EIP-712 permit payload.
\`usePermitNft\`'s \`NFT_PERMIT_ABI\` is wrapped in viem's \`parseAbi\` so the
human-readable function signatures are converted to the object-form
AbiItem array the multicall bridge needs to discover \`nonces\`,
\`positions\`, \`name\`, \`DOMAIN_SEPARATOR\`, and \`PERMIT_TYPEHASH\` via
\`findFunctionItem\`. Without this, all five \`useSingleCallResult\` reads
on the permit hook would silently short-circuit to \`INVALID_CALL_STATE\`.
…cProvider Remove \`utils/getContract.ts\` (ethers \`Contract\` factory plus the classic-router / zap helpers) and \`constants/providers.ts\` (\`AppJsonRpcProvider extends StaticJsonRpcProvider\`). Nothing in the app constructs ethers contracts or read-side \`JsonRpcProvider\` instances any more. Migrate the last two consumers: - components/swapv2/LimitOrder/ListOrder/useRequestCancelOrder.tsx: \`useGetEncodeLimitOrder\` swaps \`getReadingContract(...).nonce(account)\` for viem \`readContract\` against \`LIMIT_ORDER_ABI\`, pulling \`chainId\` from \`useActiveWeb3React\` and dropping the \`useKyberSwapConfig\` \`readProvider\` dependency entirely. The hard-cancel send path now coerces the returned bigint via \`Number(nonce as bigint)\` for the numeric \`nonce\` slot on the cancel payload. - pages/Earns/hooks/useCollectFees.tsx: the existence gate switches to \`getNftManagerContractAddress(dex, chainId)\` plus an explicit \`EARN_DEXES[dex].nftManagerContractAbi\` check, matching the original two-part guard from the deleted \`getNftManagerContract\` helper. \`pages/Earns/utils/index.ts\` drops the \`getNftManagerContract\` export along with its \`getReadingContractWithCustomChain\` import. \`state/application/hooks.ts\` removes the \`cacheCalc\` rpc-cache helper and the \`readProvider\` field from \`useKyberSwapConfig\`'s return; no remaining caller consumed \`readProvider\`. \`services/ksSetting.ts\` mirrors the change by deleting \`readProvider: AppJsonRpcProvider | undefined\` from \`KyberSwapConfig\`.
…m app code
Replace the last three places that consumed \`useWeb3React().library\`
directly (outside of the paymaster path) with viem-native equivalents.
- pages/Earns/components/ClaimModal/index.tsx and ClaimAllModal/index.tsx:
the \`library.listAccounts()\` "is a wallet connected" probe is replaced
by a truthiness check on \`useActiveWeb3React().account\`. The hook
switches from \`useWeb3React\` to \`useActiveWeb3React\` to expose the
wagmi-resolved address, and the dependency arrays are updated
accordingly. The chain-mismatch / not-connected branch still arms
\`autoClaim\` and triggers \`changeNetwork\` exactly as before.
- pages/Oauth/Login.tsx: SIWE sign-in moves from
\`provider.getSigner().signMessage(message)\` to
\`getGatedWalletClient({ chainId }).signMessage({ account, message })\`.
Routing through the gated wallet client also runs the existing
Blackjack \`ensureNotBlacklisted\` check before the user can sign the
SIWE message — previously the SIWE path went straight through ethers
and bypassed that gate. The \`useWeb3React\` import is removed since no
other call site in this file needed it.
…aster path Add \`calculateGasMarginBigInt(value, chainId)\` alongside the existing BigNumber-typed \`calculateGasMargin\`, replicating the same formula (\`total = estimate + max(20k, 20% * estimate)\`, 50% on Polygon and Optimism) in native bigint arithmetic. \`sendEVMTransaction\`'s non-paymaster branch routes its viem \`estimateGas\` result (already bigint) through the new helper directly and forwards it as \`gas: gasLimit\` to viem's \`sendTransaction\`, eliminating the previous \`bigIntToBigNumber → calculateGasMargin → BigInt(toString())\` round-trip. The paymaster branch keeps using the BigNumber variant because \`@holdstation/paymaster-helper\` consumes \`PopulatedTransaction.gasLimit: BigNumber\`. The \`bigIntToBigNumber\` import is no longer needed in \`sendTransaction.ts\` and is dropped from the \`migration.ts\` re-exports list this file pulls from.
…l and TransactionError
Remove \`utils/migration.ts\` (\`bigNumberToBigInt\`, \`bigIntToBigNumber\`,
\`hashToTxResponse\`) and the \`wrapBigInts\` helper in the multicall layer
that wrapped every viem-decoded \`bigint\` back as an ethers \`BigNumber\`
for legacy callers. Multicall reads now expose native \`bigint\` for
uint256/int256 fields end-to-end.
Update the consumers that were relying on the bridge:
- hooks/usePools.ts: \`slot0.sqrtP === 0n\` (the bigint truthiness check
also collapses with \`!slot0.sqrtP\`), and Pool's \`BigintIsh\` args are
fed decimal strings via \`(value as bigint).toString()\`. The current
tick uses \`Number(slot0.currentTick)\`.
- hooks/useProAmmPositions.ts: \`result.pos.<field> as bigint\` for the
uint256-shaped fields (\`feeGrowthInsideLast\`, \`nonce\`, \`liquidity\`,
\`rTokenOwed\`), and \`Number(balanceResult[0] as bigint)\` for the
account balance read. Token IDs are returned as \`bigint\` directly.
- hooks/usePermit.ts: \`Number(tokenNonceState.result[0] as bigint)\` for
the EIP-2612 nonce slot.
- hooks/useTracking.ts: \`formatUnits(gas_price as bigint, ...)\` and
\`Number(actual_gas as bigint)\` for the SWAP_COMPLETED analytics
payload, which now matches the native \`bigint\` shape that the
transactions updater writes for \`receipt.gasUsed\` /
\`effectiveGasPrice\`.
- data/Reserves.ts: \`JSBI.BigInt(feeInPrecision.toString())\` and
\`JSBI.BigInt(amp[0].toString())\` because JSBI's constructor does not
accept native \`bigint\` directly.
- pages/KyberDAO/StakeKNC/StakeKNCComponent.tsx: the "max" / "half"
button divides the balance using native bigint division (\`balance /
(half ? 2n : 1n)\`) and formats the result with viem's \`formatUnits\`.
utils/sendTransaction.ts inlines \`{ hash } as TransactionResponse\` in
place of \`hashToTxResponse\` — all consumers only read \`.hash\`, so the
old \`.wait()\` shim was dead weight.
utils/transactionError.ts narrows \`TransactionError.rawData\` from the
ethers \`Deferrable<TransactionRequest>\` to a local structural
\`TransactionErrorRawData = { from?, to?, data?, value?, gasLimit?,
accessList? }\`, dropping \`@ethersproject/abstract-provider\` and
\`ethers/lib/utils\` imports.
Remove the Holdstation paymaster gas-token feature end-to-end:
the GasTokenSetting UI, GAS_TOKENS list, paymentToken Redux state and
usePaymentToken hook, paymaster rows in SwapForm/SlippageSettingGroup
and SwapModal/SwapDetails, GasTokenSetting refs across SwapV3 pages,
and the GAS_TOKEN_CHANGED tracking event.
Simplify sendEVMTransaction to take { account, contractAddress,
encodedData, value: bigint, errorInfo, isSmartConnector, chainId } and
run gas estimation + submission through viem getPublicClient /
getGatedWalletClient. Drop the BigNumber-based calculateGasMargin and
keep only calculateGasMarginBigInt.
Drop the useWeb3React.library Proxy wrapper, SIGNING_METHODS array,
and useEthersProvider helper. Migrate the remaining library consumers
to viem: typed-data signing via signTypedDataSafe and walletClient
.signTypedData; personal_sign via walletClient.signMessage;
wallet_addEthereumChain via walletClient.request; transaction receipt
lookup via publicClient.getTransactionReceipt; account presence checks
via the wagmi-resolved account.
Change Earns submitTransaction to take { account, chainId, txData,
onError, isSmartConnector } and getTokenId to take (chainId, hash,
dex), updating all callers (kyberdao, useApproveCallback, useClaim
Reward, useElasticLegacy, useWrapCallback, useSwapCallbackV3, Earns
hooks, RemoveLiquidity pages, ElasticSnapshot modals, Campaign hooks,
MarketOverview, Oauth, and friends).
Guard MigrateLiquidityDescription's getTokenId effect on transaction
success so it no longer fires for pending or failed transactions.
Remove ethers, @holdstation/paymaster-helper, and the eleven
@ethersproject/* sub-packages from apps/kyberswap-interface
dependencies.
Remove (walletClient as any) and (publicClient as any) wrappers from viem action calls that the wagmi-resolved client types already accept: walletClient.signTypedData (signTypedDataSafe, usePermitNft V3/V4, InstantClaimModal, VestingClaimModal), walletClient.signMessage (Oauth/Login, ChooseGrantModal, SelectTreasuryGrant, useFavoritePool, MarketOverview/TableContent, JoinReferral, useSafePalCampaignJoin, useRaffleCampaignJoin), walletClient.sendTransaction (sendEVMTransaction and the zkMe bridge in SelectTreasuryGrant), and publicClient.getGasPrice (useEstimateGasTxs, useSendToken). For publicClient.estimateGas, viem's chain-specific argument union genuinely exceeds the TS instantiation depth at the wagmi-resolved client. Cast through the base PublicClient type instead of any so the action argument shape is still type-checked. Applied in sendEVMTransaction, useEstimateGasTxs, and the WalletPopup SendToken gas-fee preview. In SelectTreasuryGrant's zkMe delegateTransaction bridge, type the forwarded tx.to as Address and tx.data as Hex now that the cast is gone, and drop the redundant \`hash as string\` return.
Tighten doc comments in walletClient, sendTransaction, transactionError, viem, and useContract to describe the current behavior only, removing references to the prior ethers Web3Provider Proxy, the useWeb3React.library API, and the ongoing migration framing. The behavioral contracts (Blackjack gate at the EIP-1193 boundary, the EIP712Domain strip in signTypedDataSafe, the account-gated null return in useSigningContract, the central viem re-export module) are preserved; only the historical "legacy"/"post-migration" context that no longer aids a fresh reader is removed.
…to feat/wagmi-migration
Restore the chain-mismatch guard the previous useWeb3React Proxy
enforced: the gated walletClient now compares the wallet's actual
chainId from getAccount() against the requested opts.chainId and
throws ChainMismatchError on any signing or sending method when they
diverge.
Close the lint gap that let CrossChainSwap reach for wagmi's raw
useWalletClient: ban that import in .eslintrc.js and add a
useGatedWalletClient hook that installs the Blackjack gate on the
reactive wagmi walletClient. Wire it into ConfirmationPopup and the
cross-chain swap hook so adapter executeSwap paths use the gated
client. Source the hook's chainId from useActiveWeb3React (Redux's
expected chain) so the new mismatch guard fires consistently, and
catch promise rejections from getGatedWalletClient.
Thread customChainId through ContractRef into the multicall hooks:
useReadingContract now stores chainId on the ref and the wagmi
useReadContract / useReadContracts calls pass it through. Fix the
hardcoded ChainId.MAINNET in buildRef by switching to viem's
chain-agnostic isAddress.
Tolerate partial multicall failure in useFetchERC20TokenFromRPC:
only decimals is hard-required; name and symbol fall back to '' so
legacy ERC20s without name() or symbol() (e.g. bytes32 returns) no
longer drop the token entirely.
Other touch-ups in the same pass:
- drop the redundant \`response = await sendApprove(0n)\` assignment;
the 0-approval reset does not add a transaction entry
- omit EIP712Domain from usePermit's typed-data construction since
signTypedDataSafe strips it before forwarding to viem
- delete two console.log debug statements left in usePermitNft
Type ABIs once at the import site: drop the per-call \`as Abi\` casts
in favour of a single typed barrel module (\`constants/abis/index.ts\`)
that re-exports all 33 ABI JSONs as viem \`Abi\`, plus the four
Hardhat-artifact wrappers as \`{ abi: Abi; [k: string]: unknown }\`.
Inline the previous erc20 / dmmPool / argent-wallet-detector wrapper
modules into the same barrel so the abi layer is one file. Consumers
import named exports from 'constants/abis'.
Strip ~20 redundant \`chainId as number\` casts at call sites where
the receiver is an internal helper typed \`chainId: number\`
(getGatedWalletClient, signTypedDataSafe, getTokenId,
getOrCreateSignature, etc.). Keep the cast where wagmi's chainId is
the literal union of registered chains.
Rewrite every ./ and ../ import in the branch's touched files to
the baseUrl: 'src'-relative form (e.g. 'utils/viem',
'hooks/useContract', 'pages/Earns/utils'). Drops the /index
suffix where TypeScript module resolution treats 'X' and 'X/index'
as equivalent (matching the dominant project convention). Keeps
'constants/index' explicit because Node.js ships a built-in
constants module that would shadow the local one at baseUrl
resolution. Touches sibling-import from './foo' patterns and
dynamic import('./bar') calls alike.
Stop the RemoveLiquidity multi-method retry loop from re-prompting the wallet on send-failure: only fall back to the next method when the error is a TransactionError of type 'estimateGas' (no wallet prompt has happened yet). If sendTransaction itself fails — most commonly user rejection — bail out instead of popping a second wallet dialog for the supportingFeeOnTransferTokens variant. Applied in TokenPair.tsx and ZapOut.tsx. Stop appending BASE_BUILDER_CODE to gas-refund claim calldata for smart-wallet users in ClaimButton: re-derive isSmartConnector from useWeb3React and pass it through to sendEVMTransaction instead of the hardcoded false. Make useTransactionAdder resolve the publicClient per-tx against desiredChainId, not the static active chainId. Cross-chain transactions previously looked up to/nonce/data on the wrong chain and came back empty, breaking the replacement-tx detection in state/transactions/updater.tsx (it gates on sentAtBlock && from && nonce !== undefined). Keep the EIP-2612 nonce as bigint through usePermit's message — Number(BigInt) silently truncates for nonces > 2^53 and would produce wrong permits for unusual tokens. Smaller hygiene: - drop account.toLowerCase() as Address in usePermitNft (V3/V4); viem already accepts the checksummed form - drop redundant chainId && guard in sendEVMTransaction's access-list path (chainId is now non-optional) - document why buildRef rejects the zero address
… refetch Normalize viem getTransactionReceipt's status: 'success' | 'reverted' into the numeric 0 | 1 that the rest of the app already expects (SerializableTransactionReceipt.status: number, getTransactionStatus's receipt?.status === 1 check). Without this, every successful tx fired NotificationType.ERROR, swap output / revokePermit / analytics tracking that branched on receipt.status === 1 never ran, and WalletPopup showed every confirmed tx as failed. Match the Safe path which was already normalizing via txStatus === SUCCESS ? 1 : 0. Restore per-block refetch for wagmi-backed multicall hooks: the application updater now invalidates the readContract / readContracts query caches every time the Redux block number advances. The default TanStack Query staleTime: 0 only refetches on focus / reconnect, so balances, pool reserves, positions, and gas-refund eligibility went stale right after mount. Return INVALID_CALL_STATE (not LOADING_CALL_STATE) in useSingleContractMultipleData when the contract is null — chains without that contract (e.g. oldStaticContract factory) previously got stuck reporting loading: true forever, mirroring the bug pattern useSingleCallResult had already fixed. Cache decimals per (chainId, address) in useTokenBalance: the value is immutable, so the balance refresh after every tx now skips a redundant RPC call. Stop dropping the reset-to-zero approval tx from useApproveCallback: USDT-style tokens that require approve(0) before a new non-zero amount now surface in the wallet history under TRANSACTION_TYPE.APPROVE. Use === undefined to gate the V4 NFT permit readiness check in usePermitNft: a fresh account legitimately returns a 0n nonce bitmap, which the previous falsy guard treated as "not ready". Swap the ethers-shape error.code === 4001 rejection branch in useSafePalCampaignJoin for didUserReject, which already handles viem's UserRejectedRequestError. Smaller hygiene: - drop the redundant value && in sendEVMTransaction's txValue derivation (0n is falsy on its own) - drop the unreachable (error as any)?.transactionHash recovery branch in sendEVMTransaction's catch — viem's TransactionExecutionError doesn't surface transactionHash - pass hash: response.hash explicitly to addTransactionWithType in useRequestCancelOrder instead of spreading the whole response - promote the ethers-import ESLint guardrail from warn to error now that the codebase is clean
…to feat/wagmi-migration
…IP712Domain typehash Pass the permit's EIP712Domain spec explicitly in `types` and switch from signTypedDataSafe to signTypedDataRaw so the payload reaches the wallet unchanged. viem's auto-derived EIP712Domain was being built from the non-standard `domain` key order, producing a typehash that doesn't match the permit contract's on-chain domain separator — strict wallets reject it by returning a malformed (s=0) signature, which parseSignature then crashes on with "expected valid s/r: ... got 0". Other wallets sign the wrong digest and ecrecover lands on a phantom address. Reorder the domain fields to the canonical EIP-712 layout (name, version, chainId, verifyingContract) and stringify the nonce as hex so JSON.stringify in signTypedDataRaw doesn't choke on bigint. Wrap parseSignature in a guard so any future malformed response is translated into "Invalid permit signature", which friendlyError maps to a user-readable "Invalid Permit Signature" toast instead of the generic "An error occurred".
…to feat/wagmi-migration
Drop the @ethersproject/bignumber import and the BigNumber.from wrapper in PoolRewardsInfo. `reward.amount` is already a string and the helper accepts `bigint | string`, so the wrapper added a dependency the project otherwise forbids (no-restricted-imports flags @ethersproject/* and tsc can't resolve the module).
Pass the account in lowercase form to so strict EIP-712 implementations (OKX, Coinbase, SafePal, Binance) accept the request. With the checksummed address these wallets return a malformed (s=0) signature that crashes parseSignature; MetaMask and Rabby tolerate either form, which masks the issue locally.
… to actions walletClient.sendTransaction was failing on chains that opt into the eth_createAccessList path (currently only Monad). viem omits `type` from the eth_sendTransaction payload when only `accessList` is set, so the MetaMask Connect SDK infers type=0x1 (EIP-2930) from the access list and then rejects the tx because the wallet still attaches EIP-1559 fee fields (maxFeePerGas / maxPriorityFeePerGas). Pass `type: 'eip1559'` alongside `accessList` so the envelope is consistent. Also fix the getGatedWalletClient proxy. wagmi's getWalletClient calls \`client.extend(walletActions)\` BEFORE we mutate \`client.request\`, so every action closure (sendTransaction, signTypedData, ...) captures the pre-mutation client and dispatches through its original, ungated request. The Blackjack and chain-mismatch checks bound to gatedRequest silently did nothing for signing methods. Re-extend with walletActions after the mutation so action closures pick up the gated request.
The Porto connector hardcodes its supported chain list and throws on connect with "Could not find a compatible Porto chain on the given chain configuration" whenever the dapp is on a chain it doesn't support (e.g. Monad). The previous onError swallowed this into console.log, so the user clicked Porto and saw nothing happen. Match the chain-incompat error message in useConnect's onError and surface an ERROR notification naming the wallet and the chain, so the user knows to switch chains or pick a different wallet.
…endlyError
The shared `friendlyError` parsers in @kyber/utils, swap-widgets and
pancake-liquidity-widgets all matched any error containing
"insufficient" with the slippage-focused INCREASE_SLIPPAGE copy. When
the wallet rejected a tx with "insufficient funds for gas * price +
value", users were told to increase max slippage instead of being told
their balance couldn't cover gas.
Add a dedicated branch that catches the funds-related variants
("insufficient funds", "insufficient balance for transfer",
"outoffund") before the generic `insufficient` branch and returns
"Your current balance falls short of covering the required gas fee."
Add a matching Lingui translation entry for the new message so the
in-app modal stays localized.
Porto, Safe and other smart-wallet connectors produce EIP-1271 contract
signatures, but Uniswap V3/V4's NFT `permit()` validates via ecrecover.
ecrecover returns a different address than the smart account, so the
on-chain owner check reverts ("NOT_AUTHORIZED") and the widget's
estimateGas surfaces a "Failed to build zap route" toast on Zap Out /
Zap In / Zap Migration.
Pass `signTypedData: undefined` to the widget when `isSmartConnector` is
true. The shared `usePermitNft` hook gates `permitState` on
`typeof signTypedData === 'function'`, so it resolves to NOT_APPLICABLE
and the widget falls back to the regular NFT approve flow, which the
smart wallet's bundler handles correctly.
…t_addEthereumChain Extend the chain-incompat notification handler so it also fires when the connector throws "<wallet> does not support 'wallet_addEthereumChain' method" — Compass and similar single-chain extensions reject the post-pair switchChain that way, which wagmi then wraps inside a UserRejectedRequestError. Walk the error's `cause` chain so the underlying message is reachable even after the wrap. Both this pattern and the previous Porto-style "compatible chain on the given chain configuration" message now surface the same "Wallet not supported on this chain" toast naming the wallet and chain, instead of failing silently.
The submitTransaction onError callbacks in useKemRewards, useClaimMerklRewards and useCollectFees were notifying with the raw viem error.message, which dumps "Request Arguments: from/to/data/gas/ Details/Version" into the toast — most visibly on user-reject for the claim-all-reward flow on /earn/positions. Wrap the error with `friendlyError` so the toast shows "User rejected the transaction." (and the other known patterns) instead of the multi-line viem payload.
`BodyWrapper` has `position: relative; z-index: 1`, which creates a CSS stacking context. Notification toasts (Popups) rendered inside it had their z-index capped at that context, so modal portals mounted on `document.body` rendered on top of them — most visibly when a tx error toast fired while a confirmation modal was open. Hoist `<Popups />` out of `BodyWrapper` so it sits at the app root alongside modal portals. Also bump `POPUP_NOTIFICATION` above the `MODAL` z-index so the ordering between toast and modal at the root level is unambiguous.
Smart Exit relies on Uniswap V3/V4 NFT permit, which verifies via
ecrecover and so can't accept EIP-1271 signatures from smart wallets
(Porto, Safe, etc.). usePermitNft would crash on parseSignature with
"Invalid yParityOrV value" because the smart wallet returned an
EIP-1271 blob instead of an ECDSA r/s/v.
Gate permitState to NOT_APPLICABLE for smart connectors so the hook
never reaches the signing path. Replace the Smart Exit setup modal
with an explanatory view for smart wallets ("Smart Exit unavailable
with your current wallet …") plus a Switch wallet button that dismisses
the modal and opens the wallet picker. Keep the Confirmation guard as a
defensive fallback.
SafePal hardware fails approve / swap with "(-104) show tx info failed" because viem's walletClient.sendTransaction for json-rpc accounts ships a minimal payload (from, to, data, gas) and relies on the wallet to fill in `type`, `maxFeePerGas`, `maxPriorityFeePerGas`. Software wallets auto-fill, hardware wallets don't, so the regression only hits SafePal-class devices. Call publicClient.prepareTransactionRequest with the `fees` and `type` parameter set ahead of sendTransaction, then forward the resolved EIP-1559 fees and `type: 'eip1559'` into the payload. Match ethers v5 `signer.populateTransaction` behavior without touching nonce, which hardware wallets should still source themselves. Fall back to the bare payload when fee preparation fails so legacy / RPC-quirk chains still go through.
… wallets The previous fix relied on viem's `prepareTransactionRequest` to inject EIP-1559 fees — that path can silently fall through (RPC quirks, chain config mismatch) and viem's `walletClient.sendTransaction` strips chainId from the wire payload either way. SafePal still saw an under-populated tx and kept failing with "(-104) show tx info failed". Bypass viem's wallet action: read fees from `publicClient.estimateFeesPerGas()`, then hand a fully ethers-shaped payload (from, to, data, gas, chainId, value, accessList, type=0x2, maxFeePerGas, maxPriorityFeePerGas) to `walletClient.request` directly. The wallet still owns the nonce so the device's counter stays authoritative. Centralized in sendEVMTransaction, so it covers swap, ERC20 approve and the Zap widgets' NFT approve / approve-all paths too.
…to feat/wagmi-migration
`BASE_BUILDER_CODE` was being appended to every Base tx, including ERC20 / ERC721 approves. Hardware wallets (notably SafePal) decode approve calldata via strict ABI and reject anything with trailing bytes outside the expected `approve(address,uint256)` layout — the device renders "(-104) show tx info failed". The issue only surfaces on tokens SafePal has a rich label for (WETH), generic tokens fall back to a lenient "contract interaction" view that tolerates the extra bytes. Builder-code attribution only matters for the swap/router call anyway, so suffix it only when the selector isn't an approve variant (`0x095ea7b3`, `0xa22cb465`). Swap calldata still carries the code; the Earn zap widgets' NFT approve / approve-all paths also benefit from this since they go through the same helper.
Monad is the only chain that opts into `eth_createAccessList`, so every tx ends up as EIP-1559 type-2 with an access list. SafePal hardware can't sign that combo cleanly — the device either bails at preview or signs over the wrong payload, and the broadcasted tx reverts. Software wallets (Rabby, MetaMask) handle the combo fine, so the regression is visible only on the hardware path. Resolve the active connector at submit time and skip the access-list build for connectors known to choke on it (SafePal). The SafePal user loses Monad's gas refund on accessList-pre-warmed slots, but the swap becomes signable; everyone else keeps the optimization.
…rmit Coinbase Wallet SDK serves both EOA and Smart Wallet (passkey-based AA) accounts through the same connector id, so the existing \`SMART_WALLETS\` array can't distinguish them. The Smart Wallet path signs EIP-1271 contract signatures that ERC-2612 / Uniswap NFT \`permit()\` can't verify via ecrecover, leading to "Invalid permit signature" in QA when the user connects via passkey. Add a \`useIsSmartAccount\` hook with two probes: - \`useBytecode\` (eth_getCode) — catches smart wallets already deployed on the active chain. - \`useCapabilities\` (EIP-5792 wallet_getCapabilities) — catches counterfactual smart wallets like Coinbase Smart Wallet that haven't been deployed yet but advertise atomicBatch / auxiliaryFunds / paymasterService. Gate both \`usePermit\` and \`usePermitNft\` (token permit, V3/V4 NFT permit used by Smart Exit + zap widgets) on the hook so smart-account users fall back to the regular approve flow instead of hitting the parseSignature crash.
…nect SafePal's extension lazy-attaches its EVM provider at window.__safepalEthereumBootstrap__.activeProvider instead of the legacy window.safepalProvider, and never fires an EIP-6963 announce. wagmi's mount-time reconnect therefore reads an undefined provider from the custom connector's target, bails before isAuthorized(), and never retries when the bootstrap object lands — leaving returning users disconnected after refresh. Read the provider through a getSafepalProvider() helper that prefers the bootstrap's activeProvider (discriminated by isSafePal: true) and falls back to the legacy global. Use it in the connector's target, the install-prompt guard, and a polling recovery effect that retries reconnect once the provider appears (5s ceiling, skipped while another connector is current or wagmi is already reconnecting). Also pin the connector's rdns to SafePal's announced value so wagmi's mipd dedup folds any future EIP-6963 announce onto it, drop the WalletModal filter that hid SafePal when its legacy global was absent (the connector now always surfaces and opens the download page from its own connect() when no provider is installed, matching the install-link pattern used by Rabby/Binance/Bitget), and add unstable_shimAsyncInject to protect the explicit connect() path through isAuthorized().
…to feat/wagmi-migration
…nto feat/wagmi-migration
Approve flow tries up to three sendApprove() calls: maxUint256, then the exact amount, then a zero-reset for USDT-style tokens. The retries had no user-rejection guard, so rejecting the first wallet prompt re-popped the wallet twice more (exact-amount, then zero-reset) for a single click. Check didUserReject at every catch and abort silently — the wallet already surfaced the rejection.
The old ethers-based sendTransaction caught the case where a wallet/RPC threw an error AFTER successfully broadcasting the tx (mobile sessions dropping mid-response, providers reporting failure once the hash already returned, etc.) by reading `error.transactionHash` and treating it as success. The viem rewrite dropped that handler, so the same scenario now surfaces as a generic "Transaction failed" toast even though the tx is on-chain — users re-submit and pay gas twice. Restore the recovery in the eth_sendTransaction catch. Look at the legacy ethers field plus the two places viem typically nests provider data (cause / details / data) and validate the candidate is a 0x + 64-hex string before returning.
usePermitNft skips the permit (NOT_APPLICABLE) for both connector-level
smart wallets (Porto, Safe — SMART_WALLETS list) and account-level smart
wallets detected via on-chain bytecode / EIP-5792 capabilities (Coinbase
Smart Wallet via passkey, Argent, Ambire, EIP-7702 delegated EOAs). The
Smart Exit UI only checked isSmartConnector, so account-level smart
wallets bypassed the gate: the outer modal let them open the
Confirmation step, the button still read "Permit NFT", and clicking
called signPermitNft() which silently returned because permitState was
NOT_APPLICABLE, not READY_TO_SIGN. Users got stuck with no explanation.
Combine isSmartConnector with useIsSmartAccount() in both the outer
modal and the Confirmation step. Expand the unavailable-wallet copy to
name the concrete smart-wallet products and explain why ("position
permit can't verify their contract signatures").
viem's decodeFunctionResult returns a positional array for multi-output
functions, not an object with named keys (those exist only in the TS type).
The `as { token0Owed; token1Owed }` cast hid this, so accessing
tmp.token0Owed.toString() threw inside the unawaited getData(), leaving every
legacy position's fees at ['0','0'] and zeroing the remove-with-fee flow.
Access the result via tmp[0]/tmp[1], and add .catch() to both getData()
invocations so future decode mismatches log instead of silently zeroing the UI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Auto Deploy Pull RequestADPR instance has been created! Information
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
viem's decodeFunctionResult returns a positional array for multi-output functions, not an object with named keys (those exist only in the TS type). The
as { token0Owed; token1Owed }cast hid this, so accessing tmp.token0Owed.toString() threw inside the unawaited getData(), leaving every legacy position's fees at ['0','0'] and zeroing the remove-with-fee flow.Access the result via tmp[0]/tmp[1], and add .catch() to both getData() invocations so future decode mismatches log instead of silently zeroing the UI.