Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.walletconnect.com/llms.txt

Use this file to discover all available pages before exploring further.

The simple WalletConnect Pay flow signs an EIP-3009 transferWithAuthorization typed-data payload — the gasless transfer used by USDC and similar stablecoins. USDT does not implement EIP-3009, so WalletConnect Pay routes it through the Permit2 contract instead. The first time a user pays a given token on a given chain, the wallet must approve Permit2 on-chain. Subsequent payments on the same token only require the typed-data signature.

The two-action flow

When an option needs a Permit2 approval, getRequiredPaymentActions returns two actions — in the order they must execute:
  1. eth_sendTransaction — ERC-20 approve(Permit2, amount) on the token contract
  2. eth_signTypedData_v4 — Permit2 typed-data payload authorizing the transfer
signatures[] passed to confirmPayment must match actions[] in order and length — the approve tx hash first, then the Permit2 signature.

Detecting an approval-required option

Inspect the action list for an eth_sendTransaction entry. In standalone SDK flows, this means option.actions; in WalletKit flows, this means the actions returned by getRequiredPaymentActions. If one is present, the option is a Permit2 flow and your wallet should prepare for an on-chain step.
// WalletKit: inspect the actions returned by getRequiredPaymentActions
fun requiresApproval(requiredActions: List<Wallet.Model.RequiredAction>?): Boolean =
    requiredActions
        ?.filterIsInstance<Wallet.Model.RequiredAction.WalletRpc>()
        ?.any { it.action.method == "eth_sendTransaction" }
        ?: false

Showing the approval gas estimate

The approve tx costs native gas (e.g. POL on Polygon, ETH on mainnet) — separate from the token amount the user is paying. Estimate it and show the user the expected fee before they confirm so they aren’t surprised by a wallet prompt for a one-time on-chain cost. This can live wherever your wallet collects user intent — a review screen, a confirmation sheet, or inline on the option row.
suspend fun estimateApprovalFee(
    chainId: String,
    txParams: JSONObject,
): BigInteger? = runCatching {
    val gasLimit = rpcEstimateGas(chainId, txParams)
    val maxFeePerGas = fetchFeeData(chainId).maxFeePerGas
    gasLimit.multiply(maxFeePerGas)
}.getOrNull()
If estimation fails (RPC error, missing fields, etc.), don’t block the flow. Fall back to a generic message such as “Network fee set by wallet” — the wallet’s signing prompt will still surface the actual cost when the user confirms.

Executing the actions in order

Because the Permit2 typed data signs against the token allowance the approve tx grants, the approve must be mined before you sign.
  1. Submit eth_sendTransaction (action 1) and wait for the receipt.
  2. Parse the typed data from eth_signTypedData_v4 (action 2) and sign it.
  3. Push both results into signatures[] in the order the actions were returned.
  4. Call confirmPayment(signatures).
val signatures = mutableListOf<String>()

for (action in actions.filterIsInstance<Wallet.Model.RequiredAction.WalletRpc>()) {
    signatures += when (action.action.method) {
        "eth_sendTransaction" -> {
            val txHash = wallet.sendTransaction(action.action.chainId, action.action.params)
            wallet.awaitReceipt(action.action.chainId, txHash)
            txHash
        }
        "eth_signTypedData_v4" ->
            wallet.signTypedData(action.action.chainId, action.action.params)
        else -> error("Unsupported method: ${action.action.method}")
    }
}
EIP-712 library quirks. EIP-712 signing libraries disagree on whether types.EIP712Domain must be present in the payload. The Permit2 typed data returned by WalletConnect Pay omits it.
  • Libraries that require EIP712Domain (e.g. eth_sig_util_plus on Flutter, some Android libraries): synthesize an EIP712Domain entry in types from the fields actually present in domain before signing.
  • Libraries that reject EIP712Domain (e.g. ethers v5 _signTypedData): strip it from types before signing.
  • Yttrium’s signTypedData is currently hardcoded to ERC-3009 (from/to/value/validAfter/validBefore/nonce) and rejects Permit2. Wallets using Yttrium need a generic EIP-712 hasher — see EIP712TypedData.swift for a reference implementation.
If your signing library throws on a Permit2 typed-data payload, normalize the payload before retrying.

Loader UX

The two-action flow has two distinct user-visible steps that can each take several seconds:
  1. The approve tx is broadcast and you wait for the receipt.
  2. The user is prompted to sign the Permit2 typed data.
Show different loader copy for each step so the user understands what is happening — especially during the on-chain wait, which is much slower than a typed-data signature on its own. Pick whatever wording fits your wallet’s voice.

Sample wallets

The four reference wallets below all just shipped this two-action flow and are good starting points to copy from.

React Native

wallets/rn_cli_wallet — see PaymentUtil.ts, PaymentTransactionUtil.ts, PaymentStore.ts.

Kotlin

sample/wallet — see PaymentUtil.kt, PaymentTransactionUtil.kt, PaymentViewModel.kt.

Swift

Example/WalletApp — see PaymentUtil.swift, PayTransactionService.swift, PayPresenter.swift, EIP712TypedData.swift.

Flutter

packages/reown_walletkit/example — see evm_service.dart, wcp_payment_details.dart, wcp_confirming_payment.dart.