Asset decimals registry.
Centralizes the atomic-vs-human unit conversion that every payment
protocol needs but no single protocol owns. Without this, SpendingPolicy
caps written in human dollars (Decimal.new("1.00")) get compared
against raw atomic amounts (USDC 1_000_000 = $1), and small policy
caps reject every legitimate payment.
Lookups support two shapes:
decimals(chain_id, contract_address)-- for protocols that carry both the chain id and the ERC-20 contract address (x402).decimals(ticker)-- for protocols that only carry a currency symbol (MPP).
Unknown assets default to 6 decimals (USDC convention on the major networks). This is the safe-ish default for stablecoin flows and the wrong default for native-token flows -- always pass the contract or ticker for non-USDC payments.
Adding new assets
Add to @addresses (chain+contract) or @tickers (symbol). Keep
addresses lowercase. Tests in test/raxol/payments/assets_test.exs
pin the known set.
Summary
Functions
Resolve a token symbol to its contract address on chain_id.
Human-readable chain name, or "Chain <id>" when unknown.
Look up decimals by ticker symbol. Returns @default_decimals
(6) when unknown. Case-insensitive.
Look up decimals by chain id and ERC-20 contract address.
True when (chain_id, address) is a registered contract, i.e. decimals/2
returns a pinned value rather than the @default_decimals fallback. An
unregistered token resolves to 6 decimals, which is wrong for an 18-decimal
token like WETH. Case-insensitive; accepts an integer chain id or a CAIP-2
string. EVM only.
The native gas token decimals for chain_id. Every supported EVM chain uses
18; unknown chains default to 18.
The native gas token symbol for chain_id (e.g. "ETH", "POL"). Accepts an
integer chain id or a CAIP-2 string; returns nil for an unknown chain.
The EVM chain ids covered by the solver-fillable table, ascending. This is
the static universe Raxol.Payments.Xochi.Capabilities.fallback/0 derives
from; live corridor availability comes from the capabilities endpoint.
Resolve a (chain_id, contract_address) back to its token symbol: the reverse
of address/2. Covers the solver-fillable set (USDC, USDT, WETH, plus USDG on
Robinhood Chain) on the six EVM chains. Case-insensitive; accepts an integer
chain id or a CAIP-2 string. Returns nil for an unregistered pair, so a
caller can tell "same asset" from "unknown" without guessing.
Convert a human-decimal amount to atomic units.
Convert an atomic-unit amount to a human-decimal Decimal.t/0.
True when address is a known USDC contract on chain_id. Case-insensitive;
accepts an integer chain id or a CAIP-2 string. EVM only.
Functions
Resolve a token symbol to its contract address on chain_id.
Covers the solver-fillable set (USDC, USDT, WETH, plus USDG on Robinhood
Chain) across the six supported EVM chains (1, 10, 137, 8453, 42161, 4663).
The symbol is case-insensitive; the chain id accepts an integer or a CAIP-2
string. Returns :error for an unknown (chain, symbol) pair.
Human-readable chain name, or "Chain <id>" when unknown.
@spec decimals(String.t() | nil) :: pos_integer()
Look up decimals by ticker symbol. Returns @default_decimals
(6) when unknown. Case-insensitive.
@spec decimals(integer() | String.t() | nil, String.t() | nil) :: pos_integer()
Look up decimals by chain id and ERC-20 contract address.
Both chain_id integer and CAIP-2 string ("eip155:8453") are
accepted. Address case is normalized. Returns @default_decimals
(6) when unknown.
@spec evm_tokens() :: %{ required(String.t()) => %{required(pos_integer()) => String.t()} }
The solver-fillable token table: symbol -> chain id -> lowercase address.
Exposed for fallback derivation; prefer known?/2 / address/2 for
lookups.
True when (chain_id, address) is a registered contract, i.e. decimals/2
returns a pinned value rather than the @default_decimals fallback. An
unregistered token resolves to 6 decimals, which is wrong for an 18-decimal
token like WETH. Case-insensitive; accepts an integer chain id or a CAIP-2
string. EVM only.
@spec native_decimals(integer() | String.t() | nil) :: pos_integer()
The native gas token decimals for chain_id. Every supported EVM chain uses
18; unknown chains default to 18.
The native gas token symbol for chain_id (e.g. "ETH", "POL"). Accepts an
integer chain id or a CAIP-2 string; returns nil for an unknown chain.
@spec supported_chain_ids() :: [pos_integer()]
The EVM chain ids covered by the solver-fillable table, ascending. This is
the static universe Raxol.Payments.Xochi.Capabilities.fallback/0 derives
from; live corridor availability comes from the capabilities endpoint.
Resolve a (chain_id, contract_address) back to its token symbol: the reverse
of address/2. Covers the solver-fillable set (USDC, USDT, WETH, plus USDG on
Robinhood Chain) on the six EVM chains. Case-insensitive; accepts an integer
chain id or a CAIP-2 string. Returns nil for an unregistered pair, so a
caller can tell "same asset" from "unknown" without guessing.
@spec symbols() :: [String.t()]
The token symbols with a resolvable address/2 (USDC, USDT, WETH).
@spec to_atomic(integer() | String.t() | Decimal.t(), pos_integer()) :: non_neg_integer()
Convert a human-decimal amount to atomic units.
E.g. to_atomic("0.01", 6) -> 10_000.
@spec to_human(integer() | String.t() | Decimal.t(), pos_integer()) :: Decimal.t()
Convert an atomic-unit amount to a human-decimal Decimal.t/0.
E.g. to_human(10_000, 6) -> Decimal.new("0.01").
True when address is a known USDC contract on chain_id. Case-insensitive;
accepts an integer chain id or a CAIP-2 string. EVM only.