MPP.Methods.Tempo (mpp v0.16.0)

Copy Markdown View Source

Tempo payment method — verifies payment via on-chain TIP-20 token transfer.

Tempo supports two credential types:

  • type="hash" — Client already broadcast the transaction; server verifies the receipt via RPC (eth_getTransactionReceipt).
  • type="transaction" — Client sends a signed Tempo Transaction (0x76); server decodes, optionally adds fee payer signature, broadcasts, and verifies.

Configuration

Pass Tempo-specific config via :method_config in MPP.Plug opts:

plug MPP.Plug,
  secret_key: "hmac-secret",
  realm: "api.example.com",
  method: MPP.Methods.Tempo,
  amount: "1000000",
  currency: "0x20c0000000000000000000000000000000000000",
  method_config: %{
    "rpc_url" => "https://rpc.moderato.tempo.xyz",
    "chain_id" => 42431,
    "fee_payer" => false
  }

Config Keys

  • "rpc_url" — (required) Tempo RPC endpoint URL
  • "chain_id" — (optional) network chain ID, defaults to 42431 (Moderato testnet)
  • "fee_payer" — (optional) enable server-side fee sponsorship, defaults to false. When true, the server co-signs client transactions with domain 0x78 to pay transaction fees. Requires "fee_payer_private_key" and "fee_token", unless "fee_payer_url" is set for hosted sponsorship. Sponsorship also requires an explicitly selected atomic "store".
  • "fee_payer_url" — (optional) URL of a hosted fee-payer service that implements eth_fillTransaction. When set, the server delegates co-signing to the remote endpoint instead of using "fee_payer_private_key". Mutually exclusive with the local key path.
  • "sponsor_budget_id" — required with "fee_payer_url"; stable identity shared by every endpoint using the same hosted sponsor wallet. It is normalized before use and should normally be that wallet's address.
  • "fee_payer_private_key" — (required when fee_payer: true and no fee_payer_url) hex-encoded 32-byte secp256k1 private key for the fee payer account
  • "fee_token" — (required for local co-sign when fee_payer: true and no fee_payer_url) hex address of a USD-denominated TIP-20 token to use for fee payment (e.g., pathUSD). Must be on the sponsor allowlist (fee_payer_allowed_fee_tokens or per-chain defaults).
  • "fee_payer_allowed_fee_tokens" — (optional, fee_payer: true only) list of hex addresses overriding the default sponsor fee-token allowlist.
  • "fee_payer_policy" — (optional, fee_payer: true only) map of sponsor ceilings overriding the per-chain defaults: "max_gas", "max_fee_per_gas", "max_priority_fee_per_gas", "max_total_fee" (wei), and "max_validity_window_seconds" (seconds). Bounds the client-supplied gas fields and validity window before the server co-signs so a malicious client cannot drain the fee-payer wallet via inflated gas price, total fee budget, or a padded access list, nor hold a co-signed sponsorship broadcastable far into the future. The same map accepts "max_in_flight_total_fee" and "max_in_flight_reservations" for aggregate worst-case exposure. See MPP.Methods.Tempo.FeePayerPolicy.
  • "sponsor_budget_reconcile" — (optional, defaults to false) when true, an at-capacity request checks a bounded set of pending async transaction receipts before rejecting.
  • "memo" — (optional) bytes32 hex memo for transferWithMemo
  • "wait_for_confirmation" — (optional) when false, broadcasts without waiting for on-chain confirmation. Pre-simulates the full co-signed transaction via eth_simulateV1 first (same guard as the default path) to reject a tx that would revert before broadcast, then broadcasts async and returns an optimistic receipt. Default true.
  • "store" — (optional) replay-dedup store. On by default — when absent, the app-started MPP.Tempo.ConCacheStore is used so replay protection is enabled out of the box (issue #7). Pass a module implementing MPP.Tempo.Store (Redis, Postgres, etc. — for multi-node deployments) or {MPP.Tempo.ConCacheStore, opts} to configure the built-in store (for example a custom cache :name). A configured store MUST implement the atomic check_and_mark/2. Pass store: false to explicitly opt out of dedup (not recommended; incompatible with a static "memo"). Fee sponsorship is stricter: the store must be selected explicitly and implement atomic update/3. MPP.Tempo.ConCacheStore provides a single-node bound. Every node and endpoint sponsoring the same wallet must use the same physical shared atomic backend for a cluster-wide bound.
  • "machine_token_enabled" — (optional) advertise and verify first-party machine-token (MPP Credits / machineUSD) charge payments. When true, 402 method details include "machineTokenEnabled" => true, hash receipts may settle from the canonical swapper, and type="transaction" credentials may match the exact [approve, swapTo] route. Supported only on Tempo mainnet (4217) and Moderato (42431). Defaults to false.
  • "require_presenter_binding" — (optional) require type="hash" and type="transaction" credential presenters to prove control of the transfer's sender wallet via a "presenterSignature" payload field (see Presenter binding below). Defaults to false.

Credential Payload

The credential payload map must contain one of:

  • "type" => "hash", "hash" => "0x..." — transaction hash for receipt verification
  • "type" => "transaction", "signature" => "..." — RLP-serialized signed Tempo Transaction

Both may additionally carry "presenterSignature" => "0x..." (see below).

Recurring subscriptions

Routes configured with intent: "subscription" use the shared MPP.Intents.Subscription schema and delegate activation and renewal to MPP.Methods.Tempo.Subscription. Subscription configuration requires "rpc_url", "chain_id", and "subscription_access_key_private_key". It accepts a "subscription_store" implementing MPP.Subscription.Store; the application-started MPP.Subscription.ETSStore is the single-node default. Subscription fee sponsorship supports the local fee-payer path, not "fee_payer_url".

Presenter Binding

On the hash/transaction paths, dedup is keyed on the tx hash alone — nothing in the base protocol proves the credential presenter controls the wallet that broadcast the transfer, so a third party who observes a settled transfer can race its hash against their own fresh challenge (the residual documented in GHSA-34g7-vx6g-82mq). Setting "require_presenter_binding" => true closes that race: the presenter must include "presenterSignature", an EIP-712 signature over the same Proof typed data the type="proof" path uses (domain {name: "MPP", version: "3", chainId}, struct {account, challengeId, realm} — see MPP.Methods.Tempo.Proof), signed by the transfer sender's wallet or one of its authorized access keys.

  • type="hash" — the credential's top-level source (a did:pkh:eip155: DID) is required and names the account; the signature must recover to it, and the matched transfer's from must equal it (or, when "machine_token_enabled" is set, equal the canonical swapper while the transaction sender equals it).
  • type="transaction" — the account is the sender recovered from the signed transaction itself; a source, when present, must match it.

When the flag is off (default) a supplied "presenterSignature" is still verified (an invalid one is rejected), so compliant clients can send it unconditionally. The requirement is advertised to clients as "presenterBinding" => true in the 402 challenge's method details.

This is a deliberate hardening extension beyond the reference SDKs: neither mpp-rs nor mppx binds the presenter on the hash path (both default the expected sender to the receipt's fromrefs/mpp-rs/src/protocol/methods/tempo/method.rs verify_hash, refs/mppx/src/tempo/server/Charge.ts hash branch), which is why it is opt-in.

Dependencies

Requires the onchain and onchain_tempo packages for RPC calls, transfer log parsing, and 0x76 Tempo transaction handling.

API Functions

FunctionArityDescriptionParam Kinds
challenge_method_details1Return Tempo-specific fields (chainId, feePayer, memo, machineTokenEnabled, presenterBinding) for the 402 challenge.charge: value
verify2Verify a Tempo credential by checking on-chain settlement.payload: value, charge: value
validate_config!1Validate Tempo method_config at init time. Raises on missing rpc_url or unavailable onchain / onchain_tempo dependencies.config: value
credential_types0Return the Tempo charge payload types: hash, transaction, and proof.-
method_name0Return the payment method identifier for Tempo.-

Summary

Functions

Return Tempo-specific fields (chainId, feePayer, memo, machineTokenEnabled, presenterBinding) for the 402 challenge.

Return the Tempo charge payload types: hash, transaction, and proof.

Return the payment method identifier for Tempo.

Validate Tempo method_config at init time. Raises on missing rpc_url or unavailable onchain / onchain_tempo dependencies.

Verify a Tempo credential by checking on-chain settlement.

Functions

challenge_method_details(charge)

@spec challenge_method_details(MPP.Method.intent()) :: map() | nil
@spec challenge_method_details(MPP.Intents.Charge.t()) :: map()
@spec challenge_method_details(MPP.Intents.Subscription.t()) :: map()

Return Tempo-specific fields (chainId, feePayer, memo, machineTokenEnabled, presenterBinding) for the 402 challenge.

Parameters

  • charge - Charge struct with method_details containing chain_id, fee_payer, and optionally memo (value)

Returns

Map with chainId (default 42431), feePayer (default false), optional memo, machineTokenEnabled (present and true only when enabled), and presenterBinding (present and true only when required) (map)

# descripex:contract
%{
  params: %{
    charge: %{
      description: "Charge struct with method_details containing `chain_id`, `fee_payer`, and optionally `memo`",
      kind: :value
    }
  },
  returns: %{
    type: :map,
    description: "Map with `chainId` (default 42431), `feePayer` (default false), optional `memo`, `machineTokenEnabled` (present and `true` only when enabled), and `presenterBinding` (present and `true` only when required)"
  }
}

credential_types()

@spec credential_types() :: [String.t()]
@spec credential_types() :: [String.t()]

Return the Tempo charge payload types: hash, transaction, and proof.

# descripex:contract
%{}

method_name()

@spec method_name() :: String.t()

Return the payment method identifier for Tempo.

# descripex:contract
%{}

validate_config!(config)

@spec validate_config!(map()) :: :ok
@spec validate_config!(map()) :: :ok

Validate Tempo method_config at init time. Raises on missing rpc_url or unavailable onchain / onchain_tempo dependencies.

Parameters

  • config - method_config map to validate (value)

Returns

:ok on success, raises ArgumentError on missing keys (atom)

# descripex:contract
%{
  params: %{
    config: %{description: "method_config map to validate", kind: :value}
  },
  returns: %{
    type: :atom,
    description: "`:ok` on success, raises `ArgumentError` on missing keys"
  }
}

verify(payload, charge)

@spec verify(map(), MPP.Intents.Charge.t()) ::
  {:ok, MPP.Receipt.t()} | {:error, MPP.Errors.t()}
@spec verify(map(), MPP.Intents.Charge.t()) ::
  {:ok, MPP.Receipt.t()} | {:error, MPP.Errors.t()}
@spec verify(map(), MPP.Intents.Charge.t()) ::
  {:ok, MPP.Receipt.t()} | {:error, MPP.Errors.t()}
@spec verify(map(), MPP.Intents.Subscription.t()) ::
  {:ok, MPP.Receipt.t()} | {:error, MPP.Errors.t()}
@spec verify(map(), MPP.Intents.Charge.t()) :: {:error, MPP.Errors.t()}

Verify a Tempo credential by checking on-chain settlement.

Parameters

  • payload - Credential payload map with "type" ("hash" or "transaction") and corresponding proof field (value)
  • charge - Charge intent struct with amount, currency, and method_details (including rpc_url) (value)

Returns

{:ok, receipt} on success, {:error, error} on failure (tagged_tuple)

Errors

  • :invalid_payload
  • :verification_failed
  • :sponsor_capacity_exhausted
# descripex:contract
%{
  params: %{
    payload: %{
      description: "Credential payload map with `\"type\"` (`\"hash\"` or `\"transaction\"`) and corresponding proof field",
      kind: :value
    },
    charge: %{
      description: "Charge intent struct with amount, currency, and method_details (including `rpc_url`)",
      kind: :value
    }
  },
  errors: [:invalid_payload, :verification_failed, :sponsor_capacity_exhausted],
  returns: %{
    type: :tagged_tuple,
    description: "`{:ok, receipt}` on success, `{:error, error}` on failure"
  }
}