MPP.Methods.Tempo (mpp v0.8.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.
  • "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.
  • "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. See MPP.Methods.Tempo.FeePayerPolicy.
  • "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").
  • "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).

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.
  • 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, 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
method_name0Return the payment method identifier for Tempo.-

Summary

Functions

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

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.Intents.Charge.t()) :: map() | nil
@spec challenge_method_details(MPP.Intents.Charge.t()) :: map()

Return Tempo-specific fields (chainId, feePayer, memo, 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, 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`, and `presenterBinding` (present and `true` only when required)"
  }
}

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.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
# 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],
  returns: %{
    type: :tagged_tuple,
    description: "`{:ok, receipt}` on success, `{:error, error}` on failure"
  }
}