Packages
Autonomous payment capabilities for Raxol agents. Xochi cross-chain intent settlement, x402/MPP auto-pay, wallet management, and spending controls.
Current section
Files
Jump to
Current section
Files
lib/raxol/payments/chain_reader/jsonrpc.ex
defmodule Raxol.Payments.ChainReader.JSONRPC do
@moduledoc """
`Req`-backed `Raxol.Payments.ChainReader` over per-chain JSON-RPC endpoints.
Implements only the two read-only methods the accounting layer needs
(`eth_getTransactionReceipt`, `eth_getBalance`). Self-contained on purpose:
`raxol_payments` cannot depend on `raxol_acp`'s RPC (that package depends on
this one), so this mirrors the shape of `Raxol.ACP.Onchain.RPC` without the
dependency.
"""
@behaviour Raxol.Payments.ChainReader
@doc """
Build a reader handle over a `chain_id => rpc_url` map.
Options:
* `:chains` -- `%{pos_integer() => String.t()}` (required)
* `:req_options` -- extra `Req` options merged into every request (e.g.
`plug:` for `Req.Test`, `receive_timeout:`)
"""
@spec new(keyword()) :: Raxol.Payments.ChainReader.reader()
def new(opts) do
chains = Keyword.fetch!(opts, :chains)
req_options = Keyword.get(opts, :req_options, [])
{__MODULE__, %{chains: chains, req_options: req_options}}
end
@impl true
def get_receipt(state, chain_id, "0x" <> _ = tx_hash) do
case call(state, chain_id, "eth_getTransactionReceipt", [tx_hash]) do
{:ok, nil} -> {:ok, :pending}
{:ok, receipt} when is_map(receipt) -> parse_receipt(receipt)
{:error, _} = err -> err
end
end
def get_receipt(_state, _chain_id, _tx_hash), do: {:error, :invalid_tx_hash}
@impl true
def get_balance(state, chain_id, "0x" <> _ = address) do
with {:ok, hex} <- call(state, chain_id, "eth_getBalance", [address, "latest"]) do
decode_quantity(hex)
end
end
def get_balance(_state, _chain_id, _address), do: {:error, :invalid_address}
# balanceOf(address) selector 0x70a08231 + the 32-byte left-padded owner.
@balance_of_selector "0x70a08231"
@impl true
def get_erc20_balance(state, chain_id, "0x" <> _ = token, "0x" <> _ = owner) do
data = @balance_of_selector <> pad_address(owner)
with {:ok, hex} <- call(state, chain_id, "eth_call", [%{to: token, data: data}, "latest"]) do
decode_quantity(hex)
end
end
def get_erc20_balance(_state, _chain_id, _token, _owner), do: {:error, :invalid_argument}
# -- internal --
defp pad_address("0x" <> hex), do: hex |> String.downcase() |> String.pad_leading(64, "0")
defp parse_receipt(receipt) do
with {:ok, gas_used} <- decode_quantity(receipt["gasUsed"]),
{:ok, gas_price} <- effective_gas_price(receipt) do
{:ok,
%{
gas_used: gas_used,
effective_gas_price: gas_price,
status: receipt_status(receipt["status"])
}}
end
end
# Post-London receipts carry effectiveGasPrice; fall back to gasPrice for older
# nodes/chains that omit it.
defp effective_gas_price(%{"effectiveGasPrice" => hex}) when is_binary(hex),
do: decode_quantity(hex)
defp effective_gas_price(%{"gasPrice" => hex}) when is_binary(hex),
do: decode_quantity(hex)
defp effective_gas_price(_), do: {:ok, 0}
defp receipt_status("0x1"), do: :success
defp receipt_status(_), do: :reverted
defp call(state, chain_id, method, params) do
case Map.fetch(state.chains, chain_id) do
{:ok, url} -> do_call(url, state.req_options, method, params)
:error -> {:error, {:no_rpc_for_chain, chain_id}}
end
end
defp do_call(url, req_options, method, params) do
body = %{
jsonrpc: "2.0",
id: System.unique_integer([:positive]),
method: method,
params: params
}
req =
[url: url, headers: [{"content-type", "application/json"}]]
|> Keyword.merge(req_options)
|> Req.new()
case Req.post(req, json: body) do
{:ok, %Req.Response{status: 200, body: %{"result" => result}}} ->
{:ok, result}
{:ok, %Req.Response{status: 200, body: %{"error" => err}}} ->
{:error, {:rpc_error, err}}
{:ok, %Req.Response{status: status, body: resp_body}} ->
{:error, {:http, status, resp_body}}
{:error, reason} ->
{:error, {:transport, reason}}
end
end
# 0x-prefixed hex quantity -> integer. "0x" and "0x0" both decode as 0.
defp decode_quantity("0x"), do: {:ok, 0}
defp decode_quantity("0x" <> hex) do
case Integer.parse(hex, 16) do
{n, ""} -> {:ok, n}
_ -> {:error, {:hex_decode, "0x" <> hex}}
end
end
defp decode_quantity(other), do: {:error, {:hex_decode, other}}
end