Gate scoped BTCPay connect to sandbox + non-mainnet
Slices 3-4 of agent-payment-connect: a scoped key carrying the a-la-carte payment_providers:write scope may connect a BTCPay provider, but only on a sandbox daemon (KEYSAT_SANDBOX_MODE) and only for a non-mainnet (regtest/testnet/signet) store. Master may connect any network; disconnect and production/mainnet reconnect stay master-only. A credential that can repoint settlement is a fund-redirection key, so the gate is deliberately narrow and fails closed. - require_provider_connect: outer gate (sandbox flag) at start_connect - btcpay/network.rs classify_address_network + client::fetch_onchain_network: resolve the store network at finish_connect, fail-closed to mainnet on any ambiguity (no on-chain method, non-2xx, non-JSON, unknown prefix), before any webhook/persist side effect - initiator carried across the OAuth round-trip via btcpay_authorize_state (migration 0025: scoped_initiator + initiator_actor_hash); scoped connects are audited - the GET callback now returns the error's HTTP status (was a misleading 200 on a denied connect) - openapi.rs documents the BTCPay connect/callback/status/disconnect paths and the key-creation scopes field Validated end-to-end against a live regtest BTCPay. Full suite green; adds gate + network unit/integration tests.
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
//! Live re-validation of the agent-payment-connect network detection against a
|
||||
//! real BTCPay regtest box. Exercises the daemon's ACTUAL
|
||||
//! `btcpay::client::fetch_onchain_network` (not a curl reimplementation), which
|
||||
//! is what the scoped-connect gate calls at callback time.
|
||||
//!
|
||||
//! `#[ignore]` by default — it needs a running BTCPay regtest stack and reads
|
||||
//! its connection params from the environment (no secrets in the tree). Bring
|
||||
//! the box up and run:
|
||||
//!
|
||||
//! ```sh
|
||||
//! cd ../onboarding-harness/stage2/btcpay-regtest && docker compose -p keysat-btcpay up -d
|
||||
//! # mint a canmodifystoresettings token + a store with an on-chain wallet, then:
|
||||
//! source ../onboarding-harness/stage2/btcpay-regtest/.live-env
|
||||
//! cargo test --test btcpay_network_live -- --ignored --nocapture
|
||||
//! ```
|
||||
//!
|
||||
//! Spec: `plans/agent-payment-connect-scope.md` §6.1 — "BTCPay on-chain address
|
||||
//! network detection MUST be validated against a live regtest box."
|
||||
|
||||
use keysat::btcpay::client::fetch_onchain_network;
|
||||
use keysat::btcpay::network::BitcoinNetwork;
|
||||
|
||||
fn env(key: &str) -> Option<String> {
|
||||
std::env::var(key).ok().filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "needs a live BTCPay regtest box; set KEYSAT_LIVE_BTCPAY_* env"]
|
||||
async fn regtest_store_resolves_to_regtest() {
|
||||
let (Some(base), Some(key), Some(store)) = (
|
||||
env("KEYSAT_LIVE_BTCPAY_URL"),
|
||||
env("KEYSAT_LIVE_BTCPAY_KEY"),
|
||||
env("KEYSAT_LIVE_BTCPAY_STORE_REGTEST"),
|
||||
) else {
|
||||
eprintln!("SKIP: set KEYSAT_LIVE_BTCPAY_URL / _KEY / _STORE_REGTEST");
|
||||
return;
|
||||
};
|
||||
|
||||
let net = fetch_onchain_network(&base, &key, &store)
|
||||
.await
|
||||
.expect("detection call should not transport-error against a live box");
|
||||
println!("regtest store {store} resolved to {net:?}");
|
||||
assert_eq!(
|
||||
net,
|
||||
Some(BitcoinNetwork::Regtest),
|
||||
"the on-chain wallet's bcrt1 address must classify as Regtest (non-mainnet → scoped connect allowed)"
|
||||
);
|
||||
assert!(!net.unwrap().is_mainnet(), "regtest must not be mainnet");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "needs a live BTCPay regtest box; set KEYSAT_LIVE_BTCPAY_* env"]
|
||||
async fn store_without_onchain_wallet_is_undetermined() {
|
||||
let (Some(base), Some(key), Some(store)) = (
|
||||
env("KEYSAT_LIVE_BTCPAY_URL"),
|
||||
env("KEYSAT_LIVE_BTCPAY_KEY"),
|
||||
env("KEYSAT_LIVE_BTCPAY_STORE_NOWALLET"),
|
||||
) else {
|
||||
eprintln!("SKIP: set KEYSAT_LIVE_BTCPAY_URL / _KEY / _STORE_NOWALLET");
|
||||
return;
|
||||
};
|
||||
|
||||
let net = fetch_onchain_network(&base, &key, &store)
|
||||
.await
|
||||
.expect("detection call should not transport-error");
|
||||
println!("no-wallet store {store} resolved to {net:?}");
|
||||
// No on-chain wallet → undetermined → caller fails closed to mainnet → deny.
|
||||
assert_eq!(
|
||||
net, None,
|
||||
"a store with no on-chain wallet must be undetermined so the gate fails closed"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user