Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/scripts/testnet-bens-ui.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
47 changes: 45 additions & 2 deletions .github/scripts/tests/test_validate_testnet_bens.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import threading
import unittest
from pathlib import Path
from urllib.parse import urljoin


ROOT = Path(__file__).resolve().parents[3]
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/dependency-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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" \
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/deploy-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docker-compose/Caddyfile-gcp-testnet
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions docker-compose/docker-compose-testnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
2 changes: 1 addition & 1 deletion docker-compose/envs/common-blockscout-testnet.env
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 16 additions & 0 deletions docs/superpowers/plans/2026-08-12-ensv2-bens-testnet.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 10 additions & 2 deletions scripts/validate-testnet-bens.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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", {})
Expand All @@ -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:
Expand Down
Loading