From c646c92f8296d97b7df6d98f1f6baa226eafc831 Mon Sep 17 00:00:00 2001 From: John Mitsch Date: Tue, 11 Aug 2026 14:53:32 -0400 Subject: [PATCH 1/3] feat(rpc): support Arc Testnet faucet drips --- IMPLEMENTATION_PLAN.md | 7 +++++++ README.md | 6 +++--- src/commands/agent/context.md | 4 ++-- src/commands/rpc/pay_asset.rs | 9 ++++++++ src/commands/rpc/pay_network.rs | 2 ++ src/commands/rpc/x402.rs | 29 +++++++++++++++++++------- tests/rpc_payment.rs | 37 ++++++++++++++++++++++++++++++++- 7 files changed, 81 insertions(+), 13 deletions(-) diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 3b6dc30..31f5a9a 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -17,3 +17,10 @@ **Goal**: Validate both repositories with formatting, tests, clippy, and builds. **Success criteria**: Required checks pass with no new warnings. **Status**: Complete + +## Stage 5: Faucet compatibility and Arc Testnet +**Goal**: Support both asynchronous Circle transfer responses and direct Arc Testnet +transaction responses from `qn rpc x402 drip`. +**Success criteria**: Arc network and USDC resolution work, both response shapes +render correctly, and the CLI passes all verification checks against the local SDK. +**Status**: In Progress diff --git a/README.md b/README.md index 3aff81c..3fb0195 100644 --- a/README.md +++ b/README.md @@ -427,8 +427,8 @@ successful response). EVM and Solana payment wallets are supported: ```sh qn wallet generate --vm evm --name payer # dedicated wallet -# Testnet only: fund the wallet from the faucet (Base Sepolia, once per -# account). Prints the funding tx; mainnet wallets are funded normally. +# Testnet only: fund the wallet from the faucet (Base Sepolia or Arc Testnet). +# Circle-backed networks return an asynchronous transfer ID; Arc returns a tx hash. qn rpc x402 drip --payment-wallet payer --payment-network base-sepolia # Buy prepaid credits with the funded wallet (moves real funds; gated — pass @@ -514,7 +514,7 @@ channels. Exhausting the deposit points you at `qn rpc mpp top-up`; after The flag stack is the same across all four paths: -- `--payment-network` takes a Quicknode network name (`base-sepolia`, +- `--payment-network` takes a Quicknode network name (`base-sepolia`, `arc-testnet`, `solana-devnet`, `tempo-testnet`, ...) or a raw CAIP-2 id (`eip155:84532`, `solana:EtWTRA...`); anything with a `:` passes through verbatim. - `--payment-asset` takes a token address (EVM), a mint (Solana), or a symbol diff --git a/src/commands/agent/context.md b/src/commands/agent/context.md index b441875..18861af 100644 --- a/src/commands/agent/context.md +++ b/src/commands/agent/context.md @@ -189,8 +189,8 @@ Top-level nouns (plurals like `endpoints`/`streams` and `ls` are accepted aliase **x402 drawdown** uses a cached gateway session. `qn rpc x402 buy-credits` signs the credit purchase and requires the full payment flags. `balance` and `drip` authenticate the wallet but do not sign a payment, so they need only - the wallet and payment network. `drip` works on Base Sepolia only. Fund - Solana wallets out of band. `buy-credits` is gated and needs `--yes` in a + the wallet and payment network. `drip` works on Base Sepolia and Arc Testnet. + Fund other wallets out of band. `buy-credits` is gated and needs `--yes` in a script. `qn rpc call --x402-drawdown` needs the query network and wallet; it uses one credit after a successful response and does not need an asset or spend ceiling. The pay network defaults to the query network. Credits can pay diff --git a/src/commands/rpc/pay_asset.rs b/src/commands/rpc/pay_asset.rs index d38ceaf..e7da202 100644 --- a/src/commands/rpc/pay_asset.rs +++ b/src/commands/rpc/pay_asset.rs @@ -29,6 +29,11 @@ const PAY_ASSETS: &[(&str, &str, &str)] = &[ "usdc", "0x20c0000000000000000000000000000000000000", ), + ( + "eip155:5042002", + "usdc", + "0x3600000000000000000000000000000000000000", + ), ( "eip155:8453", "usdc", @@ -119,6 +124,10 @@ mod tests { resolve("usdc", "eip155:42431").unwrap(), "0x20c0000000000000000000000000000000000000" ); + assert_eq!( + resolve("usdc", "eip155:5042002").unwrap(), + "0x3600000000000000000000000000000000000000" + ); } #[test] diff --git a/src/commands/rpc/pay_network.rs b/src/commands/rpc/pay_network.rs index 8df41f6..f9db223 100644 --- a/src/commands/rpc/pay_network.rs +++ b/src/commands/rpc/pay_network.rs @@ -10,6 +10,7 @@ const PAY_NETWORKS: &[(&str, &str)] = &[ ("abstract-testnet", "eip155:11124"), ("arbitrum-mainnet", "eip155:42161"), ("arbitrum-sepolia", "eip155:421614"), + ("arc-testnet", "eip155:5042002"), ("ault-mainnet", "eip155:904"), ("ault-testnet", "eip155:10904"), ("avalanche-mainnet", "eip155:43114"), @@ -143,6 +144,7 @@ mod tests { assert_eq!(resolve("base-sepolia").unwrap(), "eip155:84532"); assert_eq!(resolve("xdai").unwrap(), "eip155:100"); assert_eq!(resolve("tempo-testnet").unwrap(), "eip155:42431"); + assert_eq!(resolve("arc-testnet").unwrap(), "eip155:5042002"); assert_eq!( resolve("solana-devnet").unwrap(), "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1" diff --git a/src/commands/rpc/x402.rs b/src/commands/rpc/x402.rs index a495d4b..cdb4507 100644 --- a/src/commands/rpc/x402.rs +++ b/src/commands/rpc/x402.rs @@ -332,14 +332,25 @@ async fn run_balance(args: SessionArgs, global: GlobalArgs) -> Result<(), CliErr } async fn run_drip(args: SessionArgs, global: GlobalArgs) -> Result<(), CliError> { - reject_non_base_drip(&args, &global)?; + reject_unsupported_drip(&args, &global)?; let ctx = session_setup(&args, global.clone())?; let session = ensure_gateway_session(&ctx, &global).await?; let receipt = ctx.sdk.rpc.gateway_drip(&session).await?; + let settlement = receipt + .transfer_id + .as_deref() + .map(|id| format!("transfer: {id}")) + .or_else(|| { + receipt + .transaction_hash + .as_deref() + .map(|hash| format!("tx: {hash}")) + }) + .unwrap_or_else(|| "settlement pending".to_string()); ctx.out.note(&format!( - "✓ Faucet funded {} (tx: {})", - receipt.account_id, receipt.transaction_hash + "✓ Faucet request accepted for {} ({settlement})", + receipt.account_id )); let wallet = args.payment_wallet.as_deref().unwrap_or(""); let pay_net = args.payment_network.as_deref().unwrap_or(""); @@ -406,17 +417,21 @@ async fn run_drip(args: SessionArgs, global: GlobalArgs) -> Result<(), CliError> if matches!(ctx.global.format, Some(f) if f.is_structured()) { let v = serde_json::json!({ "account_id": receipt.account_id, + "wallet_address": receipt.wallet_address, + "network": receipt.network, + "transfer_id": receipt.transfer_id, + "amount_usdc": receipt.amount_usdc, "transaction_hash": receipt.transaction_hash, }); return super::emit_result(&ctx, &v); } if !ctx.out.stdout_is_tty { - println!("{}", receipt.transaction_hash); + println!("{settlement}"); } Ok(()) } -fn reject_non_base_drip(args: &SessionArgs, global: &GlobalArgs) -> Result<(), CliError> { +fn reject_unsupported_drip(args: &SessionArgs, global: &GlobalArgs) -> Result<(), CliError> { let section = load_payment_section(global)?; let Some(network) = args .payment_network @@ -426,9 +441,9 @@ fn reject_non_base_drip(args: &SessionArgs, global: &GlobalArgs) -> Result<(), C return Ok(()); }; let resolved = super::pay_network::resolve(&network)?; - if resolved != "eip155:84532" { + if !matches!(resolved.as_str(), "eip155:84532" | "eip155:5042002") { return Err(CliError::Arg( - "x402 drip is available only on Base Sepolia. Fund Solana wallets out of band." + "x402 drip is available on Base Sepolia and Arc Testnet. Fund other wallets out of band." .to_string(), )); } diff --git a/tests/rpc_payment.rs b/tests/rpc_payment.rs index 18a9ca4..d87b01e 100644 --- a/tests/rpc_payment.rs +++ b/tests/rpc_payment.rs @@ -1261,7 +1261,9 @@ async fn x402_drip_reports_funding_tx() { .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "accountId": "eip155:84532:0xabc", "walletAddress": "0xabc", - "transactionHash": "0xfeed" + "network": "eip155:84532", + "transferId": "transfer-1", + "amountUsdc": "10" }))) .mount(&server) .await; @@ -1275,6 +1277,39 @@ async fn x402_drip_reports_funding_tx() { assert_eq!(out.exit_code, 0, "stderr={}", out.stderr); } +#[tokio::test] +async fn x402_drip_accepts_arc_transaction_hash() { + let server = MockServer::start().await; + mount_auth(&server).await; + Mock::given(method("POST")) + .and(path("/drip")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "accountId": "eip155:5042002:0xabc", + "walletAddress": "0xabc", + "network": "eip155:5042002", + "transactionHash": "0xfeed" + }))) + .mount(&server) + .await; + + let dir = tempfile::tempdir().unwrap(); + let cfg = dir.path().join("config.toml").to_str().unwrap().to_string(); + let (_guard, key_path) = key_file(); + let args = [ + "--config-file", + cfg.as_str(), + "rpc", + "x402", + "drip", + "--payment-key-file", + key_path.as_str(), + "--payment-network", + "arc-testnet", + ]; + let out = run_qn(&server.uri(), &args).await; + assert_eq!(out.exit_code, 0, "stderr={}", out.stderr); +} + #[tokio::test] async fn x402_drip_rejects_solana_without_network_request() { let server = MockServer::start().await; From 035525b7b6cbaf9c77745cfd664e915958fa822a Mon Sep 17 00:00:00 2001 From: John Mitsch Date: Tue, 11 Aug 2026 15:06:09 -0400 Subject: [PATCH 2/3] build: pin CLI to faucet SDK fix --- Cargo.lock | 3 +-- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d94b0db..e34d146 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3246,8 +3246,7 @@ dependencies = [ [[package]] name = "quicknode-sdk" version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3e7b2ee767ea526c6508b09639ad4c35edeca3e7f642babc5fdff249b54861" +source = "git+https://github.com/quicknode/sdk?rev=123cbf3#123cbf328e784b3b3d9aaa2f9d2de6eae42dcc38" dependencies = [ "alloy-consensus", "alloy-primitives", diff --git a/Cargo.toml b/Cargo.toml index ec4bd66..f6342f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ name = "qn" path = "src/lib.rs" [dependencies] -quicknode-sdk = { version = "0.8.1", features = [ +quicknode-sdk = { git = "https://github.com/quicknode/sdk", rev = "123cbf3", version = "0.8.1", features = [ "payments", # x402/EVM "payments-svm", # + x402/Solana "payments-tempo", # + MPP/Tempo From adc4065edfbef1f3adf017d1b41e517f99ef526c Mon Sep 17 00:00:00 2001 From: John Mitsch Date: Tue, 11 Aug 2026 15:54:52 -0400 Subject: [PATCH 3/3] build: use released SDK 0.8.2 --- Cargo.lock | 5 +++-- Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e34d146..598b55b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3245,8 +3245,9 @@ dependencies = [ [[package]] name = "quicknode-sdk" -version = "0.8.1" -source = "git+https://github.com/quicknode/sdk?rev=123cbf3#123cbf328e784b3b3d9aaa2f9d2de6eae42dcc38" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f55e09e33fa583b368558949ac98bdbb061c193cd717ee9d9621fedf8325ac8" dependencies = [ "alloy-consensus", "alloy-primitives", diff --git a/Cargo.toml b/Cargo.toml index f6342f2..c23dd8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ name = "qn" path = "src/lib.rs" [dependencies] -quicknode-sdk = { git = "https://github.com/quicknode/sdk", rev = "123cbf3", version = "0.8.1", features = [ +quicknode-sdk = { version = "0.8.2", features = [ "payments", # x402/EVM "payments-svm", # + x402/Solana "payments-tempo", # + MPP/Tempo