diff --git a/.github/scripts/testnet-bens-ui.spec.mjs b/.github/scripts/testnet-bens-ui.spec.mjs index 3da33536616..4a1a6995647 100644 --- a/.github/scripts/testnet-bens-ui.spec.mjs +++ b/.github/scripts/testnet-bens-ui.spec.mjs @@ -82,4 +82,21 @@ test("DOS Name search resolves through the Testnet UI", async ({ page }) => { ).toBeVisible({ timeout: 30_000 }); await expect(page.getByText(smokeName, { exact: true }).first()).toBeVisible(); await expect(page.getByText("Oops! Something went wrong")).toHaveCount(0); + + const tokenLink = page.locator('a[href*="/token/"][href*="/instance/"]').first(); + await expect(tokenLink).toBeVisible(); + const tokenUrl = new URL(await tokenLink.getAttribute("href"), baseUrl); + expect(tokenUrl.origin).toBe(new URL(baseUrl).origin); + expect(tokenUrl.protocol).toBe("https:"); + const [tokenPage] = await Promise.all([ + page.waitForEvent("popup"), + tokenLink.click(), + ]); + await tokenPage.waitForLoadState("domcontentloaded"); + await expect(tokenPage).toHaveURL(tokenUrl.toString()); + const tokenId = tokenUrl.pathname.split("/").at(-1); + await expect(tokenPage.getByRole("heading").filter({ hasText: tokenId })).toBeVisible(); + await expect(tokenPage.getByRole("heading", { name: "Page not found" })).toHaveCount(0); + await expect(tokenPage.getByText("Oops! Something went wrong")).toHaveCount(0); + await tokenPage.close(); }); diff --git a/.github/scripts/tests/test_validate_testnet_bens.py b/.github/scripts/tests/test_validate_testnet_bens.py index 2bc5ead29b2..db08781fab0 100644 --- a/.github/scripts/tests/test_validate_testnet_bens.py +++ b/.github/scripts/tests/test_validate_testnet_bens.py @@ -10,6 +10,7 @@ import threading import unittest from pathlib import Path +from urllib.parse import urljoin ROOT = Path(__file__).resolve().parents[3] @@ -36,6 +37,48 @@ def setUp(self): def test_repository_configuration_is_valid(self): self.assertEqual([], self.module.validate()) + def test_validator_rejects_public_http_routing_for_internal_bens_history(self): + template = json.loads(self.module.BENS_TEMPLATE.read_text(encoding="utf-8")) + template["subgraphs_reader"]["networks"]["3939"]["blockscout"]["url"] = ( + "https://test.doscan.io" + ) + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "config.template.json" + path.write_text(json.dumps(template), encoding="utf-8") + original_template = self.module.BENS_TEMPLATE + self.module.BENS_TEMPLATE = path + try: + self.assertIn( + "BENS transaction history must keep internal backend routing", + self.module.validate(), + ) + finally: + self.module.BENS_TEMPLATE = original_template + + def test_renderer_preserves_internal_history_url_for_official_bens(self): + spec = importlib.util.spec_from_file_location( + "render_testnet_bens", ROOT / "scripts" / "render-testnet-bens.py" + ) + renderer = importlib.util.module_from_spec(spec) + spec.loader.exec_module(renderer) + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / "config.json" + renderer.render_config( + self.module.BENS_TEMPLATE, + output, + { + "contracts": { + "rootRegistry": "0x1111111111111111111111111111111111111111" + } + }, + ) + config = json.loads(output.read_text(encoding="utf-8")) + base = config["subgraphs_reader"]["networks"]["3939"]["blockscout"]["url"] + self.assertEqual( + "http://backend:4000/api/v2/transactions/0x1234", + urljoin(base, "/api/v2/transactions/0x1234"), + ) + def test_ensv2_registry_is_not_misreported_as_an_ensv1_nft(self): template = json.loads( (ROOT / "docker-compose" / "bens" / "config.template.json").read_text( @@ -270,7 +313,7 @@ def test_pin_split_keeps_mainnet_out_of_a_testnet_only_deploy(self): " DOS_NAMES_MAINNET_SUBGRAPH_REF: " "6224395661280a739c20ebd8a420913a0dd7fd6e\n" " DOS_NAMES_TESTNET_SUBGRAPH_REF: " - "3345d09ec2cad8ebbca710b89b630c7ba04481d7" + "2d47fb77bb09dcbdc9d7bfeee77d12ddbebc2e4d" ) self.assertIn(split_pins, workflow) previous_workflow = workflow.replace( @@ -1319,7 +1362,7 @@ def test_frontend_bens_host_has_no_path_and_caddy_routes_bens_api(self): self.assertIn("@bens_api path", caddy) self.assertIn("/api/v1/domains*", caddy) self.assertIn("/api/v1/addresses/*", caddy) - self.assertIn("reverse_proxy bens:8050", caddy) + self.assertIn("reverse_proxy bens-ensv2-adapter:8050", caddy) def test_renderer_rejects_wrong_chain(self): renderer_path = ROOT / "scripts" / "render-testnet-bens.py" diff --git a/.github/workflows/dependency-build.yml b/.github/workflows/dependency-build.yml index 2b3748c8020..0473e87f53b 100644 --- a/.github/workflows/dependency-build.yml +++ b/.github/workflows/dependency-build.yml @@ -53,7 +53,7 @@ env: ELIXIR_VERSION: "1.19.4" MIX_ENV: prod DOS_NAMES_REPOSITORY: https://github.com/DOS/DOS-Names-Contracts.git - DOS_NAMES_SUBGRAPH_REF: 3345d09ec2cad8ebbca710b89b630c7ba04481d7 + DOS_NAMES_SUBGRAPH_REF: 2d47fb77bb09dcbdc9d7bfeee77d12ddbebc2e4d AA_SOURCE_REPOSITORY: https://github.com/eth-infinitism/account-abstraction.git AA_SOURCE_REF: 4cbc06072cdc19fd60f285c5997f4f7f57a588de @@ -105,6 +105,7 @@ jobs: git -C "${checkout}" merge-base --is-ancestor \ "${DOS_NAMES_SUBGRAPH_REF}" refs/remotes/origin/dos git -C "${checkout}" checkout --detach -q "${DOS_NAMES_SUBGRAPH_REF}" + node --test "${checkout}/subgraph/dos-names/adapter/server.test.mjs" cp "${checkout}/contracts/deployments/dos-testnet-3939.json" \ docker-compose/bens/deployment.json node "${checkout}/subgraph/dos-names/scripts/render-manifest.mjs" \ diff --git a/.github/workflows/deploy-config.yml b/.github/workflows/deploy-config.yml index 484f461e252..6f21321a68d 100644 --- a/.github/workflows/deploy-config.yml +++ b/.github/workflows/deploy-config.yml @@ -66,7 +66,7 @@ env: GCP_TESTNET_STAGING_BUCKET: dos-doscan-testnet-staging-535583621422 DOS_NAMES_REPOSITORY: https://github.com/DOS/DOS-Names-Contracts.git DOS_NAMES_MAINNET_SUBGRAPH_REF: 6224395661280a739c20ebd8a420913a0dd7fd6e - DOS_NAMES_TESTNET_SUBGRAPH_REF: 3345d09ec2cad8ebbca710b89b630c7ba04481d7 + DOS_NAMES_TESTNET_SUBGRAPH_REF: 2d47fb77bb09dcbdc9d7bfeee77d12ddbebc2e4d jobs: changes: @@ -896,6 +896,7 @@ jobs: [ "$(git -C "${checkout}" rev-parse HEAD)" = "${DOS_NAMES_TESTNET_SUBGRAPH_REF}" ] test -f "${checkout}/subgraph/dos-names/subgraph.template.yaml" test -f "${checkout}/contracts/deployments/dos-testnet-3939.json" + test -s "${checkout}/subgraph/dos-names/adapter/server.mjs" cp -a "${checkout}/subgraph/dos-names" docker-compose/bens/subgraph cp "${checkout}/contracts/deployments/dos-testnet-3939.json" \ docker-compose/bens/deployment.json @@ -1056,7 +1057,7 @@ jobs: sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ docker compose config --services 2>/dev/null || true )" - for service in bens bens-graph-node bens-ipfs bens-db; do + for service in bens-ensv2-adapter bens bens-graph-node bens-ipfs bens-db; do if grep -qx "${service}" <<<"${current_services}"; then sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ docker compose rm -sf "${service}" || restore_rc=1 @@ -1571,6 +1572,8 @@ jobs: docker compose --profile bens-deploy run --rm bens-deployer sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ docker compose up -d bens + sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ + docker compose up -d --force-recreate bens-ensv2-adapter graph_synced=0 for attempt in $(seq 1 60); do diff --git a/docker-compose/Caddyfile-gcp-testnet b/docker-compose/Caddyfile-gcp-testnet index 3cf9134797c..d7cc2d92508 100644 --- a/docker-compose/Caddyfile-gcp-testnet +++ b/docker-compose/Caddyfile-gcp-testnet @@ -32,7 +32,7 @@ } @bens_api path /api/v1/protocols /api/v1/domains* /api/v1/addresses:lookup /api/v1/addresses/* handle @bens_api { - reverse_proxy bens:8050 + reverse_proxy bens-ensv2-adapter:8050 } handle /api/* { reverse_proxy backend:4000 @@ -73,7 +73,7 @@ reverse_proxy visualizer:8050 } handle_path /name-service/* { - reverse_proxy bens:8050 + reverse_proxy bens-ensv2-adapter:8050 } handle { reverse_proxy frontend:3000 diff --git a/docker-compose/docker-compose-testnet.yml b/docker-compose/docker-compose-testnet.yml index 59f580668dc..039354ae654 100644 --- a/docker-compose/docker-compose-testnet.yml +++ b/docker-compose/docker-compose-testnet.yml @@ -117,6 +117,33 @@ services: volumes: - ./bens/config.json:/app/config/dos-testnet.json:ro + bens-ensv2-adapter: + image: node:22.18.0-alpine3.22@sha256:1b2479dd35a99687d6638f5976fd235e26c5b37e8122f786fcd5fe231d63de5b + platform: linux/amd64 + restart: unless-stopped + init: true + user: "1000:1000" + read_only: true + cap_drop: [ALL] + security_opt: + - no-new-privileges:true + mem_limit: 128m + cpus: 0.5 + depends_on: + bens: + condition: service_started + bens-graph-node: + condition: service_started + volumes: + - ./bens/subgraph/adapter:/app:ro + - ./bens/deployment.json:/app/deployment.json:ro + command: ["node", "/app/server.mjs"] + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8050/health').then(r=>r.json()).then(r=>process.exit(r.status==='SERVING'?0:1)).catch(()=>process.exit(1))"] + interval: 30s + timeout: 12s + retries: 5 + bens-deployer: image: node:22.18.0-alpine3.22@sha256:1b2479dd35a99687d6638f5976fd235e26c5b37e8122f786fcd5fe231d63de5b profiles: ["bens-deploy"] diff --git a/docker-compose/envs/common-blockscout-testnet.env b/docker-compose/envs/common-blockscout-testnet.env index 589df8941ce..7698903d034 100644 --- a/docker-compose/envs/common-blockscout-testnet.env +++ b/docker-compose/envs/common-blockscout-testnet.env @@ -18,7 +18,7 @@ BLOCKSCOUT_HOST=test.doscan.io # DOS Name Service is enabled only on Testnet until the ENSv2 stack is deployed on Mainnet. MICROSERVICE_BENS_ENABLED=true -MICROSERVICE_BENS_URL=http://bens:8050/ +MICROSERVICE_BENS_URL=http://bens-ensv2-adapter:8050/ MICROSERVICE_BENS_PROTOCOLS=dos-names # Keep Testnet media separate from Mainnet media in the shared public bucket. diff --git a/docs/superpowers/plans/2026-08-12-ensv2-bens-testnet.md b/docs/superpowers/plans/2026-08-12-ensv2-bens-testnet.md index c5986c5a8b4..a7a00419d43 100644 --- a/docs/superpowers/plans/2026-08-12-ensv2-bens-testnet.md +++ b/docs/superpowers/plans/2026-08-12-ensv2-bens-testnet.md @@ -18,6 +18,22 @@ - No secret is committed or printed. - DOS-Chain is read-only. +## Testnet Acceptance Follow-up - 2026-10-25 + +- Owner: this DOScan task, branch `codex/testnet-nft-policy-acceptance`. +- Authorized outcome: fix public NFT links and finish policy-registrar claim/renew verification on Testnet only. No Mainnet deployment or configuration change. +- Acceptance: generated NFT URLs use `https://test.doscan.io`; the browser opens the NFT detail page; successful policy claim and paid renewal preserve ownership/token identity and extend expiry; invalid authorization and replay fail; registrar metadata is visible through the deployed subgraph/BENS. +- Scope: DOScan BENS Testnet config, validators, acceptance tests and this record. DOS Names and DOS-Me remain read-only unless their required changes receive explicit repository authority. +- Existing evidence: deployment `39591240503` is live; health, lookup and search pass. Its CI browser test was skipped, not passed. +- Investigation: the pinned DOS Names revision already includes dynamic registrar discovery from `LabelRegistered.sender`. Verify it with real policy events before changing the subgraph. +- Next: pass URL regression tests, verify the live Testnet policy bindings and existing authorized signers, then run bounded Testnet-only acceptance and deploy the reviewed DOScan patch. +- Approved scope extension: JOY approved a minimal read-only metadata adapter in DOS-Names-Contracts and its Testnet runtime integration. It must not hold keys or submit transactions; contracts and official BENS remain unchanged. +- Verified NFT blocker: official BENS derives wrapped token IDs from namehash. The smoke domain's ERC-1155 ID comes from `Domain.tokenId` and differs. The adapter reads canonical subgraph state, preserves domain identity and corrects token metadata. BENS keeps its internal backend URL for transaction history; the adapter publishes the public explorer URL. +- Claim evidence: Testnet policy emitted successful claims for the dedicated `policy-smoke` and `e2epolicy01` fixtures. No renewal event was present at this checkpoint. Do not count fixture contract tests as authenticated DOS.Me user acceptance. +- Source adapter accepted: DOS Names PR #11 merged as `2d47fb77bb09dcbdc9d7bfeee77d12ddbebc2e4d`, with final-head independent review and successful required CI. Adapter tests cover canonical/retired tokens, subdomains, regeneration, invalid state and client-disconnect cancellation. +- Testnet fixture renewal accepted: `0xce800d5b23a7eb1a0cf7318585af76b02e0e93e0b7ce468038ff941732c1094c` succeeded at block 299. `policy-smoke.dos` expiry changed from 1824131273 to 1855667273 (+365 days); token ID and owner were unchanged. Positive payment transfer, paidExpiry and post-renewal nonce replay rejection were verified. BENS history includes the claim and renewal receipts, and its expiry is `2028-10-20T15:07:53Z`. +- Negative policy simulations passed for WrongWallet, InvalidVoucher, VoucherExpired, PriceExceedsVoucher, NonceAlreadyUsed and UnauthorizedRegistrationController. Only existing Testnet fixture keys were used in memory; no keys/signatures were logged or persisted. These checks do not prove DOS.Me authentication/score admission for a real user. + --- ### Task 1: Establish the DOS Names Subgraph Package diff --git a/scripts/validate-testnet-bens.py b/scripts/validate-testnet-bens.py index eb8640b9985..60d493098e9 100644 --- a/scripts/validate-testnet-bens.py +++ b/scripts/validate-testnet-bens.py @@ -181,7 +181,7 @@ def validate() -> list[str]: expected_backend = { "MICROSERVICE_BENS_ENABLED": "true", - "MICROSERVICE_BENS_URL": "http://bens:8050/", + "MICROSERVICE_BENS_URL": "http://bens-ensv2-adapter:8050/", "MICROSERVICE_BENS_PROTOCOLS": "dos-names", } for key, expected in expected_backend.items(): @@ -198,7 +198,7 @@ def validate() -> list[str]: if actual != expected: errors.append(f"{key} must be {expected!r}, got {actual!r}") - if "handle_path /name-service/*" not in caddy or "reverse_proxy bens:8050" not in caddy: + if "handle_path /name-service/*" not in caddy or "reverse_proxy bens-ensv2-adapter:8050" not in caddy: errors.append("Caddy must expose BENS under /name-service without route overlap") protocols = config.get("subgraphs_reader", {}).get("protocols", {}) @@ -216,6 +216,14 @@ def validate() -> list[str]: errors.append("BENS network 3939 must enable only dos-names") if networks.get("3939", {}).get("rpc_url") != CANONICAL_TESTNET_BENS_RPC: errors.append("BENS must use the live Testnet r1 RPC") + if networks.get("3939", {}).get("blockscout", {}).get("url") != "http://backend:4000": + errors.append("BENS transaction history must keep internal backend routing") + if ( + " bens-ensv2-adapter:" not in compose + or "./bens/subgraph/adapter:/app:ro" not in compose + or "./bens/deployment.json:/app/deployment.json:ro" not in compose + ): + errors.append("Testnet must run the immutable DOS Names metadata adapter") ref_match = re.search(r"DOS_NAMES_TESTNET_SUBGRAPH_REF: ([0-9a-f]{40})", workflow) if ref_match is None: