Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/ci-unified.yml
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,31 @@ jobs:
sudo apt-get update
sudo apt-get install -y tmux

# raxol_payments' EIP-712 conformance vectors and its vendored fee oracle
# (priv/fee-oracle/schedule.json) are held to ONE published @xochi/spec
# release, pinned in test/fixtures/XOCHI_SPEC_VERSION -- never a sibling
# checkout. Under CI both suites fail when these variables are unset, so a
# broken fetch is a red cell rather than a green run over zero vectors.
- name: Set up Node (pinned @xochi/spec)
if: matrix.package == 'raxol_payments'
uses: actions/setup-node@v7
with:
node-version: '22'

- name: Fetch pinned @xochi/spec
if: matrix.package == 'raxol_payments'
working-directory: packages/raxol_payments
run: |
version="$(cat test/fixtures/XOCHI_SPEC_VERSION)"
dest="$RUNNER_TEMP/xochi-spec"
mkdir -p "$dest"
npm pack "@xochi/spec@${version}" --pack-destination "$dest"
tar -xzf "$dest/xochi-spec-${version}.tgz" -C "$dest"
test -f "$dest/package/conformance/conformance.json"
test -f "$dest/package/fee/schedule.json"
echo "XOCHI_SPEC_DIR=$dest/package" >> "$GITHUB_ENV"
echo "CONFORMANCE_FIXTURE_PATH=$dest/package/conformance/conformance.json" >> "$GITHUB_ENV"

# `--check-locked` rather than a bare `deps.get`: a plain fetch REWRITES
# a stale mix.lock in place and carries on, so a package whose committed
# lock does not match what resolution produces builds green in CI and
Expand Down
2 changes: 1 addition & 1 deletion packages/raxol_earn/test/test_helper.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# (off-chain intent signature + captured on-chain write).
# :live_parity -- re-checks the live Xochi quote shape against the committed
# fixture. Read-only, no funds, but it does call out to the network.
# :cli_signer -- spawns the riddler-client CLI; auto-enabled when
# :cli_signer -- spawns Riddler's e2e-signer CLI; auto-enabled when
# RIDDLER_CLI_DIR is set.
#
# A single ExUnit.start sets the full list; a second ExUnit.configure(exclude:)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Raxol.Payments.FeeSchedule do

Authoritative source is `Riddler.Integrations.Xochi.FeePolicy`. This module
is its in-code mirror on the raxol side, exactly as
`@riddler/sdk-taker`'s `fee.ts` is on the TypeScript side, and it is pinned
`@xochi/client`'s `fee.ts` is on the TypeScript side, and it is pinned
against the same generated projection both of those are checked against
(`priv/fee-oracle/schedule.json`). `fee_schedule_test.exs` fails when the two
drift, so a fee rate cannot quietly go stale here while the solver charges
Expand Down
1 change: 1 addition & 0 deletions packages/raxol_payments/test/fixtures/XOCHI_SPEC_VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.8.1
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ defmodule Raxol.Payments.Conformance.Permit2ConformanceTest do
end
end

# `by_protocol/1` answers [] both when the fixture is absent and when it loads
# but carries no permit2 vectors, and a `for` over [] generates no tests at
# all. The moduletag above covers the first case; this covers the second,
# where the suite would otherwise report a green run having asserted nothing
# about Permit2.
describe "fixture coverage" do
test "the loaded fixture yields at least one permit2 vector" do
assert ConformanceFixture.by_protocol("permit2") != [],
"fixture loaded but produced no permit2 vectors -- every generated " <>
"test below was silently skipped out of existence"
end
end

describe "Permit2 EIP-712 conformance vs CLI" do
for vec <- ConformanceFixture.by_protocol("permit2") do
@vec vec
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Raxol.Payments.Conformance.WalletSignerParityTest do
@moduledoc """
Byte-for-byte parity between the wallet signer and the reference signer
(ethers, via the riddler-client CLI).
(ethers, via Riddler's `packages/e2e-signer` CLI).

Both sign the same EIP-712 digest with the same key using deterministic
(RFC 6979, low-s) ECDSA, so `r`, `s`, and the recovery byte `v` must be
Expand All @@ -10,8 +10,9 @@ defmodule Raxol.Payments.Conformance.WalletSignerParityTest do
`ecrecover` (ERC-3009 origin pull / Permit2 / x402) rejects. Regression guard
for the `v` normalization in `Raxol.Payments.EIP712.pack_signature/1`.

Tagged `:cli_signer` (excluded by default): needs node and the CLI checkout.
Set `RIDDLER_CLI_DIR`, or check the CLI out beside the raxol repo.
Tagged `:cli_signer` (excluded by default): needs node and a Riddler
checkout with `packages/e2e-signer` built. Set `RIDDLER_CLI_DIR` to it, or
check Riddler out beside the raxol repo.
"""
use ExUnit.Case, async: true

Expand All @@ -28,12 +29,14 @@ defmodule Raxol.Payments.Conformance.WalletSignerParityTest do
setup do
System.put_env(@env_var, @key)
on_exit(fn -> System.delete_env(@env_var) end)
{:ok, cli_dir: locate_cli()}
{:ok, cli_dir: CliSigner.locate()}
end

test "Env wallet signs an ERC-3009 digest identically to the reference CLI", %{cli_dir: cwd} do
unless cwd do
flunk("CLI not available; set RIDDLER_CLI_DIR or run with --exclude cli_signer")
flunk(
"CLI not available; set RIDDLER_CLI_DIR to a built Riddler checkout or run with --exclude cli_signer"
)
end

# acp-buyer-auth signs an ERC-3009 ReceiveWithAuthorization digest with
Expand Down Expand Up @@ -66,27 +69,6 @@ defmodule Raxol.Payments.Conformance.WalletSignerParityTest do
assert :binary.last(sig) in [27, 28]
end

# Resolve the CLI checkout: explicit env var, then the two common dev layouts
# (beside the raxol repo under a shared parent, or inside it). Returns nil when
# absent so the test flunks with a clear message under `--only cli_signer`.
# The CLI repo (axol-io/riddler-sdk, formerly riddler-client) became a
# monorepo and its entry point moved to packages/sdk-taker/src/cli.ts.
# Probing for the retired src/index.js + src/acp.js answered nil even with
# RIDDLER_CLI_DIR set correctly, which silently retired this oracle: the test
# is :cli_signer-tagged and excluded by default, so the flunk below was never
# reached. Probe the entry point CliSigner actually spawns.
defp locate_cli do
[
System.get_env("RIDDLER_CLI_DIR"),
Path.expand("../../../../../../../riddler-sdk", __DIR__),
Path.expand("../../../../../../riddler-sdk", __DIR__),
Path.expand("../../../../../../../riddler-client", __DIR__),
Path.expand("../../../../../../riddler-client", __DIR__)
]
|> Enum.reject(&is_nil/1)
|> Enum.find(&File.exists?(Path.join([&1, "packages", "sdk-taker", "src", "cli.ts"])))
end

defp decode_hex("0x" <> hex), do: Base.decode16(hex, case: :mixed)
defp decode_hex(_), do: :error
end
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@ defmodule Raxol.Payments.Conformance.X402ConformanceTest do
]
}

# `by_protocol/1` answers [] both when the fixture is absent and when it loads
# but carries no erc3009 vectors, and a `for` over [] generates no tests at
# all. The moduletag above covers the first case; this covers the second,
# where the suite would otherwise report a green run having asserted nothing
# about ERC-3009.
describe "fixture coverage" do
test "the loaded fixture yields at least one erc3009 vector" do
assert ConformanceFixture.by_protocol("erc3009") != [],
"fixture loaded but produced no erc3009 vectors -- every generated " <>
"test below was silently skipped out of existence"
end
end

describe "ERC-3009 (x402) EIP-712 conformance vs CLI" do
for vec <- ConformanceFixture.by_protocol("erc3009") do
@vec vec
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@ defmodule Raxol.Payments.FeeScheduleTest do
Parity against the canonical schedule, plus the arithmetic on top of it.

The oracle in `priv/fee-oracle/schedule.json` is the generated projection of
`Riddler.Integrations.Xochi.FeePolicy` -- the same artifact `@riddler/sdk-taker`
checks its TypeScript mirror against. Pinning to it is what makes this module
a mirror rather than a second opinion: if Riddler reprices, these fail instead
of raxol.io quietly advertising a rate nobody charges.
`Riddler.Integrations.Xochi.FeePolicy` -- the same artifact `@xochi/client`
checks its TypeScript mirror (`fee.ts`) against. Pinning to it is what makes
this module a mirror rather than a second opinion: if Riddler reprices, these
fail instead of raxol.io quietly advertising a rate nobody charges.
"""
use ExUnit.Case, async: true

alias Raxol.Payments.FeeSchedule
alias Raxol.Payments.Test.XochiSpec

@oracle "fee-oracle/schedule.json"
|> then(&Path.join(:code.priv_dir(:raxol_payments), &1))
|> File.read!()
|> Jason.decode!()
@oracle_path Path.join(:code.priv_dir(:raxol_payments), "fee-oracle/schedule.json")
@oracle @oracle_path |> File.read!() |> Jason.decode!()

defp tier_atom(name), do: String.to_existing_atom(name)

Expand Down Expand Up @@ -71,6 +70,35 @@ defmodule Raxol.Payments.FeeScheduleTest do
end
end

# The oracle is a vendored copy, so the parity above only proves this module
# agrees with the copy. This proves the copy is the pinned release's bytes.
describe "vendored oracle vs the pinned @xochi/spec" do
if is_nil(XochiSpec.dir()) and not XochiSpec.ci?() do
@tag skip: "XOCHI_SPEC_DIR unset (the unpacked @xochi/spec tarball root)"
end

test "priv/fee-oracle/schedule.json is byte-identical to fee/schedule.json" do
version = XochiSpec.pinned_version()

dir =
XochiSpec.dir() ||
flunk(
"XOCHI_SPEC_DIR is unset under CI. Unpack npm pack " <>
"@xochi/spec@#{version} and set it to the tarball's package/ directory."
)

manifest = dir |> Path.join("package.json") |> File.read!() |> Jason.decode!()

assert {manifest["name"], manifest["version"]} == {"@xochi/spec", version},
"XOCHI_SPEC_DIR=#{dir} is not @xochi/spec@#{version} " <>
"(test/fixtures/XOCHI_SPEC_VERSION)"

assert File.read!(Path.join([dir, "fee", "schedule.json"])) == File.read!(@oracle_path),
"priv/fee-oracle/schedule.json drifted from @xochi/spec@#{version} " <>
"fee/schedule.json; copy the release's file over it"
end
end

describe "headline rates" do
test "are the sum of the three layers" do
# The published totals: stable 22/19/15/12/10, volatile 40/35/29/25/22.
Expand Down
72 changes: 36 additions & 36 deletions packages/raxol_payments/test/support/cli_signer.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defmodule Raxol.Payments.Test.CliSigner do
@moduledoc """
Spawn the `riddler-client` CLI as a black-box signing oracle.
Spawn Riddler's `packages/e2e-signer` CLI as a black-box signing oracle.

The CLI emits a single `RESULT_JSON: { ... }` line on stdout for the
sign-only and ACP buyer-auth subcommands; this module spawns the CLI
Expand All @@ -11,17 +11,22 @@ defmodule Raxol.Payments.Test.CliSigner do
raxol_earn end-to-end tests that exercise the buyer-side authorization
path.

## Resolving the CLI repo
## Resolving the CLI

Lookup order:
The taker CLI (formerly axol-io/riddler-sdk `packages/sdk-taker`) now lives
in the private Riddler repo as `packages/e2e-signer`. It is run from its
built output, so build it first (`npm ci && npm run build` in
`packages/e2e-signer`). Riddler is private, so `:cli_signer` stays a
local-only tag.

Lookup order for the Riddler checkout root:

1. `opts[:cwd]`
2. `System.get_env("RIDDLER_CLI_DIR")`
3. Sibling-directory fallback assuming `riddler-client` is
checked out beside this monorepo's parent.
3. A sibling `Riddler` checkout beside this monorepo.

If neither `cwd` nor `RIDDLER_CLI_DIR` is set and the sibling fallback
doesn't exist, every call raises `Raxol.Payments.Test.CliSigner.CliNotFoundError`.
If none holds `packages/e2e-signer/dist/cli.js`, every call raises
`Raxol.Payments.Test.CliSigner.CliNotFoundError`.

## Example

Expand Down Expand Up @@ -49,8 +54,8 @@ defmodule Raxol.Payments.Test.CliSigner do

@result_json_regex ~r/^RESULT_JSON:\s*(\{.*\})\s*$/m

# Path (relative to the riddler-client repo root) of the CLI entry point.
@cli_entry "packages/sdk-taker/src/cli.ts"
# Path (relative to the Riddler repo root) of the built CLI entry point.
@cli_entry "packages/e2e-signer/dist/cli.js"

@type flag :: {atom() | String.t(), String.t() | integer() | boolean()}
@type opts :: [
Expand Down Expand Up @@ -79,12 +84,10 @@ defmodule Raxol.Payments.Test.CliSigner do
{:ok, result()} | {:error, {:cli_failed, integer(), String.t()}}
def run(subcommand, flags, opts \\ []) do
cwd = resolve_cwd(opts)
# riddler-client is now a monorepo; the CLI lives in the riddler-sdk package
# and is TypeScript, so it is run from source via tsx (no build step needed).
args = ["tsx", @cli_entry, subcommand | encode_flags(flags)]
args = [@cli_entry, subcommand | encode_flags(flags)]
env = build_env(opts)

{stdout, exit_code} = System.cmd("npx", args, cd: cwd, env: env, stderr_to_stdout: true)
{stdout, exit_code} = System.cmd("node", args, cd: cwd, env: env, stderr_to_stdout: true)

case exit_code do
0 ->
Expand Down Expand Up @@ -127,40 +130,37 @@ defmodule Raxol.Payments.Test.CliSigner do
run("xochi-flow", flags, opts)
end

@doc """
The Riddler checkout holding the built CLI (`RIDDLER_CLI_DIR`, then a
sibling `Riddler` checkout), or nil when neither has it.
"""
@spec locate() :: Path.t() | nil
def locate do
Enum.find([System.get_env("RIDDLER_CLI_DIR"), sibling_default()], &cli_present?/1)
end

# -- Internals --

defp resolve_cwd(opts) do
cwd =
opts[:cwd] ||
System.get_env("RIDDLER_CLI_DIR") ||
sibling_default()
cwd = opts[:cwd] || System.get_env("RIDDLER_CLI_DIR") || sibling_default()

if cwd && File.dir?(cwd) && File.exists?(Path.join([cwd | Path.split(@cli_entry)])) do
if cli_present?(cwd) do
cwd
else
raise CliNotFoundError,
message:
"Could not locate riddler-client CLI. Set RIDDLER_CLI_DIR or pass cwd: ... " <>
"(tried #{inspect(cwd)})"
"Could not locate the Riddler e2e-signer CLI (#{@cli_entry}). " <>
"Set RIDDLER_CLI_DIR to a Riddler checkout with packages/e2e-signer " <>
"built, or pass cwd: ... (tried #{inspect(cwd)})"
end
end

# Best-effort fallback: a sibling checkout of the CLI repo (axol-io/riddler-sdk,
# formerly riddler-client) beside the raxol monorepo, the common dev layout
# where both live under ~/CODE/.
#
# The previous expansion was one level short and landed on
# <raxol>/riddler-client, inside this repo, so the fallback never resolved
# under either repo name.
defp sibling_default do
Enum.find(
[
Path.expand("../../../../../riddler-sdk", __DIR__),
Path.expand("../../../../../riddler-client", __DIR__)
],
&File.dir?/1
)
end
defp cli_present?(nil), do: false
defp cli_present?(dir), do: File.regular?(Path.join(dir, @cli_entry))

# Best-effort fallback: a Riddler checkout beside the raxol monorepo, the
# common dev layout where both live under ~/CODE/.
defp sibling_default, do: Path.expand("../../../../../Riddler", __DIR__)

defp encode_flags(flags) do
Enum.flat_map(flags, fn
Expand Down
Loading
Loading