Transaction Science open standards monorepo. Each subdirectory is a self-contained reference protocol with its own README and licence. - openpay/ Apache-2.0 payment-acceptance stack (Rust, 26 crates) - smart-byte/ CC-BY-4.0 content-addressed value carrier (spec) - eoc/ CC-BY-4.0 energy-optimised compute (spec) The wire format and the right to fork are public. The optional hosted services are the offer; the protocols are not.
193 lines
8.5 KiB
Markdown
193 lines
8.5 KiB
Markdown
# Phase 25 — Crypto rail: stablecoin settlement at sub-cent cost
|
||
|
||
**Status**: Draft v0.25
|
||
**Date**: 2026-05-22
|
||
|
||
## Why
|
||
|
||
The mission has always been "bring margin back to the vendors."
|
||
Card networks charge 100–300 basis points per transaction. A USDC
|
||
transfer on Base or Solana costs **single-cents or fractions of a
|
||
cent**. For a $1M/month vendor that's the difference between
|
||
$30,000 and $5 in fees. That is the entire point.
|
||
|
||
Phase 25 adds the crypto / stablecoin rail to OpenPay. First of
|
||
three sequenced phases (25 → 26 → 27): crypto first because it's
|
||
the most direct path to the margin win, then persistent backends
|
||
(deployable), then subscriptions (feature surface).
|
||
|
||
## What shipped
|
||
|
||
| # | Item | Where |
|
||
|--:|---|---|
|
||
| 1 | `RailKind::Crypto` variant in op-core | `op-core/src/rails.rs` |
|
||
| 2 | `PaymentMethod::Crypto(CryptoAddress)` variant in op-core | `op-core/src/method.rs` |
|
||
| 3 | `CryptoAddress { chain, address }` type with chain-agnostic constructor | `op-core/src/method.rs` |
|
||
| 4 | `op-rails-crypto` crate — `CryptoGateway` trait, `CryptoTransferReq` / `CryptoDecision` / `CryptoStatus`, `StableToken` curated catalog | `crates/op-rails-crypto/` |
|
||
| 5 | `op-orchestrator::CryptoAdapter` — wraps any `CryptoGateway` as a `RailAdapter` | `op-orchestrator/src/adapters/crypto.rs` |
|
||
| 6 | `PolicyRouter::with_crypto_drivers` + `rail_order` carve-out (Crypto is method-exclusive) | `op-orchestrator/src/router.rs` |
|
||
| 7 | `DeterministicCryptoGateway` in op-driver-sdk — per-key overrides, amount rules, transport-error mode, history | `op-driver-sdk/src/crypto.rs` |
|
||
| 8 | `conformance::run_crypto` harness — `CryptoMissingTxHashOnFinalized`, `CryptoCrossChainAccepted` checks | `op-driver-sdk/src/conformance.rs` |
|
||
| 9 | Orchestrator integration: crypto-routed approve + reject flows, all-three-rails conformance | `op-driver-sdk/tests/orchestrator_integration.rs` |
|
||
| 10 | `RailKind` codec in op-graph rail_telemetry covers `Crypto` | `op-graph/src/rail_telemetry.rs` |
|
||
|
||
Workspace at the end of Phase 25:
|
||
|
||
| Check | Result |
|
||
|---|---|
|
||
| `cargo build --workspace --all-targets` | **0 errors, 0 warnings** |
|
||
| `cargo test --workspace` | **957 passing, 0 failing** (+27 vs Phase 24) |
|
||
| `cargo clippy --workspace --all-targets` | **0 warnings** |
|
||
|
||
Test-count delta: op-rails-crypto +7 (token / status), op-orchestrator
|
||
adapters/crypto +7, op-driver-sdk +11 (8 mock + 3 conformance), e2e +2.
|
||
|
||
## Architecture
|
||
|
||
```text
|
||
on-chain
|
||
▲
|
||
PaymentIntent ──► PolicyRouter ──► CryptoAdapter ──► CryptoGateway ──► chain client
|
||
│ (driver-owned) │
|
||
│ ▼
|
||
└──Idempotency key flows through unchanged──► signer / RPC / Fireblocks
|
||
```
|
||
|
||
Three trait layers, mirroring the existing card / A2A pattern:
|
||
|
||
1. **`CryptoGateway`** (in `op-rails-crypto`) — the only thing
|
||
driver authors implement. Methods: `submit_transfer`,
|
||
`query_status`, plus accessors (`name`, `chain`, `token`,
|
||
`supports`).
|
||
2. **`CryptoAdapter`** (in `op-orchestrator`) — wraps any
|
||
`CryptoGateway` as a `RailAdapter` the orchestrator can route
|
||
through. Status → outcome mapping:
|
||
`Finalized → Success`, `Rejected → HardDecline`,
|
||
`Pending|Confirming → SoftFailure`, `Transient → SoftFailure`.
|
||
3. **`DeterministicCryptoGateway`** (in `op-driver-sdk`) —
|
||
programmable mock for operator tests + driver-author reference.
|
||
|
||
## Why no chain SDK dependency
|
||
|
||
The reference stack does NOT pull in `solana-sdk` or `ethers-rs`.
|
||
Three reasons:
|
||
|
||
1. **Footprint.** `ethers-rs` alone is ~250 crates.
|
||
2. **Signing security.** Operators sign with HSMs, KMS, multisig,
|
||
Fireblocks. Baking in a software keystore would be a footgun.
|
||
3. **Chain evolution.** New chains and L2s appear quarterly; the
|
||
reference stack stays neutral by exposing only the trait
|
||
surface.
|
||
|
||
Operators wire their preferred client behind a `CryptoGateway`
|
||
impl — a `RpcClient` for Solana, an `EthersProvider` for EVM,
|
||
Fireblocks for custody, whatever fits their security posture.
|
||
|
||
## StableToken catalog
|
||
|
||
Curated `(chain, contract, decimals, symbol)` constants for the
|
||
common deployments:
|
||
|
||
| Constructor | Chain | Symbol | Decimals |
|
||
|---|---|---|---|
|
||
| `StableToken::UsdcSolana` | solana | USDC | 6 |
|
||
| `StableToken::UsdcBase` | base | USDC | 6 |
|
||
| `StableToken::UsdcEthereum` | ethereum | USDC | 6 |
|
||
| `StableToken::UsdcPolygon` | polygon | USDC | 6 |
|
||
| `StableToken::UsdcArbitrum` | arbitrum | USDC | 6 |
|
||
| `StableToken::EurcBase` | base | EURC | 6 |
|
||
| `StableToken::PyusdEthereum` | ethereum | PYUSD | 6 |
|
||
|
||
Drivers **must** re-validate contract addresses against the
|
||
issuer's official docs before broadcasting — token contracts have
|
||
been upgraded / re-deployed in the past, and minting a transfer to
|
||
a stale contract burns funds.
|
||
|
||
## CryptoStatus taxonomy
|
||
|
||
```rust
|
||
Pending // submitted, not yet in a block
|
||
Confirming // in a block, below operator confirmation threshold
|
||
Finalized // meets operator finality threshold — settled
|
||
Rejected // chain refused (revert, sim failure, invalid sig)
|
||
Transient // RPC/network — may or may not have broadcast
|
||
```
|
||
|
||
`funds_moved()` is true only for `Finalized`. Operator-acceptable
|
||
finality is driver-side (1 confirmation on Solana, 12 on Ethereum
|
||
mainnet, varies on L2s).
|
||
|
||
## Routing changes
|
||
|
||
`PolicyRouter::rail_order` short-circuits: any
|
||
`PaymentMethod::Crypto(_)` routes exclusively to crypto drivers.
|
||
No fallback to card or A2A — those rails physically can't fulfill
|
||
a crypto destination, so the orchestrator doesn't try.
|
||
|
||
```rust
|
||
let router = PolicyRouter::new(vec![], vec![])
|
||
.with_crypto_drivers(vec!["usdc-base".to_owned(), "usdc-solana".to_owned()]);
|
||
```
|
||
|
||
The chain-level routing (which `(chain, token)` driver) is the
|
||
operator's choice — they register one adapter per pair and rely on
|
||
`supports()` to filter by destination chain.
|
||
|
||
## Conformance additions
|
||
|
||
```rust
|
||
let gateway = MyChainGateway::sandbox(rpc_url, signer);
|
||
let report = op_driver_sdk::conformance::run_crypto(&gateway);
|
||
assert!(report.is_clean(), "{:?}", report.failures);
|
||
```
|
||
|
||
New failure modes:
|
||
|
||
- `CryptoMissingTxHashOnFinalized` — finalized without a tx_hash;
|
||
callers have no handle for reconciliation.
|
||
- `CryptoCrossChainAccepted { gateway_chain, address_chain }` —
|
||
driver accepted a destination on a chain it doesn't service. A
|
||
Solana driver shouldn't broadcast EVM addresses.
|
||
|
||
## Honest concerns (carry-forward)
|
||
|
||
- **No on-chain signing.** That's operator-side by design (see
|
||
"Why no chain SDK dependency" above).
|
||
- **No bridging.** Cross-chain transfers (USDC@Base → USDC@Solana)
|
||
are out of scope. Operators handle multi-chain by registering
|
||
one driver per `(chain, token)` and routing on destination chain.
|
||
- **Confirmation polling is operator-side.** A transfer that
|
||
comes back `Confirming` is reported as such; the operator drives
|
||
the polling loop via `gateway.query_status(...)` and decides
|
||
when depth meets their finality threshold.
|
||
- **Fiat ⇄ stablecoin conversion is operator-side.** The intent's
|
||
`Money` flows through unchanged. Operators quoting in USD and
|
||
settling in USDC (1:1 by definition) handle the 2-decimal vs
|
||
6-decimal scaling in their quote layer.
|
||
- **Memo support varies by chain.** Solana has the memo program;
|
||
EVM has no standard memo. Drivers either emit an indexed event
|
||
log or skip memos entirely.
|
||
- **Reorg handling is operator-side.** If a chain reorgs and a
|
||
previously-finalized tx un-confirms, the operator's polling
|
||
loop is the right layer to detect it and post a compensating
|
||
ledger entry.
|
||
- **No L2-specific routing logic.** Drivers register per `(chain,
|
||
token)`; cost-optimization across L2s (route Base when fees are
|
||
low, Arbitrum when not) is a higher-layer concern operators
|
||
build on top.
|
||
|
||
## Test totals
|
||
|
||
```
|
||
op-rails-crypto 7 (token catalog 3, status predicates 4)
|
||
op-orchestrator/crypto 7 (adapter status mapping + idempotency + chain check)
|
||
op-driver-sdk crypto mock 8 (default / overrides / amount / transport / xchain / etc.)
|
||
op-driver-sdk conformance 3 (deterministic passes, missing tx_hash, cross-chain accept)
|
||
e2e integration 2 (crypto routed through Orchestrator, rejection path)
|
||
----
|
||
+27 net
|
||
```
|
||
|
||
`cargo test --workspace`: **957 passing, 0 failing.**
|
||
`cargo build --workspace --all-targets`: **0 warnings.**
|
||
`cargo clippy --workspace --all-targets`: **0 warnings.**
|