Skip to content

Commit 266e912

Browse files
authored
Merge pull request #23 from etherspot/spec/complete-compliance
spec: complete Generic Relayer Architecture compliance
2 parents fcb6d34 + 10bbab7 commit 266e912

6 files changed

Lines changed: 502 additions & 163 deletions

File tree

README.md

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ cargo build --release
7575
| `--db-path` || `./relayx_db` | RocksDB path |
7676
| `--config` | `RELAYX_CONFIG` || JSON config file path |
7777
| `--relayer-private-key` | `RELAYX_PRIVATE_KEY` || Hex-encoded signer key |
78+
| `--disable-simulation` | `RELAYX_DISABLE_SIMULATION` | `false` | Skip pre-flight simulation (use default gas limit) |
79+
| `--disable-multichain` | `RELAYX_DISABLE_MULTICHAIN` | `false` | Return `4212` for `relayer_sendTransactionMultichain` calls |
7880

7981
Additional environment variables:
8082

@@ -94,7 +96,12 @@ Additional environment variables:
9496
"log_level": "info",
9597
"relayerPrivateKey": "0x...",
9698
"feeCollector": "0x55f3a93f544e01ce4378d25e927d7c493b863bd6",
99+
"feeCollectors": {
100+
"1": "0x55f3a93f544e01ce4378d25e927d7c493b863bd6",
101+
"137": "0x66f3a93f544e01ce4378d25e927d7c493b863bd7"
102+
},
97103
"defaultToken": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
104+
"disableMultichain": false,
98105
"rpcs": {
99106
"1": "https://ethereum.publicnode.com",
100107
"137": "https://polygon-rpc.com"
@@ -116,6 +123,8 @@ Additional environment variables:
116123
}
117124
```
118125

126+
> **`feeCollectors`** (optional) — per-chain overrides for the fee collector address. Takes precedence over the global `feeCollector` value for the specified chain.
127+
119128
## JSON-RPC Methods
120129

121130
All methods use JSON-RPC 2.0. The server listens on `POST /`.
@@ -134,7 +143,7 @@ Submit a single transaction for relay.
134143
| `payment` | `Payment` || How the relayer fee is paid |
135144
| `to` | `string` || Smart account (wallet) address |
136145
| `data` | `string` || ABI-encoded `executeWithRelayer(...)` calldata |
137-
| `context` | `object` || Arbitrary metadata; `callbackUrl` is read from here |
146+
| `context` | `object` || Arbitrary metadata; `callbackUrl` and `expiry` are read from here |
138147
| `authorizationList` | `AuthorizationItem[]` || EIP-7702 authorization entries |
139148
| `taskId` | `string` || Client-provided 32-byte hex task ID (`0x`-prefixed, 66 chars) |
140149

@@ -275,8 +284,8 @@ Query the status of a submitted transaction.
275284
"hash": "0x9b7bb827...",
276285
"receipt": {
277286
"blockHash": "0xf19bbafd...",
278-
"blockNumber": "0xabcd",
279-
"gasUsed": "0xdef",
287+
"blockNumber": "43981",
288+
"gasUsed": "3567",
280289
"transactionHash": "0x9b7bb827...",
281290
"logs": [
282291
{
@@ -361,7 +370,10 @@ Get current fee pricing for a token on a chain.
361370
| `chainId` | Chain the fee data applies to |
362371
| `token` | Token descriptor (`address`, `decimals`) |
363372
| `rate` | Tokens per 1 unit of native currency (e.g. USDC/ETH ≈ 2000.5); always `1.0` for native |
364-
| `gasPrice` | Current gas price in wei (hex string) |
373+
| `gasPrice` | Current gas price in wei (decimal string) |
374+
| `maxFeePerGas` | EIP-1559 max fee per gas in wei (decimal string, omitted on pre-EIP-1559 chains) |
375+
| `maxPriorityFeePerGas` | EIP-1559 max priority fee per gas in wei (decimal string, omitted on pre-EIP-1559 chains) |
376+
| `feeCollector` | Address clients should send payment tokens to (mirrors `relayer_getCapabilities`) |
365377
| `expiry` | Unix timestamp when this quote expires |
366378
| `minFee` | Minimum fee in token units (optional) |
367379
| `context` | Opaque signed quote context (optional) |
@@ -386,7 +398,10 @@ curl -X POST http://localhost:4937 \
386398
"chainId": "1",
387399
"token": { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "decimals": 6 },
388400
"rate": 2000.5,
389-
"gasPrice": "0x4a817c800",
401+
"gasPrice": "20000000000",
402+
"maxFeePerGas": "40000000000",
403+
"maxPriorityFeePerGas": "1000000000",
404+
"feeCollector": "0x55f3a93f544e01ce4378d25e927d7c493b863bd6",
390405
"expiry": 1741234867
391406
},
392407
"id": 1
@@ -434,7 +449,8 @@ Pass a `callbackUrl` inside `context` to receive a `POST` when the transaction s
434449
"to": "0x...",
435450
"data": "0x...",
436451
"context": {
437-
"callbackUrl": "https://yourapp.com/webhook/relay-status"
452+
"callbackUrl": "https://yourapp.com/webhook/relay-status",
453+
"expiry": 1741234867
438454
}
439455
}
440456
```
@@ -466,17 +482,28 @@ Callback failures are logged and silently dropped; they never affect the relay f
466482

467483
## Error Codes
468484

469-
| Code | Name | Description |
485+
Full set of spec-defined error codes (per the [Generic Relayer Architecture spec](https://hackmd.io/T4TkZYFQQnCupiuW231DYw)):
486+
487+
| Code | Name | Where raised |
470488
|------|------|-------------|
471-
| 4200 | Invalid Params | Missing or malformed request fields |
472-
| 4201 | Unsupported Chain | Chain ID not in relayer's config |
473-
| 4202 | Simulation Failed | `eth_call` reverted during pre-flight check |
474-
| 4203 | Insufficient Balance | Wallet balance too low to cover gas |
475-
| 4204 | Invalid Authorization List | Malformed EIP-7702 authorization entries |
476-
| 4205 | Unsupported Payment Token | ERC-20 token not in configured token list |
477-
| 4206 | Unsupported Capability | Payment type not supported |
478-
| 4207 | Task Not Found | No request found for given task ID |
479-
| 4214 | Duplicate Task ID | Client-provided task ID already in use |
489+
| `-32602` | Invalid Params | Missing or malformed request fields |
490+
| `4200` | Insufficient Payment | Fee-verification middleware: payment below required minimum |
491+
| `4201` | Invalid Signature | Signature-validation middleware: signer mismatch |
492+
| `4202` | Unsupported Payment Token | ERC-20 token not in configured token list |
493+
| `4203` | Rate Limit Exceeded | Rate-limiting middleware: per-address or per-key quota exceeded |
494+
| `4204` | Quote Expired | `context.expiry` is in the past at submission time |
495+
| `4205` | Insufficient Balance | Wallet native balance too low to cover gas cost |
496+
| `4206` | Unsupported Chain | Chain ID not in relayer's config |
497+
| `4207` | Transaction Too Large | Calldata exceeds 128 KB |
498+
| `4208` | Unknown Transaction ID | No request found for the given task ID |
499+
| `4209` | Unsupported Capability | Payment type not supported |
500+
| `4210` | Invalid Authorization List | Malformed EIP-7702 authorization entries |
501+
| `4211` | Simulation Failed | `eth_call` reverted during pre-flight check |
502+
| `4212` | Multichain Not Supported | `--disable-multichain` is set on this instance |
503+
| `4213` | Invalid Task ID | Client-provided `taskId` is not a valid 32-byte hex string |
504+
| `4214` | Duplicate Task ID | Client-provided `taskId` already has an associated job |
505+
506+
> Codes `4200`, `4201`, and `4203` are available as `pub(crate)` helpers for operators adding auth or rate-limiting middleware; they are not raised by the core relay path itself.
480507
481508
---
482509

@@ -533,7 +560,7 @@ Background Monitor (tokio::spawn)
533560
# Build
534561
cargo build --release
535562

536-
# Run all tests (28 tests, ~20ms)
563+
# Run all tests (40 tests: 12 unit + 28 integration, ~20ms)
537564
cargo test
538565

539566
# Lint

src/config.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ pub struct Config {
6767
/// Sentry DSN for error tracking (optional)
6868
#[arg(long = "sentry-dsn", env = "SENTRY_DSN")]
6969
pub sentry_dsn: Option<String>,
70+
71+
/// Disable multichain transaction support (relayer_sendTransactionMultichain returns 4212)
72+
#[arg(long = "disable-multichain", env = "RELAYX_DISABLE_MULTICHAIN")]
73+
pub disable_multichain: bool,
7074
}
7175

7276
impl Config {
@@ -138,6 +142,22 @@ impl Config {
138142
.map(|s| s.to_string())
139143
}
140144

145+
/// Returns the fee collector address for a specific chain.
146+
/// Checks `{ "feeCollectors": { chainId: "0x..." } }` first, then falls back to the global
147+
/// `feeCollector` value.
148+
pub fn fee_collector_for_chain(&self, chain_id: &str) -> Option<String> {
149+
if let Some(root) = self.get_json_config() {
150+
if let Some(addr) = root
151+
.get("feeCollectors")
152+
.and_then(|m| m.get(chain_id))
153+
.and_then(|v| v.as_str())
154+
{
155+
return Some(addr.to_string());
156+
}
157+
}
158+
self.fee_collector()
159+
}
160+
141161
/// Returns Chainlink native token/USD aggregator address for a chain
142162
/// Expects JSON structure: { "chainlink": { "nativeUsd": { "1": "0x..." } } }
143163
pub fn chainlink_native_usd(&self, chain_id: &str) -> Option<String> {
@@ -306,6 +326,18 @@ impl Config {
306326
.unwrap_or(false)
307327
}
308328

329+
/// Returns true when multichain transactions are administratively disabled.
330+
/// Operators can set `--disable-multichain`, `RELAYX_DISABLE_MULTICHAIN=true`, or
331+
/// `{ "disableMultichain": true }` in the JSON config.
332+
pub fn is_multichain_disabled(&self) -> bool {
333+
if self.disable_multichain {
334+
return true;
335+
}
336+
self.get_json_config()
337+
.and_then(|v| v.get("disableMultichain").and_then(|s| s.as_bool()))
338+
.unwrap_or(false)
339+
}
340+
309341
/// Get Sentry DSN from config.json or CLI/env
310342
pub fn get_sentry_dsn(&self) -> Option<String> {
311343
if let Some(cli_dsn) = self.sentry_dsn.as_ref().filter(|s| !s.is_empty()) {

0 commit comments

Comments
 (0)