Serve the shared Mempool explorer through the Angor indexer
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
# Angor Indexer
|
# Angor Indexer
|
||||||
|
|
||||||
Headless mainnet API endpoint for Angor. The service reuses this node's Mempool
|
Mainnet indexer endpoint for Angor, serving the existing Mempool explorer at
|
||||||
backend and Electrum index instead of creating a second blockchain database.
|
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
|
An unpruned, fully synced Bitcoin node is required. Installing against a pruned
|
||||||
node must show the existing archival-node requirement; it must never silently
|
node must show the existing archival-node requirement; it must never silently
|
||||||
unprune or replace its Bitcoin data.
|
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
|
clients. Its storage and configuration are separate from the node's internal
|
||||||
relay; installing or uninstalling it does not change the internal relay.
|
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
|
## Packaging
|
||||||
|
|
||||||
Build the pinned image with:
|
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.
|
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
|
Install Mempool Explorer first. The declarative `install_prerequisites` check
|
||||||
refuses a new adapter installation if its Mempool API component is absent, before
|
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.
|
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.
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ http {
|
|||||||
zone mempool_backend 64k;
|
zone mempool_backend 64k;
|
||||||
server mempool-api:8999 resolve;
|
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 {
|
server {
|
||||||
listen 8080;
|
listen 8080;
|
||||||
client_max_body_size 4m;
|
client_max_body_size 4m;
|
||||||
@@ -23,7 +31,8 @@ http {
|
|||||||
proxy_send_timeout 30s;
|
proxy_send_timeout 30s;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
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 Authorization "";
|
||||||
proxy_set_header Cookie "";
|
proxy_set_header Cookie "";
|
||||||
proxy_hide_header Access-Control-Allow-Origin;
|
proxy_hide_header Access-Control-Allow-Origin;
|
||||||
@@ -35,10 +44,6 @@ http {
|
|||||||
# Mempool's backend uses /api/v1. Match its frontend's shorter /api
|
# Mempool's backend uses /api/v1. Match its frontend's shorter /api
|
||||||
# surface too, without doubling already-versioned Angor URLs.
|
# surface too, without doubling already-versioned Angor URLs.
|
||||||
rewrite ^/api/(?!v1/)(.*)$ /api/v1/$1 last;
|
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.
|
# Readiness checks the indexing backend, not this gateway's process.
|
||||||
location = /health {
|
location = /health {
|
||||||
limit_except GET { deny all; }
|
limit_except GET { deny all; }
|
||||||
@@ -54,10 +59,23 @@ http {
|
|||||||
limit_except GET POST { deny all; }
|
limit_except GET POST { deny all; }
|
||||||
proxy_pass http://mempool_backend;
|
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/ {
|
location /api/ {
|
||||||
limit_except GET { deny all; }
|
limit_except GET { deny all; }
|
||||||
proxy_pass http://mempool_backend;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
app:
|
app:
|
||||||
id: angor-indexer
|
id: angor-indexer
|
||||||
name: Angor Indexer
|
name: Angor Indexer
|
||||||
version: 1.0.1
|
version: 1.0.2
|
||||||
description: Headless Bitcoin indexer endpoint for Angor. Reuses this node’s Mempool
|
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
|
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
|
address as the custom indexer in Angor settings. A relay is optional and installed
|
||||||
separately.
|
separately.
|
||||||
category: money
|
category: money
|
||||||
install_prerequisites:
|
install_prerequisites:
|
||||||
|
- mempool
|
||||||
- mempool-api
|
- mempool-api
|
||||||
upstream:
|
upstream:
|
||||||
kind: github
|
kind: github
|
||||||
repo: block-core/angor
|
repo: block-core/angor
|
||||||
container:
|
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
|
pull_policy: if-not-present
|
||||||
network: archy-net
|
network: archy-net
|
||||||
dependencies:
|
dependencies:
|
||||||
|
- app_id: mempool
|
||||||
|
version: '>=3.0.0'
|
||||||
- app_id: mempool-api
|
- app_id: mempool-api
|
||||||
version: '>=3.0.0'
|
version: '>=3.0.0'
|
||||||
- bitcoin:archival
|
- bitcoin:archival
|
||||||
@@ -40,8 +44,8 @@ app:
|
|||||||
interfaces:
|
interfaces:
|
||||||
main:
|
main:
|
||||||
name: Angor Indexer API
|
name: Angor Indexer API
|
||||||
description: Use this origin as Angor’s custom mainnet indexer URL. HTTPS is
|
description: Use this origin as Angor’s custom mainnet indexer URL, or open it
|
||||||
required for browser clients.
|
to view the existing Mempool explorer. HTTPS is required for browser clients.
|
||||||
type: api
|
type: api
|
||||||
port: 8998
|
port: 8998
|
||||||
protocol: http
|
protocol: http
|
||||||
@@ -63,6 +67,7 @@ app:
|
|||||||
repo: https://github.com/block-core/angor
|
repo: https://github.com/block-core/angor
|
||||||
features:
|
features:
|
||||||
- Angor mainnet API
|
- Angor mainnet API
|
||||||
|
- Mempool explorer on the same origin
|
||||||
- Reuses existing Mempool indexing
|
- Reuses existing Mempool indexing
|
||||||
- No separate blockchain database
|
- No separate blockchain database
|
||||||
- Optional independent relay
|
- Optional independent relay
|
||||||
|
|||||||
@@ -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.
|
`/var/lib/archipelago/angor-relay` and its separate configuration directory.
|
||||||
It never opens, reconfigures or shares the node's internal strfry database.
|
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.
|
The configuration is seeded only when absent, preserving operator changes.
|
||||||
Stop the service before making a consistent backup of its event database.
|
Stop the service before making a consistent backup of its event database.
|
||||||
Ordinary start/restart/recreation preserves both mounts. Use the standard app
|
Ordinary start/restart/recreation preserves both mounts. Use the standard app
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Opt-in disposable rootless Angor gateway integration checks. No native app changes."""
|
"""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
|
import yaml
|
||||||
if os.environ.get('ARCHY_ALLOW_DISPOSABLE_CONTAINERS') != '1':
|
if os.environ.get('ARCHY_ALLOW_DISPOSABLE_CONTAINERS') != '1':
|
||||||
raise SystemExit('Set ARCHY_ALLOW_DISPOSABLE_CONTAINERS=1 to run isolated test containers')
|
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=manifest['health_check']
|
||||||
health_url=health['endpoint'].rstrip('/')+health.get('path','/')
|
health_url=health['endpoint'].rstrip('/')+health.get('path','/')
|
||||||
run_id=uuid.uuid4().hex[:12]
|
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):
|
def run(*a):
|
||||||
r=subprocess.run(a,capture_output=True,text=True)
|
r=subprocess.run(a,capture_output=True,text=True)
|
||||||
if r.returncode:raise RuntimeError(r.stderr)
|
if r.returncode:raise RuntimeError(r.stderr)
|
||||||
return r.stdout.strip()
|
return r.stdout.strip()
|
||||||
def req(path,data=None,method=None,headers={}):
|
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:
|
try:
|
||||||
with urllib.request.urlopen(r,timeout=10) as f:return f.status,f.headers,f.read()
|
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()
|
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('<html>Mempool explorer fixture</html>');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 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):
|
def ready(seconds=40):
|
||||||
end=time.monotonic()+seconds
|
end=time.monotonic()+seconds
|
||||||
@@ -31,8 +34,9 @@ def ready(seconds=40):
|
|||||||
assert subprocess.run(['podman','network','exists',net]).returncode==1
|
assert subprocess.run(['podman','network','exists',net]).returncode==1
|
||||||
run('podman','network','create',net)
|
run('podman','network','create',net)
|
||||||
try:
|
try:
|
||||||
start_backend()
|
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:19098:8080','source.archipelago-foundation.org/chaum/angor-indexer:1.0.1')
|
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()
|
ready()
|
||||||
run('podman','healthcheck','run',gateway)
|
run('podman','healthcheck','run',gateway)
|
||||||
assert json.loads(run('podman','inspect',gateway))[0]['State']['Health']['Status']=='healthy'
|
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',method='OPTIONS')[0]==204
|
||||||
assert req('/api/v1/tx',b'x'*(4*1024*1024+1))[0]==413
|
assert req('/api/v1/tx',b'x'*(4*1024*1024+1))[0]==413
|
||||||
assert req('/unknown')[0]==404
|
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()<end:
|
||||||
|
if req('/')[0]==200:break
|
||||||
|
time.sleep(1)
|
||||||
|
else:raise RuntimeError('Frontend recreation did not recover')
|
||||||
|
print('PASS frontend outage is explicit; API stays available; frontend DNS recreation recovers',flush=True)
|
||||||
d=json.loads(run('podman','inspect',gateway))[0];assert d['Config']['User']=='101:101' and not d['BoundingCaps']
|
d=json.loads(run('podman','inspect',gateway))[0];assert d['Config']['User']=='101:101' and not d['BoundingCaps']
|
||||||
print('PASS API paths/query/body, transaction-only POST, method/size limits, CORS, credential stripping and unprivileged read-only image',flush=True)
|
print('PASS API paths/query/body, transaction-only POST, method/size limits, CORS, credential stripping and unprivileged read-only image',flush=True)
|
||||||
run('podman','stop',backend)
|
run('podman','stop',backend)
|
||||||
@@ -58,5 +85,5 @@ except BaseException:
|
|||||||
subprocess.run(['podman','logs','--tail','15',gateway],check=False)
|
subprocess.run(['podman','logs','--tail','15',gateway],check=False)
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
for name in [gateway,backend]:subprocess.run(['podman','rm','-f','--time','3',name],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
for name in [gateway,backend,frontend]:subprocess.run(['podman','rm','-f','--time','3',name],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
||||||
subprocess.run(['podman','network','rm',net],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
subprocess.run(['podman','network','rm',net],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/* Opt-in, read-only public acceptance. Uses a disposable browser profile; no wallet
|
||||||
|
* actions, transaction broadcast, relay publication, or browser TLS bypass. */
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { createHash } = require('node:crypto');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { createRequire } = require('node:module');
|
||||||
|
const requireUi = createRequire(path.resolve(__dirname, '../../neode-ui/package.json'));
|
||||||
|
const { chromium } = requireUi('playwright');
|
||||||
|
const WebSocket = requireUi('ws');
|
||||||
|
|
||||||
|
const indexer = process.env.ANGOR_TEST_INDEXER;
|
||||||
|
const relay = process.env.ANGOR_TEST_RELAY;
|
||||||
|
const projectId = 'angor1qryhse38vcyqnp0j6976q9f00a9jpj2ary03nlc';
|
||||||
|
const address = 'bc1qryhse38vcyqnp0j6976q9f00a9jpj2ar62vfzd';
|
||||||
|
const fundingTx = '72d14227b78a260c9410a65585cb49e81e35eaa95b1b23ec702eb0512ca016a7';
|
||||||
|
const eventId = 'adbf94d6152097f3d503cd68cc00dee34c1e1007914c00cfc3d6698d1ee01834';
|
||||||
|
const relays = JSON.parse(process.env.ANGOR_TEST_RELAYS || JSON.stringify([relay]));
|
||||||
|
const app = process.env.ANGOR_TEST_APP || 'https://angor.io';
|
||||||
|
assert(indexer && new URL(indexer).protocol === 'https:', 'Set ANGOR_TEST_INDEXER to an HTTPS origin');
|
||||||
|
assert(relay && new URL(relay).protocol === 'wss:', 'Set ANGOR_TEST_RELAY to a WSS origin');
|
||||||
|
assert(relays.some(url => 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; });
|
||||||
@@ -19,6 +19,13 @@ class ServiceMetadata(unittest.TestCase):
|
|||||||
self.assertEqual(app['ports'][0]['bind'], '127.0.0.1')
|
self.assertEqual(app['ports'][0]['bind'], '127.0.0.1')
|
||||||
self.assertEqual(app['security']['capabilities'], [])
|
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):
|
def test_indexer_health_targets_ipv4_listener(self):
|
||||||
app = yaml.safe_load((ROOT / 'apps/angor-indexer/manifest.yml').read_text())['app']
|
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')
|
self.assertEqual(app['health_check']['endpoint'], 'http://127.0.0.1:8080')
|
||||||
|
|||||||
Reference in New Issue
Block a user