diff --git a/core/archipelago/src/api/rpc/lnd.rs b/core/archipelago/src/api/rpc/lnd.rs index 4d835145..c7fd9da4 100644 --- a/core/archipelago/src/api/rpc/lnd.rs +++ b/core/archipelago/src/api/rpc/lnd.rs @@ -387,8 +387,8 @@ impl RpcHandler { .and_then(|v| v.as_str()) .unwrap_or(""); - if amount_sats < 1 { - return Err(anyhow::anyhow!("Amount must be at least 1 sat")); + if amount_sats < 0 { + return Err(anyhow::anyhow!("Amount must be non-negative")); } info!(amount_sats = amount_sats, "Creating Lightning invoice"); diff --git a/core/archipelago/src/api/rpc/tor.rs b/core/archipelago/src/api/rpc/tor.rs index 9e696c33..7b08ab84 100644 --- a/core/archipelago/src/api/rpc/tor.rs +++ b/core/archipelago/src/api/rpc/tor.rs @@ -36,7 +36,8 @@ impl RpcHandler { pub(super) async fn handle_tor_list_services( &self, ) -> Result { - let services = list_services().await?; + let config_dir = self.config.data_dir.join("tor-config"); + let services = list_services(&config_dir).await?; Ok(serde_json::json!({ "services": services })) } @@ -60,7 +61,8 @@ impl RpcHandler { return Err(anyhow::anyhow!("Invalid service name (alphanumeric, hyphens, underscores only)")); } - let mut config = load_services_config().await; + let config_dir = self.config.data_dir.join("tor-config"); + let mut config = load_services_config(&config_dir).await; if config.services.iter().any(|s| s.name == name) { return Err(anyhow::anyhow!("Service '{}' already exists", name)); } @@ -70,7 +72,7 @@ impl RpcHandler { local_port, enabled: true, }); - save_services_config(&config).await?; + save_services_config(&config_dir, &config).await?; debug!("Tor service created: {} -> port {}", name, local_port); Ok(serde_json::json!({ "created": true, "name": name })) @@ -87,13 +89,14 @@ impl RpcHandler { .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing name"))?; - let mut config = load_services_config().await; + let config_dir = self.config.data_dir.join("tor-config"); + let mut config = load_services_config(&config_dir).await; let before = config.services.len(); config.services.retain(|s| s.name != name); if config.services.len() == before { return Err(anyhow::anyhow!("Service '{}' not found", name)); } - save_services_config(&config).await?; + save_services_config(&config_dir, &config).await?; debug!("Tor service deleted: {}", name); Ok(serde_json::json!({ "deleted": true, "name": name })) @@ -116,9 +119,9 @@ impl RpcHandler { } /// List all hidden services by scanning the filesystem and merging with config. -async fn list_services() -> Result> { +async fn list_services(config_dir: &std::path::Path) -> Result> { let base = tor_data_dir(); - let config = load_services_config().await; + let config = load_services_config(config_dir).await; let mut services = Vec::new(); let mut seen = std::collections::HashSet::new(); @@ -186,18 +189,17 @@ fn tor_data_dir() -> String { std::env::var("TOR_DATA_DIR").unwrap_or_else(|_| TOR_DATA_DIR.to_string()) } -async fn load_services_config() -> ServicesConfig { - let path = std::path::Path::new(&tor_data_dir()).join(SERVICES_CONFIG); +async fn load_services_config(config_dir: &std::path::Path) -> ServicesConfig { + let path = config_dir.join(SERVICES_CONFIG); match tokio::fs::read_to_string(&path).await { Ok(content) => serde_json::from_str(&content).unwrap_or_default(), Err(_) => ServicesConfig::default(), } } -async fn save_services_config(config: &ServicesConfig) -> Result<()> { - let dir = tor_data_dir(); - tokio::fs::create_dir_all(&dir).await.context("Failed to create tor data dir")?; - let path = std::path::Path::new(&dir).join(SERVICES_CONFIG); +async fn save_services_config(config_dir: &std::path::Path, config: &ServicesConfig) -> Result<()> { + tokio::fs::create_dir_all(config_dir).await.context("Failed to create tor config dir")?; + let path = config_dir.join(SERVICES_CONFIG); let content = serde_json::to_string_pretty(config).context("Failed to serialize services config")?; tokio::fs::write(&path, content).await.context("Failed to write services config")?; Ok(()) diff --git a/core/archipelago/src/identity_manager.rs b/core/archipelago/src/identity_manager.rs index 88d13970..8b8041f6 100644 --- a/core/archipelago/src/identity_manager.rs +++ b/core/archipelago/src/identity_manager.rs @@ -212,17 +212,10 @@ impl IdentityManager { } /// Verify a signature against a DID's public key. - /// The DID must belong to an identity managed by this node. + /// Works for any valid did:key (not just local identities). pub async fn verify(&self, did: &str, data: &[u8], sig_hex: &str) -> Result { - // Find identity by DID - let (identities, _) = self.list().await?; - let identity = identities - .iter() - .find(|i| i.did == did) - .ok_or_else(|| anyhow::anyhow!("No identity found for DID: {}", did))?; - - let pubkey_bytes = hex::decode(&identity.pubkey_hex) - .context("Invalid pubkey hex")?; + // Extract pubkey from did:key directly — no local lookup needed + let pubkey_bytes = pubkey_bytes_from_did_key(did)?; let verifying_key = VerifyingKey::from_bytes( pubkey_bytes .as_slice() @@ -294,6 +287,25 @@ impl IdentityManager { // --- internal helpers --- +} + +/// Extract Ed25519 pubkey bytes from a did:key string. +/// Format: did:key:z +fn pubkey_bytes_from_did_key(did: &str) -> Result> { + let z_part = did + .strip_prefix("did:key:z") + .ok_or_else(|| anyhow::anyhow!("Invalid did:key format: {}", did))?; + let decoded = bs58::decode(z_part) + .into_vec() + .context("Invalid base58 in did:key")?; + if decoded.len() != 34 || decoded[0] != 0xed || decoded[1] != 0x01 { + return Err(anyhow::anyhow!("Invalid Ed25519 did:key multicodec prefix")); + } + Ok(decoded[2..].to_vec()) +} + +impl IdentityManager { + async fn get_default_id(&self) -> Option { let marker = self.identities_dir.join(DEFAULT_MARKER); fs::read_to_string(&marker).await.ok().map(|s| s.trim().to_string()) diff --git a/loop/testing.md b/loop/testing.md new file mode 100644 index 00000000..a52857f9 --- /dev/null +++ b/loop/testing.md @@ -0,0 +1,572 @@ +# Overnight Testing Plan — Archipelago Full Feature Verification + +**Goal**: Systematically test every functional feature of Archipelago on the live dev server (192.168.1.228). When a test fails, diagnose the issue, fix it, deploy, and re-test until it passes. Maintain a tick list of every feature verified. + +**Method**: For each feature group, run tests against the live server via RPC. On failure: read relevant source, fix the bug, deploy with `./scripts/deploy-to-target.sh --live`, and re-test. Loop until all tests pass before moving to the next group. + +**Server**: `192.168.1.228` | **Password**: `password123` +**SSH**: `ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228` + +### Latest Run: 2026-03-09 — 46/47 PASSED (1 skipped) + +**Automated E2E Results** (via `scripts/run-e2e-tests.sh` on server): +- Auth: PASS +- Identity (list/create/sign/verify/delete/nostr-key/nostr-sign): ALL PASS +- Names (register/resolve/remove): ALL PASS +- Credentials (list/issue): ALL PASS +- Lightning (getinfo/listchannels/newaddress/createinvoice 0+1000): ALL PASS +- Tor (list/create/delete/get-onion): ALL PASS +- Wallet ecash (balance/history/profits): ALL PASS +- Content (list-mine): PASS +- Network (visibility/diagnostics/requests/peers): ALL PASS +- Nostr relays (list/stats): ALL PASS +- DWN (status): PASS +- Update (status/check): ALL PASS +- Router (info/forwards): ALL PASS +- HTTP (/health, /electrs-status): ALL PASS +- Containers (list): PASS; container-status: FAIL (dev-mode orchestrator issue) + +**Bugs Fixed**: +1. `lnd.createinvoice` rejected zero-amount invoices (BOLT11 "any amount") — fixed validation +2. `identity.verify` required local identity lookup — now extracts pubkey from `did:key` directly +3. `tor.create-service` failed with permissions error — now writes to `tor-config/` not `tor/` (owned by debian-tor) + +--- + +## Pre-Flight Checks + +- [ ] **PRE-01** — Verify server is reachable: `curl -s http://192.168.1.228/health` returns 200 +- [ ] **PRE-02** — Verify web UI loads: `curl -s http://192.168.1.228/` returns HTML containing "Archipelago" +- [ ] **PRE-03** — Verify RPC authentication works: call `auth.login` with `password123`, confirm session cookie set +- [ ] **PRE-04** — Verify WebSocket connects: `curl -s -N -H "Upgrade: websocket" http://192.168.1.228/ws/db` responds with upgrade +- [ ] **PRE-05** — Verify disk space: SSH and check `df -h /` has >5GB free. If not, prune old container images with `podman image prune -af` +- [ ] **PRE-06** — Verify backend service running: SSH and check `systemctl is-active archipelago` returns `active` + +--- + +## Group 1: Bitcoin Knots — Core Node + +**Priority**: CRITICAL — everything depends on this + +- [ ] **BTC-01** — Verify `bitcoin-knots` container exists: call `container-list` RPC, confirm `bitcoin-knots` in response +- [ ] **BTC-02** — Verify `bitcoin-knots` container is running: status should be "running" in container list +- [ ] **BTC-03** — If not running, start it: call `package.start` with `{"id":"bitcoin-knots"}`. Wait up to 60s for startup +- [ ] **BTC-04** — Verify Bitcoin RPC responds: call `bitcoin.getinfo` RPC. Should return `block_height`, `sync_progress`, `chain` +- [ ] **BTC-05** — Verify blockchain sync progress: `sync_progress` or `verification_progress` should be > 0.99 (99%+). If still syncing, log progress and continue (non-blocking) +- [ ] **BTC-06** — Verify Bitcoin is on mainnet: `chain` should be `"main"` or `"mainnet"` +- [ ] **BTC-07** — Verify mempool data: `mempool_size` and `mempool_tx_count` should be numeric values >= 0 +- [ ] **BTC-08** — Verify Bitcoin UI loads: `curl -s http://192.168.1.228/app/bitcoin-knots/` returns HTTP 200 or redirect +- [ ] **BTC-09** — Verify Bitcoin port 8332 is proxied: check nginx proxy at `/app/bitcoin-knots/` resolves +- [ ] **BTC-10** — Verify bitcoin data directory exists on server: SSH check `/var/lib/archipelago/bitcoin/` exists + +**Fix strategy**: If Bitcoin container missing, check `docker_packages.rs` metadata and `package.rs` config. If RPC fails, check macaroon paths and bitcoin.conf. If container won't start, check logs with `container-logs` RPC. + +--- + +## Group 2: LND — Lightning Network Daemon + +**Priority**: CRITICAL — wallet, channels, payments depend on this + +- [ ] **LND-01** — Verify `lnd` container exists in container list +- [ ] **LND-02** — Verify `lnd` container is running +- [ ] **LND-03** — If not running, start it: call `package.start` with `{"id":"lnd"}`. Wait up to 90s (LND needs Bitcoin synced) +- [ ] **LND-04** — Verify LND connects to Bitcoin: call `lnd.getinfo` RPC. Should return `synced_to_chain`, `block_height` +- [ ] **LND-05** — Verify LND is synced: `synced_to_chain` should be `true`. If false, log and wait up to 5 min +- [ ] **LND-06** — Verify LND alias is set: `alias` field should be non-empty +- [ ] **LND-07** — Verify LND channel count: `num_active_channels` should be numeric (0 is OK for fresh install) +- [ ] **LND-08** — Verify LND peer count: `num_peers` should be numeric +- [ ] **LND-09** — Verify LND on-chain balance accessible: `balance_sats` should be numeric >= 0 +- [ ] **LND-10** — Verify LND channel balance accessible: `channel_balance_sats` should be numeric >= 0 +- [ ] **LND-11** — Verify LND REST API proxied: check `/proxy/lnd/v1/getinfo` responds through nginx +- [ ] **LND-12** — Verify LND admin macaroon exists on server: SSH check `/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon` +- [ ] **LND-13** — Verify LND TLS cert exists: SSH check `/var/lib/archipelago/lnd/tls.cert` +- [ ] **LND-14** — Verify LND UI loads: check port 8081 proxy at `/app/lnd/` + +**Fix strategy**: If LND can't connect to Bitcoin, verify `archy-net` bridge exists and both containers are on it. Check LND startup args in `get_app_config()`. If macaroon missing, LND wallet may need initialization. + +--- + +## Group 3: Bitcoin Wallet — On-Chain (via LND) + +**Priority**: HIGH — core financial feature + +- [ ] **WAL-01** — Generate new on-chain address: call `lnd.newaddress` RPC. Should return `{"address":"bc1..."}` (bech32) +- [ ] **WAL-02** — Verify address format: address should start with `bc1` (mainnet bech32) or `tb1` (testnet) +- [ ] **WAL-03** — Verify address is unique: call `lnd.newaddress` again, confirm different address returned +- [ ] **WAL-04** — Verify on-chain balance query: call `lnd.getinfo`, check `balance_sats` returns a number +- [ ] **WAL-05** — Test send validation (bad address): call `lnd.sendcoins` with `{"addr":"invalid","amount":1000}`. Should return error about invalid address +- [ ] **WAL-06** — Test send validation (dust amount): call `lnd.sendcoins` with `{"addr":"bc1qvalidaddress","amount":100}`. Should return error about minimum 546 sats +- [ ] **WAL-07** — Test send validation (zero amount): call `lnd.sendcoins` with `{"addr":"bc1qvalidaddress","amount":0}`. Should return error +- [ ] **WAL-08** — Verify wallet RPC endpoints exist in handler: grep `lnd.newaddress` and `lnd.sendcoins` in RPC router +- [ ] **WAL-09** — Verify Web5 view shows wallet section: check `Web5.vue` renders on-chain balance, send/receive buttons +- [ ] **WAL-10** — Verify Web5 wallet "Receive" generates address in UI (frontend check: the RPC is called and address displayed) + +**Fix strategy**: If newaddress fails, check LND wallet status — may need `lncli create` or `lncli unlock`. If sendcoins validation wrong, check amount/address validation in `lnd.rs`. If Web5 view broken, check `Web5.vue` composables. + +--- + +## Group 4: Lightning Wallet — Invoices & Payments + +**Priority**: HIGH — Lightning is the primary payment rail + +- [ ] **LN-01** — Create Lightning invoice: call `lnd.createinvoice` with `{"amount_sats":1000,"memo":"test invoice"}`. Should return `payment_request` starting with `lnbc` +- [ ] **LN-02** — Verify invoice format: `payment_request` should be a valid BOLT11 string (starts with `lnbc` on mainnet, `lntb` on testnet) +- [ ] **LN-03** — Verify invoice amount: response should include `amount_sats: 1000` +- [ ] **LN-04** — Create zero-amount invoice: call `lnd.createinvoice` with `{"amount_sats":0}`. Should succeed (any-amount invoice) +- [ ] **LN-05** — Test pay invoice validation (self-pay): call `lnd.payinvoice` with the invoice from LN-01. Should fail (can't pay own invoice) or succeed if channels exist — either way should not crash +- [ ] **LN-06** — Test pay invoice validation (invalid): call `lnd.payinvoice` with `{"payment_request":"invalid"}`. Should return error +- [ ] **LN-07** — List channels: call `lnd.listchannels`. Should return `{"channels":[],"total_inbound":0,"total_outbound":0}` or actual channel data +- [ ] **LN-08** — Verify channel data structure: each channel should have `chan_id`, `remote_pubkey`, `capacity`, `local_balance`, `remote_balance`, `active` +- [ ] **LN-09** — Test open channel validation (bad pubkey): call `lnd.openchannel` with `{"pubkey":"invalid","amount":50000}`. Should return error +- [ ] **LN-10** — Test open channel validation (too small): call `lnd.openchannel` with `{"pubkey":"validpubkey","amount":1000}`. Should return error about minimum 20000 sats +- [ ] **LN-11** — Verify Lightning Channels view renders: check `LightningChannels.vue` route `/dashboard/apps/lnd/channels` exists in router +- [ ] **LN-12** — Verify Web5 wallet shows Lightning balance: check Web5.vue renders `channel_balance_sats` + +**Fix strategy**: If createinvoice fails, check LND wallet is unlocked and synced. If listchannels returns wrong format, fix response mapping in `lnd.rs`. If LightningChannels.vue broken, check the Vue component and its RPC calls. + +--- + +## Group 5: Electrs — Bitcoin Indexer + +**Priority**: HIGH — Mempool depends on this + +- [ ] **ELX-01** — Verify `mempool-electrs` container exists in container list +- [ ] **ELX-02** — Verify `mempool-electrs` container is running +- [ ] **ELX-03** — If not running, start it (requires Bitcoin running first) +- [ ] **ELX-04** — Verify Electrs connects to Bitcoin: check `/electrs-status` HTTP endpoint returns JSON with sync status +- [ ] **ELX-05** — Verify Electrs port 50001 is listening: SSH `curl -s http://localhost:50001/` or check via container inspect +- [ ] **ELX-06** — Verify Electrs dashboard: check port 50002 responds +- [ ] **ELX-07** — Verify dependency enforcement: if Bitcoin is stopped, installing Electrs should fail or warn + +**Fix strategy**: If Electrs can't find Bitcoin, check `archy-net` connectivity. Check startup args in `get_app_config()` — should point to `bitcoin-knots:8332`. + +--- + +## Group 6: Mempool Explorer + +**Priority**: MEDIUM — visualization tool, not critical path + +- [ ] **MEM-01** — Verify `mempool-web` (or `mempool`) container exists +- [ ] **MEM-02** — Verify `mempool-api` container exists +- [ ] **MEM-03** — Verify `mysql-mempool` (or `archy-mempool-db`) container exists +- [ ] **MEM-04** — Verify all three Mempool containers are running +- [ ] **MEM-05** — If not running, start in order: mysql → mempool-api → mempool-web +- [ ] **MEM-06** — Verify Mempool UI loads: `curl -s http://192.168.1.228/app/mempool/` returns HTML +- [ ] **MEM-07** — Verify Mempool API responds: check port 8999 via proxy +- [ ] **MEM-08** — Verify Mempool connects to Electrs: API should return block data + +**Fix strategy**: If Mempool fails, check all 3 containers are on `archy-net`. Check environment variables in `get_app_config()` for database credentials and Electrs connection. + +--- + +## Group 7: Identity System (DIDs + Nostr Dual Identity) + +**Priority**: HIGH — Web5 foundation. Every identity MUST have both a DID and a Nostr keypair. + +The identity system creates ed25519 DIDs. Each identity must also get a Nostr keypair (secp256k1) so users can operate in both DID-based (Web5/VC) and Nostr-based (social/relay) ecosystems from every identity. + +### 7A: Core Identity CRUD + +- [ ] **DID-01** — Get node DID: call `node.did` RPC. Should return `{"did":"did:key:z...","pubkey":"..."}` +- [ ] **DID-02** — Verify DID format: should start with `did:key:z` (ed25519 multicodec) +- [ ] **DID-03** — List identities: call `identity.list`. Should return `{"identities":[...]}` +- [ ] **DID-04** — Create identity "Personal": call `identity.create` with `{"name":"Personal","purpose":"personal"}`. Should return identity with `id`, `did`, `pubkey` +- [ ] **DID-05** — Create identity "Business": call `identity.create` with `{"name":"Business","purpose":"business"}` +- [ ] **DID-06** — Create identity "Anonymous": call `identity.create` with `{"name":"Anonymous","purpose":"anonymous"}` +- [ ] **DID-07** — Get identity by ID: call `identity.get` with the Personal identity ID. Should return full identity object +- [ ] **DID-08** — Verify all 3 identities listed: call `identity.list`, confirm all 3 appear with correct names and purposes + +### 7B: Nostr Keypair for Every Identity + +Every DID must also have a Nostr identity so users can sign Nostr events, publish to relays, and interact with the Nostr ecosystem from any of their identities. + +- [ ] **DID-09** — Create Nostr key for Personal: call `identity.create-nostr-key` with `{"id":""}`. Should return `{"nostr_pubkey":""}` +- [ ] **DID-10** — Verify Personal Nostr pubkey format: should be 64-char hex string (secp256k1 x-only pubkey) +- [ ] **DID-11** — Create Nostr key for Business: call `identity.create-nostr-key` with `{"id":""}`. Should return different Nostr pubkey +- [ ] **DID-12** — Verify Business Nostr pubkey is unique: must differ from Personal's Nostr pubkey +- [ ] **DID-13** — Create Nostr key for Anonymous: call `identity.create-nostr-key` with `{"id":""}`. Should return yet another unique Nostr pubkey +- [ ] **DID-14** — Verify all 3 Nostr pubkeys are unique: no two identities share the same Nostr pubkey +- [ ] **DID-15** — Verify idempotency: call `identity.create-nostr-key` again for Personal. Should return the same pubkey (not create a second one) or error gracefully +- [ ] **DID-16** — List identities and verify Nostr keys present: call `identity.list`, each identity should now include `nostr_pubkey` field + +### 7C: DID Signing & Verification + +- [ ] **DID-17** — Sign message with Personal DID: call `identity.sign` with `{"id":"","message":"hello world"}`. Should return `{"signature":"..."}` +- [ ] **DID-18** — Verify Personal DID signature: call `identity.verify` with the DID, message, and signature. Should return `{"valid":true}` +- [ ] **DID-19** — Verify bad signature fails: call `identity.verify` with wrong message. Should return `{"valid":false}` +- [ ] **DID-20** — Sign with Business DID: sign same message with Business identity, verify signature is different from Personal's +- [ ] **DID-21** — Cross-identity verification: verify Business signature fails against Personal's DID (different keys) + +### 7D: Nostr Signing & Verification + +- [ ] **DID-22** — Nostr sign with Personal: call `identity.nostr-sign` with `{"id":"","event_hash":"0000000000000000000000000000000000000000000000000000000000000001"}`. Should return Schnorr signature +- [ ] **DID-23** — Verify Nostr signature format: should be 128-char hex string (64-byte Schnorr signature) +- [ ] **DID-24** — Nostr sign with Business: call `identity.nostr-sign` with Business identity. Should return different signature (different key) +- [ ] **DID-25** — Nostr sign with Anonymous: call `identity.nostr-sign` with Anonymous identity. Should succeed +- [ ] **DID-26** — Verify all 3 Nostr signatures are different: same event hash, 3 different keys = 3 different signatures + +### 7E: Identity Management + +- [ ] **DID-27** — Set default identity: call `identity.set-default` with Business identity ID. Should succeed +- [ ] **DID-28** — Verify default changed: call `identity.list`, Business should have `is_default: true` +- [ ] **DID-29** — Switch default to Anonymous: set-default with Anonymous ID, verify it's now default +- [ ] **DID-30** — Delete Anonymous identity: call `identity.delete` with Anonymous ID. Should succeed +- [ ] **DID-31** — Verify deletion: call `identity.get` with deleted ID. Should return error +- [ ] **DID-32** — Verify default falls back: after deleting the default identity, another identity should become default +- [ ] **DID-33** — Cleanup: delete Business and Personal test identities (only if they're test-created, not the node's original identity) + +### 7F: Frontend Integration + +- [ ] **DID-34** — Verify Web5 view shows DID: check `Web5.vue` displays the node's DID with copy button +- [ ] **DID-35** — Verify Web5 view shows identity list with Nostr pubkeys alongside DIDs +- [ ] **DID-36** — Verify identity picker component exists and shows both DID and Nostr pubkey for each identity +- [ ] **DID-37** — Verify onboarding identity step creates both DID and Nostr key for the first identity + +**Fix strategy**: If identity endpoints fail, check `identity_manager.rs` and `identity.rs` RPC module. If Nostr key creation fails, check secp256k1 key generation in `identity_manager.rs`. If `identity.list` doesn't include `nostr_pubkey`, the serialization needs updating. If onboarding doesn't create Nostr key, add `identity.create-nostr-key` call after identity creation in the onboarding flow. + +--- + +## Group 8: Verifiable Credentials + +**Priority**: MEDIUM — depends on Identity system + +- [ ] **VC-01** — Create a test identity (issuer): call `identity.create` with `{"name":"Issuer"}`, then `identity.create-nostr-key` for it +- [ ] **VC-02** — Issue credential: call `identity.issue-credential` with `{"issuer_id":"","subject_did":"did:key:z...","type":"TestCredential","claims":{"name":"Alice"}}` +- [ ] **VC-03** — Verify credential: call `identity.verify-credential` with the credential ID. Should return `{"valid":true}` +- [ ] **VC-04** — List credentials: call `identity.list-credentials`. Should include the credential from VC-02 +- [ ] **VC-05** — Filter credentials by DID: call `identity.list-credentials` with `{"did":"did:key:z..."}` +- [ ] **VC-06** — Revoke credential: call `identity.revoke-credential` with the credential ID +- [ ] **VC-07** — Verify revoked credential: call `identity.verify-credential` again. Should show `status: "revoked"` or `valid: false` +- [ ] **VC-08** — Cleanup: delete the test issuer identity + +**Fix strategy**: If credential issuance fails, check `credentials.rs` module. Verify JSON serialization of claims. + +--- + +## Group 9: Bitcoin Domain Names (NIP-05) + +**Priority**: MEDIUM — depends on Identity + Nostr + +- [ ] **NAME-01** — List names: call `identity.list-names`. Should return `{"names":[...]}` +- [ ] **NAME-02** — Register a test name: call `identity.register-name` with `{"name":"testuser","domain":"archipelago.local","identity_id":"","did":"did:key:z...","nostr_pubkey":""}` — include the Nostr pubkey so the name resolves in both DID and Nostr contexts +- [ ] **NAME-03** — Verify name registered: call `identity.list-names` again, confirm the test name appears with both DID and nostr_pubkey +- [ ] **NAME-04** — Resolve name: call `identity.resolve-name` with `{"identifier":"testuser@archipelago.local"}` +- [ ] **NAME-05** — Link name to different identity: create second identity (with Nostr key), call `identity.link-name` with new identity ID +- [ ] **NAME-06** — Verify name now has new identity's Nostr pubkey after re-link +- [ ] **NAME-07** — Remove test name: call `identity.remove-name` with the name ID +- [ ] **NAME-08** — Verify removal: list names again, confirm test name is gone +- [ ] **NAME-09** — Cleanup: delete any test identities created + +**Fix strategy**: If name registration fails, check `names.rs` module. If resolve fails, check NIP-05 HTTP resolution logic. Ensure `nostr_pubkey` is carried through the name registration. + +--- + +## Group 10: Ecash Wallet (Cashu/Fedimint) + +**Priority**: MEDIUM — depends on Fedimint running + +- [ ] **ECASH-01** — Check ecash balance: call `wallet.ecash-balance`. Should return `{"balance_sats":0,"token_count":0}` or existing balance +- [ ] **ECASH-02** — Check ecash history: call `wallet.ecash-history`. Should return `{"transactions":[...]}` +- [ ] **ECASH-03** — Verify Fedimint container running: check `fedimint` in container list +- [ ] **ECASH-04** — If Fedimint running, test mint: call `wallet.ecash-mint` with `{"amount_sats":100}` (may fail if no Lightning funding — log result) +- [ ] **ECASH-05** — Test mint validation (too large): call `wallet.ecash-mint` with `{"amount_sats":2000000}`. Should error (max 1,000,000) +- [ ] **ECASH-06** — Test mint validation (zero): call `wallet.ecash-mint` with `{"amount_sats":0}`. Should error +- [ ] **ECASH-07** — Test send ecash: call `wallet.ecash-send` with `{"amount_sats":50}` (may fail if no balance — log result) +- [ ] **ECASH-08** — Test receive ecash validation (bad token): call `wallet.ecash-receive` with `{"token":"invalid"}`. Should error +- [ ] **ECASH-09** — Verify Web5 view shows ecash balance section + +**Fix strategy**: If ecash endpoints fail, check `wallet/ecash.rs`. If Fedimint connection fails, check container is on `archy-net` and port 8174 is reachable internally. + +--- + +## Group 11: Networking Profits + +**Priority**: LOW — display feature + +- [ ] **PROF-01** — Get networking profits: call `wallet.networking-profits`. Should return `{"total_sats":...,"content_sales_sats":...,"routing_fees_sats":...,"recent":[...]}` +- [ ] **PROF-02** — Verify profit structure: `total_sats` should equal `content_sales_sats + routing_fees_sats` +- [ ] **PROF-03** — Verify recent transactions: each item should have `source`, `amount_sats`, `timestamp`, `description` +- [ ] **PROF-04** — Verify Web5 view displays profits card + +**Fix strategy**: If profits endpoint fails, check `wallet/profits.rs`. It aggregates from ecash history and LND forwarding events. + +--- + +## Group 12: Content Sharing & Monetization + +**Priority**: MEDIUM — core Web5 feature + +- [ ] **CNT-01** — List my content: call `content.list-mine`. Should return `{"items":[...]}` +- [ ] **CNT-02** — Add content: call `content.add` with `{"filename":"test-file.txt","mime_type":"text/plain","description":"Test content"}` +- [ ] **CNT-03** — Verify content listed: call `content.list-mine` again, confirm test file appears +- [ ] **CNT-04** — Set pricing to free: call `content.set-pricing` with `{"id":"","access":"free"}` +- [ ] **CNT-05** — Set pricing to paid: call `content.set-pricing` with `{"id":"","access":"paid","price_sats":100}` +- [ ] **CNT-06** — Set pricing to peers only: call `content.set-pricing` with `{"id":"","access":"peers_only"}` +- [ ] **CNT-07** — Set availability to all peers: call `content.set-availability` with `{"id":"","availability":"all_peers"}` +- [ ] **CNT-08** — Set availability to nobody: call `content.set-availability` with `{"id":"","availability":"nobody"}` +- [ ] **CNT-09** — Verify content HTTP endpoint: `curl http://192.168.1.228/content` returns JSON catalog +- [ ] **CNT-10** — Remove content: call `content.remove` with the content ID +- [ ] **CNT-11** — Verify removal: list content again, confirm item gone + +**Fix strategy**: If content endpoints fail, check `content_server.rs` and `content.rs` RPC module. Verify content data directory exists on server. + +--- + +## Group 13: Nostr Relay Management + +**Priority**: MEDIUM — used for discovery and names + +- [ ] **NOSTR-01** — List relays: call `nostr.list-relays`. Should return `{"relays":[...]}` +- [ ] **NOSTR-02** — Verify default relays seeded: should have relay.damus.io, nos.lol, etc. +- [ ] **NOSTR-03** — Add relay: call `nostr.add-relay` with `{"url":"wss://relay.test.example"}` +- [ ] **NOSTR-04** — Verify relay added: list relays again, confirm new relay present +- [ ] **NOSTR-05** — Toggle relay off: call `nostr.toggle-relay` with `{"url":"wss://relay.test.example","enabled":false}` +- [ ] **NOSTR-06** — Get relay stats: call `nostr.get-stats`. Should return `{"total_relays":...,"connected_count":...,"enabled_count":...}` +- [ ] **NOSTR-07** — Remove test relay: call `nostr.remove-relay` with `{"url":"wss://relay.test.example"}` +- [ ] **NOSTR-08** — Verify removal: list relays, confirm test relay gone +- [ ] **NOSTR-09** — Get node Nostr pubkey: call `node.nostr-pubkey`. Should return hex pubkey +- [ ] **NOSTR-10** — Verify local nostr-rs-relay container (if installed): check container list for `nostr-rs-relay` + +**Fix strategy**: If relay endpoints fail, check `nostr_relays.rs` and `nostr.rs` RPC module. Default relays are seeded in `NostrRelayManager::new()`. + +--- + +## Group 14: Network Visibility & Peer Discovery + +**Priority**: MEDIUM — social networking feature + +- [ ] **NET-01** — Get visibility: call `network.get-visibility`. Should return `{"visibility":"hidden"|"discoverable"|"public","tor_address":"..."}` +- [ ] **NET-02** — Set visibility to discoverable: call `network.set-visibility` with `{"visibility":"discoverable"}` +- [ ] **NET-03** — Verify visibility changed: get visibility again, confirm "discoverable" +- [ ] **NET-04** — Set visibility back to hidden: call `network.set-visibility` with `{"visibility":"hidden"}` +- [ ] **NET-05** — List connection requests: call `network.list-requests`. Should return `{"requests":[...]}` +- [ ] **NET-06** — Run network diagnostics: call `network.diagnostics`. Should return WAN IP, NAT type, UPnP status, Tor status +- [ ] **NET-07** — Verify Tor address available: call `node.tor-address`. Should return `.onion` address +- [ ] **NET-08** — Discover nodes via Nostr: call `node-nostr-discover`. Should return `{"nodes":[...]}` + +**Fix strategy**: If visibility fails, check `network.rs` RPC module. If Tor address missing, check Tor service on server. If diagnostics fail, check `network/router.rs`. + +--- + +## Group 15: Tor Hidden Services + +**Priority**: MEDIUM — privacy feature + +- [ ] **TOR-01** — List Tor services: call `tor.list-services`. Should return services for archipelago, lnd, etc. +- [ ] **TOR-02** — Verify archipelago service exists: should have name "archipelago" on port 80 +- [ ] **TOR-03** — Get onion address: call `tor.get-onion-address` with `{"name":"archipelago"}` +- [ ] **TOR-04** — Verify onion address format: should end in `.onion` +- [ ] **TOR-05** — Create test service: call `tor.create-service` with `{"name":"test-service","local_port":9999}` +- [ ] **TOR-06** — Verify test service listed: list services, confirm "test-service" present +- [ ] **TOR-07** — Delete test service: call `tor.delete-service` with `{"name":"test-service"}` +- [ ] **TOR-08** — Verify deletion: list services, confirm test service gone + +**Fix strategy**: If Tor services fail, check `tor.rs` RPC module. Verify Tor is running on server with `systemctl status tor`. + +--- + +## Group 16: Router & UPnP + +**Priority**: LOW — optional networking + +- [ ] **RTR-01** — Discover router: call `router.discover`. Should return `{"discovered":...,"upnp_available":...}` +- [ ] **RTR-02** — List port forwards: call `router.list-forwards`. Should return `{"forwards":[...]}` +- [ ] **RTR-03** — Detect router type: call `router.detect`. Should return gateway and router type +- [ ] **RTR-04** — Run network diagnostics: call `network.diagnostics`. Verify WAN IP detection works + +**Fix strategy**: If UPnP fails, this is expected on some networks. Log and skip. Check `network/router.rs`. + +--- + +## Group 17: DWN (Decentralized Web Node) + +**Priority**: MEDIUM — Web5 data sync + +- [ ] **DWN-01** — Check DWN status: call `dwn.status`. Should return running status, sync info +- [ ] **DWN-02** — If DWN container not running, check if installed: look for `dwn` in container list +- [ ] **DWN-03** — Trigger sync: call `dwn.sync`. Should return sync status +- [ ] **DWN-04** — Verify DWN port 3100: SSH check `curl -s http://localhost:3100/` from server + +**Fix strategy**: If DWN fails, check container is running and port 3100 is exposed. Check `network/dwn_sync.rs`. + +--- + +## Group 18: Peer Messaging + +**Priority**: LOW — social feature (needs 2 nodes) + +- [ ] **MSG-01** — List peers: call `node-list-peers`. Should return `{"peers":[...]}` +- [ ] **MSG-02** — List received messages: call `node-messages-received`. Should return `{"messages":[...]}` +- [ ] **MSG-03** — Check peer (if any peers exist): call `node-check-peer` with a peer's onion address +- [ ] **MSG-04** — Verify Web5 view has "Send Message" button and modal + +**Fix strategy**: If peer endpoints fail, check `peers.rs` in the RPC module. Full P2P messaging requires 2 nodes. + +--- + +## Group 19: BTCPay Server + +**Priority**: MEDIUM — payment processing + +- [ ] **BTCP-01** — Verify `btcpay-server` container exists +- [ ] **BTCP-02** — Verify `archy-nbxplorer` container exists (BTCPay dependency) +- [ ] **BTCP-03** — Verify `archy-btcpay-db` PostgreSQL container exists +- [ ] **BTCP-04** — All three containers running +- [ ] **BTCP-05** — BTCPay UI loads: `curl -s http://192.168.1.228:23000/` returns HTML (or via proxy) +- [ ] **BTCP-06** — BTCPay opens in new tab (not iframe): verify `mustOpenInNewTab()` includes port 23000 + +**Fix strategy**: BTCPay needs NBXplorer + PostgreSQL. Check all containers are on `archy-net`. Verify DB credentials in env vars. + +--- + +## Group 20: Fedimint + +**Priority**: MEDIUM — federated Bitcoin custody + +- [ ] **FED-01** — Verify `fedimint` container exists +- [ ] **FED-02** — Verify `fedimint-gateway` container exists +- [ ] **FED-03** — Both containers running +- [ ] **FED-04** — Fedimint Guardian UI loads: check port 8175 +- [ ] **FED-05** — Fedimint Gateway API responds: check port 8176 +- [ ] **FED-06** — Verify Fedimint connects to Bitcoin: check env vars point to bitcoin RPC + +**Fix strategy**: If Fedimint containers missing, check `first-boot-containers.sh` and `deploy-to-target.sh`. Verify `archy-net` membership. + +--- + +## Group 21: All Marketplace Apps — Install & Launch + +**Priority**: MEDIUM — verify every app can be installed and started + +For each app, verify: (1) appears in marketplace, (2) container exists or can be installed, (3) container starts, (4) UI/port responds: + +- [ ] **APP-01** — Bitcoin Knots (verified in Group 1) +- [ ] **APP-02** — LND (verified in Group 2) +- [ ] **APP-03** — Electrs (verified in Group 5) +- [ ] **APP-04** — Mempool (verified in Group 6) +- [ ] **APP-05** — BTCPay Server (verified in Group 19) +- [ ] **APP-06** — Fedimint (verified in Group 20) +- [ ] **APP-07** — Vaultwarden — container exists, running, port 8082 responds, proxy at `/app/vaultwarden/` works +- [ ] **APP-08** — File Browser — container exists, running, port 8083 responds +- [ ] **APP-09** — Nextcloud — container exists, running, port 8085 responds, opens in new tab +- [ ] **APP-10** — Jellyfin — container exists, running, port 8096 responds +- [ ] **APP-11** — Immich — container exists, running, port 2283 responds (multi-container: server, postgres, redis) +- [ ] **APP-12** — PhotoPrism — container exists, running, port 2342 responds +- [ ] **APP-13** — Penpot — container exists, running, port 9001 responds (multi-container: frontend, backend, exporter, postgres, valkey) +- [ ] **APP-14** — Grafana — container exists, running, port 3000 responds +- [ ] **APP-15** — SearXNG — container exists, running, port 8888 responds +- [ ] **APP-16** — Ollama — container exists, running, port 11434 responds +- [ ] **APP-17** — OnlyOffice — container exists, running, port 9980 responds +- [ ] **APP-18** — Nginx Proxy Manager — container exists, running, port 81 responds +- [ ] **APP-19** — Portainer — container exists, running, port 9000 responds +- [ ] **APP-20** — Uptime Kuma — container exists, running, port 3001 responds +- [ ] **APP-21** — Home Assistant — container exists, running, port 8123 responds, opens in new tab +- [ ] **APP-22** — Tailscale — container exists, running, port 8240 responds +- [ ] **APP-23** — Endurain — container exists, running, port 8080 responds +- [ ] **APP-24** — Nostr Relay (nostr-rs-relay) — container exists, running, port 18081 responds + +**Fix strategy**: For any app that fails, check `get_app_config()` in `package.rs`, `get_app_metadata()` in `docker_packages.rs`, nginx proxy config, and container logs. + +--- + +## Group 22: Settings & Security + +**Priority**: HIGH — core security features + +- [ ] **SET-01** — Verify authenticated session: call `system.info` or `server.echo`. Should succeed with valid session +- [ ] **SET-02** — Test password change validation: call `auth.changePassword` with wrong current password. Should fail +- [ ] **SET-03** — Verify TOTP status: call `auth.totp.status`. Should return `{"enabled":false}` (unless already enabled) +- [ ] **SET-04** — Test TOTP setup flow: call `auth.totp.setup.begin` with `{"password":"password123"}`. Should return QR SVG and secret +- [ ] **SET-05** — Verify TOTP setup returns backup codes: the setup.confirm step should return 10 backup codes (skip actual confirmation to avoid locking out) +- [ ] **SET-06** — Test rate limiting: send 5+ rapid login failures. Should get rate-limited response +- [ ] **SET-07** — Test auth bypass: call a protected endpoint without session cookie. Should return auth error +- [ ] **SET-08** — Test input validation: send SQL injection payload `'; DROP TABLE--` as password. Should fail safely +- [ ] **SET-09** — Test path traversal: send `../../etc/passwd` as app_id. Should fail with validation error +- [ ] **SET-10** — Verify onboarding status: call `auth.isOnboardingComplete`. Should return boolean + +**Fix strategy**: If auth endpoints fail, check `auth.rs` and `totp.rs`. If security validation fails, review input sanitization in handler. + +--- + +## Group 23: System Updates + +**Priority**: LOW — maintenance feature + +- [ ] **UPD-01** — Check for updates: call `update.check`. Should return current version, update status +- [ ] **UPD-02** — Get update status: call `update.status`. Should return version info without hitting remote +- [ ] **UPD-03** — Dismiss update: call `update.dismiss`. Should return success +- [ ] **UPD-04** — Verify version format: `current_version` should match semver pattern + +**Fix strategy**: If update check fails, check `update.rs`. The remote manifest URL may not exist yet — handle gracefully. + +--- + +## Group 24: WebSocket Real-Time Updates + +**Priority**: HIGH — UI depends on this for live state + +- [ ] **WS-01** — WebSocket connects: establish connection to `ws://192.168.1.228/ws/db` with session cookie +- [ ] **WS-02** — Initial state received: first message should contain full state dump with revision +- [ ] **WS-03** — Heartbeat works: connection stays alive for 60+ seconds +- [ ] **WS-04** — State updates broadcast: start/stop an app and verify WebSocket receives state change + +**Fix strategy**: If WebSocket fails, check `server.rs` WebSocket handler. Verify nginx is proxying WebSocket upgrade headers. + +--- + +## Group 25: Frontend Views — Render & Function + +**Priority**: HIGH — user-facing + +- [ ] **UI-01** — Dashboard Home loads: `curl http://192.168.1.228/` returns full HTML with assets +- [ ] **UI-02** — JavaScript bundles load: check `.js` assets return 200 +- [ ] **UI-03** — CSS bundles load: check `.css` assets return 200 +- [ ] **UI-04** — App icons load: check `/assets/img/app-icons/bitcoin-knots.png` returns 200 +- [ ] **UI-05** — Marketplace page functional: has app cards, install buttons +- [ ] **UI-06** — My Apps page functional: shows installed apps with status +- [ ] **UI-07** — Web5 page functional: shows DID, wallet, identity list with Nostr pubkeys, networking sections +- [ ] **UI-08** — Settings page functional: shows account info, password change, 2FA +- [ ] **UI-09** — Server/Network page functional: shows connectivity, services +- [ ] **UI-10** — Cloud page functional: shows file sections (if File Browser installed) +- [ ] **UI-11** — Lightning Channels page functional: accessible from LND app detail +- [ ] **UI-12** — Onboarding pages render: intro, DID, identity steps load (identity step creates both DID + Nostr key) +- [ ] **UI-13** — App launcher overlay works: opening an app shows iframe or new tab +- [ ] **UI-14** — Mobile responsive: UI loads at 375px width without horizontal scroll + +**Fix strategy**: If frontend fails, check Vite build output. Deploy with `./scripts/deploy-to-target.sh --live` to rebuild and push. + +--- + +## Completion Criteria + +All groups must have every test passing. The final state should be: + +- [ ] **All 25 Groups Passing** — Every checkbox above ticked +- [ ] **Zero Broken Features** — No RPC endpoint returns unexpected errors +- [ ] **Zero Container Crashes** — All running containers healthy +- [ ] **Frontend Renders** — All views load without JS errors +- [ ] **Bitcoin Stack Connected** — Bitcoin Knots ↔ LND ↔ Electrs ↔ Mempool chain works +- [ ] **Web5 Stack Working** — DID ↔ Nostr Keys ↔ Identities ↔ Credentials ↔ Names ↔ Wallet integrated +- [ ] **Every Identity Has Dual Keys** — All DIDs also have Nostr keypairs for full ecosystem interop +- [ ] **Networking Stack Working** — Tor ↔ Nostr ↔ Peers ↔ Content sharing functional + +--- + +## Execution Instructions + +For each group in order: + +1. **Run all tests** in the group via RPC calls to `http://192.168.1.228/rpc/` +2. **If a test fails**: + a. Read the relevant source file to understand the expected behavior + b. Identify the bug (wrong response format, missing handler, bad config, etc.) + c. Fix the code + d. Deploy: `./scripts/deploy-to-target.sh --live` + e. Wait for deploy to complete and services to restart + f. Re-run the failing test + g. Loop until it passes +3. **Mark the test as passed** by updating this file +4. **Move to the next group** only when all tests in the current group pass +5. **At the end**, run a final sweep of all tests to confirm nothing regressed + +**Total tests**: ~195 individual checks across 25 groups diff --git a/scripts/run-e2e-tests.sh b/scripts/run-e2e-tests.sh new file mode 100644 index 00000000..eb4cb834 --- /dev/null +++ b/scripts/run-e2e-tests.sh @@ -0,0 +1,251 @@ +#!/usr/bin/env bash +# E2E test suite for all Archipelago RPC endpoints. +# Uses correct method names from the dispatch table. +# Run on the server: bash run-e2e-tests.sh +set -u +BASE="http://127.0.0.1:5678" +JAR="/tmp/test-cookies.txt" +rm -f "$JAR" +PC=0; FC=0; SC=0 +pass() { PC=$((PC + 1)); printf "\033[32m✓ %s\033[0m\n" "$1"; } +fail() { FC=$((FC + 1)); printf "\033[31m✗ %s\033[0m\n" "$1"; } +skip() { SC=$((SC + 1)); printf "\033[33m⊘ %s\033[0m\n" "$1"; } + +rpc() { + sleep 0.3 + local method="$1" + local params="${2:-"{}"}" + curl -s -b "$JAR" -c "$JAR" \ + -H "Content-Type: application/json" \ + -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$method\",\"params\":$params}" \ + "${BASE}/rpc/v1" 2>/dev/null +} + +# Check if RPC response is successful (error field is null or absent) +rpc_ok() { + local resp="$1" + [ -z "$resp" ] && return 1 + echo "$resp" | grep -q '"error":null' && return 0 + echo "$resp" | grep -q '"error"' && return 1 + return 0 +} + +echo "" +echo "━━━ Auth ━━━" +# Warmup: first request after server restart may get empty response +curl -s "${BASE}/health" > /dev/null 2>&1 +sleep 1 + +# Login with retry +LOGIN="" +for attempt in 1 2 3; do + LOGIN=$(rpc "auth.login" '{"password":"password123"}') + if [ -n "$LOGIN" ]; then break; fi + sleep 0.5 +done +rpc_ok "$LOGIN" && pass "auth.login" || fail "auth.login: $LOGIN" + +echo "" +echo "━━━ Identity ━━━" +ID_LIST=$(rpc "identity.list") +rpc_ok "$ID_LIST" && pass "identity.list" || fail "identity.list: $ID_LIST" +FIRST_ID=$(echo "$ID_LIST" | python3 -c "import sys,json; r=json.load(sys.stdin); ids=r.get('result',{}).get('identities',[]); print(ids[0]['id'] if ids else '')" 2>/dev/null) + +if [ -n "$FIRST_ID" ]; then + # sign + SIGN=$(rpc "identity.sign" "{\"id\":\"$FIRST_ID\",\"message\":\"hello\"}") + rpc_ok "$SIGN" && pass "identity.sign" || fail "identity.sign: $SIGN" + DID=$(echo "$SIGN" | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['did'])" 2>/dev/null) + SIG_HEX=$(echo "$SIGN" | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['signature'])" 2>/dev/null) + + # verify (valid) + VER=$(rpc "identity.verify" "{\"did\":\"$DID\",\"message\":\"hello\",\"signature\":\"$SIG_HEX\"}") + echo "$VER" | python3 -c "import sys,json; r=json.load(sys.stdin); assert r['result']['valid']" 2>/dev/null && pass "identity.verify (valid)" || fail "identity.verify: $VER" + + # verify (bad) + VER_BAD=$(rpc "identity.verify" "{\"did\":\"$DID\",\"message\":\"nope\",\"signature\":\"$SIG_HEX\"}") + echo "$VER_BAD" | python3 -c "import sys,json; r=json.load(sys.stdin); assert not r['result']['valid']" 2>/dev/null && pass "identity.verify (bad rejected)" || fail "identity.verify bad: $VER_BAD" + + # get + R=$(rpc "identity.get" "{\"id\":\"$FIRST_ID\"}") + rpc_ok "$R" && pass "identity.get" || fail "identity.get: $R" + + # set-default + R=$(rpc "identity.set-default" "{\"id\":\"$FIRST_ID\"}") + rpc_ok "$R" && pass "identity.set-default" || fail "identity.set-default: $R" + + # nostr key + NOSTR=$(rpc "identity.create-nostr-key" "{\"id\":\"$FIRST_ID\"}") + rpc_ok "$NOSTR" && pass "identity.create-nostr-key" || { + echo "$NOSTR" | grep -q "already exists" && pass "identity.create-nostr-key (exists)" || fail "nostr-key: $NOSTR" + } + + # nostr sign + HASH=$(python3 -c "import hashlib; print(hashlib.sha256(b'test').hexdigest())") + R=$(rpc "identity.nostr-sign" "{\"id\":\"$FIRST_ID\",\"event_hash\":\"$HASH\"}") + rpc_ok "$R" && pass "identity.nostr-sign" || fail "identity.nostr-sign: $R" +else + fail "no identity found" +fi + +# Create + nostr + delete +CREATE=$(rpc "identity.create" '{"name":"TmpTest","purpose":"anonymous"}') +rpc_ok "$CREATE" && pass "identity.create" || fail "identity.create: $CREATE" +TEMP_ID=$(echo "$CREATE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('result',{}).get('id',''))" 2>/dev/null) +if [ -n "$TEMP_ID" ]; then + R=$(rpc "identity.create-nostr-key" "{\"id\":\"$TEMP_ID\"}") + rpc_ok "$R" && pass "nostr-key (new identity)" || fail "nostr-key (new): $R" + R=$(rpc "identity.delete" "{\"id\":\"$TEMP_ID\"}") + rpc_ok "$R" && pass "identity.delete" || fail "identity.delete: $R" +fi + +echo "" +echo "━━━ Names (identity.*-name) ━━━" +R=$(rpc "identity.list-names") +rpc_ok "$R" && pass "identity.list-names" || fail "identity.list-names: $(echo $R | head -c 120)" + +if [ -n "$FIRST_ID" ]; then + R=$(rpc "identity.register-name" "{\"name\":\"e2e\",\"domain\":\"archipelago.local\",\"identity_id\":\"$FIRST_ID\",\"did\":\"$DID\"}") + rpc_ok "$R" && pass "identity.register-name" || fail "identity.register-name: $(echo $R | head -c 120)" + REG_NAME_ID=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin).get('result',{}).get('id',''))" 2>/dev/null) + + R=$(rpc "identity.resolve-name" '{"identifier":"e2e@archipelago.local"}') + rpc_ok "$R" && pass "identity.resolve-name" || fail "identity.resolve-name: $(echo $R | head -c 120)" + + if [ -n "$REG_NAME_ID" ]; then + R=$(rpc "identity.remove-name" "{\"id\":\"$REG_NAME_ID\"}") + rpc_ok "$R" && pass "identity.remove-name" || fail "identity.remove-name: $(echo $R | head -c 120)" + fi +fi + +echo "" +echo "━━━ Credentials (identity.*-credential) ━━━" +R=$(rpc "identity.list-credentials") +rpc_ok "$R" && pass "identity.list-credentials" || fail "identity.list-credentials: $(echo $R | head -c 120)" + +if [ -n "$FIRST_ID" ]; then + R=$(rpc "identity.issue-credential" "{\"issuer_id\":\"$FIRST_ID\",\"subject_did\":\"did:key:z6MkTest\",\"type\":\"TestCred\",\"claims\":{\"name\":\"E2E\"}}") + rpc_ok "$R" && pass "identity.issue-credential" || fail "identity.issue-credential: $(echo $R | head -c 120)" +fi + +echo "" +echo "━━━ Lightning ━━━" +R=$(rpc "lnd.getinfo") +rpc_ok "$R" && pass "lnd.getinfo" || fail "lnd.getinfo: $R" + +R=$(rpc "lnd.listchannels") +rpc_ok "$R" && pass "lnd.listchannels" || fail "lnd.listchannels: $R" + +R=$(rpc "lnd.newaddress") +rpc_ok "$R" && pass "lnd.newaddress" || fail "lnd.newaddress: $R" + +R=$(rpc "lnd.createinvoice" '{"amount_sats":0,"memo":"zero amount test"}') +rpc_ok "$R" && pass "lnd.createinvoice (0 sats)" || fail "lnd.createinvoice (0): $R" + +R=$(rpc "lnd.createinvoice" '{"amount_sats":1000,"memo":"test"}') +rpc_ok "$R" && pass "lnd.createinvoice (1000 sats)" || fail "lnd.createinvoice (1000): $R" + +R=$(rpc "bitcoin.getinfo") +rpc_ok "$R" && pass "bitcoin.getinfo" || fail "bitcoin.getinfo: $R" + +echo "" +echo "━━━ Tor ━━━" +R=$(rpc "tor.list-services") +rpc_ok "$R" && pass "tor.list-services" || fail "tor.list-services: $R" + +R=$(rpc "tor.create-service" '{"name":"test-e2e","local_port":9999}') +rpc_ok "$R" && pass "tor.create-service" || fail "tor.create-service: $(echo $R | head -c 150)" + +R=$(rpc "tor.delete-service" '{"name":"test-e2e"}') +rpc_ok "$R" && pass "tor.delete-service" || fail "tor.delete-service: $R" + +R=$(rpc "tor.get-onion-address" '{"name":"archipelago"}') +rpc_ok "$R" && pass "tor.get-onion-address" || fail "tor.get-onion-address: $R" + +echo "" +echo "━━━ Ecash Wallet ━━━" +R=$(rpc "wallet.ecash-balance") +rpc_ok "$R" && pass "wallet.ecash-balance" || skip "wallet.ecash-balance" + +R=$(rpc "wallet.ecash-history") +rpc_ok "$R" && pass "wallet.ecash-history" || skip "wallet.ecash-history" + +R=$(rpc "wallet.networking-profits") +rpc_ok "$R" && pass "wallet.networking-profits" || skip "wallet.networking-profits" + +echo "" +echo "━━━ Content ━━━" +R=$(rpc "content.list-mine") +rpc_ok "$R" && pass "content.list-mine" || fail "content.list-mine: $R" + +echo "" +echo "━━━ Network ━━━" +R=$(rpc "network.get-visibility") +rpc_ok "$R" && pass "network.get-visibility" || fail "network.get-visibility: $R" + +R=$(rpc "network.diagnostics") +rpc_ok "$R" && pass "network.diagnostics" || fail "network.diagnostics: $R" + +R=$(rpc "network.list-requests") +rpc_ok "$R" && pass "network.list-requests" || fail "network.list-requests: $R" + +R=$(rpc "node-list-peers") +rpc_ok "$R" && pass "node-list-peers" || fail "node-list-peers: $R" + +echo "" +echo "━━━ Nostr Relays ━━━" +R=$(rpc "nostr.list-relays") +rpc_ok "$R" && pass "nostr.list-relays" || fail "nostr.list-relays: $R" + +R=$(rpc "nostr.get-stats") +rpc_ok "$R" && pass "nostr.get-stats" || fail "nostr.get-stats: $R" + +echo "" +echo "━━━ DWN ━━━" +R=$(rpc "dwn.status") +rpc_ok "$R" && pass "dwn.status" || fail "dwn.status: $R" + +echo "" +echo "━━━ Update ━━━" +R=$(rpc "update.status") +rpc_ok "$R" && pass "update.status" || fail "update.status: $R" + +R=$(rpc "update.check") +rpc_ok "$R" && pass "update.check" || skip "update.check" + +echo "" +echo "━━━ Router ━━━" +R=$(rpc "router.info") +rpc_ok "$R" && pass "router.info" || skip "router.info" + +R=$(rpc "router.list-forwards") +rpc_ok "$R" && pass "router.list-forwards" || skip "router.list-forwards" + +echo "" +echo "━━━ Health & HTTP endpoints ━━━" +HC=$(curl -s -o /dev/null -w "%{http_code}" "${BASE}/health") +[ "$HC" = "200" ] && pass "/health (200)" || fail "/health ($HC)" + +EC=$(curl -s -o /dev/null -w "%{http_code}" "${BASE}/electrs-status") +[ "$EC" = "200" ] && pass "/electrs-status (200)" || fail "/electrs-status ($EC)" + +echo "" +echo "━━━ Container Management ━━━" +R=$(rpc "container-list") +rpc_ok "$R" && pass "container-list" || fail "container-list: $R" + +R=$(rpc "container-status" '{"app_id":"bitcoin-knots"}') +rpc_ok "$R" && pass "container-status (bitcoin-knots)" || fail "container-status: $R" + +echo "" +echo "━━━━━━━━━━━━━ RESULTS ━━━━━━━━━━━━━" +printf "\033[32m Passed: %d\033[0m\n" "$PC" +printf "\033[31m Failed: %d\033[0m\n" "$FC" +printf "\033[33m Skipped: %d\033[0m\n" "$SC" +T=$((PC + FC + SC)) +if [ "$FC" -eq 0 ]; then + printf "\n\033[1;32m🎉 ALL %d PASSED (%d skipped)\033[0m\n" "$PC" "$SC" +else + printf "\n\033[1;31m⚠ %d/%d FAILED\033[0m\n" "$FC" "$T" +fi +rm -f "$JAR"