X402.Scheme.ExactSVM (X402 v0.6.0)

Copy Markdown View Source

Built-in X402.Scheme for exact payments on Solana (solana:*) networks.

Follows the x402 exact SVM scheme specification: the client builds a version 0 Solana transaction with the reference instruction layout —

  1. Compute Budget SetComputeUnitLimit
  2. Compute Budget SetComputeUnitPrice
  3. SPL Token / Token-2022 TransferChecked to the Associated Token Account derived from payTo and asset
  4. SPL Memo (the seller's extra.memo, or a random nonce for transaction uniqueness)

— signs it with the payer's Ed25519 key, and leaves the fee payer's signature slot as a 64-byte zero placeholder (a partially signed transaction). The wire payload is %{"transaction" => base64}, exactly as the reference TypeScript and Python clients produce.

Roles

  • Clientsign/3 builds and partially signs the transaction. The signer must implement the optional X402.Signer.sign_ed25519/2 callback (X402.Signer.SolanaKey does). An entry is signable when extra.feePayer is present: the sponsor's public key is required (it becomes account 0 / the empty signature slot).
  • Servervalidate_payload/3 runs structural checks that any valid exact SVM payment must satisfy (decodable Base64, the 1232-byte transaction size cap, a parseable v0/legacy transaction, fee payer match). precheck/3 additionally enforces the facilitator's static verification path whitelist (spec §3.1): 3–7 instructions in the reference order, compute-budget bounds, fee payer isolation, transfer semantics against the requirements, and memo enforcement.

On-chain verification and settlement remain facilitator-delegated: this module never talks to a Solana RPC node. The facilitator resolves address lookup tables, simulates, signs as feePayer, and submits (spec §2–3). Transactions using address lookup tables skip precheck/3 (the account set cannot be resolved locally) and are left to the facilitator, and smart-wallet (CPI-wrapped) payments — the spec's opt-in Path 2 — will fail the static-path pre-checks; gates fronting a facilitator with enableSmartWalletVerification should disable :local_prechecks for those routes.

Client options

sign/3 honors these X402.Client.build_payment/3 options:

  • :svm_blockhash — a Base58 recent blockhash for the transaction lifetime. Used when the requirements' extra.recentBlockhash hint is absent or malformed; keeps the module RPC-free.
  • :svm_blockhash_fetcher — a 1-arity fun receiving the CAIP-2 network and returning {:ok, blockhash} (for example a wrapper around your RPC client's getLatestBlockhash). Consulted after :svm_blockhash.
  • :svm_decimals / :svm_token_program — the mint's decimals and owning token program, needed by TransferChecked. Defaults come from the reference SDKs' known-asset table (USDC, USDT, USDG, PYUSD, CASH); for other mints pass both explicitly (production clients read them from the mint account via RPC).

Blockhash resolution order (per the spec): a valid extra.recentBlockhash from the server wins, then :svm_blockhash, then :svm_blockhash_fetcher; with none, {:error, :missing_blockhash}.

Summary

Types

Reasons precheck/3 fails fast with {:error, {:precheck_failed, reason}}.

Functions

Returns ["solana:*"] — every Solana network.

Static-path pre-checks (spec §3.1) before the facilitator round-trip.

Returns "exact".

Builds and partially signs the SVM exact transaction.

Whether the entry carries the required sponsor data.

Structural validation of a decoded PAYMENT-SIGNATURE payload.

Types

precheck_failure()

@type precheck_failure() ::
  :invalid_transaction
  | :fee_payer_not_isolated
  | :instruction_count
  | :invalid_compute_limit_instruction
  | :invalid_compute_price_instruction
  | :compute_price_too_high
  | :missing_transfer_instruction
  | :amount_mismatch
  | :mint_mismatch
  | :recipient_mismatch
  | :unknown_optional_instruction
  | :memo_count
  | :memo_mismatch

Reasons precheck/3 fails fast with {:error, {:precheck_failed, reason}}.

Functions

networks()

(since 0.6.0)
@spec networks() :: [String.t()]

Returns ["solana:*"] — every Solana network.

Examples

iex> X402.Scheme.ExactSVM.networks()
["solana:*"]

precheck(payload, requirements, opts)

(since 0.6.0)
@spec precheck(map(), map(), keyword()) ::
  :ok | {:error, {:precheck_failed, precheck_failure()}}

Static-path pre-checks (spec §3.1) before the facilitator round-trip.

Enforces the facilitator's static verification whitelist as far as it is verifiable without RPC: 3–7 top-level instructions in the reference order (SetComputeUnitLimit, SetComputeUnitPrice, TransferChecked, then only Lighthouse/Memo), the ≤ 5 lamports/CU priority-fee cap, fee payer isolation (§2.1.1 — the fee payer referenced by no instruction), transfer semantics against the requirements (amount equality, mint, destination ATA), and memo enforcement when extra.memo is present.

Transactions using address lookup tables pass through with :ok — their account set cannot be resolved without RPC, so the facilitator remains the authority (§2.1.2). Failures return {:error, {:precheck_failed, reason}} and the gate answers 402 without a facilitator call.

scheme()

(since 0.6.0)
@spec scheme() :: String.t()

Returns "exact".

Examples

iex> X402.Scheme.ExactSVM.scheme()
"exact"

sign(requirements, signer, opts)

(since 0.6.0)
@spec sign(map(), X402.Signer.t(), keyword()) :: {:ok, map()} | {:error, term()}

Builds and partially signs the SVM exact transaction.

Returns {:ok, %{"transaction" => base64}} — the wire scheme payload — or a structured error: {:error, :missing_fee_payer} when the requirements lack extra.feePayer, {:error, :missing_blockhash} when no blockhash source is available, {:error, {:unknown_asset, mint}} for mints outside the known-asset table without explicit :svm_decimals/:svm_token_program, and {:error, :unsupported_signer} when the signer cannot sign Ed25519.

signable?(requirements)

(since 0.6.0)
@spec signable?(map()) :: boolean()

Whether the entry carries the required sponsor data.

extra.feePayer is required by the SVM exact scheme (the sponsor's public key becomes the transaction's fee payer), and asset/payTo must be valid Solana addresses.

Examples

iex> X402.Scheme.ExactSVM.signable?(%{
...>   "asset" => "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
...>   "payTo" => "GyGKxMyg1p9SsHfm15MkNUu1u9TN2JtTspcdmrtGUdse",
...>   "extra" => %{"feePayer" => "9hSR6S7WPtxmTojgo6GG3k4yDPecgJY292j7xrsUGWBu"}
...> })
true

iex> X402.Scheme.ExactSVM.signable?(%{"extra" => %{}})
false

validate_payload(payload, requirements, opts)

(since 0.6.0)
@spec validate_payload(map(), map(), keyword()) ::
  :ok | {:error, {:invalid_scheme_payment, atom()}}

Structural validation of a decoded PAYMENT-SIGNATURE payload.

Checks what any valid exact SVM payment must satisfy without RPC: payload.transaction present, Base64-decodable, within the network's 1232-byte transaction size cap, parseable as a v0 or legacy Solana transaction, and — when the requirements advertise extra.feePayer — carrying that fee payer as account 0. Failures return {:error, {:invalid_scheme_payment, reason}}.

Examples

iex> X402.Scheme.ExactSVM.validate_payload(%{"payload" => %{}}, %{}, [])
{:error, {:invalid_scheme_payment, :missing_transaction}}

iex> X402.Scheme.ExactSVM.validate_payload(
...>   %{"payload" => %{"transaction" => "!!!"}},
...>   %{},
...>   []
...> )
{:error, {:invalid_scheme_payment, :invalid_base64}}