Skip to content

fix(elastic-legacy): decode position fees by index, not named keys#3147

Draft
viet-nv wants to merge 77 commits into
mainfrom
fix/elastic-legacy-fee-decode
Draft

fix(elastic-legacy): decode position fees by index, not named keys#3147
viet-nv wants to merge 77 commits into
mainfrom
fix/elastic-legacy-fee-decode

Conversation

@viet-nv

@viet-nv viet-nv commented Jun 1, 2026

Copy link
Copy Markdown
Member

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.

tienkane added 30 commits May 12, 2026 14:55
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.
…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.
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
tienkane and others added 27 commits May 20, 2026 13:03
…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".
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.
`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().
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>
@kyber-ci-bot

Copy link
Copy Markdown

Auto Deploy Pull Request

ADPR instance has been created!

Information

Base automatically changed from feat/wagmi-migration to main June 2, 2026 02:29
@neikop neikop marked this pull request as draft June 15, 2026 06:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants