Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions plugins/appkit.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createAppKit } from '@reown/appkit/vue'
import { WagmiAdapter } from '@reown/appkit-adapter-wagmi'
import { arbitrumSepolia } from '@reown/appkit/networks'
import type { AppKitNetwork } from '@reown/appkit/networks'
import { http } from '@wagmi/core'
import { invoke } from '@tauri-apps/api/core'
import {
WALLETCONNECT_PROJECT_ID,
Expand Down Expand Up @@ -71,6 +72,18 @@ export default defineNuxtPlugin(async () => {
projectId: WALLETCONNECT_PROJECT_ID,
networks,
connectors: [browserBridge()],
// Without an explicit transport, the adapter routes every read and
// gas estimate through Reown's RPC proxy (rpc.walletconnect.org),
// which 403s any request body over 16KB — large payForMerkleTree /
// payForQuotes estimates exceed that. Its own "fallback" is the same
// proxy URL again (extendCaipNetwork rewrites rpcUrls.default), so
// the chain's real RPC was never tried. Pinning the canonical
// Arbitrum endpoints makes them primary; the adapter keeps the Reown
// proxy as an automatic fallback leg behind each.
transports: {
[SUPPORTED_CHAIN.id]: http('https://arb1.arbitrum.io/rpc'),
[arbitrumSepolia.id]: http('https://sepolia-rollup.arbitrum.io/rpc'),
},
})

const appkit = createAppKit({
Expand Down
23 changes: 23 additions & 0 deletions tests/utils/payment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,29 @@ describe('payment', () => {
expect(writeContract).not.toHaveBeenCalled()
})

it('reports a transport failure as an unreachable RPC, not an on-chain verdict', async () => {
vi.useFakeTimers()
// Shape of a real failure: viem wraps the transport error, keeping it
// in the cause chain (e.g. Reown's RPC proxy 403ing a >16KB estimate).
const httpError = Object.assign(new Error('HTTP request failed.\nURL: …'), {
name: 'HttpRequestError',
shortMessage: 'HTTP request failed.',
})
const wrapped = Object.assign(new Error('Gas estimation failed'), {
shortMessage: 'HTTP request failed.',
cause: httpError,
})
estimateContractGas.mockRejectedValue(wrapped)

const assertion = expect(payForQuotes({} as any, PAYMENTS)).rejects.toThrow(
"Couldn't reach the Arbitrum RPC to estimate gas: HTTP request failed.",
)
await vi.advanceTimersByTimeAsync(3_000)
await assertion

expect(writeContract).not.toHaveBeenCalled()
})

it('preflights the approve tx too when an approval is needed', async () => {
vi.mocked(readContract).mockResolvedValue(0n)
estimateContractGas.mockResolvedValue(60_000n)
Expand Down
18 changes: 18 additions & 0 deletions utils/payment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,17 @@ function shortReason(e: any): string {
return e?.shortMessage ?? String(e?.message ?? e).split('\n')[0]
}

/** True when the estimate never reached the chain: the RPC transport failed
* (HTTP error, timeout) rather than the node simulating a revert. viem
* nests the transport error inside the `cause` chain of the wrapper that
* estimateContractGas throws. */
function isTransportError(e: any): boolean {
for (let err = e, depth = 0; err && depth < 10; err = err.cause, depth++) {
if (err.name === 'HttpRequestError' || err.name === 'TimeoutError') return true
}
return false
}

/**
* Estimate the gas limit for a contract write on our own transport, with
* retries: right after an approve receipt, a load-balanced public RPC can
Expand All @@ -138,6 +149,13 @@ async function preflightGasLimit(
}
}
}
if (isTransportError(lastError)) {
// Not an on-chain verdict — we never got one. Say so instead of
// implying the payment itself is doomed.
throw new Error(`Couldn't reach the Arbitrum RPC to estimate gas: ${shortReason(lastError)}`, {
cause: lastError,
})
}
throw new Error(`${label} would fail on-chain: ${shortReason(lastError)}`, { cause: lastError })
}

Expand Down