Raxol.Payments.Xochi.DepositAttestation (Raxol Payments v0.2.0)

Copy Markdown View Source

Client-side verification of a Xochi deposit-route quote's deposit_attestation.

A non-EVM origin (Tron/Solana) has no gasless pull, so a deposit-route quote returns a bare deposit_address for the payer to send funds to directly. A MITM or a compromised quote endpoint could swap that address and steal the deposit, so Riddler signs the quote's binding fields with a dedicated, solver-distinct key whose address is published in GET /xochi/capabilities as deposit_attestation_signer. raxol pins that address and verifies the attestation recovers to it BEFORE any funds move, failing closed otherwise.

Byte-exact parity with the Riddler signer (Riddler.Integrations.Xochi.DepositAttestation): the signed message is seven lines joined by a single \n (no trailing newline), UTF-8:

xochi-deposit-attestation:v1
<intent_id>
<quote_id>
<from_chain_id>
<from_token>
<from_amount>
<deposit_address>

EVM (0x-hex) addresses are lowercased; base58 values (a Tron TRC-20 contract, the Tron deposit address) are verbatim. from_chain_id and from_amount are decimal; from_amount is in base units, matching the quote's from_amount. The signature is 0x + 65 bytes (r || s || v), v in {27, 28}, an EIP-191 personal_sign.

Summary

Functions

The byte-exact attestation message for the given quote binding fields.

Recover the signer address (lowercased 0x) from an attestation over fields.

Verify signature over fields recovers to expected_signer, fail closed.

Types

fields()

@type fields() :: %{
  intent_id: String.t(),
  quote_id: String.t(),
  from_chain_id: integer(),
  from_token: String.t(),
  from_amount: String.t() | integer(),
  deposit_address: String.t()
}

Functions

message(map)

@spec message(fields()) :: String.t()

The byte-exact attestation message for the given quote binding fields.

recover(fields, hex)

@spec recover(fields(), String.t()) :: {:ok, String.t()} | {:error, term()}

Recover the signer address (lowercased 0x) from an attestation over fields.

verify(fields, signature, expected_signer)

@spec verify(fields(), String.t() | nil, String.t() | nil) :: :ok | {:error, atom()}

Verify signature over fields recovers to expected_signer, fail closed.

  • {:error, :missing_attestation} -- the quote carried no signature.
  • {:error, :signer_unavailable} -- no signer is pinned (nothing to verify against; refuse rather than trust the bare deposit address).
  • {:error, :attestation_mismatch} -- it recovers to a different address.
  • {:error, :invalid_signature} -- it cannot be recovered.