diff --git a/apps/angor-indexer/README.md b/apps/angor-indexer/README.md index dfa2e5ad..ba7d1ef2 100644 --- a/apps/angor-indexer/README.md +++ b/apps/angor-indexer/README.md @@ -1,7 +1,8 @@ # Angor Indexer -Headless mainnet API endpoint for Angor. The service reuses this node's Mempool -backend and Electrum index instead of creating a second blockchain database. +Mainnet indexer endpoint for Angor, serving the existing Mempool explorer at +the same origin. The service reuses this node's Mempool frontend/backend and +Electrum index instead of creating another explorer or blockchain database. An unpruned, fully synced Bitcoin node is required. Installing against a pruned node must show the existing archival-node requirement; it must never silently unprune or replace its Bitcoin data. @@ -32,12 +33,45 @@ Install **Angor Relay** separately to host project metadata locally, then add clients. Its storage and configuration are separate from the node's internal relay; installing or uninstalling it does not change the internal relay. +## Verify the complete client flow + +The root URL opens the Mempool explorer. `/health` and fee +estimates establish API availability; they do not prove that project discovery, +address history, or browser CORS works. Test a known funded project's address +history, its original Nostr announcement, the Explore page, and project details +in the actual Angor client. A certificate alone does not establish public routing. + +Keep existing discovery relays when adding a new relay. A new relay has no +historical project data and does not automatically replicate other relays. +Even with existing relays, an empty Explore page can be a client discovery +failure: Angor Hub v2.0.0 was observed to stop after a batch whose announcements +all failed on-chain validation. The same failure reproduced with our indexer +and Angor's public indexer. Do not bypass the funding transaction's event-ID +commitment or substitute an unsigned announcement to make a project appear. + +For opt-in read-only browser acceptance, install the frontend test dependencies +and Playwright Chromium, then run: + +```sh +ANGOR_TEST_INDEXER=https://indexer.example.com/ \ +ANGOR_TEST_RELAY=wss://relay.example.com/ \ +ANGOR_TEST_RELAYS='["wss://relay.angor.io","wss://relay.example.com/"]' \ +node tests/lifecycle/angor-public-browser.cjs +``` + +The relay under test must already contain the known original public project +announcement documented in the test. The test does not import events, send +funds, change your browser profile, or disable TLS verification. It checks the +funding transaction/event commitment and real browser discovery and details. +Relay signed writes, invalid-signature rejection, persistence, full node sync, +and proxy upgrade/renewal tests remain separate acceptance requirements. + ## Packaging Build the pinned image with: ``` -podman build -t source.archipelago-foundation.org/chaum/angor-indexer:1.0.1 apps/angor-indexer/container +podman build -t source.archipelago-foundation.org/chaum/angor-indexer:1.0.2 apps/angor-indexer/container ``` The image runs as UID 101 with a read-only root filesystem and no capabilities. @@ -55,3 +89,21 @@ address query is indexed at the latest Bitcoin tip. Install Mempool Explorer first. The declarative `install_prerequisites` check refuses a new adapter installation if its Mempool API component is absent, before creating an installed-app record. It does not install or resync Bitcoin for you. + +## Explorer on the public indexer origin + +The linked official deployment guide exposes **Mempool frontend and API together** +on the public indexer URL. It uses standard Mempool images and requires no custom +Angor fork or `ANGOR_ENABLED` flag. + +The operator now requires that same browser experience: opening the configured +indexer domain must show the existing Mempool explorer, while Angor API requests +continue working on that origin. Reuse the existing Mempool stack, including its +live WebSocket feed; do not install a second explorer or blockchain database. + +**Candidate 1.0.2:** `/` and frontend paths proxy to the existing Mempool +frontend; `/api/`, `/api/v1/`, `/health` and the WebSocket feed retain their +indexer routes. Version 1.0.1 served only service JSON at `/`. The candidate +remains pending deployment/release acceptance, which must cover assets and deep links, +desktop/mobile rendering, WebSocket updates, API/CORS/broadcast, trusted HTTPS, +restart/upgrade and management-access isolation before documenting it as shipped. diff --git a/apps/angor-indexer/container/nginx.conf b/apps/angor-indexer/container/nginx.conf index 19a6a957..13eaf9fb 100644 --- a/apps/angor-indexer/container/nginx.conf +++ b/apps/angor-indexer/container/nginx.conf @@ -15,6 +15,14 @@ http { zone mempool_backend 64k; server mempool-api:8999 resolve; } + upstream mempool_frontend { + zone mempool_frontend 64k; + server mempool:8080 resolve; + } + map $http_upgrade $angor_connection_upgrade { + default upgrade; + '' close; + } server { listen 8080; client_max_body_size 4m; @@ -23,7 +31,8 @@ http { proxy_send_timeout 30s; proxy_http_version 1.1; proxy_set_header Host $host; - proxy_set_header Connection ""; + proxy_set_header Connection $angor_connection_upgrade; + proxy_set_header Upgrade $http_upgrade; proxy_set_header Authorization ""; proxy_set_header Cookie ""; proxy_hide_header Access-Control-Allow-Origin; @@ -35,10 +44,6 @@ http { # Mempool's backend uses /api/v1. Match its frontend's shorter /api # surface too, without doubling already-versioned Angor URLs. rewrite ^/api/(?!v1/)(.*)$ /api/v1/$1 last; - location = / { - default_type application/json; - return 200 '{"service":"Angor Indexer","network":"mainnet","api":"/api/v1","health":"/health"}\n'; - } # Readiness checks the indexing backend, not this gateway's process. location = /health { limit_except GET { deny all; } @@ -54,10 +59,23 @@ http { limit_except GET POST { deny all; } proxy_pass http://mempool_backend; } + location = /api/v1/ws { + limit_except GET { deny all; } + proxy_read_timeout 600s; + proxy_send_timeout 600s; + proxy_pass http://mempool_backend; + } location /api/ { limit_except GET { deny all; } proxy_pass http://mempool_backend; } - location / { return 404; } + # Share the already-installed explorer; no second frontend or index DB. + # Its SPA handles transaction/block deep links and static assets. + location / { + limit_except GET { deny all; } + proxy_pass http://mempool_frontend; + proxy_intercept_errors on; + error_page 500 502 503 504 =503 @waiting; + } } } diff --git a/apps/angor-indexer/manifest.yml b/apps/angor-indexer/manifest.yml index 0601c5c0..4fbc41a3 100644 --- a/apps/angor-indexer/manifest.yml +++ b/apps/angor-indexer/manifest.yml @@ -1,22 +1,26 @@ app: id: angor-indexer name: Angor Indexer - version: 1.0.1 - description: Headless Bitcoin indexer endpoint for Angor. Reuses this node’s Mempool + version: 1.0.2 + description: Bitcoin indexer endpoint for Angor with the existing Mempool explorer. + Reuses this node’s Mempool and Electrum index; requires a synced, unpruned Bitcoin node. Add this service’s address as the custom indexer in Angor settings. A relay is optional and installed separately. category: money install_prerequisites: + - mempool - mempool-api upstream: kind: github repo: block-core/angor container: - image: source.archipelago-foundation.org/chaum/angor-indexer:1.0.1 + image: source.archipelago-foundation.org/chaum/angor-indexer:1.0.2 pull_policy: if-not-present network: archy-net dependencies: + - app_id: mempool + version: '>=3.0.0' - app_id: mempool-api version: '>=3.0.0' - bitcoin:archival @@ -40,8 +44,8 @@ app: interfaces: main: name: Angor Indexer API - description: Use this origin as Angor’s custom mainnet indexer URL. HTTPS is - required for browser clients. + description: Use this origin as Angor’s custom mainnet indexer URL, or open it + to view the existing Mempool explorer. HTTPS is required for browser clients. type: api port: 8998 protocol: http @@ -63,6 +67,7 @@ app: repo: https://github.com/block-core/angor features: - Angor mainnet API + - Mempool explorer on the same origin - Reuses existing Mempool indexing - No separate blockchain database - Optional independent relay diff --git a/apps/angor-relay/README.md b/apps/angor-relay/README.md index 343f6330..1c7e7fa8 100644 --- a/apps/angor-relay/README.md +++ b/apps/angor-relay/README.md @@ -8,6 +8,18 @@ this is a public relay, not a private messaging archive. It mounts only `/var/lib/archipelago/angor-relay` and its separate configuration directory. It never opens, reconfigures or shares the node's internal strfry database. +For a public domain, proxy HTTPS to node port **8091**, enable WebSocket upgrade, +and add `wss://your-relay-domain/` in Angor. Test both NIP-11 metadata (send +`Accept: application/nostr+json`) and a real Nostr subscription over WSS. An +Archipelago login page at this domain is a routing failure, not relay readiness. + +New relays start without project history. Keep existing discovery relays alongside +yours until the needed original signed announcements and metadata are available +locally. Relays do not automatically synchronize. Any history import must retain +the original event IDs and signatures; verify funded projects against their +on-chain commitments. A working WebSocket with zero stored events is not proof +that the client's project discovery works. See the indexer README's browser test. + The configuration is seeded only when absent, preserving operator changes. Stop the service before making a consistent backup of its event database. Ordinary start/restart/recreation preserves both mounts. Use the standard app diff --git a/docs/angor-client-acceptance-20261001.md b/docs/angor-client-acceptance-20261001.md new file mode 100644 index 00000000..db1f6ec9 --- /dev/null +++ b/docs/angor-client-acceptance-20261001.md @@ -0,0 +1,112 @@ +# Angor client acceptance and remaining project recovery + +Status: **OPEN — live services and one complete project flow pass; full project recovery is incomplete.** + +The operator requires actual Angor-client verification and any necessary application +fixes to reach the next OTA, app catalog and ISO. Main release owner acknowledged +ownership of NPM/public management isolation; this investigation owns Angor evidence, +app setup documentation and the scoped browser test. Do not treat this report as +permission to waive the outstanding discovery/recovery requirements. + +## Verified live behavior + +- Trusted HTTPS indexer health, fees, address transactions and cursor pagination work. +- Real browser requests from `https://angor.io` reach the selected custom indexer; + checked transaction IDs, confirmation data and output scripts match the reference. +- The indexer serves all 35 funding transactions listed in the public reference + snapshot, confirmed, with matching original announcement commitments. This checks + that snapshot, not every possible Bitcoin address or the entire project universe. +- Relay NIP-11 and encrypted WebSocket/Nostr read work. An invalid signature was + rejected before importing the valid original announcement; the original signed + announcement was accepted unchanged and read back exactly. +- Imported ONLY the original public kind3030 event + `adbf94d6152097f3d503cd68cc00dee34c1e1007914c00cfc3d6698d1ee01834`, after checking its + Schnorr signature with nostr-tools and matching funding transaction + `72d14227b78a260c9410a65585cb49e81e35eaa95b1b23ec702eb0512ca016a7` on our indexer. + No fabricated, re-signed or modified announcement, wallet operation or payment. +- Restarted only the managed Angor relay. Its original announcement persisted; + node-internal strfry container ID and start time remained unchanged. +- Clean Chromium, configured with our indexer plus original relays and our relay, + now displays **Casa Bitcoin sv** in Explore and loads its full project page, + metadata, funding transaction and Project Statistics. +- `tests/lifecycle/angor-public-browser.cjs` passes all three acceptance groups: + trusted public API/WSS and chain commitment; actual Explore discovery; + actual project details/statistics. It is opt-in and read-only, uses a disposable + profile and known immutable public fixture, and does not disable TLS checks. + +Live test command (run from repository root): + +```sh +ANGOR_TEST_INDEXER=https://angor-indexer.tx1138.com/ \ +ANGOR_TEST_RELAY=wss://angor-relay.tx1138.com/ \ +ANGOR_TEST_RELAYS='["wss://relay.angor.io","wss://relay2.angor.io","wss://angor-relay.tx1138.com"]' \ +node tests/lifecycle/angor-public-browser.cjs +``` + +## Empty project list: reproduced upstream behavior + +The user retained the original relays. Our earlier suggestion that an empty new +relay explained the entire failure was incorrect. + +Angor Hub v2.0.0 (`main-575CWKJX.js`) was tested in separate clean Chromium profiles +using our indexer and `https://indexer.angor.io/`. Both received candidate Nostr +announcements, but the current batch failed validation: `event-mismatch` for +updated announcements of one funded project, and `not-found-in-mempool` for another +address that both indexers correctly return empty. Both displayed zero projects. +The default primary `fulcrum.angor.online` separately failed browser CORS in a +fresh default-config test. These upstream failures must not be attributed to +missing data in our indexer without evidence. + +Current `block-core/angor-hub` `src/app/services/indexer.service.ts` discovers +kind3030 events, filters by network, and calls `validateAndAddProjects` in the +background. When a batch has candidates but none validate, it does not continue +discovery to older valid original announcements. The Load More UI is unavailable +when the project list is empty. An empty relay can coexist with this bug but is +not its sole cause. After the authentic announcement was copied to our relay, +one original passed the unchanged validation and appeared. This is a bounded +history-availability recovery, NOT an upstream UI fix or global relay sync. + +The current browser uses ordinary `/api/v1/address/.../txs`, not the deprecated +`/api/v1/query/Angor/projects` listing. Our adapter's 404 for the latter is NOT the +observed browser failure. Do not add an opaque upstream proxy to hide that 404. + +## Other projects remain unresolved + +The public reference query returned 35 project records (limit50). All35 on-chain +funding commitments were verified through our indexer. Exact original-event-ID +queries were sent to the configured relays `relay.angor.io`, `relay2.angor.io`, +`nos.lol`, `relay.primal.net`, and `relay.damus.io` (Damus had an initial connection +failure, then answered EOSE on retry). Only one original announcement was found. +Additional queries to `relay.snort.social`, `nostr.mom`, and `no.str.cr` returned +no matches; `relay.nostr.band` and `relay.f7z.io` were unavailable. This does not +prove the other34 events are globally lost or that all storage sources were checked. + +Exact project IDs, transaction IDs, event IDs and confirmed commitment results: +[recovery inventory](angor-project-recovery-20261001.json). Recover originals from +founders/known archives or authoritative relay backups and retain their signatures. +On-chain commitments cannot reconstruct missing off-chain project content. +Do not fabricate replacements or accept mismatched events to make cards appear. + +## Release handoff and open gates + +- [x] Release owner acknowledges these final findings and the 34-project gap. + Confirmed 2026-10-01 18:46:50 UTC in `/tmp/angor-final-handoff-ack.txt`: full + recovery remains open; publication held for required gates; preserve relay + data and rerun browser acceptance after bridge migration and OTA. +- [ ] Resolve or explicitly carry the upstream discovery bug with accurate user + guidance; do not claim full recovery because the fixture passes. +- [ ] Locate and verify remaining original project metadata/history, or obtain an + explicit operator scope decision; one project is not complete acceptance. +- [ ] Preserve the verified public relay event/data through the candidate upgrade. +- [ ] Repeat scoped browser acceptance after NPM bridge migration and OTA install. +- [ ] Verify fresh ISO setup, NPM host propagation, public IP/default-host isolation, + certificate issuance/renewal and relay WebSocket routing; see NPM handoff. +- [ ] Include updated app setup documentation and the read-only acceptance test. + No Angor image/config change was justified by the verified transaction data; + no image or catalog version should be bumped solely to disguise upstream failure. +- [ ] Existing signed transaction-broadcast integration tests remain required; + this live investigation did not send real Bitcoin or test a funded investment. + +Raw local diagnostic logs are in `/tmp/angor-*-browser*.log` and +`/tmp/angor-public-browser-acceptance.log`. The durable facts and recovery inventory +above must remain available after temporary logs expire. diff --git a/docs/angor-project-recovery-20261001.json b/docs/angor-project-recovery-20261001.json new file mode 100644 index 00000000..24074147 --- /dev/null +++ b/docs/angor-project-recovery-20261001.json @@ -0,0 +1,286 @@ +{ + "status": "INCOMPLETE: 34 original announcements not recovered from queried relays", + "source_inventory": "https://indexer.angor.io/api/v1/query/Angor/projects?limit=50", + "projects": [ + { + "project": "angor1qryhse38vcyqnp0j6976q9f00a9jpj2ary03nlc", + "txid": "72d14227b78a260c9410a65585cb49e81e35eaa95b1b23ec702eb0512ca016a7", + "event_id": "adbf94d6152097f3d503cd68cc00dee34c1e1007914c00cfc3d6698d1ee01834", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": true + }, + { + "project": "angor1q3eh4xg7t2hge7ctqk4yhmj7q23t6mdqa0ahg28", + "txid": "f5ff2e1a6b7340ddb9944c2ae6477c6b0b12993c9919f6e9b7b9e7a5e9a68f6d", + "event_id": "f4417e39c9b47afafd84cdda2017207e0929df2852badbf8d909bf6f647f4f2d", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1qde7y830vtajref5vwag5x459m9w5y75gd49v4t", + "txid": "205ce39688572ef0168c1c1231c25ec632abb2c2b64c52fccb4f53eb0a49e300", + "event_id": "ca76084c2bc3301f8fe00dd8a93c6d6c0ad015e50676cdd3e44c8bd765f157d5", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1q9k9976ytwkkmswlq24e89l2fxuxl57gfp8yxgr", + "txid": "00a0120818698ef3d72d360e29e9e4e7d0a35c180967186d1e715aef797c26a8", + "event_id": "a04d1eed8c1261cb5fca6a6faf16a5f87a9672cf40d1117b25f2da60e6e8f84c", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1q67h8cktwy8yv32t0uk0c8zrtpmtwgtd56sylzx", + "txid": "1bfa726353a40d5fce1a76ad86dc7915bee214573d30d5751cd6312c94519089", + "event_id": "b3189f84b490da422e17b6b857d393b13781bb82e1c8813328769e3d2840098a", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1qm999ekmrdjl4sq3qywl889w74qetxe3ghdxthl", + "txid": "ae9d471dd6e58b318120fbdf72929c621cf23d9c6f047df3c57923a8a915d01c", + "event_id": "89ae5e612e857f89dbbdc662198b894f1f7b70564769bedf08ec37f5e7b5efe0", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1qtunna00uqyx52rr0nvqa059z2gknhrmn7urd6z", + "txid": "c2cb77ebf6b9ded802b677c600c3869b81fc76f31a3e4742b6a9cb16e0ac3dee", + "event_id": "f5e66707f8fa0fc601bef403020e64d9a164d6cf201599ec0c3ddb0acf58491e", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1qu8h0ezv596z35uggg0r0ppkma93tnreyjg5du7", + "txid": "fb6bc0b721810957ae8910d5dd6e7ebc45d804a3e1f636d153df50f036e90645", + "event_id": "ce61a11b79aa258238db63f67472341169944bce02600045bf7278992aeb796c", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1q8u9c2h2l0eqjn3qeyvdps89dkkvwteg5q3m708", + "txid": "04ed05297e146206c104c7b5d8a51fc3453f1829950675b4694f91a7ea609be1", + "event_id": "f31c6ef3a66e74ebfc26f5b2905e6d4641f73bdb407aa92022a2202b9be54716", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1qqnxefpr4f59r5f4u34c7crzst2ya83wavh8407", + "txid": "bae879110e78ad44624980ea4a47de21b03d3994139714bb09e910187027583d", + "event_id": "cbd23077098059c5092bb7ea8df14dd73f21d47d1b690ff1e6fa789ad118864b", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1qfydf802v2efvxufevrfg0mvtlrxln63rkzmdxw", + "txid": "4bbb04fed973c968e76faaca880197092be288d9897072ab5e6dfb719c19eb69", + "event_id": "e9761e1303de7eca0ade33936489d2b4b7d51f4b52e8ecbc1acfc394df48b95d", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1qpt96uj5jg77q5566pmcdggyuztt26xn3xymwhn", + "txid": "bc46fea91acf4714f5b7949e9bb937f39e425468242c2459b581a14914e641b4", + "event_id": "f683c12dbc3d574797bd2251d7e9d96e1d33d6263f75a1538d2ce5fd0c86e064", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1q3nleprspa3h6thy9c25wzu4q7ywajhcdg3j9au", + "txid": "7b76a8297f16ad5ff3d69fc85e37405ad4f77a71dfc8f70206a5a9c1827698ba", + "event_id": "893e5b7f09a12368f208120deb9c02b3692aee15240a914fb428e7e4f86b1123", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1qc8jlugwgp90vzkhf336d8exldhwd8z5u4ssaen", + "txid": "5e06eb3684bf0d3402d20d643d6fe1b5a295680a29db440e070a89f80e52a241", + "event_id": "375eafd0e03c50d683de4f465501e919a8816ae618ca2b5c14e8d3f56daa90c0", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1qdhwn7s0p8wcr9kw2932m448y6hd8ywl005fdwu", + "txid": "7cc5bff45f0b5e9ae81cadaf787dd4d4680387ab9fddedf8438eae8f87ef34ec", + "event_id": "6c7767eaf6ee0ae4ffe4b23c8477f2293216279e5908dd979abd4abdf1557578", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1q8qzylut0gv7urycxukdfumzw6znghrk4g928k2", + "txid": "2c7eed8d9b8eef530a4a8f39339f7dfaa82d5ec09261305203d1a367a624be1b", + "event_id": "24323aae4bca910ddb48544b788860119c3cc13ac19b24694221f1ba2521cd6b", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1qzwrm6hf5l0tgen99anr9hj7ylk38v6zhmnq7w0", + "txid": "934c45244c283ff24834bab7d01a0964192433cd2bc11143fc5e590b2b954deb", + "event_id": "6cfae243c276a602ce2da33842ad930019d75e80ffe81b4cd84b1b187830bba7", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1qpdp5d2ahknhtr7kawsl62m7y4hktsfn4uwg6r3", + "txid": "dc85d36b324ff829a4d49a606573e136fbc29498bc707d95f5bd0d9ef49956d0", + "event_id": "c6d22deef6d1babc99716746f50509e5413b97445d991a0869f0567b7301fc97", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1q9v88fe2q2efr4z57wed4gklmkh7trmx6usac0d", + "txid": "b98b23e49630f92ac0dedb7e0a8ac5ad2c51f813039f2d9dd708c9a0861b2ca2", + "event_id": "e8f518eaed658e9331b3a3feba49f36eda5eb7fe68c81c7b3f71057844729bcb", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1q0gtztyk24ea2028ewfn026838k3ll322qrvrhu", + "txid": "31b38cf48cf18936b7c370ee72e8bda6e9ee40f24ec676a85b9b8b1abefebdd4", + "event_id": "fe34db328e51cc10f9c4d035b0f0aab7f9137a4cdee3a2afc4c6610404e79fe4", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1q65fuwpyvek3fxk5p757zyknjd9k9sava3fd98j", + "txid": "0b7bf7b9119157edc778bcdc7088b70a439fa5dc8c46e839b95fd2f0a8fbf046", + "event_id": "991d0f7d1c261e4d79507238e3d5e4b3d3adce267cf40107ee2ade32485f5cf6", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1qjqfp23ac4s4llgcgs3qnenjhpasgrmvt373usp", + "txid": "8b4887aba04b12729e609658b0fdbb1e6f1b172ba10360c7a5261c3e9bd590f9", + "event_id": "254910567be531d7862afcbcc80b490ee92a5f9ed801424f3d56108d9a114b80", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1qfyzk83tqznc3aqg5ly0cewn7ytsrwl474v4dyn", + "txid": "3e2ea870b17eab2023a04dca267c45d2c53a41abb3f20b206e095fcbba6c5f02", + "event_id": "1e27aa63f276048ba78ef73f69dc5045441feafaa090aff9b995341883f22fb4", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1qw7m67a5r6vpmtw2jqpsp4xtxvtu6mmnkgj6gxw", + "txid": "f3d3549b2a30c78e7c3b51bc9122e0ae8a7ecb378f9b3564ebf931769637df49", + "event_id": "fc4289a4888bfe157588e507204d93723a8c07074cde37bb98cf866793f81c98", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1qgdc07hkk0l4kntg2k5fczk7tcx0qeqqx2saru0", + "txid": "c1332c51da2c5706f6fc74e3275856438304a7761ced4dcd2e738a7e3c62bd2f", + "event_id": "ae1d1f7f883907dea36902fc2dc78c8ad81d22bd59bd7293ba1725c6cc959846", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1qhklyl9mw5zwzwynxv6ekaz5f9h5zv3wnd8dn2c", + "txid": "be637cd8a23c80f49195345eca5d4c29bf4a9fa0600e0625af702444e300d218", + "event_id": "78a1443b14cb252b2510925263889e8c8f40b12fd911d137d63b1dbc2b841a3a", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1q4heck5tf6svq7x8jp5twak329xtv9805xppvq5", + "txid": "509db524eba0b90e8607396e9eb42ed379a270f3aa602a0bc16eec050c959823", + "event_id": "dec392d7d962e7dfd2c9eaa2b05dfc03c909d87982c4d73b9f321dc13487622f", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1qc6gykcw82hu3pa4pn94gfxwgpp8dewlsh45rwc", + "txid": "087390128a78270cd1465656e3ab63b9b47982dca1a910b7a5664f67f8ab9970", + "event_id": "cf634987daa4ee17bb2d160ddb04ce56a0d73e664b9822e5e9a8edf870630a9c", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1qfzxd8n94r592gt3mrmgwe7knk6dhajmutpuhkx", + "txid": "29cb361fe78b6f199f169b7b4a7d9663b7e7f46607f382ddcf8cb2224a6023f1", + "event_id": "ca7a6e0bb27beb46fb8e709378908feb5357e7edd43a9262318cadf72da5f141", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1q0whhl3qgq28g443h9mj6dl8n2ssshm6hkd3xse", + "txid": "4d70fba03ec51d87df7df16498a95a907b9cff00035455e3ab8242935f260030", + "event_id": "3a19b34d76ec7b99eb98ed299737c06cd3710268febf7d2acf6ce1fece9ba459", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1q2a5m2zcwpmkh49z05pg6gd9cxm4dhx3ywfclem", + "txid": "5da06a119db273bbb7c64e2cc103a8edb48a9934ab9b5b972ae55f697023ce07", + "event_id": "8c3a8dcaf9c55e7797cf4ff9a25b0e7cd7dcbe78c8ed248d53a49c70f9ea4a6b", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1qq9ngpm2w8xss5sf0amt63z076y2x885wh0jfv7", + "txid": "7f42da75e4b3dd92f9870aefa177c2fb3783f189996abf193b3b353ee21e805e", + "event_id": "2f0c6e3b29a74be45033062742b3fbac4a4c7b7be4eeecc7021df4a8b1b195cf", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1qwdgxjuzhjykgpn5q8p3l2q9vyrgqdlrkfp5sjr", + "txid": "c15d07fd14d58e7204889ba24fe47a9b86aa7bea9992d30f4be36864e7634725", + "event_id": "733b28b35f771839bc719125af94916a5400675185d4a75edb09645c8eb4cc24", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1qyuj8z532tnhy7srutwecu3j789z22peu2t8c7v", + "txid": "821193743f7ca7b0b178a78379d79f5783d874a182a8bf36b70a0a2a6cb12d24", + "event_id": "56fce837c628728953138ca57895c7ef9533640a989934c432d1040808781b9f", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + }, + { + "project": "angor1qznva9wmw3anr7qdhhs4v0xptd0pz07gdr2fuwz", + "txid": "187e39a0ba9714ae3543bbe775dd9fff9c7a4ac946ce0a850480c3871de287fe", + "event_id": "baff907b6f1fb97893e63e1bef6919f819c1b6e37560ed2d4255b77cc26d08cd", + "confirmed": true, + "commitment_matches": true, + "original_announcement_recovered": false + } + ] +} diff --git a/tests/lifecycle/angor-proxy-check.py b/tests/lifecycle/angor-proxy-check.py index 3f1ce778..2b0bc352 100644 --- a/tests/lifecycle/angor-proxy-check.py +++ b/tests/lifecycle/angor-proxy-check.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """Opt-in disposable rootless Angor gateway integration checks. No native app changes.""" -import subprocess,pathlib,json,urllib.request,urllib.error,time,tempfile,os,uuid,shlex +import subprocess,pathlib,json,urllib.request,urllib.error,time,tempfile,os,uuid,shlex,socket import yaml if os.environ.get('ARCHY_ALLOW_DISPOSABLE_CONTAINERS') != '1': raise SystemExit('Set ARCHY_ALLOW_DISPOSABLE_CONTAINERS=1 to run isolated test containers') @@ -8,17 +8,20 @@ manifest=yaml.safe_load((pathlib.Path(__file__).resolve().parents[2]/'apps/angor health=manifest['health_check'] health_url=health['endpoint'].rstrip('/')+health.get('path','/') run_id=uuid.uuid4().hex[:12] -net='archy-angor-test-'+run_id;backend='angor-test-backend-'+run_id;gateway='angor-test-gateway-'+run_id +net='archy-angor-test-'+run_id;backend='angor-test-backend-'+run_id;gateway='angor-test-gateway-'+run_id;frontend='angor-test-frontend-'+run_id +port=None def run(*a): r=subprocess.run(a,capture_output=True,text=True) if r.returncode:raise RuntimeError(r.stderr) return r.stdout.strip() def req(path,data=None,method=None,headers={}): - r=urllib.request.Request('http://127.0.0.1:19098'+path,data=data,method=method,headers=headers) + r=urllib.request.Request(f'http://127.0.0.1:{port}'+path,data=data,method=method,headers=headers) try: with urllib.request.urlopen(r,timeout=10) as f:return f.status,f.headers,f.read() except urllib.error.HTTPError as e:return e.code,e.headers,e.read() -script="""require('http').createServer((q,r)=>{let b='';q.on('data',x=>b+=x);q.on('end',()=>{r.setHeader('Access-Control-Allow-Origin','https://wrong.example');if(q.url==='/api/v1/blocks/tip/height'){r.end('900000');return}r.setHeader('Content-Type','application/json');r.end(JSON.stringify({url:q.url,method:q.method,body:b,cookie:q.headers.cookie||null,auth:q.headers.authorization||null}))})}).listen(8999,'0.0.0.0')""" +script="""const server=require('http').createServer((q,r)=>{let b='';q.on('data',x=>b+=x);q.on('end',()=>{r.setHeader('Access-Control-Allow-Origin','https://wrong.example');if(q.url==='/api/v1/blocks/tip/height'){r.end('900000');return}r.setHeader('Content-Type','application/json');r.end(JSON.stringify({url:q.url,method:q.method,body:b,cookie:q.headers.cookie||null,auth:q.headers.authorization||null}))})});server.on('upgrade',(q,s)=>{if(q.url!=='/api/v1/ws'||q.headers.cookie||q.headers.authorization){s.destroy();return}const accept=require('crypto').createHash('sha1').update(q.headers['sec-websocket-key']+'258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');s.end('HTTP/1.1 101 Switching Protocols\\r\\nUpgrade: websocket\\r\\nConnection: Upgrade\\r\\nSec-WebSocket-Accept: '+accept+'\\r\\n\\r\\n'+String.fromCharCode(129,11)+'fixture-tip','latin1')});server.listen(8999,'0.0.0.0')""" +frontend_script="""require('http').createServer((q,r)=>{if(q.headers.cookie||q.headers.authorization){r.writeHead(500);r.end('credential leak');return}if(q.url==='/asset.js'){r.setHeader('Content-Type','application/javascript');r.end('window.explorer=true');return}if(q.url==='/'||q.url==='/tx/fixture'){r.setHeader('Content-Type','text/html');r.end('Mempool explorer fixture');return}r.writeHead(404);r.end('not found')}).listen(8080,'0.0.0.0')""" +def start_frontend():run('podman','run','-d','--name',frontend,'--network',net,'--network-alias','mempool','--cap-drop=all','--security-opt=no-new-privileges','docker.io/library/node:24-alpine','node','-e',frontend_script) def start_backend():run('podman','run','-d','--name',backend,'--network',net,'--network-alias','mempool-api','--cap-drop=all','--security-opt=no-new-privileges','docker.io/library/node:24-alpine','node','-e',script) def ready(seconds=40): end=time.monotonic()+seconds @@ -31,8 +34,9 @@ def ready(seconds=40): assert subprocess.run(['podman','network','exists',net]).returncode==1 run('podman','network','create',net) try: - start_backend() - run('podman','run','-d','--name',gateway,'--network',net,'--read-only','--cap-drop=all','--security-opt=no-new-privileges','--memory','128m','--health-cmd','wget -q -T 5 -O /dev/null '+shlex.quote(health_url),'--health-interval','5s','--health-retries','2','-p','127.0.0.1:19098:8080','source.archipelago-foundation.org/chaum/angor-indexer:1.0.1') + start_backend();start_frontend() + run('podman','run','-d','--name',gateway,'--network',net,'--read-only','--cap-drop=all','--security-opt=no-new-privileges','--memory','128m','--health-cmd','wget -q -T 5 -O /dev/null '+shlex.quote(health_url),'--health-interval','5s','--health-retries','2','-p','127.0.0.1::8080',manifest['container']['image']) + port=int(json.loads(run('podman','inspect',gateway))[0]['NetworkSettings']['Ports']['8080/tcp'][0]['HostPort']) ready() run('podman','healthcheck','run',gateway) assert json.loads(run('podman','inspect',gateway))[0]['State']['Health']['Status']=='healthy' @@ -48,6 +52,29 @@ try: assert req('/api/v1/tx',method='OPTIONS')[0]==204 assert req('/api/v1/tx',b'x'*(4*1024*1024+1))[0]==413 assert req('/unknown')[0]==404 + for path in ['/', '/tx/fixture', '/asset.js']: + status,headers,body=req(path,headers={'Cookie':'private=secret','Authorization':'Bearer secret'}) + assert status==200 and (b'explorer' in body), (path,status,body) + assert req('/',b'not-allowed')[0]==403 + with socket.create_connection(('127.0.0.1',port),timeout=10) as stream: + stream.sendall(b'GET /api/v1/ws HTTP/1.1\r\nHost: indexer.example\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nCookie: private=secret\r\nAuthorization: Bearer secret\r\n\r\n') + response=b'' + while b'fixture-tip' not in response: + chunk=stream.recv(4096) + if not chunk:break + response+=chunk + assert b'101 Switching Protocols' in response and b'fixture-tip' in response,response + print('PASS shared explorer root/assets/deep links and real WebSocket upgrade/frame; credentials stripped',flush=True) + run('podman','stop',frontend) + assert req('/')[0]==503 + assert req('/health')[0]==200 + run('podman','rm',frontend);start_frontend() + end=time.monotonic()+40 + while time.monotonic() new URL(url).href === new URL(relay).href), + 'Discovery relays must include the relay under test'); + +async function read(endpoint, json = true) { + const response = await fetch(new URL(endpoint, indexer), { signal: AbortSignal.timeout(15000) }); + assert.equal(response.status, 200, `${endpoint}: ${response.status}`); + return json ? response.json() : response.text(); +} +function relayEvent() { + return new Promise((resolve, reject) => { + const ws = new WebSocket(relay, { handshakeTimeout: 15000, origin: new URL(app).origin }); + const timer = setTimeout(() => finish(Error('Relay project event timeout')), 15000); + function finish(err, value) { + clearTimeout(timer); + ws.removeAllListeners(); + ws.on('error', () => {}); + ws.close(); + err ? reject(err) : resolve(value); + } + ws.on('error', err => finish(err)); + ws.on('open', () => ws.send(JSON.stringify(['REQ', 'acceptance', { ids: [eventId] }]))); + ws.on('message', raw => { + try { + const m = JSON.parse(raw.toString()); + if (m[0] === 'EVENT' && m[1] === 'acceptance') finish(null, m[2]); + if (m[0] === 'EOSE') finish(Error('Relay lacks the known signed project fixture; provision public history first')); + if (m[0] === 'CLOSED') finish(Error(`Relay subscription rejected: ${m[2]}`)); + } catch (err) { finish(err); } + }); + }); +} + +(async () => { + assert.match((await read('/health', false)).trim(), /^\d+$/); + assert(Number.isFinite((await read('/api/v1/fees/recommended')).fastestFee)); + const txs = await read(`/api/v1/address/${address}/txs`); + const tx = txs.find(t => t.txid === fundingTx); + assert(tx?.status.confirmed, 'Known project funding transaction missing'); + const event = await relayEvent(); + assert.equal(event.id, eventId); + assert.equal(event.kind, 3030); + assert.equal(createHash('sha256').update(JSON.stringify([0, event.pubkey, event.created_at, + event.kind, event.tags, event.content])).digest('hex'), eventId); + assert(tx.vout.some(v => v.scriptpubkey_type === 'op_return' && v.scriptpubkey.includes(eventId)), + 'Project announcement must match the immutable funding commitment'); + assert.equal(JSON.parse(event.content).projectIdentifier, projectId); + // This checks a known immutable fixture's hash/chain binding, not a general + // Schnorr verifier. Signed publish and invalid-signature rejection are separate gates. + console.log('PASS public TLS, indexed funding data, relay WSS, immutable project/event commitment'); + + const browser = await chromium.launch({ headless: true }); + try { + const page = await browser.newPage(); + const failures = []; + let browserIndexerRead = false; + page.on('requestfailed', r => { + if (r.url().startsWith(new URL(indexer).origin)) failures.push(`${r.url()}: ${r.failure()?.errorText}`); + }); + page.on('response', r => { + if (r.url().startsWith(new URL(indexer).origin) && r.url().includes(`/address/${address}/txs`) && r.status() === 200) + browserIndexerRead = true; + }); + await page.addInitScript(({ indexer, relays }) => { + localStorage.setItem('angor-network', 'main'); + localStorage.setItem('angor-indexers', JSON.stringify({ mainnet: [{ url: indexer.replace(/\/?$/, '/'), isPrimary: true }], testnet: [] })); + localStorage.setItem('angor-hub-relays', JSON.stringify(relays)); + }, { indexer, relays }); + await page.goto(new URL('/explore', app).href, { waitUntil: 'domcontentloaded' }); + const link = page.locator(`a[href*="/project/${projectId}"]`).first(); + await link.waitFor({ state: 'visible', timeout: 60000 }); + assert(browserIndexerRead, 'Browser did not validate the project using the configured indexer'); + assert.deepEqual(failures, [], 'Browser indexer/CORS requests failed'); + console.log('PASS official Explore page discovers the known project with the configured services'); + await page.goto(new URL(`/project/${projectId}`, app).href, { waitUntil: 'domcontentloaded' }); + await page.getByText(projectId, { exact: true }).first().waitFor({ state: 'visible', timeout: 60000 }); + await page.getByText('Project Statistics', { exact: true }).waitFor({ state: 'visible', timeout: 30000 }); + assert.deepEqual(failures, [], 'Browser detail/indexer requests failed'); + console.log('PASS official project detail and statistics render; no funds sent'); + } finally { + await browser.close(); + } +})().catch(err => { console.error(err); process.exitCode = 1; }); diff --git a/tests/regression/angor-service-metadata.py b/tests/regression/angor-service-metadata.py index 7f118a7f..2a5b8144 100644 --- a/tests/regression/angor-service-metadata.py +++ b/tests/regression/angor-service-metadata.py @@ -19,6 +19,13 @@ class ServiceMetadata(unittest.TestCase): self.assertEqual(app['ports'][0]['bind'], '127.0.0.1') self.assertEqual(app['security']['capabilities'], []) + def test_indexer_reuses_both_existing_mempool_components(self): + app = yaml.safe_load((ROOT / 'apps/angor-indexer/manifest.yml').read_text())['app'] + self.assertEqual(set(app['install_prerequisites']), {'mempool', 'mempool-api'}) + self.assertTrue({'mempool', 'mempool-api'} <= { + dep['app_id'] for dep in app['dependencies'] if 'app_id' in dep}) + self.assertEqual(app.get('volumes', []), []) + def test_indexer_health_targets_ipv4_listener(self): app = yaml.safe_load((ROOT / 'apps/angor-indexer/manifest.yml').read_text())['app'] self.assertEqual(app['health_check']['endpoint'], 'http://127.0.0.1:8080')