From 95aeb9ac55f33211872dc4679f9a1ccc10e9c170 Mon Sep 17 00:00:00 2001 From: Kevin Nzeng Essimengane Date: Sun, 26 Jul 2026 21:22:09 +0200 Subject: [PATCH 01/22] Adapt Dotify for Product DevNet --- .gitignore | 2 + README.md | 28 +- docs/README.md | 2 + docs/backlog/24-access-streaming-v2.md | 8 +- docs/backlog/README.md | 26 +- docs/backlog/improvement-plan.md | 15 +- ...oduct-readiness-and-killer-dapp-roadmap.md | 60 +- docs/design/dotify-v2-access-and-streaming.md | 2 +- docs/explanation/architecture-overview.md | 5 +- .../product-devnet-architecture.md | 209 + docs/index.html | 21 +- docs/operations/deployment-configuration.md | 66 +- docs/operations/product-devnet-deployment.md | 174 + docs/reference/environment-variables.md | 68 +- services/api/.env.example | 2 + services/api/fly.toml | 1 + services/api/package.json | 2 +- services/api/src/app.ts | 5 +- services/api/src/config.ts | 17 +- services/api/src/cors.test.ts | 36 + spec.md | 36 +- web/.env.example | 6 + web/.env.product-devnet | 22 + web/README.md | 18 + web/eslint.config.js | 2 +- web/fly.signal.toml | 1 + web/package-lock.json | 4100 ++++++++++++++++- web/package.json | 4 + web/polkadot-app-deploy.config.ts | 16 + web/product-icon.png | Bin 0 -> 23535 bytes web/product-icon.svg | 13 + web/src/app/providers/SessionProvider.tsx | 12 +- web/src/app/providers/WalletProvider.tsx | 32 +- web/src/components/WalletModal.tsx | 77 +- .../features/productHost/productHost.test.ts | 53 + web/src/features/productHost/productHost.ts | 76 + web/src/features/rooms/roomState.test.ts | 4 + web/src/hooks/useArtistConsole.ts | 3 + web/src/hooks/useCatalog.ts | 4 +- web/src/hooks/useSession.ts | 21 +- web/src/hooks/useWallet.ts | 59 +- .../shared/config/deploymentSafety.test.ts | 28 + web/src/shared/config/deploymentSafety.ts | 14 + web/vite.product.config.ts | 31 + 44 files changed, 5002 insertions(+), 379 deletions(-) create mode 100644 docs/explanation/product-devnet-architecture.md create mode 100644 docs/operations/product-devnet-deployment.md create mode 100644 services/api/src/cors.test.ts create mode 100644 web/.env.product-devnet create mode 100644 web/polkadot-app-deploy.config.ts create mode 100644 web/product-icon.png create mode 100644 web/product-icon.svg create mode 100644 web/src/features/productHost/productHost.test.ts create mode 100644 web/src/features/productHost/productHost.ts create mode 100644 web/vite.product.config.ts diff --git a/.gitignore b/.gitignore index 3214ad8..1d6542e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,11 +8,13 @@ dist/ .playwright* !.env.example !web/.env.example +!web/.env.product-devnet !contracts/evm/.env.example web/node_modules/ web/dist/ web/dist-bulletin* +web/dist-product/ web/.playwright* web/playwright-report/ web/test-results/ diff --git a/README.md b/README.md index eed1f79..a4b2fe2 100644 --- a/README.md +++ b/README.md @@ -34,20 +34,23 @@ aura lights the whole field (`web/src/styles/aura.css`). creates one personal `SmartRuntime` per artist, and `ArtistDirectory` indexes artist addresses to their runtimes. -**Frontend**: Static React + Vite web app deployed to dot.li. +**Frontend**: Static React + Vite web app deployed to Netlify and, through the +Product profile, Bulletin/DotNS at `dotify.dot`. **WebRTC**: real-time music streaming. **Socket.IO**: signaling for room discovery and SDP/ICE exchange. A future iteration can move signaling to statement-store style infrastructure. -**Product SDK direction**: Dotify remains a standalone web app first. Product -SDK / Playground / Humanity work is a progressive-enhancement track documented -in -[`docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md`](docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md). -The current SDK snapshot is prototype/reference/unaudited and must be proven -against Dotify's Host, key-delivery, room, and contract constraints before it -becomes a production dependency. +**Product SDK direction**: Dotify now has an adaptive Product DevNet build for +`dotify.dot`. It keeps standalone link-first rooms and Free listening intact, +adds explicit app-scoped Product identity, and publishes through +Bulletin/DotNS. Product host signing is not yet accepted for contract writes or +protected key delivery; those boundaries remain passkey/EVM until the CDM/PAPI +and backend signature adapters are proven. See +[`docs/explanation/product-devnet-architecture.md`](docs/explanation/product-devnet-architecture.md) +and the +[`Product roadmap`](docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md). ## Deployed @@ -118,7 +121,8 @@ npm run dev | Variable | Required | Purpose | | --------------------------- | ---------------- | -------------------------------------------------------- | -| `API_ORIGIN` | Production | Frontend origin allowed by API CORS | +| `API_ORIGIN` | Compatibility | Singular frontend CORS origin fallback | +| `API_ORIGINS` | Production | Comma-separated exact frontend CORS origins | | `PASEO_ASSET_HUB_RPC` | Key requests | Paseo Asset Hub EVM RPC used for access checks | | `DOTIFY_DIRECTORY_ADDRESS` | Key requests | ArtistDirectory address used to resolve artist runtimes | | `DOTIFY_CHAIN_ID` | Key requests | Chain ID expected in wallet-signed key requests | @@ -430,9 +434,9 @@ handle: and decide whether a backend read-through gateway is needed. 4. Keep demo-mode browser-exposed Pinata/content secrets out of public deployments. -5. Run Product SDK feasibility spikes: Host detection, Product account signing, - resource allocation, Playground/Bulletin/DotNS deployment, and PolkaVM/CDM - contract portability. +5. Validate the Product host/account and Bulletin/DotNS deployment baseline, + then implement Product signature verification, resource allocation, and + PolkaVM/CDM contract portability. 6. Add a production artist dashboard on `/artists`: release drafts, edit metadata, royalty analytics, and profile verification state. 7. Deploy and monitor a public signaling server for DotNS / Bulletin builds. diff --git a/docs/README.md b/docs/README.md index 9b47fa3..f1fa94d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,6 +15,7 @@ Conceptual documents that help you understand why Dotify works the way it does. | [Content Protection](./explanation/content-protection.md) | All | Audio encryption pipeline, what it protects, and what it does not | | [Royalty Settlement](./explanation/royalty-settlement.md) | All | How DOT payments flow from listener wallet to artist wallet | | [Listening Rooms](./explanation/listening-rooms.md) | All | WebRTC peer-to-peer streaming, signaling protocol, known limitations | +| [Product DevNet Architecture](./explanation/product-devnet-architecture.md) | Maintainers | Dual-host boundaries, Product account capabilities, rooms, storage, and the proposed contract port | --- @@ -40,6 +41,7 @@ Runbooks for hosted configuration and production validation. | Document | Summary | |---|---| | [Deployment Configuration](./operations/deployment-configuration.md) | Netlify and Fly dashboard settings, secrets, catalog persistence, validation, and the update checklist for future env/config changes | +| [Product DevNet Deployment](./operations/product-devnet-deployment.md) | Build, publish, validate, and roll back the `dotify.dot` Product DevNet app | --- diff --git a/docs/backlog/24-access-streaming-v2.md b/docs/backlog/24-access-streaming-v2.md index 556fccd..1267a60 100644 --- a/docs/backlog/24-access-streaming-v2.md +++ b/docs/backlog/24-access-streaming-v2.md @@ -139,12 +139,14 @@ P3 first vertical slice delivered (`agent/audio-v2-p3`): the browser/device validation matrix, startup telemetry export, and the backend read-through gateway decision. -Product SDK replanning note (2026-07-14): +Product SDK adaptation note (2026-07-26): -- Product SDK (`@parity/product-sdk` 0.17.0 at - `2f359bba28ca72855207a0a519d4118b37b4438c`) is prototype/reference/unaudited. +- Product SDK 0.19.1 and deploy tooling 0.13.1 remain + prototype/reference/unaudited. - Host APIs are progressive enhancement for Product containers; standalone web remains a supported mode. +- Host detection, explicit Product account identity, a Product DevNet build, + canonical room links, and dual-origin Fly configuration are implemented. - Product SDK contracts use `pallet-revive`, PolkaVM artifacts, and CDM manifests. Dotify's current Hardhat + viem + Paseo Asset Hub EVM path needs a portability spike before adopting that layer. diff --git a/docs/backlog/README.md b/docs/backlog/README.md index 5afea40..8dec0ef 100644 --- a/docs/backlog/README.md +++ b/docs/backlog/README.md @@ -104,18 +104,18 @@ ticket 18 preview assets are consciously retired by access model v2. `improvement-plan.md` tracks the July 2026 review of the implementation against the product/technical/philosophical memory and the current Parity Product SDK direction. The plan is now dual-mode: standalone web remains the -first public listening path, while Product SDK / Playground / Humanity -integration is a gated feasibility track. Nothing in that track may imply live -Host, Statement Store, Product account, Humanity, or `.dot` deployment support -until the relevant spike proves the current API, environment, and security -boundary. +first public listening path, while the Product DevNet build adds `dotify.dot`, +explicit Host detection, app-scoped Product identity, and canonical +Product-origin room links. It does not imply Product-signed contract writes, +protected key access, Statement Store rooms, or Humanity decisions; those +remain gated until their adapters prove the current API and security boundary. The Product SDK evidence snapshot used for this replanning is -`paritytech/product-sdk@2f359bba28ca72855207a0a519d4118b37b4438c` -(`@parity/product-sdk` 0.17.0), fetched on 2026-07-14. It is explicitly -prototype / reference / unaudited code. Paseo and Summit are the live preset -environments; Product SDK contracts target `pallet-revive` / PolkaVM CDM flows, -not Dotify's current viem + EVM RPC path; Statement Store is useful for small +`@parity/product-sdk` 0.19.1 and +`@polkadot-community-foundation/polkadot-app-deploy` 0.13.1, +verified on 2026-07-26. They remain prototype / reference / unaudited code. +Product SDK contracts target `pallet-revive` / PolkaVM CDM flows, not Dotify's +current viem + EVM RPC write path; Statement Store is useful for small ephemeral presence, not full chat, SDP/ICE, durable media metadata, or guest reactions. @@ -160,9 +160,9 @@ on `main`. The remaining order is: signaling and production-env evidence are closed through #36/#37. 3. Improve room resilience and shared-listening depth only where it preserves the link-first guest doctrine. -4. Run Product SDK feasibility spikes: Host capability detection, Product - account signing, resource allocation, PolkaVM/CDM contract portability, - Playground/Bulletin/DotNS deployment, and Statement Store presence. +4. Validate the delivered Product host/account and Bulletin/DotNS baseline, + then port writes through CDM/PAPI, add backend Product-signature + verification, and run bounded resource-allocation/Statement Store spikes. 5. Build live Humanity / Individuality only after the research ticket proves a privacy-preserving source, proof shape, address-binding story, and fallback UX. diff --git a/docs/backlog/improvement-plan.md b/docs/backlog/improvement-plan.md index 49937b0..23fd81f 100644 --- a/docs/backlog/improvement-plan.md +++ b/docs/backlog/improvement-plan.md @@ -62,25 +62,24 @@ Where it falls short of its own standards: ## Product SDK feasibility track -This track runs in parallel with standalone hardening, but it does not block -first sound and must not be sold as a delivered capability. +This track runs in parallel with standalone hardening and does not block first +sound. Only the baseline rows marked delivered may be presented as live. Product SDK snapshot used for this plan: -- `paritytech/product-sdk@2f359bba28ca72855207a0a519d4118b37b4438c` - (fetched 2026-07-14); -- `@parity/product-sdk` 0.17.0; +- `@parity/product-sdk` 0.19.1 (verified 2026-07-26); +- `@polkadot-community-foundation/polkadot-app-deploy` 0.13.1; - explicit prototype / reference / unaudited status; -- live preset environments: Paseo and Summit; +- Product target: DevNet Asset Hub / People / Bulletin; - contracts package: `pallet-revive`, PolkaVM artifacts, and CDM manifests; - Statement Store: 512-byte statement payload, 1024-byte user total, default 30-second TTL. | Item | Tracking | Status | | --- | --- | --- | -| Product SDK baseline: pin SDK versions, document compatible Host surfaces, and add feature detection for Host local storage, signing, permissions, resource allocation, payments, and chain support. | #85, `polkadot-product-readiness-and-killer-dapp-roadmap.md` | Proposed | +| Product SDK baseline: pin SDK versions, detect Host availability, connect an app-scoped account explicitly, and separate presence identity from EVM signing authority. | #85, `polkadot-product-readiness-and-killer-dapp-roadmap.md` | Delivered on Product adaptation branch | | Contract portability spike: compare Dotify's current Paseo Asset Hub EVM / viem / Hardhat flow with Product SDK contracts on `pallet-revive`, PolkaVM artifacts, and CDM manifests. | #85 | Proposed | -| Playground deployment spike: determine whether Dotify's static build can use Playground/Bulletin/DotNS deploy flows without weakening current secret and publication boundaries. | #85 | Proposed | +| Product deployment baseline: build a browser-safe multi-file bundle, publish through Bulletin/DotNS tooling, preserve backend key custody, and use a canonical public room URL. | #85 | Delivered on Product adaptation branch; live publication pending operator credentials | | Statement Store presence spike: use it for small, signed, ephemeral discovery/presence only. Do not move SDP/ICE, full chat history, media metadata, or link-only guest reactions there until signer, TTL, and size constraints are solved. | #89, `20-room-social-layer.md`, `21-room-collaborative-queue.md` | Proposed | | Humanity / Individuality research rewrite: prove the canonical live source, privacy-preserving proof shape, product-account/identity-account binding, and fallback UX before promoting Human free from research to build. | #12, `11-proof-of-personhood-integration-research.md` | Open | diff --git a/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md b/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md index d6d497f..1a8daab 100644 --- a/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md +++ b/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md @@ -1,15 +1,16 @@ # Polkadot product readiness and killer dapp roadmap -Status: active planning note, supersedes the stale draft from PR #91. +Status: active execution note; the Product DevNet baseline is implemented on +`feat/product-devnet-adaptation`. -Last Product SDK verification: 2026-07-14 against -`paritytech/product-sdk@2f359bba28ca72855207a0a519d4118b37b4438c` -(`@parity/product-sdk` 0.17.0). +Last Product SDK verification: 2026-07-26 against +`@parity/product-sdk` 0.19.1 and +`@polkadot-community-foundation/polkadot-app-deploy` 0.13.1. ## Verdict -Dotify should align with the Polkadot product ecosystem, but it should not -replace its standalone production path with Product SDK assumptions yet. +Dotify should align with the Polkadot product ecosystem without replacing its +standalone production path with Product SDK assumptions. The right product shape is dual-mode: @@ -23,6 +24,18 @@ The right product shape is dual-mode: product failure state. It must not fall back to demo secrets, hidden signers, or bypassed access checks. +The first adaptive slice is now implemented: + +- a separate Product DevNet build and `dotify.dot` manifest; +- explicit Host detection and app-scoped Product account connection; +- Product identity for room presence without claiming EVM/EIP-191 authority; +- canonical `.dev-dot.li` room links; +- shared Fly API/signaling allowlists for Netlify and Product origins; +- a pinned build/deploy workflow and operator rollback guide. + +Contract writes, Product-signed key requests, Product personhood, and Product +presence transport remain gated follow-up work. + ## Product ecosystem evidence The current Parity product direction is coherent: Levity for publishing, @@ -35,9 +48,8 @@ The SDK details matter for Dotify: - Product SDK and Playground are explicitly prototype / reference / unaudited code. -- Product SDK preset chains are live for Paseo and Summit. Polkadot and Kusama - preset paths are gated because Bulletin / Individuality descriptors are not - live there. +- Product DevNet exposes the Asset Hub, People, and Bulletin system-chain + topology used by the current Product tooling. - Product SDK contract helpers target `pallet-revive`, PolkaVM artifacts, and CDM manifests. Dotify currently uses Hardhat Solidity, generated EVM ABIs, viem, and Paseo Asset Hub EVM RPCs. @@ -131,24 +143,27 @@ Goal: deepen rooms without breaking the room-guest doctrine. Goal: prove the Product host path with small spikes before committing the app. -- Pin Product SDK versions and add a compatibility matrix. -- Detect Host availability and supported chain/capability surfaces. -- Prototype Product account connection, signing, identity prompt behavior, and - resource allocation. +- Delivered: pin Product SDK/deploy versions and add a compatibility matrix. +- Delivered: detect Host availability without blocking standalone first sound. +- Delivered: connect the app-scoped Product account only on explicit action and + separate identity capability from EVM signing capability. +- Delivered: publishable Bulletin/DotNS build and dual-origin Fly boundary. +- Remaining: prototype host transaction signing and resource allocation. - Compare Dotify's Hardhat/EVM runtime with Product SDK PolkaVM/CDM contracts. -- Prototype Playground deployment against Dotify's single-file build and secret - boundary. - Prototype Statement Store presence with strict payload, TTL, and signer limits. ### Phase 4 - Product integration -Goal: ship Product mode as progressive enhancement. +Goal: deepen the delivered Product mode one adapter at a time. -- Add Product-mode adapters behind explicit ports, leaving standalone adapters - intact. -- Use Host signing and Product accounts only when the Host path is available. -- Surface Host permission denial as actionable UI state. +- Delivered: keep standalone adapters intact and lazy-load Product host code. +- Delivered: use the Product account as presence identity only when available. +- Delivered: surface host absence and unsupported signer boundaries explicitly. +- Next: extract typed runtime read/write ports and add a generated CDM/PAPI + adapter. +- Next: add a backend signature-scheme adapter that verifies Product account + signatures and address binding before key delivery. - Keep backend key delivery authoritative unless a Product-host design proves a stronger key-custody boundary. - Keep `.dot`/Playground deployment separate from access enforcement. @@ -219,8 +234,9 @@ Recommended Project 5 fields: - #36: closed after hosted signaling operation evidence. - #37: closed after #99 and manually checked deploy-host production env evidence. -- #85: split into Product SDK baseline, contract portability, Playground deploy, - Statement Store presence, and integration adapter spikes. +- #85: Product SDK baseline and Product DevNet deployment slice implemented; + keep open for contract portability, backend Product signatures, resource + allocation, and bounded Statement Store presence. - #86: implementation active on `codex/86-catalog-read-model`; keep In Progress until review and public performance evidence close the warm/cold budgets. - #87: keep for responsive cover/gateway pipeline. diff --git a/docs/design/dotify-v2-access-and-streaming.md b/docs/design/dotify-v2-access-and-streaming.md index 19bb0ab..8927c7c 100644 --- a/docs/design/dotify-v2-access-and-streaming.md +++ b/docs/design/dotify-v2-access-and-streaming.md @@ -390,7 +390,7 @@ Dotify mapping: | Product SDK / Host API | Replace bespoke chain, signing, storage, and permission glue only where the SDK gives equivalent or better behavior. | | Proof of Personhood | Replace the current admin/personhood mock with the live verified-human source for `human-free`. | | Coinage | Candidate future payment rail for paid access; EVM runtime remains the settlement record until Coinage design is explicit. | -| DotNS | Keep `dotify.dot.li` / `.dot` resolution aligned with the Bulletin single-file build. | +| DotNS | Publish the Product profile as `dotify.dot` / `https://dotify.dev-dot.li`; keep the legacy single-file path separate. | | Bulletin Chain | Continue as a publication and availability layer for product bundles and manifests. | | Statement Store | Future presence/chat/room-discovery layer; Socket.IO remains SDP/ICE relay until a separate migration is designed. | diff --git a/docs/explanation/architecture-overview.md b/docs/explanation/architecture-overview.md index 13d95e2..7816fe4 100644 --- a/docs/explanation/architecture-overview.md +++ b/docs/explanation/architecture-overview.md @@ -174,4 +174,7 @@ The signaling server is a lightweight Socket.IO process (`server/signaling.mjs`) - Your payments — they go directly to your EVM address via smart contract. - Your track records — they live on Paseo Asset Hub (and optionally Bulletin Chain). -The frontend is itself distributed via IPFS/DotNS at `dotify.dot.li`. +The standalone frontend is deployed through Netlify. The Product DevNet build +is publishable through Bulletin/DotNS as `dotify.dot` and resolves publicly at +`https://dotify.dev-dot.li`; the older `dotify.dot.li` artifact remains legacy +deployment evidence. diff --git a/docs/explanation/product-devnet-architecture.md b/docs/explanation/product-devnet-architecture.md new file mode 100644 index 0000000..b2c09fc --- /dev/null +++ b/docs/explanation/product-devnet-architecture.md @@ -0,0 +1,209 @@ +# Product DevNet Architecture + +## Decision + +Dotify uses an adaptive dual-host architecture: + +- the standalone Netlify app remains a complete public entry point; +- the Product DevNet build publishes the same listener and room experience as + `dotify.dot`; +- Product-host capabilities are added through explicit adapters; +- a missing or denied host capability never enables a demo secret, hidden + signer, or weaker access path. + +This keeps Dotify's north star intact. A guest can still follow a room link and +hear a host without first adopting wallet infrastructure. An artist's access +policy and protected source remain authoritative regardless of which frontend +host serves the app. + +## Why The Host Is An Adapter + +The Product environment and Dotify's existing runtime do not expose the same +signing contract. + +The Product SDK returns an app-scoped account and a PAPI `PolkadotSigner`. +Dotify's deployed contract writes and content-key requests currently use viem, +EIP-1193, and EIP-191. Treating those signers as interchangeable would either +fail at runtime or create an unverifiable access claim. + +The first Product adaptation therefore uses the host account for: + +- an explicit, user-initiated Product account connection; +- an SS58 account for display and future Product-native adapters; +- a derived H160 address for local room-name persistence and read-only + runtime/catalog correlation. + +It does not use that account for: + +- Classic payments; +- artist runtime creation or release publication; +- protected content-key requests; +- Bulletin artist publication through the existing PAPI v1 integration. + +Those actions continue to require the existing passkey or EVM wallet until the +chain and backend adapters described below are delivered. + +## Runtime Topology + +```text +Standalone browser Product host +https://muzinga.netlify.app https://dotify.dev-dot.li + | | + +---------------+----------------------+ + | + same Dotify frontend + | + +-----------+-----------+ + | | + dotify-api.fly.dev dotify-signal.fly.dev + catalog, uploads, room discovery, SDP/ICE, + access, content keys chat and presence + | | + +-----------+-----------+ + | + Product DevNet Asset Hub + existing Dotify runtimes +``` + +The Product build is a normal relative-path Vite bundle. `pad` publishes its +files to Bulletin and binds the result to DotNS. Keeping multiple static chunks +allows incremental uploads; the older single-file Bulletin build remains +available for its original workflow. + +The two frontend origins share the same Fly services. `API_ORIGINS` and +`SIGNAL_ORIGINS` are explicit comma-separated allowlists. This is required for +cross-origin catalog reads, key requests, Socket.IO, and WebRTC signaling. + +## Capability Matrix + +| Capability | Standalone | Product build now | Product-native target | +| --- | --- | --- | --- | +| Browse catalog | Fly cache + EVM RPC | Same | Host-routed read adapter where it improves reliability | +| Play Free track | No wallet | No wallet | Same | +| Join room link | No wallet | No wallet | Same | +| Host room | Socket.IO + WebRTC | Same | Keep until a multiparty replacement proves equivalent UX | +| Product identity | Not applicable | App-scoped SS58/H160 | Host identity with explicit capability grants | +| Classic payment | Passkey/EVM wallet | Passkey/EVM wallet | CDM/PAPI write adapter | +| Protected key request | EIP-191 | EIP-191 | Backend-verified Product signature scheme | +| Artist publication | viem/EVM | viem/EVM | Generated CDM contract adapter | +| Personhood | Current on-chain policy source | No new claim | Privacy-preserving Product proof after verification | +| Static delivery | Netlify | Bulletin + DotNS | Bulletin + DotNS | + +## Rooms Stay Host-Neutral + +Rooms are a product primitive, not a deployment detail. The current signaling +service supports anonymous discovery, one host with multiple listeners, +short-lived chat/reactions/requests, and WebRTC negotiation. Product messaging +and Statement Store do not currently provide a verified drop-in replacement +for that wallet-free multiparty flow. + +The Product build therefore keeps the Socket.IO/WebRTC room layer. It adds one +important boundary: `VITE_PUBLIC_APP_URL` makes every copied room link point to +the public `.dev-dot.li` origin rather than an internal container or content +gateway URL. + +A future Product-native presence spike may mirror a compact host-signed +heartbeat into Statement Store. It must not carry SDP, ICE candidates, audio, +durable chat, or source keys, and it must remain optional for guests. + +## Storage Boundaries + +Product static hosting replaces the web server for the Product build. It does +not replace: + +- Pinata-backed artist uploads; +- DAV2 audio encryption; +- backend-held `CONTENT_KEY_MASTER_SECRET`; +- server-side access verification; +- the durable catalog snapshot. + +Product cloud storage is host-scoped and experimental. Moving encrypted media +or key custody there requires a separate threat model, Range/startup evidence, +and a recovery plan. Until then, Fly remains the security boundary and IPFS +gateways remain the delivery boundary. + +## Proposed Contract Port + +The next contract phase should split the current integration into two typed +ports rather than add Product conditionals throughout feature hooks: + +```text +RuntimeReadPort + listArtists() + listReleases() + getAccessDecision() + getRoyaltyState() + +RuntimeWritePort + createArtistRuntime() + publishRelease() + setAccessMode() + payForAccess() +``` + +Adapters: + +- `ViemRuntimeAdapter`: current standalone EVM implementation; +- `ProductRuntimeAdapter`: generated CDM/ABI bindings submitted with the host + PAPI signer; +- `CatalogApiAdapter`: the existing server-side read model, shared by both + frontends. + +The backend authentication protocol must then gain an explicit signature +scheme field. A Product signature is accepted only after the server can bind +the signed payload, Product account public key, derived H160, chain, nonce, +purpose, and expiry. EIP-191 remains supported for standalone clients. Unknown +schemes fail closed. + +This avoids a second frontend business model and allows Product mode to replace +one infrastructure adapter at a time. + +## Permission And Failure Rules + +1. Host detection may run on startup; account access only runs after the user + chooses **Use Polkadot app**. +2. The integration does not request a username, identity proof, transaction + permission, or personhood proof before value is visible. +3. If the host is absent, catalog browsing, Free playback, and room links still + work. The wallet modal explains why the Product account is unavailable. +4. A Product account without an EVM signing adapter is not a protected + listener. Dotify passes no requester address to the key service. +5. A denied key, RPC failure, or unsupported signature never falls back to a + browser content secret. +6. The Product SDK and deploy tooling are prototype/reference dependencies. + Version changes require the compatibility checks below. + +## Compatibility Gate + +The initial baseline is: + +| Component | Pinned/target value | +| --- | --- | +| Node | 22 | +| `@parity/product-sdk` | `0.19.1` | +| `@polkadot-community-foundation/polkadot-app-deploy` | `0.13.1` in the deploy command | +| Product network | `devnet` | +| Product domain | `dotify.dot` | +| Public gateway | `https://dotify.dev-dot.li` | +| Asset Hub EVM chain ID | `420420417` | + +For every SDK or deploy-tool upgrade: + +1. verify host detection outside and inside the container; +2. connect the Product account only on explicit action; +3. verify SS58 and derived H160 stability; +4. run normal and Product builds; +5. join one room across Netlify and Product origins; +6. verify Free playback remains walletless; +7. verify protected actions still fail closed without a supported signer; +8. inspect the static bundle and npm audit delta; +9. update this document, the environment reference, and the deployment runbook. + +## Source References + +- [Product documentation](https://docs.polkadotcommunity.foundation/) +- [Build and publish guide](https://docs.polkadotcommunity.foundation/guides/build-and-publish/) +- [Platform Services SDK guide](https://docs.polkadotcommunity.foundation/guides/platform-services-sdk/) +- [Product network reference](https://docs.polkadotcommunity.foundation/reference/networks/) +- [Product identity architecture](https://docs.polkadotcommunity.foundation/architecture/identity/) +- [Product messaging architecture](https://docs.polkadotcommunity.foundation/architecture/messaging/) diff --git a/docs/index.html b/docs/index.html index 3b9ae43..ef25722 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1537,11 +1537,13 @@

Operate the spine and validate first sound

  • -

    Sequenced later

    -

    Product SDK, personhood, and cultural transmission

    +

    Adaptive Product path

    +

    Product DevNet now, sovereignty adapters next

    - Product SDK mode, Humanity/Individuality proofs, consented provenance, and ambassador - mechanics remain future work until the current APIs and privacy boundaries are proven. + Dotify now has a publishable dotify.dot build, explicit app-scoped Product + identity, and public room links that preserve wallet-free entry. CDM/PAPI contract writes, + Product-signed key requests, Humanity/Individuality proofs, consented provenance, and + ambassador mechanics remain sequenced behind verified security and privacy boundaries.

  • @@ -1601,11 +1603,20 @@

    Explore the project

    +
  • + + + Product DevNet architecture + What the Product host adapts now, what remains on Fly, and how contract signing ports next. + + + +
  • Product SDK roadmap - How Dotify aligns with Product SDK, Playground, Statement Store, and Humanity. + The delivered Product baseline and the remaining CDM, signature, presence, and Humanity work. diff --git a/docs/operations/deployment-configuration.md b/docs/operations/deployment-configuration.md index a6c239f..e825ebb 100644 --- a/docs/operations/deployment-configuration.md +++ b/docs/operations/deployment-configuration.md @@ -1,7 +1,7 @@ # Deployment Configuration Runbook This runbook is the operator checklist for Dotify's hosted configuration across -Netlify and Fly.io. Use it when changing dashboard values, deploy contexts, +Netlify, Product DevNet, and Fly.io. Use it when changing dashboard values, deploy contexts, `*.toml` settings, hosted origins, secrets, catalog persistence, or production smoke settings. @@ -30,16 +30,18 @@ Keep this document aligned with | Surface | Host | App/project | Source config | Purpose | | --- | --- | --- | --- | --- | | Frontend | Netlify | `muzinga` | `netlify.toml` | Static Vite web app | +| Product frontend | Bulletin + DotNS | `dotify.dot` | `web/.env.product-devnet`, `web/polkadot-app-deploy.config.ts` | Product-host static app | | Backend API | Fly.io | `dotify-api` | `services/api/fly.toml` | Uploads, key delivery, catalog read model, health | | Signaling | Fly.io | `dotify-signal` | `web/fly.signal.toml` | Socket.IO room discovery and WebRTC signaling | Production URLs currently assumed by the app and docs: ```txt -Frontend: https:// +Standalone: https://muzinga.netlify.app +Product: https://dotify.dev-dot.li Backend API: https://dotify-api.fly.dev Signaling: https://dotify-signal.fly.dev -IPFS gateway: https://paseo-ipfs.polkadot.io +Product IPFS: https://devnet-ipfs.api.polkadotcommunity.foundation Asset Hub RPC: https://eth-rpc-testnet.polkadot.io/ ``` @@ -94,6 +96,7 @@ Required production variables: | Key | Value | Notes | | --- | --- | --- | | `VITE_DOTIFY_DEPLOYMENT` | `production` | Enables fail-closed production env validation. | +| `VITE_DOTIFY_HOST_MODE` | `off` | Prevents the standalone build from probing Product host APIs. | | `VITE_SIGNAL_URL` | `https://dotify-signal.fly.dev` | Public Socket.IO signaling origin. | | `VITE_DOTIFY_API_URL` | `https://dotify-api.fly.dev` | Backend API for uploads, key delivery, and cached catalog reads. | | `VITE_PINATA_GATEWAY` | `https://paseo-ipfs.polkadot.io` | Primary browser read gateway. | @@ -115,10 +118,42 @@ Optional production variables: Deploy-preview note: Netlify deploy previews usually have their own origin. The signaling service -can allow multiple origins with `SIGNAL_ORIGINS`, but the backend API currently -accepts one `API_ORIGIN`. For PR evidence, use a stable frontend origin, a -dedicated staging site, or temporarily set `API_ORIGIN` to the deploy-preview -origin and restore it after validation. +and backend both allow multiple exact origins with `SIGNAL_ORIGINS` and +`API_ORIGINS`. Add only the specific preview origin needed for evidence, then +remove it after validation. Never use `*` on the backend. + +## Product DevNet Frontend + +The browser-safe Product build profile is tracked in +`web/.env.product-devnet`. The manifest is +`web/polkadot-app-deploy.config.ts`. + +Required Product values: + +| Key | Current value | +| --- | --- | +| `VITE_DOTIFY_DEPLOYMENT` | `production` | +| `VITE_DOTIFY_HOST_MODE` | `required` | +| `VITE_DOTIFY_PRODUCT_ID` | `dotify.dot` | +| `VITE_PUBLIC_APP_URL` | `https://dotify.dev-dot.li` | +| `VITE_DOTIFY_API_URL` | `https://dotify-api.fly.dev` | +| `VITE_SIGNAL_URL` | `https://dotify-signal.fly.dev` | + +`VITE_PINATA_JWT` and `VITE_CONTENT_SECRET` are explicitly empty in that +profile so a developer's generic local `.env` cannot leak demo credentials +into the Product bundle. + +Build and publication: + +```bash +cd web +npm run build:product-devnet +npm run deploy:product-devnet +``` + +Use +[`docs/operations/product-devnet-deployment.md`](product-devnet-deployment.md) +for authentication, publication, validation, and rollback. ## Fly Backend API @@ -136,6 +171,7 @@ Non-secret runtime values are tracked in `services/api/fly.toml`: | --- | --- | | `API_PORT` | `8790` | | `NODE_ENV` | `production` | +| `API_ORIGINS` | `https://muzinga.netlify.app,https://dotify.dev-dot.li` | | `PASEO_ASSET_HUB_RPC` | `https://eth-rpc-testnet.polkadot.io/` | | `DOTIFY_FACTORY_ADDRESS` | `0xbd1a11cfce8b5ef7a37e507bc5109895f8f42a72` | | `DOTIFY_DIRECTORY_ADDRESS` | `0xcf1534c6e2b0e43b9436c1e86a076466dc0f2108` | @@ -145,7 +181,6 @@ Set server-side values in the app's Secrets area: | Secret | Required | Notes | | --- | --- | --- | -| `API_ORIGIN` | Production | Exact frontend origin allowed by API CORS. One URL only. | | `PINATA_JWT` | Uploads | Backend-only Pinata token. Never expose in Netlify. | | `CONTENT_KEY_MASTER_SECRET` | Audio upload and key delivery | 64+ hex chars, at least 32 random bytes. Do not rotate casually. | | `GIT_COMMIT_SHA` | Optional | Set by CI/build automation when available; `/version` can fall back in dev checkouts. | @@ -188,12 +223,12 @@ Non-secret runtime values are tracked in `web/fly.signal.toml`: | `SIGNAL_ROOM_TTL_MS` | `21600000` | | `SIGNAL_HOST_TIMEOUT_MS` | `120000` | | `SIGNAL_MAX_LISTENERS` | `24` | +| `SIGNAL_ORIGINS` | `https://muzinga.netlify.app,https://dotify.dev-dot.li` | -Set hosted frontend origins in the app's Secrets area: - -| Secret | Value | -| --- | --- | -| `SIGNAL_ORIGINS` | Exact comma-separated frontend origins, for example `https://muzinga.netlify.app,https://` | +The production origins are public configuration tracked in +`web/fly.signal.toml`; they are not secrets. Temporary preview origins may be +set through Fly configuration, but the tracked production allowlist must be +restored after validation. Keep `dotify-signal` on one active machine until a shared Socket.IO adapter is added. Rooms, chat, reactions, request queues, and solo-presence aggregates are @@ -227,8 +262,13 @@ curl -s https://dotify-signal.fly.dev/status cd web npm run smoke:production-env npm run smoke:signal -- --url https://dotify-signal.fly.dev --origin https:// +npm run build:product-devnet ``` +6. For a Product release, complete the cross-origin room and host-account +checks in +[`docs/operations/product-devnet-deployment.md`](product-devnet-deployment.md). + 6. For explicit origin rejection evidence, include a denied origin: ```bash diff --git a/docs/operations/product-devnet-deployment.md b/docs/operations/product-devnet-deployment.md new file mode 100644 index 0000000..ffce4fc --- /dev/null +++ b/docs/operations/product-devnet-deployment.md @@ -0,0 +1,174 @@ +# Deploy Dotify To Product DevNet + +This runbook publishes the Product build to Bulletin/DotNS and connects it to +the existing Fly API and signaling services. It does not deploy contracts or +change production secrets. + +## Prerequisites + +- Node.js 22 and npm 10+ +- a clean build from the intended commit +- access to the `dotify.dot` deployment account +- Fly access for `dotify-api` and `dotify-signal` +- the current `@polkadot-community-foundation/polkadot-app-deploy` DevNet prerequisites + +The CLI is reference/experimental tooling. Do not store a mnemonic in the +repository, shell history, `.env` files, Netlify, or Fly. + +Before the first publish, the signing account also needs: + +- DevNet native tokens on Asset Hub; +- an EVM account mapping (`dotns account map --env devnet`); +- a live Bulletin storage authorization for the same SS58 account; +- ownership of `dotify.dot`, or eligibility to register it during deploy. + +Bulletin authorization is a finite quota and may expire. A deploy that starts +failing at the upload stage after previously working should recheck that quota. +See the official +[build and publish guide](https://docs.polkadotcommunity.foundation/guides/build-and-publish/) +for the current faucet, storage console, mapping, and DotNS registration steps. + +## 1. Verify The Fly Origin Boundary + +The tracked Fly configuration must contain: + +```txt +API_ORIGINS=https://muzinga.netlify.app,https://dotify.dev-dot.li +SIGNAL_ORIGINS=https://muzinga.netlify.app,https://dotify.dev-dot.li +``` + +Deploy both services before publishing the frontend: + +```bash +cd services/api +flyctl deploy -c fly.toml + +cd ../../web +flyctl deploy -c fly.signal.toml +``` + +Keep backend secrets unchanged. `API_ORIGINS` supersedes singular +`API_ORIGIN`; the latter remains only as a compatibility fallback. + +## 2. Verify The Browser-Safe Build Profile + +Review `web/.env.product-devnet`. It must contain only public endpoints and +identifiers. In particular: + +```txt +VITE_DOTIFY_HOST_MODE=required +VITE_DOTIFY_PRODUCT_ID=dotify.dot +VITE_PUBLIC_APP_URL=https://dotify.dev-dot.li +VITE_DOTIFY_API_URL=https://dotify-api.fly.dev +VITE_SIGNAL_URL=https://dotify-signal.fly.dev +VITE_PINATA_JWT= +VITE_CONTENT_SECRET= +``` + +`VITE_PUBLIC_APP_URL` is the URL copied for room invitations. Do not replace it +with an internal host URL or a raw CID gateway. + +## 3. Build Locally + +```bash +cd web +npm ci +npm run test:unit +npm run build:product-devnet +``` + +Expected output is `web/dist-product`. The production guard must fail if a +browser upload token or content secret is present. + +## 4. Authenticate The Deploy Tool + +The repository pins the CLI version in the npm deploy command but does not add +the experimental deploy tool to the application dependency tree. + +```bash +npx --yes --package @polkadot-community-foundation/polkadot-app-deploy@0.13.1 pad login --env devnet +npx --yes --package @polkadot-community-foundation/polkadot-app-deploy@0.13.1 pad whoami --env devnet +``` + +Follow the mobile-wallet flow. Confirm the selected account owns, or can +receive, `dotify.dot` and satisfies the DevNet registration/funding rules. + +## 5. Publish + +```bash +npm run deploy:product-devnet +``` + +The command: + +1. rebuilds `dist-product`; +2. validates `polkadot-app-deploy.config.ts`; +3. creates content-addressed chunks with the JavaScript merkle implementation; +4. uploads changed content to Product DevNet Bulletin; +5. binds `dotify.dot`; +6. writes the Product manifest and executable records. + +Publisher listing is deliberately not part of the default deploy. It requires +the current Product proof-of-personhood level and signer support, and the +0.13.1 CLI help still describes environment-specific limitations. After the +app URL is verified, follow the current official **List it in Browse** guide +and record that result separately. A listing failure must not obscure a +successful static deployment. + +Record the commit, CLI version, resulting CID, DotNS transaction references, +and final public URL in the release evidence. + +## 6. Validate + +Check service CORS from both origins: + +```bash +curl -s -D - -o /dev/null \ + -H 'Origin: https://dotify.dev-dot.li' \ + https://dotify-api.fly.dev/health + +curl -s -D - -o /dev/null \ + -H 'Origin: https://muzinga.netlify.app' \ + https://dotify-api.fly.dev/health +``` + +Then verify in the Product host: + +1. `https://dotify.dev-dot.li` opens and shows catalog tracks. +2. Free playback starts without connecting an account. +3. **Use Polkadot app** connects an app-scoped Product account only after the + button is selected. +4. A protected track asks for a passkey/EVM wallet; it does not release a key + through the Product identity. +5. A Product-origin host creates a room and copies a + `https://dotify.dev-dot.li/#/rooms/` link. +6. A wallet-free browser joins that link from outside the Product host. +7. A Netlify-origin host and Product-origin guest also connect. +8. Closing the host ends the room as before. + +Inspect the browser console and Fly logs for CORS, catalog, Socket.IO, and +WebRTC failures. + +## Rollback + +The Product deployment is static. To roll back: + +1. switch to the last known-good commit; +2. run `npm ci`; +3. run the full build and smoke checks; +4. republish with `npm run deploy:product-devnet`; +5. confirm DotNS resolves to the restored content; +6. record the replacement CID and incident reason. + +Do not roll back Fly origin allowlists while either public frontend remains +active. + +## Known Limits + +- Product account signing is identity/presence only in this phase. +- Contract writes and key requests still require passkey/EVM signing. +- Rooms still depend on one in-memory Fly signaling machine. +- Product-host cloud storage does not hold Dotify audio or content keys. +- Product personhood is not yet an access decision source. +- A durable `CATALOG_SNAPSHOT_PATH` remains recommended for production-grade + catalog recovery but is not required for API startup. diff --git a/docs/reference/environment-variables.md b/docs/reference/environment-variables.md index e255b55..1465ca3 100644 --- a/docs/reference/environment-variables.md +++ b/docs/reference/environment-variables.md @@ -36,6 +36,54 @@ production build contract without printing real secret values. --- +### `VITE_DOTIFY_HOST_MODE` + +| Property | Value | +| ------------ | ------------------------------ | +| **Type** | `off`, `auto`, or `required` | +| **Required** | Product builds | +| **Default** | `off` | +| **Example** | `required` | + +Controls Product host discovery. `off` keeps the standalone app independent +from the Product SDK. `auto` enables progressive host detection. `required` +marks a Product-targeted build but does not block catalog, Free playback, or +wallet-free room entry when opened outside the host. + +Host detection does not request an account. The account is requested only when +the listener selects **Use Polkadot app**. + +--- + +### `VITE_DOTIFY_PRODUCT_ID` + +| Property | Value | +| ------------ | ---------------------- | +| **Type** | Lowercase `.dot` name | +| **Required** | Host mode is not `off` | +| **Default** | `dotify.dot` | +| **Example** | `dotify.dot` | + +DotNS identifier used by the Product host to derive Dotify's app-scoped +account. Changing it changes the Product account boundary and requires an +identity/access migration review. + +--- + +### `VITE_PUBLIC_APP_URL` + +| Property | Value | +| ------------ | -------------------------------- | +| **Type** | HTTPS URL | +| **Required** | Product production builds | +| **Default** | Current browser URL | +| **Example** | `https://dotify.dev-dot.li` | + +Canonical public origin used when copying room links. Product builds must set +this so invitations never expose an internal host/container or raw gateway URL. + +--- + ### `VITE_DOTIFY_DEBUG_PANEL` | Property | Value | @@ -246,7 +294,7 @@ Network interface to bind. | **Type** | Comma-separated URL list or `*` | | **Required** | No | | **Default** | `*` | -| **Example** | `https://muzinga.netlify.app,https://dotify.dot.li` | +| **Example** | `https://muzinga.netlify.app,https://dotify.dev-dot.li` | CORS allowed origins for Socket.IO and status endpoints. Set explicit frontend origins in production. `SIGNAL_ORIGIN` is still accepted as a backwards-compatible @@ -327,7 +375,23 @@ Port the backend API listens on. | **Required** | Production | | **Default** | `http://localhost:5273` | -Frontend origin allowed by backend CORS. +Singular frontend origin allowed by backend CORS. This remains as a +backwards-compatible fallback when `API_ORIGINS` is not set. + +--- + +### `API_ORIGINS` + +| Property | Value | +| ------------ | -------------------------------------------------------- | +| **Type** | Comma-separated HTTPS origin list | +| **Required** | Multiple hosted frontends | +| **Default** | The single `API_ORIGIN` value | +| **Example** | `https://muzinga.netlify.app,https://dotify.dev-dot.li` | + +Exact frontend origins accepted by backend CORS. When set, it takes precedence +over `API_ORIGIN`. Do not use `*`: the API carries authenticated upload and +content-key routes. --- diff --git a/services/api/.env.example b/services/api/.env.example index b86aba5..8d58c7d 100644 --- a/services/api/.env.example +++ b/services/api/.env.example @@ -6,6 +6,8 @@ API_PORT=8790 # Frontend origin allowed by CORS. API_ORIGIN=http://localhost:5273 +# Comma-separated origins take precedence over API_ORIGIN when set. +# API_ORIGINS=https://muzinga.netlify.app,https://dotify.dev-dot.li # Paseo Asset Hub EVM RPC. Required for wallet-signed content-key requests: # the key route resolves the owning artist runtime via the directory and calls diff --git a/services/api/fly.toml b/services/api/fly.toml index 6b061eb..734f35a 100644 --- a/services/api/fly.toml +++ b/services/api/fly.toml @@ -10,6 +10,7 @@ primary_region = "ams" # checks target the owner-guarded factory/directory pair. API_PORT = "8790" NODE_ENV = "production" + API_ORIGINS = "https://muzinga.netlify.app,https://dotify.dev-dot.li" PASEO_ASSET_HUB_RPC = "https://eth-rpc-testnet.polkadot.io/" DOTIFY_FACTORY_ADDRESS = "0xbd1a11cfce8b5ef7a37e507bc5109895f8f42a72" DOTIFY_DIRECTORY_ADDRESS = "0xcf1534c6e2b0e43b9436c1e86a076466dc0f2108" diff --git a/services/api/package.json b/services/api/package.json index 65f0f21..72eefec 100644 --- a/services/api/package.json +++ b/services/api/package.json @@ -12,7 +12,7 @@ "start": "node dist/index.js", "catalog:reindex": "tsx src/scripts/reindexCatalog.ts", "typecheck": "tsc --noEmit", - "test": "node --import tsx --test src/services/audioV2.test.ts src/services/replayProtection.test.ts src/services/signatures.test.ts src/services/sessionTokens.test.ts src/services/catalog/readModel.test.ts src/routes/keys.test.ts src/routes/uploads.test.ts src/routes/auth.test.ts src/routes/health.test.ts src/routes/catalog.test.ts src/app.test.ts" + "test": "node --import tsx --test src/services/audioV2.test.ts src/services/replayProtection.test.ts src/services/signatures.test.ts src/services/sessionTokens.test.ts src/services/catalog/readModel.test.ts src/routes/keys.test.ts src/routes/uploads.test.ts src/routes/auth.test.ts src/routes/health.test.ts src/routes/catalog.test.ts src/app.test.ts src/cors.test.ts" }, "dependencies": { "@fastify/cors": "^11.2.0", diff --git a/services/api/src/app.ts b/services/api/src/app.ts index 89044c4..09a1111 100644 --- a/services/api/src/app.ts +++ b/services/api/src/app.ts @@ -39,6 +39,7 @@ export type BuildAppOptions = { // Tests disable logging; production always logs. logging?: boolean; catalog?: CatalogReadModel; + apiOrigins?: string[]; }; export async function buildApp(options: BuildAppOptions = {}): Promise { @@ -48,9 +49,9 @@ export async function buildApp(options: BuildAppOptions = {}): Promise + typeof value === 'string' + ? value + .split(',') + .map(origin => origin.trim()) + .filter(Boolean) + : value, + z.array(z.string().url()).min(1).optional(), +); + const envSchema = z.object({ API_PORT: z.coerce.number().int().min(1).max(65535).default(8790), API_ORIGIN: z.string().url().default('http://localhost:5273'), + API_ORIGINS: optionalOriginList, PASEO_ASSET_HUB_RPC: z.string().url().optional(), DOTIFY_FACTORY_ADDRESS: optionalNonEmptyString, DOTIFY_DIRECTORY_ADDRESS: optionalNonEmptyString, @@ -65,7 +77,10 @@ function parseEnv() { console.error(`[dotify-api] Invalid environment configuration:\n${issues}`); process.exit(1); } - return result.data; + return { + ...result.data, + API_ORIGINS: result.data.API_ORIGINS ?? [result.data.API_ORIGIN], + }; } export const config = parseEnv(); diff --git a/services/api/src/cors.test.ts b/services/api/src/cors.test.ts new file mode 100644 index 0000000..2d6f32c --- /dev/null +++ b/services/api/src/cors.test.ts @@ -0,0 +1,36 @@ +import assert from 'node:assert/strict'; +import { afterEach, describe, it } from 'node:test'; +import type { FastifyInstance } from 'fastify'; +import { buildApp } from './app.js'; + +let app: FastifyInstance | null = null; + +afterEach(async () => { + if (app) await app.close(); + app = null; +}); + +describe('frontend origin boundary', () => { + it('allows each configured Dotify frontend and rejects unrelated origins', async () => { + app = await buildApp({ + logging: false, + apiOrigins: ['https://muzinga.netlify.app', 'https://dotify.dev-dot.li'], + }); + + for (const origin of ['https://muzinga.netlify.app', 'https://dotify.dev-dot.li']) { + const response = await app.inject({ + method: 'GET', + url: '/health', + headers: { origin }, + }); + assert.equal(response.headers['access-control-allow-origin'], origin); + } + + const unrelated = await app.inject({ + method: 'GET', + url: '/health', + headers: { origin: 'https://unrelated.example' }, + }); + assert.equal(unrelated.headers['access-control-allow-origin'], undefined); + }); +}); diff --git a/spec.md b/spec.md index aa8fae0..7aabf3c 100644 --- a/spec.md +++ b/spec.md @@ -94,7 +94,7 @@ Individuality data. ## 4. System Architecture ```text -Browser (React + Vite) +Browser or Product host (React + Vite) ├── Player and catalog UI ├── WebRTC host-to-listener audio stream ├── Socket.IO signaling server for SDP/ICE and room discovery @@ -105,20 +105,20 @@ Browser (React + Vite) └── Paseo Asset Hub EVM contracts for artist runtimes and access policy ``` -The frontend is a static React/Vite app. It can be served locally by Vite or -built as a single-file Bulletin/IPFS-friendly artifact. +The frontend is a static React/Vite app. It can be served locally by Vite, +deployed to Netlify, built as a single-file Bulletin/IPFS-friendly artifact, +or published as the multi-file `dotify.dot` Product DevNet app. Production-sensitive upload and content-key operations live behind `services/api/`. Browser-side Pinata upload and `VITE_CONTENT_SECRET` key derivation remain local/demo paths only. -Product SDK / Playground / Humanity integration is a progressive enhancement -track, not a hard dependency for first sound. The current verified SDK snapshot -(`@parity/product-sdk` 0.17.0 at -`2f359bba28ca72855207a0a519d4118b37b4438c`) must be treated as -prototype/reference/unaudited until Dotify proves Host capability detection, -Product account signing, resource allocation, contract portability, and -Statement Store constraints against the current app. +Product SDK integration is an adaptive enhancement, not a hard dependency for +first sound. The Product build pins `@parity/product-sdk` 0.19.1, detects the +host, and requests an app-scoped account only after explicit user action. That +account is currently an identity/presence capability: Classic payments, artist +publication, and protected key requests still require the passkey/EVM path +until CDM/PAPI writes and backend Product-signature verification are delivered. ## 5. Repository Layout @@ -418,6 +418,9 @@ Important browser-exposed variables: | Variable | Purpose | | ------------------------- | ------------------------------------------------------------------------------------------ | | `VITE_DOTIFY_DEPLOYMENT` | build-time deployment safety mode; set `production` for public production builds | +| `VITE_DOTIFY_HOST_MODE` | Product host mode (`off`, `auto`, or `required`) | +| `VITE_DOTIFY_PRODUCT_ID` | `.dot` name used for app-scoped Product account derivation | +| `VITE_PUBLIC_APP_URL` | canonical public room-link origin for Product/container builds | | `VITE_DOTIFY_DEBUG_PANEL` | optional flag that shows the read-only Production readiness panel under `You` | | `VITE_SIGNAL_URL` | Socket.IO signaling server URL | | `VITE_LOCAL_WS_URL` | local Substrate websocket URL | @@ -436,7 +439,8 @@ Server/script variables: | `SIGNAL_PORT` | local signaling server port | | `SIGNAL_ORIGINS` | allowed frontend origins for signaling | | `API_PORT` | backend API port | -| `API_ORIGIN` | frontend origin allowed by backend CORS | +| `API_ORIGIN` | backwards-compatible singular frontend CORS origin | +| `API_ORIGINS` | comma-separated exact frontend CORS origins | | `PASEO_ASSET_HUB_RPC` | backend RPC endpoint for access checks | | `DOTIFY_DIRECTORY_ADDRESS` | backend ArtistDirectory address for runtime lookup | | `DOTIFY_CHAIN_ID` | chain ID expected in signed key requests | @@ -446,10 +450,12 @@ Server/script variables: ## 11. Wallet And Passkey Design -Dotify supports two wallet paths in the frontend design: +Dotify supports three account paths in the frontend design: - passkey-backed local key derivation through WebAuthn PRF; - browser wallet extension signing through Polkadot/EVM wallet providers. +- Product-host app-scoped identity for presence and rooms. It is not yet an + EIP-191 or EVM transaction signer. ### 11.1 Passkey Credential ID @@ -661,9 +667,9 @@ Priority improvements: Bulletin builds. 4. Finish security hardening for publish intents, auth chain binding, durable revocation, realtime reconnect, and short-lived TURN credentials. -5. Run Product SDK feasibility spikes for Host capability detection, Product - account signing, resource allocation, Playground/Bulletin/DotNS deployment, - Statement Store presence, and PolkaVM/CDM contract portability. +5. Validate the delivered Product host/account and Bulletin/DotNS deployment + baseline, then implement CDM/PAPI contract portability, backend Product + signature verification, and a bounded Statement Store presence spike. 6. Move the large catalog, session, artist, and player workflows behind domain ports and application use cases. 7. Validate the cacheable catalog API's warm/cold p75 budgets under public seed diff --git a/web/.env.example b/web/.env.example index daabd83..0b3c0a7 100644 --- a/web/.env.example +++ b/web/.env.example @@ -4,6 +4,12 @@ # Socket.IO signaling server used by shared listening rooms. VITE_SIGNAL_URL=http://localhost:8788 +# Product host integration is disabled for ordinary local/Netlify builds. +# The checked-in `.env.product-devnet` profile sets these for `dotify.dot`. +VITE_DOTIFY_HOST_MODE=off +# VITE_DOTIFY_PRODUCT_ID=dotify.dot +# VITE_PUBLIC_APP_URL=https://dotify.dev-dot.li + # Local development endpoints used when selecting the local network preset. VITE_LOCAL_WS_URL=ws://localhost:9944 VITE_LOCAL_ETH_RPC_URL=http://localhost:8545 diff --git a/web/.env.product-devnet b/web/.env.product-devnet new file mode 100644 index 0000000..84eea89 --- /dev/null +++ b/web/.env.product-devnet @@ -0,0 +1,22 @@ +# Checked-in browser-safe Product DevNet build profile. Never add secrets here. +VITE_DOTIFY_DEPLOYMENT=production +VITE_DOTIFY_HOST_MODE=required +VITE_DOTIFY_PRODUCT_ID=dotify.dot +VITE_PUBLIC_APP_URL=https://dotify.dev-dot.li + +VITE_SIGNAL_URL=https://dotify-signal.fly.dev +VITE_DOTIFY_API_URL=https://dotify-api.fly.dev + +# Existing Dotify contracts currently remain on the Product DevNet-compatible +# Asset Hub EVM endpoint while the write adapter is ported to CDM/PAPI. +VITE_ETH_RPC_URL=https://eth-rpc-testnet.polkadot.io/ +VITE_BULLETIN_WS_URL=wss://bulletin-paseo.tservices.es:8443 + +VITE_PINATA_GATEWAY=https://devnet-ipfs.api.polkadotcommunity.foundation +VITE_IPFS_READ_GATEWAYS=https://devnet-ipfs.api.polkadotcommunity.foundation,https://ipfs.io,https://dweb.link +VITE_BLOCKSCOUT_BASE_URL=https://blockscout-testnet.polkadot.io + +# Explicitly shadow any local demo values from `.env`; Product builds use the +# Fly API for uploads and key delivery. +VITE_PINATA_JWT= +VITE_CONTENT_SECRET= diff --git a/web/README.md b/web/README.md index f0096c0..c4cfa6a 100644 --- a/web/README.md +++ b/web/README.md @@ -35,6 +35,9 @@ Useful environment variables: wallet-signed content-key requests. - `VITE_DOTIFY_DEBUG_PANEL`: set to `true` to show the read-only Production readiness panel under the `You` tab. +- `VITE_DOTIFY_HOST_MODE`, `VITE_DOTIFY_PRODUCT_ID`, and + `VITE_PUBLIC_APP_URL`: Product-host detection, app-scoped account identifier, + and canonical room-link origin. - `VITE_LOCAL_WS_URL` / `VITE_LOCAL_ETH_RPC_URL`: local development endpoints. - `VITE_BULLETIN_WS_URL`: Paseo Bulletin Chain RPC. - `VITE_PINATA_JWT`: restricted browser-exposed Pinata JWT for demo uploads @@ -155,11 +158,26 @@ npm run codegen npm run build npm run build:bulletin npm run deploy:bulletin +npm run build:product-devnet +npm run deploy:product-devnet ``` `build:bulletin` produces a single-file build via `vite-plugin-singlefile` so it can be distributed from a flat IPFS CID / DotNS record. +`build:product-devnet` produces `dist-product` with the checked-in +`.env.product-devnet` profile. `deploy:product-devnet` uses +`@polkadot-community-foundation/polkadot-app-deploy@0.13.1` through `npx`, uploads static chunks to +Product DevNet Bulletin, and binds `dotify.dot`. Browse listing is a separate +operator step because it has its own signer/personhood boundary. The Product account currently +provides app-scoped identity for presence and rooms; contract writes and +protected key requests still require the existing passkey/EVM signer. + +See +[`docs/explanation/product-devnet-architecture.md`](../docs/explanation/product-devnet-architecture.md) +and +[`docs/operations/product-devnet-deployment.md`](../docs/operations/product-devnet-deployment.md). + ### Production Deploy: Netlify + Fly Dotify's production web app is a static Vite build, but listening rooms require diff --git a/web/eslint.config.js b/web/eslint.config.js index d591c6b..186ef82 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -5,7 +5,7 @@ import eslintConfigPrettier from "eslint-config-prettier"; export default tseslint.config( { - ignores: ["dist/**", "dist-bulletin/**", "node_modules/**", ".papi/**", "tsconfig.tsbuildinfo", "src/generated/**"], + ignores: ["dist/**", "dist-bulletin/**", "dist-product/**", "node_modules/**", ".papi/**", "tsconfig.tsbuildinfo", "src/generated/**"], }, js.configs.recommended, ...tseslint.configs.recommended, diff --git a/web/fly.signal.toml b/web/fly.signal.toml index c44532b..2b1b3a2 100644 --- a/web/fly.signal.toml +++ b/web/fly.signal.toml @@ -10,6 +10,7 @@ primary_region = "ams" SIGNAL_ROOM_TTL_MS = "21600000" SIGNAL_HOST_TIMEOUT_MS = "120000" SIGNAL_MAX_LISTENERS = "24" + SIGNAL_ORIGINS = "https://muzinga.netlify.app,https://dotify.dev-dot.li" [http_service] internal_port = 8788 diff --git a/web/package-lock.json b/web/package-lock.json index 27ea880..ea7aaf1 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,6 +8,7 @@ "name": "dotify-web", "version": "0.1.0", "dependencies": { + "@parity/product-sdk": "0.19.1", "@polkadot-api/descriptors": "file:.papi/descriptors", "@polkadot-apps/chain-client": "^2.0.5", "@polkadot-apps/descriptors": "^1.0.1", @@ -974,126 +975,3523 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@ipld/dag-pb": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@ipld/dag-pb/-/dag-pb-4.1.7.tgz", + "integrity": "sha512-/i/13trFihjWfDyXlylRwhuYjtzYjvOFw0vlRjYGnZuv7d7MOgA2lV/vRuL5RfeUajM03aZfFLdq4S7cTbbTRg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "multiformats": "^14.0.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@ipld/dag-pb/node_modules/multiformats": { + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-14.0.5.tgz", + "integrity": "sha512-vbIm83F2yZ1pWJGS0yl0ysracIvv56LtbrIyiIQHoLdYDJOMoLfVFsXhh9DUH4SFdkdkFhucyWniihsNzVEjkQ==", + "license": "Apache-2.0 OR MIT" + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.2.0.tgz", + "integrity": "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.2.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@novasamatech/host-api": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/@novasamatech/host-api/-/host-api-0.6.18.tgz", + "integrity": "sha512-5U5tYRbY/v49BqHH+iHPIP6OH7KJjXUMypaXSXtZK/J3IsQAqDLBlu/LqpDrNxHqEK1sKY5EyYla840mqmkwPg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@novasamatech/scale": "0.6.18", + "@polkadot-api/utils": "^0.2.0", + "nanoevents": "9.1.0", + "nanoid": "5.1.7", + "neverthrow": "^8.2.0", + "scale-ts": "1.6.1" + } + }, + "node_modules/@novasamatech/host-api/node_modules/@novasamatech/scale": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/@novasamatech/scale/-/scale-0.6.18.tgz", + "integrity": "sha512-xRvBrzJSvCseQ62zLReS3EtiQjuiTY+c+yOyx6If9dBRzX5FL52OazFLsdSaq3wOe8441TPXL1vediotYFZlRg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@polkadot-api/utils": "^0.2.0", + "scale-ts": "1.6.1" + } + }, + "node_modules/@novasamatech/host-api/node_modules/nanoid": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.7.tgz", + "integrity": "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@parity/product-sdk": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@parity/product-sdk/-/product-sdk-0.19.1.tgz", + "integrity": "sha512-vZnXh5FUc/KSXBeMYd5v5ZTit9f4PLlXkXD7rvvESdV7L2djTlDij4uOKkr1oWg9NrRmcH4cvkHD1wAbs2Zqzg==", + "license": "Apache-2.0", + "dependencies": { + "@parity/product-sdk-address": "0.1.1", + "@parity/product-sdk-chain-client": "0.9.1", + "@parity/product-sdk-cloud-storage": "0.8.1", + "@parity/product-sdk-contracts": "0.9.2", + "@parity/product-sdk-crypto": "0.1.1", + "@parity/product-sdk-errors": "0.2.0", + "@parity/product-sdk-host": "0.14.1", + "@parity/product-sdk-keys": "0.3.16", + "@parity/product-sdk-local-storage": "0.3.2", + "@parity/product-sdk-logger": "0.1.1", + "@parity/product-sdk-signer": "0.11.1", + "@parity/product-sdk-tx": "0.3.2", + "@parity/result": "0.2.0", + "polkadot-api": "^2.1.6" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, + "node_modules/@parity/product-sdk-address": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@parity/product-sdk-address/-/product-sdk-address-0.1.1.tgz", + "integrity": "sha512-sSymun3alNGdvawhdc0Ha0KEkuqMwBZui1bsUVeZIZRJAfWvQzrV1AVaf8aah5JFlcaRdg8FYyp7xL2eP+ZplA==", + "license": "Apache-2.0", + "dependencies": { + "@noble/hashes": "^1.7.1", + "@polkadot-api/substrate-bindings": "^0.12.0" + } + }, + "node_modules/@parity/product-sdk-address/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@parity/product-sdk-address/node_modules/@polkadot-api/substrate-bindings": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-bindings/-/substrate-bindings-0.12.0.tgz", + "integrity": "sha512-cIjDeJRHW6g3z+/55UzpoG4LG1N0HbT4x3NvZsQkYg4eoio9Sw7Pw2aZZX86pWemxc7vQbNw7WSz2Gz+ckdX6Q==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.8.0", + "@polkadot-api/utils": "0.1.2", + "@scure/base": "^1.2.5", + "scale-ts": "^1.6.1" + } + }, + "node_modules/@parity/product-sdk-address/node_modules/@polkadot-api/utils": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@polkadot-api/utils/-/utils-0.1.2.tgz", + "integrity": "sha512-yhs5k2a8N1SBJcz7EthZoazzLQUkZxbf+0271Xzu42C5AEM9K9uFLbsB+ojzHEM72O5X8lPtSwGKNmS7WQyDyg==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-address/node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@parity/product-sdk-chain-client": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@parity/product-sdk-chain-client/-/product-sdk-chain-client-0.9.1.tgz", + "integrity": "sha512-NXMJAYqLGdFp0VAbNfn2HeGhcP6n78jxtVEpcv9084ZlldhuwFKJLGwXOj81xbw2QHATbufCNLDY9IsTM+9Pew==", + "license": "Apache-2.0", + "dependencies": { + "@parity/product-sdk-descriptors": "0.8.0", + "@parity/product-sdk-host": "0.14.1", + "@parity/product-sdk-logger": "0.1.1", + "polkadot-api": "^2.1.6" + } + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/@commander-js/extra-typings": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-15.0.0.tgz", + "integrity": "sha512-yeJlba62xqmkgELUsn7356MEnzLLu/fw2x4lofFqGnXh6YysRdEs2BaLeLtg1+KU0AXvMeqQvTTp+3hBEBK+EA==", + "license": "MIT", + "peerDependencies": { + "commander": "~15.0.0" + } + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/@polkadot-api/cli": { + "version": "0.21.9", + "resolved": "https://registry.npmjs.org/@polkadot-api/cli/-/cli-0.21.9.tgz", + "integrity": "sha512-9aVdF/ocF3DBb8b/gdkcicBIaQkak2kkU4EGW+TunXLIvbIj2X9hH5geOlx8uWNwt0O4O1DIHD/nkXvycG/AcA==", + "license": "MIT", + "dependencies": { + "@commander-js/extra-typings": "^15.0.0", + "@polkadot-api/codegen": "0.22.5", + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/known-chains": "0.12.1", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/observable-client": "0.18.7", + "@polkadot-api/sm-provider": "0.3.8", + "@polkadot-api/smoldot": "0.4.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0", + "@polkadot-api/wasm-executor": "^0.2.3", + "@polkadot-api/ws-middleware": "0.3.6", + "@polkadot-api/ws-provider": "0.9.1", + "@types/node": "^25.9.4", + "commander": "^15.0.0", + "execa": "^9.6.1", + "fs.promises.exists": "^1.1.4", + "ora": "^9.4.1", + "read-pkg": "^10.1.0", + "rollup": "^4.62.2", + "rollup-plugin-esbuild": "^6.2.1", + "rxjs": "^7.8.2", + "tsc-prog": "^2.3.0", + "typescript": "^6.0.3", + "write-package": "^7.2.0" + }, + "bin": { + "papi": "dist/main/src/main.js", + "polkadot-api": "dist/main/src/main.js" + } + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/@polkadot-api/codegen": { + "version": "0.22.5", + "resolved": "https://registry.npmjs.org/@polkadot-api/codegen/-/codegen-0.22.5.tgz", + "integrity": "sha512-zwZJAlviI211zhj5i6oXkrr0crrbO3GZjBd2vC9AshY1pRmyiNLV9DZsXw4E6l4JQwdAAyNWtPdnvvB9T3TpKg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/@polkadot-api/ink-contracts": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/ink-contracts/-/ink-contracts-0.6.3.tgz", + "integrity": "sha512-XqnM1VDzI5L62xgg+f8le2yEoz8QZbUKEfAfPnHMOgBj9tJiyF15FcOJmnLMO/vq3cGixqh18tzJekLG5YTxtA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/@polkadot-api/json-rpc-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.2.0.tgz", + "integrity": "sha512-lhkuBS/x06i3djQIN7p8jVkMnYuGsUAEMS6RhdWeEpz7X/8/APER4Wdih7MEBovCuwVSCTjOxl8f+alH7AZHZg==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/@polkadot-api/json-rpc-provider-proxy": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.4.1.tgz", + "integrity": "sha512-F1Hw01C60jn98KQ0vBbgcwAvEcMcKLsJ5kFzq600sgjc6Rnbn1VF/XjTdrjcIqvfpR9sXyGIrpImrov2Usbrsw==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/@polkadot-api/known-chains": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/known-chains/-/known-chains-0.12.1.tgz", + "integrity": "sha512-ZW9TYD2y5IhgNy28zi9sByMr0CaP2I9x8HcAI0pwZR2qDC0kysCG6brDlg1UoZKCjRO99SsKO/iU1o434JgMnQ==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/@polkadot-api/logs-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/logs-provider/-/logs-provider-0.2.0.tgz", + "integrity": "sha512-BH9YdxZu+ZBPPAUwGrvqHPn1hQStL2Im3MmTwYkwXOWW2HlGHULcF65QKvyK+T6/mj2vDvl3MgivnGkUw4pVxg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0" + } + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/@polkadot-api/merkleize-metadata": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/merkleize-metadata/-/merkleize-metadata-1.2.3.tgz", + "integrity": "sha512-WkPbz0p2XQ9c8yXagdnwCHEB70Gnm91okcsd6IXU393//3aPgkxKgb+/Efnz7C5/KQmg02P0zXo7q/n/W/yVCA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/@polkadot-api/metadata-builders": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-builders/-/metadata-builders-0.14.3.tgz", + "integrity": "sha512-m7CACsiqHzgVEh5WBZGkTV8AQ3CBQKR1YpPQMnlsJfCr/IkgKU0UyWM6WxCmBiReLFVkOfXMtGlpN8+GxpHmww==", + "license": "MIT", + "dependencies": { + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/@polkadot-api/metadata-compatibility": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-compatibility/-/metadata-compatibility-0.6.3.tgz", + "integrity": "sha512-/Y0uF8nDk60ijydp8Bd37YexPFdB8hBXJWwEgOJHsVlhiny8sVKXiMg+UkJ9BiEk2z+yMbZRCKmhNpTpizo7aw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3" + } + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/@polkadot-api/observable-client": { + "version": "0.18.7", + "resolved": "https://registry.npmjs.org/@polkadot-api/observable-client/-/observable-client-0.18.7.tgz", + "integrity": "sha512-/eQi3D8jbXLg/L1hZX4eX0/+nO3kTxkOfYeWEtFhtOvFdfqiiy2DdXG3Zg5QzgHmfG+11p4vSlLiPCC2UvS5Qw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/@polkadot-api/pjs-signer": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/pjs-signer/-/pjs-signer-0.7.3.tgz", + "integrity": "sha512-U7BLFZfnpFMxCh/scJoLXT6oSbfZtZgMTkiu+TbuWQljAb/1ttrQOfshub5VihNyD5rHajp/2Fq0ONZoL5N5PA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signers-common": "0.2.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/@polkadot-api/raw-client": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/raw-client/-/raw-client-0.3.0.tgz", + "integrity": "sha512-u/wM9W7ugIXxBSOEV8+zvQI43b5vgSI0pvE0Rg8PV0G65BTLK0SMc2o2SQL0HtS5+bi5sw/gg4reBJ/0a4U0cw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0" + } + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/@polkadot-api/signer": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/signer/-/signer-0.3.3.tgz", + "integrity": "sha512-bmEV65TwgwbMfiecl7ZQ3k5lfkCOx9FPwPYkVvALcPiqb/d3zYwT9LL/E00hj+EB6IKMlMelEQVuchcW5i/92w==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.2.0", + "@polkadot-api/merkleize-metadata": "1.2.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signers-common": "0.2.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/@polkadot-api/signers-common": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/signers-common/-/signers-common-0.2.3.tgz", + "integrity": "sha512-SzGLJMxug31Y1P8+0I809ICpDrayztUXFFh2TV02GuwjEnY6mDGnmX56wowZF5yCn+WteeRdcTJTK7Whk3AGYQ==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/@polkadot-api/sm-provider": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@polkadot-api/sm-provider/-/sm-provider-0.3.8.tgz", + "integrity": "sha512-jIvzBNsBsh6LIpCrUOa7miZdQPwLPns5IUQyz/8v84R3ON7URSIVbtywGSR8O83BZGyW/DmVkqa5lClLE2fkqw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/json-rpc-provider-proxy": "0.4.1" + }, + "peerDependencies": { + "@polkadot-api/smoldot": ">=0.3" + } + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/@polkadot-api/smoldot": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@polkadot-api/smoldot/-/smoldot-0.4.6.tgz", + "integrity": "sha512-gOXMJ10fXOub0zPP1cGAYeyf29BA2fB+qukrjXGcGN9b0Ya+lDgV7A7Q7y9JOrlGLeKbd1YdrYLAe/F43V7b9Q==", + "license": "MIT", + "dependencies": { + "@types/node": "^25.9.4", + "smoldot": "~3.3.1" + } + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/@polkadot-api/substrate-bindings": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-bindings/-/substrate-bindings-0.20.3.tgz", + "integrity": "sha512-9iqC71fx1ee9ld1NZV8PFime5vryi0kt1bKCSlvNgO6dqMc06sMZuZ8WPjOzWLCHiKHLuphdMs3rVBBaeCP3yg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.2.0", + "@polkadot-api/utils": "0.4.0", + "@scure/base": "^2.2.0", + "scale-ts": "^1.6.1" + } + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/@polkadot-api/substrate-client": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-client/-/substrate-client-0.7.0.tgz", + "integrity": "sha512-TWCc4MAMa5SLVQXmomLHknbj+bztQ/Yclgwm8ENBhz8hR7c9rw9FBAkCa02jMBMCAygPhp3ayGRq+UFcF8KIxQ==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/raw-client": "0.3.0", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/@polkadot-api/utils": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/utils/-/utils-0.4.0.tgz", + "integrity": "sha512-9b/hwRM0UloLWV7SfpNaSD/4k8UQAHoaACAk7Xe+1MlfAm2JtnmPiB1GfGrfTyBlsrJVUIBCZpEmbmxVMaIqBA==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/@polkadot-api/ws-provider": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/ws-provider/-/ws-provider-0.9.1.tgz", + "integrity": "sha512-Ft2QJEjLZgTyKiCEbz3urSOxNfMdqNkg+QKqJhRbOFB7JtR1qMTvLE3TjM+5d2mNlEpOc3j2KDK3APNSj7uQ2Q==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/json-rpc-provider-proxy": "0.4.1", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/polkadot-api": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/polkadot-api/-/polkadot-api-2.2.1.tgz", + "integrity": "sha512-eiZVUhI1gY4ycBtsCo/yaDXyohUeuqnyKun+bwj90ADSn+jlfjZ4sajAqIhJFwPxbCT75+Zzbln4ZjuZ5PWNWA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/cli": "0.21.9", + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/known-chains": "0.12.1", + "@polkadot-api/logs-provider": "0.2.0", + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/observable-client": "0.18.7", + "@polkadot-api/pjs-signer": "0.7.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signer": "0.3.3", + "@polkadot-api/sm-provider": "0.3.8", + "@polkadot-api/smoldot": "0.4.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0", + "@polkadot-api/ws-middleware": "0.3.6", + "@polkadot-api/ws-provider": "0.9.1", + "@rx-state/core": "^0.1.4" + }, + "bin": { + "papi": "bin/cli.js", + "polkadot-api": "bin/cli.js" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/smoldot": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/smoldot/-/smoldot-3.3.2.tgz", + "integrity": "sha512-Zl4h/0gsw8cfTZzuJ7LV7mtR6QjxltwYjMY7MsVw0oXBXrLK8zyOS6DS9Vjsy57pX1vBMg6UVhHxjbH3W905zA==", + "license": "GPL-3.0-or-later WITH Classpath-exception-2.0", + "dependencies": { + "ws": "^8.8.1" + } + }, + "node_modules/@parity/product-sdk-chain-client/node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@parity/product-sdk-cloud-storage": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@parity/product-sdk-cloud-storage/-/product-sdk-cloud-storage-0.8.1.tgz", + "integrity": "sha512-yRMY8stewHA/ImbJD+PX/ao7JGniLomj3PPyPE22+aGZXpCcYwXdxMcDFOJgNtDK69Fyr483ejaew9tAFRzXNQ==", + "license": "Apache-2.0", + "dependencies": { + "@parity/bulletin-sdk": "^0.3.0", + "@parity/product-sdk-chain-client": "0.9.1", + "@parity/product-sdk-descriptors": "0.8.0", + "@parity/product-sdk-errors": "0.2.0", + "@parity/product-sdk-host": "0.14.1", + "@parity/product-sdk-logger": "0.1.1", + "@parity/product-sdk-tx": "0.3.2", + "@parity/result": "0.2.0", + "multiformats": "^13.3.0", + "polkadot-api": "^2.1.6" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@commander-js/extra-typings": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-15.0.0.tgz", + "integrity": "sha512-yeJlba62xqmkgELUsn7356MEnzLLu/fw2x4lofFqGnXh6YysRdEs2BaLeLtg1+KU0AXvMeqQvTTp+3hBEBK+EA==", + "license": "MIT", + "peerDependencies": { + "commander": "~15.0.0" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@parity/bulletin-sdk": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@parity/bulletin-sdk/-/bulletin-sdk-0.3.0.tgz", + "integrity": "sha512-sxVwBzyH/egXze1muPXbaGwQuOkP8efVB4Lxunshixf18gJ6WT2tedgUy08QOfQ1848BDQS4wVpRRQfPfb09/g==", + "license": "GPL-3.0-or-later WITH Classpath-exception-2.0", + "dependencies": { + "@ipld/dag-pb": "^4.1.3", + "@noble/hashes": "^2.2.0", + "@polkadot-labs/hdkd-helpers": "^0.0.29", + "ipfs-unixfs": "^12.0.0" + }, + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "multiformats": "^13.4.1", + "polkadot-api": "^2.1.2" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@polkadot-api/cli": { + "version": "0.21.9", + "resolved": "https://registry.npmjs.org/@polkadot-api/cli/-/cli-0.21.9.tgz", + "integrity": "sha512-9aVdF/ocF3DBb8b/gdkcicBIaQkak2kkU4EGW+TunXLIvbIj2X9hH5geOlx8uWNwt0O4O1DIHD/nkXvycG/AcA==", + "license": "MIT", + "dependencies": { + "@commander-js/extra-typings": "^15.0.0", + "@polkadot-api/codegen": "0.22.5", + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/known-chains": "0.12.1", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/observable-client": "0.18.7", + "@polkadot-api/sm-provider": "0.3.8", + "@polkadot-api/smoldot": "0.4.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0", + "@polkadot-api/wasm-executor": "^0.2.3", + "@polkadot-api/ws-middleware": "0.3.6", + "@polkadot-api/ws-provider": "0.9.1", + "@types/node": "^25.9.4", + "commander": "^15.0.0", + "execa": "^9.6.1", + "fs.promises.exists": "^1.1.4", + "ora": "^9.4.1", + "read-pkg": "^10.1.0", + "rollup": "^4.62.2", + "rollup-plugin-esbuild": "^6.2.1", + "rxjs": "^7.8.2", + "tsc-prog": "^2.3.0", + "typescript": "^6.0.3", + "write-package": "^7.2.0" + }, + "bin": { + "papi": "dist/main/src/main.js", + "polkadot-api": "dist/main/src/main.js" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@polkadot-api/codegen": { + "version": "0.22.5", + "resolved": "https://registry.npmjs.org/@polkadot-api/codegen/-/codegen-0.22.5.tgz", + "integrity": "sha512-zwZJAlviI211zhj5i6oXkrr0crrbO3GZjBd2vC9AshY1pRmyiNLV9DZsXw4E6l4JQwdAAyNWtPdnvvB9T3TpKg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@polkadot-api/ink-contracts": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/ink-contracts/-/ink-contracts-0.6.3.tgz", + "integrity": "sha512-XqnM1VDzI5L62xgg+f8le2yEoz8QZbUKEfAfPnHMOgBj9tJiyF15FcOJmnLMO/vq3cGixqh18tzJekLG5YTxtA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@polkadot-api/json-rpc-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.2.0.tgz", + "integrity": "sha512-lhkuBS/x06i3djQIN7p8jVkMnYuGsUAEMS6RhdWeEpz7X/8/APER4Wdih7MEBovCuwVSCTjOxl8f+alH7AZHZg==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@polkadot-api/json-rpc-provider-proxy": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.4.1.tgz", + "integrity": "sha512-F1Hw01C60jn98KQ0vBbgcwAvEcMcKLsJ5kFzq600sgjc6Rnbn1VF/XjTdrjcIqvfpR9sXyGIrpImrov2Usbrsw==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@polkadot-api/known-chains": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/known-chains/-/known-chains-0.12.1.tgz", + "integrity": "sha512-ZW9TYD2y5IhgNy28zi9sByMr0CaP2I9x8HcAI0pwZR2qDC0kysCG6brDlg1UoZKCjRO99SsKO/iU1o434JgMnQ==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@polkadot-api/logs-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/logs-provider/-/logs-provider-0.2.0.tgz", + "integrity": "sha512-BH9YdxZu+ZBPPAUwGrvqHPn1hQStL2Im3MmTwYkwXOWW2HlGHULcF65QKvyK+T6/mj2vDvl3MgivnGkUw4pVxg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@polkadot-api/merkleize-metadata": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/merkleize-metadata/-/merkleize-metadata-1.2.3.tgz", + "integrity": "sha512-WkPbz0p2XQ9c8yXagdnwCHEB70Gnm91okcsd6IXU393//3aPgkxKgb+/Efnz7C5/KQmg02P0zXo7q/n/W/yVCA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@polkadot-api/metadata-builders": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-builders/-/metadata-builders-0.14.3.tgz", + "integrity": "sha512-m7CACsiqHzgVEh5WBZGkTV8AQ3CBQKR1YpPQMnlsJfCr/IkgKU0UyWM6WxCmBiReLFVkOfXMtGlpN8+GxpHmww==", + "license": "MIT", + "dependencies": { + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@polkadot-api/metadata-compatibility": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-compatibility/-/metadata-compatibility-0.6.3.tgz", + "integrity": "sha512-/Y0uF8nDk60ijydp8Bd37YexPFdB8hBXJWwEgOJHsVlhiny8sVKXiMg+UkJ9BiEk2z+yMbZRCKmhNpTpizo7aw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@polkadot-api/observable-client": { + "version": "0.18.7", + "resolved": "https://registry.npmjs.org/@polkadot-api/observable-client/-/observable-client-0.18.7.tgz", + "integrity": "sha512-/eQi3D8jbXLg/L1hZX4eX0/+nO3kTxkOfYeWEtFhtOvFdfqiiy2DdXG3Zg5QzgHmfG+11p4vSlLiPCC2UvS5Qw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@polkadot-api/pjs-signer": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/pjs-signer/-/pjs-signer-0.7.3.tgz", + "integrity": "sha512-U7BLFZfnpFMxCh/scJoLXT6oSbfZtZgMTkiu+TbuWQljAb/1ttrQOfshub5VihNyD5rHajp/2Fq0ONZoL5N5PA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signers-common": "0.2.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@polkadot-api/raw-client": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/raw-client/-/raw-client-0.3.0.tgz", + "integrity": "sha512-u/wM9W7ugIXxBSOEV8+zvQI43b5vgSI0pvE0Rg8PV0G65BTLK0SMc2o2SQL0HtS5+bi5sw/gg4reBJ/0a4U0cw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@polkadot-api/signer": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/signer/-/signer-0.3.3.tgz", + "integrity": "sha512-bmEV65TwgwbMfiecl7ZQ3k5lfkCOx9FPwPYkVvALcPiqb/d3zYwT9LL/E00hj+EB6IKMlMelEQVuchcW5i/92w==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.2.0", + "@polkadot-api/merkleize-metadata": "1.2.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signers-common": "0.2.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@polkadot-api/signers-common": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/signers-common/-/signers-common-0.2.3.tgz", + "integrity": "sha512-SzGLJMxug31Y1P8+0I809ICpDrayztUXFFh2TV02GuwjEnY6mDGnmX56wowZF5yCn+WteeRdcTJTK7Whk3AGYQ==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@polkadot-api/sm-provider": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@polkadot-api/sm-provider/-/sm-provider-0.3.8.tgz", + "integrity": "sha512-jIvzBNsBsh6LIpCrUOa7miZdQPwLPns5IUQyz/8v84R3ON7URSIVbtywGSR8O83BZGyW/DmVkqa5lClLE2fkqw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/json-rpc-provider-proxy": "0.4.1" + }, + "peerDependencies": { + "@polkadot-api/smoldot": ">=0.3" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@polkadot-api/smoldot": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@polkadot-api/smoldot/-/smoldot-0.4.6.tgz", + "integrity": "sha512-gOXMJ10fXOub0zPP1cGAYeyf29BA2fB+qukrjXGcGN9b0Ya+lDgV7A7Q7y9JOrlGLeKbd1YdrYLAe/F43V7b9Q==", + "license": "MIT", + "dependencies": { + "@types/node": "^25.9.4", + "smoldot": "~3.3.1" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@polkadot-api/substrate-bindings": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-bindings/-/substrate-bindings-0.20.3.tgz", + "integrity": "sha512-9iqC71fx1ee9ld1NZV8PFime5vryi0kt1bKCSlvNgO6dqMc06sMZuZ8WPjOzWLCHiKHLuphdMs3rVBBaeCP3yg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.2.0", + "@polkadot-api/utils": "0.4.0", + "@scure/base": "^2.2.0", + "scale-ts": "^1.6.1" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@polkadot-api/substrate-client": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-client/-/substrate-client-0.7.0.tgz", + "integrity": "sha512-TWCc4MAMa5SLVQXmomLHknbj+bztQ/Yclgwm8ENBhz8hR7c9rw9FBAkCa02jMBMCAygPhp3ayGRq+UFcF8KIxQ==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/raw-client": "0.3.0", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@polkadot-api/utils": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/utils/-/utils-0.4.0.tgz", + "integrity": "sha512-9b/hwRM0UloLWV7SfpNaSD/4k8UQAHoaACAk7Xe+1MlfAm2JtnmPiB1GfGrfTyBlsrJVUIBCZpEmbmxVMaIqBA==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@polkadot-api/ws-provider": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/ws-provider/-/ws-provider-0.9.1.tgz", + "integrity": "sha512-Ft2QJEjLZgTyKiCEbz3urSOxNfMdqNkg+QKqJhRbOFB7JtR1qMTvLE3TjM+5d2mNlEpOc3j2KDK3APNSj7uQ2Q==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/json-rpc-provider-proxy": "0.4.1", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/@polkadot-labs/hdkd-helpers": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@polkadot-labs/hdkd-helpers/-/hdkd-helpers-0.0.29.tgz", + "integrity": "sha512-yiLm1Gj3j5NrQV+VFMlFzkBgcRBNfq2Sd/U3S8iau2bzhDwgsn4gy6FDt94TRPD5xLxOzi1I3wSLOrgOs2eLVw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "^2.2.0", + "@noble/hashes": "^2.2.0", + "@scure/base": "^2.0.0", + "@scure/sr25519": "^1.0.0", + "scale-ts": "^1.6.1" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/polkadot-api": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/polkadot-api/-/polkadot-api-2.2.1.tgz", + "integrity": "sha512-eiZVUhI1gY4ycBtsCo/yaDXyohUeuqnyKun+bwj90ADSn+jlfjZ4sajAqIhJFwPxbCT75+Zzbln4ZjuZ5PWNWA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/cli": "0.21.9", + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/known-chains": "0.12.1", + "@polkadot-api/logs-provider": "0.2.0", + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/observable-client": "0.18.7", + "@polkadot-api/pjs-signer": "0.7.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signer": "0.3.3", + "@polkadot-api/sm-provider": "0.3.8", + "@polkadot-api/smoldot": "0.4.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0", + "@polkadot-api/ws-middleware": "0.3.6", + "@polkadot-api/ws-provider": "0.9.1", + "@rx-state/core": "^0.1.4" + }, + "bin": { + "papi": "bin/cli.js", + "polkadot-api": "bin/cli.js" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/smoldot": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/smoldot/-/smoldot-3.3.2.tgz", + "integrity": "sha512-Zl4h/0gsw8cfTZzuJ7LV7mtR6QjxltwYjMY7MsVw0oXBXrLK8zyOS6DS9Vjsy57pX1vBMg6UVhHxjbH3W905zA==", + "license": "GPL-3.0-or-later WITH Classpath-exception-2.0", + "dependencies": { + "ws": "^8.8.1" + } + }, + "node_modules/@parity/product-sdk-cloud-storage/node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@parity/product-sdk-contracts": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@parity/product-sdk-contracts/-/product-sdk-contracts-0.9.2.tgz", + "integrity": "sha512-4svBhyBOoNfV4K5f9feizZSPl1Hn5frYpJvf5hjR9z7zp+t+1ruM/DjUM5QCgwrAmwkpfw7oxHDFTN3SlBo0tw==", + "license": "Apache-2.0", + "dependencies": { + "@parity/product-sdk-address": "0.1.1", + "@parity/product-sdk-errors": "0.2.0", + "@parity/product-sdk-keys": "0.3.16", + "@parity/product-sdk-logger": "0.1.1", + "@parity/product-sdk-signer": "0.11.1", + "@parity/product-sdk-tx": "0.3.2", + "@parity/result": "0.2.0", + "@polkadot-labs/hdkd-helpers": "^0.0.30", + "polkadot-api": "^2.1.6", + "viem": "^2.52.0" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@commander-js/extra-typings": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-15.0.0.tgz", + "integrity": "sha512-yeJlba62xqmkgELUsn7356MEnzLLu/fw2x4lofFqGnXh6YysRdEs2BaLeLtg1+KU0AXvMeqQvTTp+3hBEBK+EA==", + "license": "MIT", + "peerDependencies": { + "commander": "~15.0.0" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@polkadot-api/cli": { + "version": "0.21.9", + "resolved": "https://registry.npmjs.org/@polkadot-api/cli/-/cli-0.21.9.tgz", + "integrity": "sha512-9aVdF/ocF3DBb8b/gdkcicBIaQkak2kkU4EGW+TunXLIvbIj2X9hH5geOlx8uWNwt0O4O1DIHD/nkXvycG/AcA==", + "license": "MIT", + "dependencies": { + "@commander-js/extra-typings": "^15.0.0", + "@polkadot-api/codegen": "0.22.5", + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/known-chains": "0.12.1", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/observable-client": "0.18.7", + "@polkadot-api/sm-provider": "0.3.8", + "@polkadot-api/smoldot": "0.4.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0", + "@polkadot-api/wasm-executor": "^0.2.3", + "@polkadot-api/ws-middleware": "0.3.6", + "@polkadot-api/ws-provider": "0.9.1", + "@types/node": "^25.9.4", + "commander": "^15.0.0", + "execa": "^9.6.1", + "fs.promises.exists": "^1.1.4", + "ora": "^9.4.1", + "read-pkg": "^10.1.0", + "rollup": "^4.62.2", + "rollup-plugin-esbuild": "^6.2.1", + "rxjs": "^7.8.2", + "tsc-prog": "^2.3.0", + "typescript": "^6.0.3", + "write-package": "^7.2.0" + }, + "bin": { + "papi": "dist/main/src/main.js", + "polkadot-api": "dist/main/src/main.js" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@polkadot-api/codegen": { + "version": "0.22.5", + "resolved": "https://registry.npmjs.org/@polkadot-api/codegen/-/codegen-0.22.5.tgz", + "integrity": "sha512-zwZJAlviI211zhj5i6oXkrr0crrbO3GZjBd2vC9AshY1pRmyiNLV9DZsXw4E6l4JQwdAAyNWtPdnvvB9T3TpKg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@polkadot-api/ink-contracts": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/ink-contracts/-/ink-contracts-0.6.3.tgz", + "integrity": "sha512-XqnM1VDzI5L62xgg+f8le2yEoz8QZbUKEfAfPnHMOgBj9tJiyF15FcOJmnLMO/vq3cGixqh18tzJekLG5YTxtA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@polkadot-api/json-rpc-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.2.0.tgz", + "integrity": "sha512-lhkuBS/x06i3djQIN7p8jVkMnYuGsUAEMS6RhdWeEpz7X/8/APER4Wdih7MEBovCuwVSCTjOxl8f+alH7AZHZg==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@polkadot-api/json-rpc-provider-proxy": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.4.1.tgz", + "integrity": "sha512-F1Hw01C60jn98KQ0vBbgcwAvEcMcKLsJ5kFzq600sgjc6Rnbn1VF/XjTdrjcIqvfpR9sXyGIrpImrov2Usbrsw==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@polkadot-api/known-chains": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/known-chains/-/known-chains-0.12.1.tgz", + "integrity": "sha512-ZW9TYD2y5IhgNy28zi9sByMr0CaP2I9x8HcAI0pwZR2qDC0kysCG6brDlg1UoZKCjRO99SsKO/iU1o434JgMnQ==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@polkadot-api/logs-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/logs-provider/-/logs-provider-0.2.0.tgz", + "integrity": "sha512-BH9YdxZu+ZBPPAUwGrvqHPn1hQStL2Im3MmTwYkwXOWW2HlGHULcF65QKvyK+T6/mj2vDvl3MgivnGkUw4pVxg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@polkadot-api/merkleize-metadata": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/merkleize-metadata/-/merkleize-metadata-1.2.3.tgz", + "integrity": "sha512-WkPbz0p2XQ9c8yXagdnwCHEB70Gnm91okcsd6IXU393//3aPgkxKgb+/Efnz7C5/KQmg02P0zXo7q/n/W/yVCA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@polkadot-api/metadata-builders": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-builders/-/metadata-builders-0.14.3.tgz", + "integrity": "sha512-m7CACsiqHzgVEh5WBZGkTV8AQ3CBQKR1YpPQMnlsJfCr/IkgKU0UyWM6WxCmBiReLFVkOfXMtGlpN8+GxpHmww==", + "license": "MIT", + "dependencies": { + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@polkadot-api/metadata-compatibility": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-compatibility/-/metadata-compatibility-0.6.3.tgz", + "integrity": "sha512-/Y0uF8nDk60ijydp8Bd37YexPFdB8hBXJWwEgOJHsVlhiny8sVKXiMg+UkJ9BiEk2z+yMbZRCKmhNpTpizo7aw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@polkadot-api/observable-client": { + "version": "0.18.7", + "resolved": "https://registry.npmjs.org/@polkadot-api/observable-client/-/observable-client-0.18.7.tgz", + "integrity": "sha512-/eQi3D8jbXLg/L1hZX4eX0/+nO3kTxkOfYeWEtFhtOvFdfqiiy2DdXG3Zg5QzgHmfG+11p4vSlLiPCC2UvS5Qw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@polkadot-api/pjs-signer": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/pjs-signer/-/pjs-signer-0.7.3.tgz", + "integrity": "sha512-U7BLFZfnpFMxCh/scJoLXT6oSbfZtZgMTkiu+TbuWQljAb/1ttrQOfshub5VihNyD5rHajp/2Fq0ONZoL5N5PA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signers-common": "0.2.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@polkadot-api/raw-client": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/raw-client/-/raw-client-0.3.0.tgz", + "integrity": "sha512-u/wM9W7ugIXxBSOEV8+zvQI43b5vgSI0pvE0Rg8PV0G65BTLK0SMc2o2SQL0HtS5+bi5sw/gg4reBJ/0a4U0cw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@polkadot-api/signer": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/signer/-/signer-0.3.3.tgz", + "integrity": "sha512-bmEV65TwgwbMfiecl7ZQ3k5lfkCOx9FPwPYkVvALcPiqb/d3zYwT9LL/E00hj+EB6IKMlMelEQVuchcW5i/92w==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.2.0", + "@polkadot-api/merkleize-metadata": "1.2.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signers-common": "0.2.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@polkadot-api/signers-common": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/signers-common/-/signers-common-0.2.3.tgz", + "integrity": "sha512-SzGLJMxug31Y1P8+0I809ICpDrayztUXFFh2TV02GuwjEnY6mDGnmX56wowZF5yCn+WteeRdcTJTK7Whk3AGYQ==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@polkadot-api/sm-provider": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@polkadot-api/sm-provider/-/sm-provider-0.3.8.tgz", + "integrity": "sha512-jIvzBNsBsh6LIpCrUOa7miZdQPwLPns5IUQyz/8v84R3ON7URSIVbtywGSR8O83BZGyW/DmVkqa5lClLE2fkqw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/json-rpc-provider-proxy": "0.4.1" + }, + "peerDependencies": { + "@polkadot-api/smoldot": ">=0.3" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@polkadot-api/smoldot": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@polkadot-api/smoldot/-/smoldot-0.4.6.tgz", + "integrity": "sha512-gOXMJ10fXOub0zPP1cGAYeyf29BA2fB+qukrjXGcGN9b0Ya+lDgV7A7Q7y9JOrlGLeKbd1YdrYLAe/F43V7b9Q==", + "license": "MIT", + "dependencies": { + "@types/node": "^25.9.4", + "smoldot": "~3.3.1" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@polkadot-api/substrate-bindings": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-bindings/-/substrate-bindings-0.20.3.tgz", + "integrity": "sha512-9iqC71fx1ee9ld1NZV8PFime5vryi0kt1bKCSlvNgO6dqMc06sMZuZ8WPjOzWLCHiKHLuphdMs3rVBBaeCP3yg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.2.0", + "@polkadot-api/utils": "0.4.0", + "@scure/base": "^2.2.0", + "scale-ts": "^1.6.1" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@polkadot-api/substrate-client": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-client/-/substrate-client-0.7.0.tgz", + "integrity": "sha512-TWCc4MAMa5SLVQXmomLHknbj+bztQ/Yclgwm8ENBhz8hR7c9rw9FBAkCa02jMBMCAygPhp3ayGRq+UFcF8KIxQ==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/raw-client": "0.3.0", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@polkadot-api/utils": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/utils/-/utils-0.4.0.tgz", + "integrity": "sha512-9b/hwRM0UloLWV7SfpNaSD/4k8UQAHoaACAk7Xe+1MlfAm2JtnmPiB1GfGrfTyBlsrJVUIBCZpEmbmxVMaIqBA==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@polkadot-api/ws-provider": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/ws-provider/-/ws-provider-0.9.1.tgz", + "integrity": "sha512-Ft2QJEjLZgTyKiCEbz3urSOxNfMdqNkg+QKqJhRbOFB7JtR1qMTvLE3TjM+5d2mNlEpOc3j2KDK3APNSj7uQ2Q==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/json-rpc-provider-proxy": "0.4.1", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/@polkadot-labs/hdkd-helpers": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@polkadot-labs/hdkd-helpers/-/hdkd-helpers-0.0.30.tgz", + "integrity": "sha512-qWmmD6ayj14RenDuDFfjF3sHS7ObqPzwIIMPcSVoDeKFSeQV7RY0HwyhC5CG4i6FoguMzak2dbtjYpNN5XQiwQ==", + "license": "MIT", + "dependencies": { + "@noble/curves": "^2.2.0", + "@noble/hashes": "^2.2.0", + "@scure/base": "^2.2.0", + "@scure/sr25519": "^1.0.0", + "scale-ts": "^1.6.1" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/polkadot-api": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/polkadot-api/-/polkadot-api-2.2.1.tgz", + "integrity": "sha512-eiZVUhI1gY4ycBtsCo/yaDXyohUeuqnyKun+bwj90ADSn+jlfjZ4sajAqIhJFwPxbCT75+Zzbln4ZjuZ5PWNWA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/cli": "0.21.9", + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/known-chains": "0.12.1", + "@polkadot-api/logs-provider": "0.2.0", + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/observable-client": "0.18.7", + "@polkadot-api/pjs-signer": "0.7.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signer": "0.3.3", + "@polkadot-api/sm-provider": "0.3.8", + "@polkadot-api/smoldot": "0.4.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0", + "@polkadot-api/ws-middleware": "0.3.6", + "@polkadot-api/ws-provider": "0.9.1", + "@rx-state/core": "^0.1.4" + }, + "bin": { + "papi": "bin/cli.js", + "polkadot-api": "bin/cli.js" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/smoldot": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/smoldot/-/smoldot-3.3.2.tgz", + "integrity": "sha512-Zl4h/0gsw8cfTZzuJ7LV7mtR6QjxltwYjMY7MsVw0oXBXrLK8zyOS6DS9Vjsy57pX1vBMg6UVhHxjbH3W905zA==", + "license": "GPL-3.0-or-later WITH Classpath-exception-2.0", + "dependencies": { + "ws": "^8.8.1" + } + }, + "node_modules/@parity/product-sdk-contracts/node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@parity/product-sdk-crypto": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@parity/product-sdk-crypto/-/product-sdk-crypto-0.1.1.tgz", + "integrity": "sha512-No6AyTLw1Nv3ym8SDdXh/tnezdClNOL9pJgaciVr9Ny6hIL5rs6MQiXsP0+1bc1Nwymz5Q4FqsYg/htE4lejNg==", + "license": "Apache-2.0", + "dependencies": { + "@noble/ciphers": "^1.2.1", + "@noble/curves": "^1.8.0", + "@noble/hashes": "^1.7.1", + "tweetnacl": "^1.0.3" + } + }, + "node_modules/@parity/product-sdk-crypto/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@parity/product-sdk-crypto/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@parity/product-sdk-descriptors": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@parity/product-sdk-descriptors/-/product-sdk-descriptors-0.8.0.tgz", + "integrity": "sha512-DfdrtzjBqiS4A/fnqoDOyW+KiBVKkOtfDDl1/BLHtvYxp3TanPkS952Sd28F7v1Rk/0xnqm8d7shD06/XoLqhg==", + "license": "Apache-2.0", + "dependencies": { + "polkadot-api": "^2.1.6" + } + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/@commander-js/extra-typings": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-15.0.0.tgz", + "integrity": "sha512-yeJlba62xqmkgELUsn7356MEnzLLu/fw2x4lofFqGnXh6YysRdEs2BaLeLtg1+KU0AXvMeqQvTTp+3hBEBK+EA==", + "license": "MIT", + "peerDependencies": { + "commander": "~15.0.0" + } + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/@polkadot-api/cli": { + "version": "0.21.9", + "resolved": "https://registry.npmjs.org/@polkadot-api/cli/-/cli-0.21.9.tgz", + "integrity": "sha512-9aVdF/ocF3DBb8b/gdkcicBIaQkak2kkU4EGW+TunXLIvbIj2X9hH5geOlx8uWNwt0O4O1DIHD/nkXvycG/AcA==", + "license": "MIT", + "dependencies": { + "@commander-js/extra-typings": "^15.0.0", + "@polkadot-api/codegen": "0.22.5", + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/known-chains": "0.12.1", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/observable-client": "0.18.7", + "@polkadot-api/sm-provider": "0.3.8", + "@polkadot-api/smoldot": "0.4.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0", + "@polkadot-api/wasm-executor": "^0.2.3", + "@polkadot-api/ws-middleware": "0.3.6", + "@polkadot-api/ws-provider": "0.9.1", + "@types/node": "^25.9.4", + "commander": "^15.0.0", + "execa": "^9.6.1", + "fs.promises.exists": "^1.1.4", + "ora": "^9.4.1", + "read-pkg": "^10.1.0", + "rollup": "^4.62.2", + "rollup-plugin-esbuild": "^6.2.1", + "rxjs": "^7.8.2", + "tsc-prog": "^2.3.0", + "typescript": "^6.0.3", + "write-package": "^7.2.0" + }, + "bin": { + "papi": "dist/main/src/main.js", + "polkadot-api": "dist/main/src/main.js" + } + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/@polkadot-api/codegen": { + "version": "0.22.5", + "resolved": "https://registry.npmjs.org/@polkadot-api/codegen/-/codegen-0.22.5.tgz", + "integrity": "sha512-zwZJAlviI211zhj5i6oXkrr0crrbO3GZjBd2vC9AshY1pRmyiNLV9DZsXw4E6l4JQwdAAyNWtPdnvvB9T3TpKg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/@polkadot-api/ink-contracts": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/ink-contracts/-/ink-contracts-0.6.3.tgz", + "integrity": "sha512-XqnM1VDzI5L62xgg+f8le2yEoz8QZbUKEfAfPnHMOgBj9tJiyF15FcOJmnLMO/vq3cGixqh18tzJekLG5YTxtA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/@polkadot-api/json-rpc-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.2.0.tgz", + "integrity": "sha512-lhkuBS/x06i3djQIN7p8jVkMnYuGsUAEMS6RhdWeEpz7X/8/APER4Wdih7MEBovCuwVSCTjOxl8f+alH7AZHZg==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/@polkadot-api/json-rpc-provider-proxy": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.4.1.tgz", + "integrity": "sha512-F1Hw01C60jn98KQ0vBbgcwAvEcMcKLsJ5kFzq600sgjc6Rnbn1VF/XjTdrjcIqvfpR9sXyGIrpImrov2Usbrsw==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/@polkadot-api/known-chains": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/known-chains/-/known-chains-0.12.1.tgz", + "integrity": "sha512-ZW9TYD2y5IhgNy28zi9sByMr0CaP2I9x8HcAI0pwZR2qDC0kysCG6brDlg1UoZKCjRO99SsKO/iU1o434JgMnQ==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/@polkadot-api/logs-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/logs-provider/-/logs-provider-0.2.0.tgz", + "integrity": "sha512-BH9YdxZu+ZBPPAUwGrvqHPn1hQStL2Im3MmTwYkwXOWW2HlGHULcF65QKvyK+T6/mj2vDvl3MgivnGkUw4pVxg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0" + } + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/@polkadot-api/merkleize-metadata": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/merkleize-metadata/-/merkleize-metadata-1.2.3.tgz", + "integrity": "sha512-WkPbz0p2XQ9c8yXagdnwCHEB70Gnm91okcsd6IXU393//3aPgkxKgb+/Efnz7C5/KQmg02P0zXo7q/n/W/yVCA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/@polkadot-api/metadata-builders": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-builders/-/metadata-builders-0.14.3.tgz", + "integrity": "sha512-m7CACsiqHzgVEh5WBZGkTV8AQ3CBQKR1YpPQMnlsJfCr/IkgKU0UyWM6WxCmBiReLFVkOfXMtGlpN8+GxpHmww==", + "license": "MIT", + "dependencies": { + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/@polkadot-api/metadata-compatibility": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-compatibility/-/metadata-compatibility-0.6.3.tgz", + "integrity": "sha512-/Y0uF8nDk60ijydp8Bd37YexPFdB8hBXJWwEgOJHsVlhiny8sVKXiMg+UkJ9BiEk2z+yMbZRCKmhNpTpizo7aw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3" + } + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/@polkadot-api/observable-client": { + "version": "0.18.7", + "resolved": "https://registry.npmjs.org/@polkadot-api/observable-client/-/observable-client-0.18.7.tgz", + "integrity": "sha512-/eQi3D8jbXLg/L1hZX4eX0/+nO3kTxkOfYeWEtFhtOvFdfqiiy2DdXG3Zg5QzgHmfG+11p4vSlLiPCC2UvS5Qw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/@polkadot-api/pjs-signer": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/pjs-signer/-/pjs-signer-0.7.3.tgz", + "integrity": "sha512-U7BLFZfnpFMxCh/scJoLXT6oSbfZtZgMTkiu+TbuWQljAb/1ttrQOfshub5VihNyD5rHajp/2Fq0ONZoL5N5PA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signers-common": "0.2.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/@polkadot-api/raw-client": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/raw-client/-/raw-client-0.3.0.tgz", + "integrity": "sha512-u/wM9W7ugIXxBSOEV8+zvQI43b5vgSI0pvE0Rg8PV0G65BTLK0SMc2o2SQL0HtS5+bi5sw/gg4reBJ/0a4U0cw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0" + } + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/@polkadot-api/signer": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/signer/-/signer-0.3.3.tgz", + "integrity": "sha512-bmEV65TwgwbMfiecl7ZQ3k5lfkCOx9FPwPYkVvALcPiqb/d3zYwT9LL/E00hj+EB6IKMlMelEQVuchcW5i/92w==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.2.0", + "@polkadot-api/merkleize-metadata": "1.2.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signers-common": "0.2.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/@polkadot-api/signers-common": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/signers-common/-/signers-common-0.2.3.tgz", + "integrity": "sha512-SzGLJMxug31Y1P8+0I809ICpDrayztUXFFh2TV02GuwjEnY6mDGnmX56wowZF5yCn+WteeRdcTJTK7Whk3AGYQ==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/@polkadot-api/sm-provider": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@polkadot-api/sm-provider/-/sm-provider-0.3.8.tgz", + "integrity": "sha512-jIvzBNsBsh6LIpCrUOa7miZdQPwLPns5IUQyz/8v84R3ON7URSIVbtywGSR8O83BZGyW/DmVkqa5lClLE2fkqw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/json-rpc-provider-proxy": "0.4.1" + }, + "peerDependencies": { + "@polkadot-api/smoldot": ">=0.3" + } + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/@polkadot-api/smoldot": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@polkadot-api/smoldot/-/smoldot-0.4.6.tgz", + "integrity": "sha512-gOXMJ10fXOub0zPP1cGAYeyf29BA2fB+qukrjXGcGN9b0Ya+lDgV7A7Q7y9JOrlGLeKbd1YdrYLAe/F43V7b9Q==", + "license": "MIT", + "dependencies": { + "@types/node": "^25.9.4", + "smoldot": "~3.3.1" + } + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/@polkadot-api/substrate-bindings": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-bindings/-/substrate-bindings-0.20.3.tgz", + "integrity": "sha512-9iqC71fx1ee9ld1NZV8PFime5vryi0kt1bKCSlvNgO6dqMc06sMZuZ8WPjOzWLCHiKHLuphdMs3rVBBaeCP3yg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.2.0", + "@polkadot-api/utils": "0.4.0", + "@scure/base": "^2.2.0", + "scale-ts": "^1.6.1" + } + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/@polkadot-api/substrate-client": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-client/-/substrate-client-0.7.0.tgz", + "integrity": "sha512-TWCc4MAMa5SLVQXmomLHknbj+bztQ/Yclgwm8ENBhz8hR7c9rw9FBAkCa02jMBMCAygPhp3ayGRq+UFcF8KIxQ==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/raw-client": "0.3.0", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/@polkadot-api/utils": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/utils/-/utils-0.4.0.tgz", + "integrity": "sha512-9b/hwRM0UloLWV7SfpNaSD/4k8UQAHoaACAk7Xe+1MlfAm2JtnmPiB1GfGrfTyBlsrJVUIBCZpEmbmxVMaIqBA==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/@polkadot-api/ws-provider": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/ws-provider/-/ws-provider-0.9.1.tgz", + "integrity": "sha512-Ft2QJEjLZgTyKiCEbz3urSOxNfMdqNkg+QKqJhRbOFB7JtR1qMTvLE3TjM+5d2mNlEpOc3j2KDK3APNSj7uQ2Q==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/json-rpc-provider-proxy": "0.4.1", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/polkadot-api": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/polkadot-api/-/polkadot-api-2.2.1.tgz", + "integrity": "sha512-eiZVUhI1gY4ycBtsCo/yaDXyohUeuqnyKun+bwj90ADSn+jlfjZ4sajAqIhJFwPxbCT75+Zzbln4ZjuZ5PWNWA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/cli": "0.21.9", + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/known-chains": "0.12.1", + "@polkadot-api/logs-provider": "0.2.0", + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/observable-client": "0.18.7", + "@polkadot-api/pjs-signer": "0.7.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signer": "0.3.3", + "@polkadot-api/sm-provider": "0.3.8", + "@polkadot-api/smoldot": "0.4.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0", + "@polkadot-api/ws-middleware": "0.3.6", + "@polkadot-api/ws-provider": "0.9.1", + "@rx-state/core": "^0.1.4" + }, + "bin": { + "papi": "bin/cli.js", + "polkadot-api": "bin/cli.js" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/smoldot": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/smoldot/-/smoldot-3.3.2.tgz", + "integrity": "sha512-Zl4h/0gsw8cfTZzuJ7LV7mtR6QjxltwYjMY7MsVw0oXBXrLK8zyOS6DS9Vjsy57pX1vBMg6UVhHxjbH3W905zA==", + "license": "GPL-3.0-or-later WITH Classpath-exception-2.0", + "dependencies": { + "ws": "^8.8.1" + } + }, + "node_modules/@parity/product-sdk-descriptors/node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@parity/product-sdk-errors": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@parity/product-sdk-errors/-/product-sdk-errors-0.2.0.tgz", + "integrity": "sha512-2rvJV0iJyNAxSjm+RHcoch3GRGYgfMDd2wCha+LmykIDZ06oUfFo+wY6Jf8z56ZMMqHvBvDO1ZNrstVgUZxlEQ==", + "license": "Apache-2.0" + }, + "node_modules/@parity/product-sdk-host": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@parity/product-sdk-host/-/product-sdk-host-0.14.1.tgz", + "integrity": "sha512-PF87O0Kb35TyZo+sDlcYo9ZvaUedR8NNbcZvfBf8PBL65Yck10jIDuXE386hl9pgpgOn6o0Y1z6iJfEolhNXsg==", + "license": "Apache-2.0", + "dependencies": { + "@parity/product-sdk-errors": "0.2.0", + "@parity/product-sdk-logger": "0.1.1", + "@parity/result": "0.2.0", + "@parity/truapi": "^0.5.0", + "@polkadot-api/json-rpc-provider": "^0.2.0", + "@polkadot-api/substrate-bindings": "^0.20.3", + "neverthrow": "^8.2.0", + "polkadot-api": "^2.1.6" + } + }, + "node_modules/@parity/product-sdk-host/node_modules/@commander-js/extra-typings": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-15.0.0.tgz", + "integrity": "sha512-yeJlba62xqmkgELUsn7356MEnzLLu/fw2x4lofFqGnXh6YysRdEs2BaLeLtg1+KU0AXvMeqQvTTp+3hBEBK+EA==", + "license": "MIT", + "peerDependencies": { + "commander": "~15.0.0" + } + }, + "node_modules/@parity/product-sdk-host/node_modules/@polkadot-api/cli": { + "version": "0.21.9", + "resolved": "https://registry.npmjs.org/@polkadot-api/cli/-/cli-0.21.9.tgz", + "integrity": "sha512-9aVdF/ocF3DBb8b/gdkcicBIaQkak2kkU4EGW+TunXLIvbIj2X9hH5geOlx8uWNwt0O4O1DIHD/nkXvycG/AcA==", + "license": "MIT", + "dependencies": { + "@commander-js/extra-typings": "^15.0.0", + "@polkadot-api/codegen": "0.22.5", + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/known-chains": "0.12.1", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/observable-client": "0.18.7", + "@polkadot-api/sm-provider": "0.3.8", + "@polkadot-api/smoldot": "0.4.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0", + "@polkadot-api/wasm-executor": "^0.2.3", + "@polkadot-api/ws-middleware": "0.3.6", + "@polkadot-api/ws-provider": "0.9.1", + "@types/node": "^25.9.4", + "commander": "^15.0.0", + "execa": "^9.6.1", + "fs.promises.exists": "^1.1.4", + "ora": "^9.4.1", + "read-pkg": "^10.1.0", + "rollup": "^4.62.2", + "rollup-plugin-esbuild": "^6.2.1", + "rxjs": "^7.8.2", + "tsc-prog": "^2.3.0", + "typescript": "^6.0.3", + "write-package": "^7.2.0" + }, + "bin": { + "papi": "dist/main/src/main.js", + "polkadot-api": "dist/main/src/main.js" + } + }, + "node_modules/@parity/product-sdk-host/node_modules/@polkadot-api/codegen": { + "version": "0.22.5", + "resolved": "https://registry.npmjs.org/@polkadot-api/codegen/-/codegen-0.22.5.tgz", + "integrity": "sha512-zwZJAlviI211zhj5i6oXkrr0crrbO3GZjBd2vC9AshY1pRmyiNLV9DZsXw4E6l4JQwdAAyNWtPdnvvB9T3TpKg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-host/node_modules/@polkadot-api/ink-contracts": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/ink-contracts/-/ink-contracts-0.6.3.tgz", + "integrity": "sha512-XqnM1VDzI5L62xgg+f8le2yEoz8QZbUKEfAfPnHMOgBj9tJiyF15FcOJmnLMO/vq3cGixqh18tzJekLG5YTxtA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-host/node_modules/@polkadot-api/json-rpc-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.2.0.tgz", + "integrity": "sha512-lhkuBS/x06i3djQIN7p8jVkMnYuGsUAEMS6RhdWeEpz7X/8/APER4Wdih7MEBovCuwVSCTjOxl8f+alH7AZHZg==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-host/node_modules/@polkadot-api/json-rpc-provider-proxy": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.4.1.tgz", + "integrity": "sha512-F1Hw01C60jn98KQ0vBbgcwAvEcMcKLsJ5kFzq600sgjc6Rnbn1VF/XjTdrjcIqvfpR9sXyGIrpImrov2Usbrsw==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-host/node_modules/@polkadot-api/known-chains": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/known-chains/-/known-chains-0.12.1.tgz", + "integrity": "sha512-ZW9TYD2y5IhgNy28zi9sByMr0CaP2I9x8HcAI0pwZR2qDC0kysCG6brDlg1UoZKCjRO99SsKO/iU1o434JgMnQ==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-host/node_modules/@polkadot-api/logs-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/logs-provider/-/logs-provider-0.2.0.tgz", + "integrity": "sha512-BH9YdxZu+ZBPPAUwGrvqHPn1hQStL2Im3MmTwYkwXOWW2HlGHULcF65QKvyK+T6/mj2vDvl3MgivnGkUw4pVxg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0" + } + }, + "node_modules/@parity/product-sdk-host/node_modules/@polkadot-api/merkleize-metadata": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/merkleize-metadata/-/merkleize-metadata-1.2.3.tgz", + "integrity": "sha512-WkPbz0p2XQ9c8yXagdnwCHEB70Gnm91okcsd6IXU393//3aPgkxKgb+/Efnz7C5/KQmg02P0zXo7q/n/W/yVCA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-host/node_modules/@polkadot-api/metadata-builders": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-builders/-/metadata-builders-0.14.3.tgz", + "integrity": "sha512-m7CACsiqHzgVEh5WBZGkTV8AQ3CBQKR1YpPQMnlsJfCr/IkgKU0UyWM6WxCmBiReLFVkOfXMtGlpN8+GxpHmww==", + "license": "MIT", + "dependencies": { + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-host/node_modules/@polkadot-api/metadata-compatibility": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-compatibility/-/metadata-compatibility-0.6.3.tgz", + "integrity": "sha512-/Y0uF8nDk60ijydp8Bd37YexPFdB8hBXJWwEgOJHsVlhiny8sVKXiMg+UkJ9BiEk2z+yMbZRCKmhNpTpizo7aw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3" + } + }, + "node_modules/@parity/product-sdk-host/node_modules/@polkadot-api/observable-client": { + "version": "0.18.7", + "resolved": "https://registry.npmjs.org/@polkadot-api/observable-client/-/observable-client-0.18.7.tgz", + "integrity": "sha512-/eQi3D8jbXLg/L1hZX4eX0/+nO3kTxkOfYeWEtFhtOvFdfqiiy2DdXG3Zg5QzgHmfG+11p4vSlLiPCC2UvS5Qw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-host/node_modules/@polkadot-api/pjs-signer": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/pjs-signer/-/pjs-signer-0.7.3.tgz", + "integrity": "sha512-U7BLFZfnpFMxCh/scJoLXT6oSbfZtZgMTkiu+TbuWQljAb/1ttrQOfshub5VihNyD5rHajp/2Fq0ONZoL5N5PA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signers-common": "0.2.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-host/node_modules/@polkadot-api/raw-client": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/raw-client/-/raw-client-0.3.0.tgz", + "integrity": "sha512-u/wM9W7ugIXxBSOEV8+zvQI43b5vgSI0pvE0Rg8PV0G65BTLK0SMc2o2SQL0HtS5+bi5sw/gg4reBJ/0a4U0cw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0" + } + }, + "node_modules/@parity/product-sdk-host/node_modules/@polkadot-api/signer": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/signer/-/signer-0.3.3.tgz", + "integrity": "sha512-bmEV65TwgwbMfiecl7ZQ3k5lfkCOx9FPwPYkVvALcPiqb/d3zYwT9LL/E00hj+EB6IKMlMelEQVuchcW5i/92w==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.2.0", + "@polkadot-api/merkleize-metadata": "1.2.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signers-common": "0.2.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-host/node_modules/@polkadot-api/signers-common": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/signers-common/-/signers-common-0.2.3.tgz", + "integrity": "sha512-SzGLJMxug31Y1P8+0I809ICpDrayztUXFFh2TV02GuwjEnY6mDGnmX56wowZF5yCn+WteeRdcTJTK7Whk3AGYQ==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-host/node_modules/@polkadot-api/sm-provider": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@polkadot-api/sm-provider/-/sm-provider-0.3.8.tgz", + "integrity": "sha512-jIvzBNsBsh6LIpCrUOa7miZdQPwLPns5IUQyz/8v84R3ON7URSIVbtywGSR8O83BZGyW/DmVkqa5lClLE2fkqw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/json-rpc-provider-proxy": "0.4.1" + }, + "peerDependencies": { + "@polkadot-api/smoldot": ">=0.3" + } + }, + "node_modules/@parity/product-sdk-host/node_modules/@polkadot-api/smoldot": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@polkadot-api/smoldot/-/smoldot-0.4.6.tgz", + "integrity": "sha512-gOXMJ10fXOub0zPP1cGAYeyf29BA2fB+qukrjXGcGN9b0Ya+lDgV7A7Q7y9JOrlGLeKbd1YdrYLAe/F43V7b9Q==", + "license": "MIT", + "dependencies": { + "@types/node": "^25.9.4", + "smoldot": "~3.3.1" + } + }, + "node_modules/@parity/product-sdk-host/node_modules/@polkadot-api/substrate-bindings": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-bindings/-/substrate-bindings-0.20.3.tgz", + "integrity": "sha512-9iqC71fx1ee9ld1NZV8PFime5vryi0kt1bKCSlvNgO6dqMc06sMZuZ8WPjOzWLCHiKHLuphdMs3rVBBaeCP3yg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.2.0", + "@polkadot-api/utils": "0.4.0", + "@scure/base": "^2.2.0", + "scale-ts": "^1.6.1" + } + }, + "node_modules/@parity/product-sdk-host/node_modules/@polkadot-api/substrate-client": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-client/-/substrate-client-0.7.0.tgz", + "integrity": "sha512-TWCc4MAMa5SLVQXmomLHknbj+bztQ/Yclgwm8ENBhz8hR7c9rw9FBAkCa02jMBMCAygPhp3ayGRq+UFcF8KIxQ==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/raw-client": "0.3.0", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-host/node_modules/@polkadot-api/utils": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/utils/-/utils-0.4.0.tgz", + "integrity": "sha512-9b/hwRM0UloLWV7SfpNaSD/4k8UQAHoaACAk7Xe+1MlfAm2JtnmPiB1GfGrfTyBlsrJVUIBCZpEmbmxVMaIqBA==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-host/node_modules/@polkadot-api/ws-provider": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/ws-provider/-/ws-provider-0.9.1.tgz", + "integrity": "sha512-Ft2QJEjLZgTyKiCEbz3urSOxNfMdqNkg+QKqJhRbOFB7JtR1qMTvLE3TjM+5d2mNlEpOc3j2KDK3APNSj7uQ2Q==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/json-rpc-provider-proxy": "0.4.1", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-host/node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@parity/product-sdk-host/node_modules/polkadot-api": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/polkadot-api/-/polkadot-api-2.2.1.tgz", + "integrity": "sha512-eiZVUhI1gY4ycBtsCo/yaDXyohUeuqnyKun+bwj90ADSn+jlfjZ4sajAqIhJFwPxbCT75+Zzbln4ZjuZ5PWNWA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/cli": "0.21.9", + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/known-chains": "0.12.1", + "@polkadot-api/logs-provider": "0.2.0", + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/observable-client": "0.18.7", + "@polkadot-api/pjs-signer": "0.7.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signer": "0.3.3", + "@polkadot-api/sm-provider": "0.3.8", + "@polkadot-api/smoldot": "0.4.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0", + "@polkadot-api/ws-middleware": "0.3.6", + "@polkadot-api/ws-provider": "0.9.1", + "@rx-state/core": "^0.1.4" + }, + "bin": { + "papi": "bin/cli.js", + "polkadot-api": "bin/cli.js" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-host/node_modules/smoldot": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/smoldot/-/smoldot-3.3.2.tgz", + "integrity": "sha512-Zl4h/0gsw8cfTZzuJ7LV7mtR6QjxltwYjMY7MsVw0oXBXrLK8zyOS6DS9Vjsy57pX1vBMg6UVhHxjbH3W905zA==", + "license": "GPL-3.0-or-later WITH Classpath-exception-2.0", + "dependencies": { + "ws": "^8.8.1" + } + }, + "node_modules/@parity/product-sdk-host/node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@parity/product-sdk-keys": { + "version": "0.3.16", + "resolved": "https://registry.npmjs.org/@parity/product-sdk-keys/-/product-sdk-keys-0.3.16.tgz", + "integrity": "sha512-dnaHPQVOyxE7yEET/NsHi/0l8rI+vkH0m1OaZQ+qMkv4zwrFqXbhcXO7z6Kj+RHp5hswkmrcpEmjl4DeV5Z2xQ==", + "license": "Apache-2.0", + "dependencies": { + "@parity/product-sdk-address": "0.1.1", + "@parity/product-sdk-crypto": "0.1.1", + "@parity/product-sdk-local-storage": "0.3.2", + "@polkadot-labs/hdkd": "^0.0.28", + "@polkadot-labs/hdkd-helpers": "^0.0.30", + "@scure/sr25519": "^2.2.0", + "polkadot-api": "^2.1.6", + "scale-ts": "^1.6.1" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@commander-js/extra-typings": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-15.0.0.tgz", + "integrity": "sha512-yeJlba62xqmkgELUsn7356MEnzLLu/fw2x4lofFqGnXh6YysRdEs2BaLeLtg1+KU0AXvMeqQvTTp+3hBEBK+EA==", + "license": "MIT", + "peerDependencies": { + "commander": "~15.0.0" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-api/cli": { + "version": "0.21.9", + "resolved": "https://registry.npmjs.org/@polkadot-api/cli/-/cli-0.21.9.tgz", + "integrity": "sha512-9aVdF/ocF3DBb8b/gdkcicBIaQkak2kkU4EGW+TunXLIvbIj2X9hH5geOlx8uWNwt0O4O1DIHD/nkXvycG/AcA==", + "license": "MIT", + "dependencies": { + "@commander-js/extra-typings": "^15.0.0", + "@polkadot-api/codegen": "0.22.5", + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/known-chains": "0.12.1", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/observable-client": "0.18.7", + "@polkadot-api/sm-provider": "0.3.8", + "@polkadot-api/smoldot": "0.4.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0", + "@polkadot-api/wasm-executor": "^0.2.3", + "@polkadot-api/ws-middleware": "0.3.6", + "@polkadot-api/ws-provider": "0.9.1", + "@types/node": "^25.9.4", + "commander": "^15.0.0", + "execa": "^9.6.1", + "fs.promises.exists": "^1.1.4", + "ora": "^9.4.1", + "read-pkg": "^10.1.0", + "rollup": "^4.62.2", + "rollup-plugin-esbuild": "^6.2.1", + "rxjs": "^7.8.2", + "tsc-prog": "^2.3.0", + "typescript": "^6.0.3", + "write-package": "^7.2.0" + }, + "bin": { + "papi": "dist/main/src/main.js", + "polkadot-api": "dist/main/src/main.js" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-api/codegen": { + "version": "0.22.5", + "resolved": "https://registry.npmjs.org/@polkadot-api/codegen/-/codegen-0.22.5.tgz", + "integrity": "sha512-zwZJAlviI211zhj5i6oXkrr0crrbO3GZjBd2vC9AshY1pRmyiNLV9DZsXw4E6l4JQwdAAyNWtPdnvvB9T3TpKg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-api/ink-contracts": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/ink-contracts/-/ink-contracts-0.6.3.tgz", + "integrity": "sha512-XqnM1VDzI5L62xgg+f8le2yEoz8QZbUKEfAfPnHMOgBj9tJiyF15FcOJmnLMO/vq3cGixqh18tzJekLG5YTxtA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-api/json-rpc-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.2.0.tgz", + "integrity": "sha512-lhkuBS/x06i3djQIN7p8jVkMnYuGsUAEMS6RhdWeEpz7X/8/APER4Wdih7MEBovCuwVSCTjOxl8f+alH7AZHZg==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-api/json-rpc-provider-proxy": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.4.1.tgz", + "integrity": "sha512-F1Hw01C60jn98KQ0vBbgcwAvEcMcKLsJ5kFzq600sgjc6Rnbn1VF/XjTdrjcIqvfpR9sXyGIrpImrov2Usbrsw==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-api/known-chains": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/known-chains/-/known-chains-0.12.1.tgz", + "integrity": "sha512-ZW9TYD2y5IhgNy28zi9sByMr0CaP2I9x8HcAI0pwZR2qDC0kysCG6brDlg1UoZKCjRO99SsKO/iU1o434JgMnQ==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-api/logs-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/logs-provider/-/logs-provider-0.2.0.tgz", + "integrity": "sha512-BH9YdxZu+ZBPPAUwGrvqHPn1hQStL2Im3MmTwYkwXOWW2HlGHULcF65QKvyK+T6/mj2vDvl3MgivnGkUw4pVxg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-api/merkleize-metadata": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/merkleize-metadata/-/merkleize-metadata-1.2.3.tgz", + "integrity": "sha512-WkPbz0p2XQ9c8yXagdnwCHEB70Gnm91okcsd6IXU393//3aPgkxKgb+/Efnz7C5/KQmg02P0zXo7q/n/W/yVCA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-api/metadata-builders": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-builders/-/metadata-builders-0.14.3.tgz", + "integrity": "sha512-m7CACsiqHzgVEh5WBZGkTV8AQ3CBQKR1YpPQMnlsJfCr/IkgKU0UyWM6WxCmBiReLFVkOfXMtGlpN8+GxpHmww==", + "license": "MIT", + "dependencies": { + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-api/metadata-compatibility": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-compatibility/-/metadata-compatibility-0.6.3.tgz", + "integrity": "sha512-/Y0uF8nDk60ijydp8Bd37YexPFdB8hBXJWwEgOJHsVlhiny8sVKXiMg+UkJ9BiEk2z+yMbZRCKmhNpTpizo7aw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-api/observable-client": { + "version": "0.18.7", + "resolved": "https://registry.npmjs.org/@polkadot-api/observable-client/-/observable-client-0.18.7.tgz", + "integrity": "sha512-/eQi3D8jbXLg/L1hZX4eX0/+nO3kTxkOfYeWEtFhtOvFdfqiiy2DdXG3Zg5QzgHmfG+11p4vSlLiPCC2UvS5Qw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-api/pjs-signer": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/pjs-signer/-/pjs-signer-0.7.3.tgz", + "integrity": "sha512-U7BLFZfnpFMxCh/scJoLXT6oSbfZtZgMTkiu+TbuWQljAb/1ttrQOfshub5VihNyD5rHajp/2Fq0ONZoL5N5PA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signers-common": "0.2.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-api/raw-client": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/raw-client/-/raw-client-0.3.0.tgz", + "integrity": "sha512-u/wM9W7ugIXxBSOEV8+zvQI43b5vgSI0pvE0Rg8PV0G65BTLK0SMc2o2SQL0HtS5+bi5sw/gg4reBJ/0a4U0cw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-api/signer": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/signer/-/signer-0.3.3.tgz", + "integrity": "sha512-bmEV65TwgwbMfiecl7ZQ3k5lfkCOx9FPwPYkVvALcPiqb/d3zYwT9LL/E00hj+EB6IKMlMelEQVuchcW5i/92w==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.2.0", + "@polkadot-api/merkleize-metadata": "1.2.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signers-common": "0.2.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-api/signers-common": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/signers-common/-/signers-common-0.2.3.tgz", + "integrity": "sha512-SzGLJMxug31Y1P8+0I809ICpDrayztUXFFh2TV02GuwjEnY6mDGnmX56wowZF5yCn+WteeRdcTJTK7Whk3AGYQ==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-api/sm-provider": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@polkadot-api/sm-provider/-/sm-provider-0.3.8.tgz", + "integrity": "sha512-jIvzBNsBsh6LIpCrUOa7miZdQPwLPns5IUQyz/8v84R3ON7URSIVbtywGSR8O83BZGyW/DmVkqa5lClLE2fkqw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/json-rpc-provider-proxy": "0.4.1" + }, + "peerDependencies": { + "@polkadot-api/smoldot": ">=0.3" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-api/smoldot": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@polkadot-api/smoldot/-/smoldot-0.4.6.tgz", + "integrity": "sha512-gOXMJ10fXOub0zPP1cGAYeyf29BA2fB+qukrjXGcGN9b0Ya+lDgV7A7Q7y9JOrlGLeKbd1YdrYLAe/F43V7b9Q==", + "license": "MIT", + "dependencies": { + "@types/node": "^25.9.4", + "smoldot": "~3.3.1" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-api/substrate-bindings": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-bindings/-/substrate-bindings-0.20.3.tgz", + "integrity": "sha512-9iqC71fx1ee9ld1NZV8PFime5vryi0kt1bKCSlvNgO6dqMc06sMZuZ8WPjOzWLCHiKHLuphdMs3rVBBaeCP3yg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.2.0", + "@polkadot-api/utils": "0.4.0", + "@scure/base": "^2.2.0", + "scale-ts": "^1.6.1" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-api/substrate-client": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-client/-/substrate-client-0.7.0.tgz", + "integrity": "sha512-TWCc4MAMa5SLVQXmomLHknbj+bztQ/Yclgwm8ENBhz8hR7c9rw9FBAkCa02jMBMCAygPhp3ayGRq+UFcF8KIxQ==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/raw-client": "0.3.0", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-api/utils": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/utils/-/utils-0.4.0.tgz", + "integrity": "sha512-9b/hwRM0UloLWV7SfpNaSD/4k8UQAHoaACAk7Xe+1MlfAm2JtnmPiB1GfGrfTyBlsrJVUIBCZpEmbmxVMaIqBA==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-api/ws-provider": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/ws-provider/-/ws-provider-0.9.1.tgz", + "integrity": "sha512-Ft2QJEjLZgTyKiCEbz3urSOxNfMdqNkg+QKqJhRbOFB7JtR1qMTvLE3TjM+5d2mNlEpOc3j2KDK3APNSj7uQ2Q==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/json-rpc-provider-proxy": "0.4.1", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-labs/hdkd": { + "version": "0.0.28", + "resolved": "https://registry.npmjs.org/@polkadot-labs/hdkd/-/hdkd-0.0.28.tgz", + "integrity": "sha512-LpdqtQRpcgZQ5Mr8J0ddMA5ZufsbI4W3KuJkVdoYMnSmWs4179LigDb1rTYAOtyCg2jWUjf7rWP0mGxQQvNrHw==", + "license": "MIT", + "dependencies": { + "@polkadot-labs/hdkd-helpers": "~0.0.29" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-labs/hdkd-helpers": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@polkadot-labs/hdkd-helpers/-/hdkd-helpers-0.0.30.tgz", + "integrity": "sha512-qWmmD6ayj14RenDuDFfjF3sHS7ObqPzwIIMPcSVoDeKFSeQV7RY0HwyhC5CG4i6FoguMzak2dbtjYpNN5XQiwQ==", + "license": "MIT", + "dependencies": { + "@noble/curves": "^2.2.0", + "@noble/hashes": "^2.2.0", + "@scure/base": "^2.2.0", + "@scure/sr25519": "^1.0.0", + "scale-ts": "^1.6.1" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-labs/hdkd-helpers/node_modules/@scure/sr25519": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@scure/sr25519/-/sr25519-1.0.0.tgz", + "integrity": "sha512-b+uhK5akMINXZP95F3gJGcb5CMKYxf+q55fwMl0GoBwZDbWolmGNi1FrBSwuaZX5AhqS2byHiAueZgtDNpot2A==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~2.0.0", + "@noble/hashes": "~2.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-labs/hdkd-helpers/node_modules/@scure/sr25519/node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@polkadot-labs/hdkd-helpers/node_modules/@scure/sr25519/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/@scure/sr25519": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/sr25519/-/sr25519-2.2.0.tgz", + "integrity": "sha512-UTOZb6Hzw44REQdl2SWNBhBFIoqOIhMLNIz3zYyVQLbqdshhuyuuxYoibKHlDg9oqdwdCHQe5LkTsevugPpUbw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~2.2.0", + "@noble/hashes": "~2.2.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/polkadot-api": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/polkadot-api/-/polkadot-api-2.2.1.tgz", + "integrity": "sha512-eiZVUhI1gY4ycBtsCo/yaDXyohUeuqnyKun+bwj90ADSn+jlfjZ4sajAqIhJFwPxbCT75+Zzbln4ZjuZ5PWNWA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/cli": "0.21.9", + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/known-chains": "0.12.1", + "@polkadot-api/logs-provider": "0.2.0", + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/observable-client": "0.18.7", + "@polkadot-api/pjs-signer": "0.7.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signer": "0.3.3", + "@polkadot-api/sm-provider": "0.3.8", + "@polkadot-api/smoldot": "0.4.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0", + "@polkadot-api/ws-middleware": "0.3.6", + "@polkadot-api/ws-provider": "0.9.1", + "@rx-state/core": "^0.1.4" + }, + "bin": { + "papi": "bin/cli.js", + "polkadot-api": "bin/cli.js" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/smoldot": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/smoldot/-/smoldot-3.3.2.tgz", + "integrity": "sha512-Zl4h/0gsw8cfTZzuJ7LV7mtR6QjxltwYjMY7MsVw0oXBXrLK8zyOS6DS9Vjsy57pX1vBMg6UVhHxjbH3W905zA==", + "license": "GPL-3.0-or-later WITH Classpath-exception-2.0", + "dependencies": { + "ws": "^8.8.1" + } + }, + "node_modules/@parity/product-sdk-keys/node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@parity/product-sdk-local-storage": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@parity/product-sdk-local-storage/-/product-sdk-local-storage-0.3.2.tgz", + "integrity": "sha512-1KJGOZrf6pj1P19j8AVbVC5NTmQe5KGE2VJ0gzJDYCHGWPYECtm7Fc0zfq6AAZbyPJkgsuZz/K4+MRijf4lf2A==", + "license": "Apache-2.0", + "dependencies": { + "@parity/product-sdk-host": "0.14.1", + "@parity/product-sdk-logger": "0.1.1" + } + }, + "node_modules/@parity/product-sdk-logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@parity/product-sdk-logger/-/product-sdk-logger-0.1.1.tgz", + "integrity": "sha512-AiSV3TTNlMZJftLQsO78BZsEymGFuJtGMSpGrJ+vUtqaZavWaW/Hc6MICBLnEYgeCrdNpv7QBso3dRsTfnAZXQ==", + "license": "Apache-2.0" + }, + "node_modules/@parity/product-sdk-signer": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@parity/product-sdk-signer/-/product-sdk-signer-0.11.1.tgz", + "integrity": "sha512-9wGaazcmqVaSkJckcZhHFkhpPQJSNVgvFRbH2qIXkvAmxKMfW9xzdplsEoMnxRcvUree6I1YK2m3kn61/dBpjw==", + "license": "Apache-2.0", + "dependencies": { + "@parity/product-sdk-address": "0.1.1", + "@parity/product-sdk-errors": "0.2.0", + "@parity/product-sdk-host": "0.14.1", + "@parity/product-sdk-keys": "0.3.16", + "@parity/product-sdk-logger": "0.1.1", + "@parity/result": "0.2.0", + "polkadot-api": "^2.1.6" + } + }, + "node_modules/@parity/product-sdk-signer/node_modules/@commander-js/extra-typings": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-15.0.0.tgz", + "integrity": "sha512-yeJlba62xqmkgELUsn7356MEnzLLu/fw2x4lofFqGnXh6YysRdEs2BaLeLtg1+KU0AXvMeqQvTTp+3hBEBK+EA==", + "license": "MIT", + "peerDependencies": { + "commander": "~15.0.0" + } + }, + "node_modules/@parity/product-sdk-signer/node_modules/@polkadot-api/cli": { + "version": "0.21.9", + "resolved": "https://registry.npmjs.org/@polkadot-api/cli/-/cli-0.21.9.tgz", + "integrity": "sha512-9aVdF/ocF3DBb8b/gdkcicBIaQkak2kkU4EGW+TunXLIvbIj2X9hH5geOlx8uWNwt0O4O1DIHD/nkXvycG/AcA==", + "license": "MIT", + "dependencies": { + "@commander-js/extra-typings": "^15.0.0", + "@polkadot-api/codegen": "0.22.5", + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/known-chains": "0.12.1", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/observable-client": "0.18.7", + "@polkadot-api/sm-provider": "0.3.8", + "@polkadot-api/smoldot": "0.4.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0", + "@polkadot-api/wasm-executor": "^0.2.3", + "@polkadot-api/ws-middleware": "0.3.6", + "@polkadot-api/ws-provider": "0.9.1", + "@types/node": "^25.9.4", + "commander": "^15.0.0", + "execa": "^9.6.1", + "fs.promises.exists": "^1.1.4", + "ora": "^9.4.1", + "read-pkg": "^10.1.0", + "rollup": "^4.62.2", + "rollup-plugin-esbuild": "^6.2.1", + "rxjs": "^7.8.2", + "tsc-prog": "^2.3.0", + "typescript": "^6.0.3", + "write-package": "^7.2.0" + }, + "bin": { + "papi": "dist/main/src/main.js", + "polkadot-api": "dist/main/src/main.js" + } + }, + "node_modules/@parity/product-sdk-signer/node_modules/@polkadot-api/codegen": { + "version": "0.22.5", + "resolved": "https://registry.npmjs.org/@polkadot-api/codegen/-/codegen-0.22.5.tgz", + "integrity": "sha512-zwZJAlviI211zhj5i6oXkrr0crrbO3GZjBd2vC9AshY1pRmyiNLV9DZsXw4E6l4JQwdAAyNWtPdnvvB9T3TpKg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-signer/node_modules/@polkadot-api/ink-contracts": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/ink-contracts/-/ink-contracts-0.6.3.tgz", + "integrity": "sha512-XqnM1VDzI5L62xgg+f8le2yEoz8QZbUKEfAfPnHMOgBj9tJiyF15FcOJmnLMO/vq3cGixqh18tzJekLG5YTxtA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-signer/node_modules/@polkadot-api/json-rpc-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.2.0.tgz", + "integrity": "sha512-lhkuBS/x06i3djQIN7p8jVkMnYuGsUAEMS6RhdWeEpz7X/8/APER4Wdih7MEBovCuwVSCTjOxl8f+alH7AZHZg==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-signer/node_modules/@polkadot-api/json-rpc-provider-proxy": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.4.1.tgz", + "integrity": "sha512-F1Hw01C60jn98KQ0vBbgcwAvEcMcKLsJ5kFzq600sgjc6Rnbn1VF/XjTdrjcIqvfpR9sXyGIrpImrov2Usbrsw==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-signer/node_modules/@polkadot-api/known-chains": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/known-chains/-/known-chains-0.12.1.tgz", + "integrity": "sha512-ZW9TYD2y5IhgNy28zi9sByMr0CaP2I9x8HcAI0pwZR2qDC0kysCG6brDlg1UoZKCjRO99SsKO/iU1o434JgMnQ==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-signer/node_modules/@polkadot-api/logs-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/logs-provider/-/logs-provider-0.2.0.tgz", + "integrity": "sha512-BH9YdxZu+ZBPPAUwGrvqHPn1hQStL2Im3MmTwYkwXOWW2HlGHULcF65QKvyK+T6/mj2vDvl3MgivnGkUw4pVxg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0" + } + }, + "node_modules/@parity/product-sdk-signer/node_modules/@polkadot-api/merkleize-metadata": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/merkleize-metadata/-/merkleize-metadata-1.2.3.tgz", + "integrity": "sha512-WkPbz0p2XQ9c8yXagdnwCHEB70Gnm91okcsd6IXU393//3aPgkxKgb+/Efnz7C5/KQmg02P0zXo7q/n/W/yVCA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-signer/node_modules/@polkadot-api/metadata-builders": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-builders/-/metadata-builders-0.14.3.tgz", + "integrity": "sha512-m7CACsiqHzgVEh5WBZGkTV8AQ3CBQKR1YpPQMnlsJfCr/IkgKU0UyWM6WxCmBiReLFVkOfXMtGlpN8+GxpHmww==", + "license": "MIT", + "dependencies": { + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-signer/node_modules/@polkadot-api/metadata-compatibility": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-compatibility/-/metadata-compatibility-0.6.3.tgz", + "integrity": "sha512-/Y0uF8nDk60ijydp8Bd37YexPFdB8hBXJWwEgOJHsVlhiny8sVKXiMg+UkJ9BiEk2z+yMbZRCKmhNpTpizo7aw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3" + } + }, + "node_modules/@parity/product-sdk-signer/node_modules/@polkadot-api/observable-client": { + "version": "0.18.7", + "resolved": "https://registry.npmjs.org/@polkadot-api/observable-client/-/observable-client-0.18.7.tgz", + "integrity": "sha512-/eQi3D8jbXLg/L1hZX4eX0/+nO3kTxkOfYeWEtFhtOvFdfqiiy2DdXG3Zg5QzgHmfG+11p4vSlLiPCC2UvS5Qw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-signer/node_modules/@polkadot-api/pjs-signer": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/pjs-signer/-/pjs-signer-0.7.3.tgz", + "integrity": "sha512-U7BLFZfnpFMxCh/scJoLXT6oSbfZtZgMTkiu+TbuWQljAb/1ttrQOfshub5VihNyD5rHajp/2Fq0ONZoL5N5PA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signers-common": "0.2.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-signer/node_modules/@polkadot-api/raw-client": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/raw-client/-/raw-client-0.3.0.tgz", + "integrity": "sha512-u/wM9W7ugIXxBSOEV8+zvQI43b5vgSI0pvE0Rg8PV0G65BTLK0SMc2o2SQL0HtS5+bi5sw/gg4reBJ/0a4U0cw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0" + } + }, + "node_modules/@parity/product-sdk-signer/node_modules/@polkadot-api/signer": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/signer/-/signer-0.3.3.tgz", + "integrity": "sha512-bmEV65TwgwbMfiecl7ZQ3k5lfkCOx9FPwPYkVvALcPiqb/d3zYwT9LL/E00hj+EB6IKMlMelEQVuchcW5i/92w==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.2.0", + "@polkadot-api/merkleize-metadata": "1.2.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signers-common": "0.2.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-signer/node_modules/@polkadot-api/signers-common": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/signers-common/-/signers-common-0.2.3.tgz", + "integrity": "sha512-SzGLJMxug31Y1P8+0I809ICpDrayztUXFFh2TV02GuwjEnY6mDGnmX56wowZF5yCn+WteeRdcTJTK7Whk3AGYQ==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-signer/node_modules/@polkadot-api/sm-provider": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@polkadot-api/sm-provider/-/sm-provider-0.3.8.tgz", + "integrity": "sha512-jIvzBNsBsh6LIpCrUOa7miZdQPwLPns5IUQyz/8v84R3ON7URSIVbtywGSR8O83BZGyW/DmVkqa5lClLE2fkqw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/json-rpc-provider-proxy": "0.4.1" + }, + "peerDependencies": { + "@polkadot-api/smoldot": ">=0.3" + } + }, + "node_modules/@parity/product-sdk-signer/node_modules/@polkadot-api/smoldot": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@polkadot-api/smoldot/-/smoldot-0.4.6.tgz", + "integrity": "sha512-gOXMJ10fXOub0zPP1cGAYeyf29BA2fB+qukrjXGcGN9b0Ya+lDgV7A7Q7y9JOrlGLeKbd1YdrYLAe/F43V7b9Q==", + "license": "MIT", + "dependencies": { + "@types/node": "^25.9.4", + "smoldot": "~3.3.1" + } + }, + "node_modules/@parity/product-sdk-signer/node_modules/@polkadot-api/substrate-bindings": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-bindings/-/substrate-bindings-0.20.3.tgz", + "integrity": "sha512-9iqC71fx1ee9ld1NZV8PFime5vryi0kt1bKCSlvNgO6dqMc06sMZuZ8WPjOzWLCHiKHLuphdMs3rVBBaeCP3yg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.2.0", + "@polkadot-api/utils": "0.4.0", + "@scure/base": "^2.2.0", + "scale-ts": "^1.6.1" + } + }, + "node_modules/@parity/product-sdk-signer/node_modules/@polkadot-api/substrate-client": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-client/-/substrate-client-0.7.0.tgz", + "integrity": "sha512-TWCc4MAMa5SLVQXmomLHknbj+bztQ/Yclgwm8ENBhz8hR7c9rw9FBAkCa02jMBMCAygPhp3ayGRq+UFcF8KIxQ==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/raw-client": "0.3.0", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-signer/node_modules/@polkadot-api/utils": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/utils/-/utils-0.4.0.tgz", + "integrity": "sha512-9b/hwRM0UloLWV7SfpNaSD/4k8UQAHoaACAk7Xe+1MlfAm2JtnmPiB1GfGrfTyBlsrJVUIBCZpEmbmxVMaIqBA==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-signer/node_modules/@polkadot-api/ws-provider": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/ws-provider/-/ws-provider-0.9.1.tgz", + "integrity": "sha512-Ft2QJEjLZgTyKiCEbz3urSOxNfMdqNkg+QKqJhRbOFB7JtR1qMTvLE3TjM+5d2mNlEpOc3j2KDK3APNSj7uQ2Q==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/json-rpc-provider-proxy": "0.4.1", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-signer/node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@parity/product-sdk-signer/node_modules/polkadot-api": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/polkadot-api/-/polkadot-api-2.2.1.tgz", + "integrity": "sha512-eiZVUhI1gY4ycBtsCo/yaDXyohUeuqnyKun+bwj90ADSn+jlfjZ4sajAqIhJFwPxbCT75+Zzbln4ZjuZ5PWNWA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/cli": "0.21.9", + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/known-chains": "0.12.1", + "@polkadot-api/logs-provider": "0.2.0", + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/observable-client": "0.18.7", + "@polkadot-api/pjs-signer": "0.7.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signer": "0.3.3", + "@polkadot-api/sm-provider": "0.3.8", + "@polkadot-api/smoldot": "0.4.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0", + "@polkadot-api/ws-middleware": "0.3.6", + "@polkadot-api/ws-provider": "0.9.1", + "@rx-state/core": "^0.1.4" + }, + "bin": { + "papi": "bin/cli.js", + "polkadot-api": "bin/cli.js" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-signer/node_modules/smoldot": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/smoldot/-/smoldot-3.3.2.tgz", + "integrity": "sha512-Zl4h/0gsw8cfTZzuJ7LV7mtR6QjxltwYjMY7MsVw0oXBXrLK8zyOS6DS9Vjsy57pX1vBMg6UVhHxjbH3W905zA==", + "license": "GPL-3.0-or-later WITH Classpath-exception-2.0", + "dependencies": { + "ws": "^8.8.1" + } + }, + "node_modules/@parity/product-sdk-signer/node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@parity/product-sdk-tx": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@parity/product-sdk-tx/-/product-sdk-tx-0.3.2.tgz", + "integrity": "sha512-Y10Sw/ZluIAA6+zB9Ty+y0bSwbrEVIeBKN3umrQXHyseDfBmxWEXkwlhjCxnusJ44wtUgrIN9BsNRGI/51ffKQ==", + "license": "Apache-2.0", + "dependencies": { + "@parity/product-sdk-errors": "0.2.0", + "@parity/product-sdk-keys": "0.3.16", + "@parity/product-sdk-logger": "0.1.1", + "@parity/result": "0.2.0", + "@polkadot-labs/hdkd-helpers": "^0.0.30", + "polkadot-api": "^2.1.6" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/@commander-js/extra-typings": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-15.0.0.tgz", + "integrity": "sha512-yeJlba62xqmkgELUsn7356MEnzLLu/fw2x4lofFqGnXh6YysRdEs2BaLeLtg1+KU0AXvMeqQvTTp+3hBEBK+EA==", + "license": "MIT", + "peerDependencies": { + "commander": "~15.0.0" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/@polkadot-api/cli": { + "version": "0.21.9", + "resolved": "https://registry.npmjs.org/@polkadot-api/cli/-/cli-0.21.9.tgz", + "integrity": "sha512-9aVdF/ocF3DBb8b/gdkcicBIaQkak2kkU4EGW+TunXLIvbIj2X9hH5geOlx8uWNwt0O4O1DIHD/nkXvycG/AcA==", + "license": "MIT", + "dependencies": { + "@commander-js/extra-typings": "^15.0.0", + "@polkadot-api/codegen": "0.22.5", + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/known-chains": "0.12.1", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/observable-client": "0.18.7", + "@polkadot-api/sm-provider": "0.3.8", + "@polkadot-api/smoldot": "0.4.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0", + "@polkadot-api/wasm-executor": "^0.2.3", + "@polkadot-api/ws-middleware": "0.3.6", + "@polkadot-api/ws-provider": "0.9.1", + "@types/node": "^25.9.4", + "commander": "^15.0.0", + "execa": "^9.6.1", + "fs.promises.exists": "^1.1.4", + "ora": "^9.4.1", + "read-pkg": "^10.1.0", + "rollup": "^4.62.2", + "rollup-plugin-esbuild": "^6.2.1", + "rxjs": "^7.8.2", + "tsc-prog": "^2.3.0", + "typescript": "^6.0.3", + "write-package": "^7.2.0" + }, + "bin": { + "papi": "dist/main/src/main.js", + "polkadot-api": "dist/main/src/main.js" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/@polkadot-api/codegen": { + "version": "0.22.5", + "resolved": "https://registry.npmjs.org/@polkadot-api/codegen/-/codegen-0.22.5.tgz", + "integrity": "sha512-zwZJAlviI211zhj5i6oXkrr0crrbO3GZjBd2vC9AshY1pRmyiNLV9DZsXw4E6l4JQwdAAyNWtPdnvvB9T3TpKg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/@polkadot-api/ink-contracts": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/ink-contracts/-/ink-contracts-0.6.3.tgz", + "integrity": "sha512-XqnM1VDzI5L62xgg+f8le2yEoz8QZbUKEfAfPnHMOgBj9tJiyF15FcOJmnLMO/vq3cGixqh18tzJekLG5YTxtA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/@polkadot-api/json-rpc-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.2.0.tgz", + "integrity": "sha512-lhkuBS/x06i3djQIN7p8jVkMnYuGsUAEMS6RhdWeEpz7X/8/APER4Wdih7MEBovCuwVSCTjOxl8f+alH7AZHZg==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-tx/node_modules/@polkadot-api/json-rpc-provider-proxy": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.4.1.tgz", + "integrity": "sha512-F1Hw01C60jn98KQ0vBbgcwAvEcMcKLsJ5kFzq600sgjc6Rnbn1VF/XjTdrjcIqvfpR9sXyGIrpImrov2Usbrsw==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-tx/node_modules/@polkadot-api/known-chains": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/known-chains/-/known-chains-0.12.1.tgz", + "integrity": "sha512-ZW9TYD2y5IhgNy28zi9sByMr0CaP2I9x8HcAI0pwZR2qDC0kysCG6brDlg1UoZKCjRO99SsKO/iU1o434JgMnQ==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-tx/node_modules/@polkadot-api/logs-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/logs-provider/-/logs-provider-0.2.0.tgz", + "integrity": "sha512-BH9YdxZu+ZBPPAUwGrvqHPn1hQStL2Im3MmTwYkwXOWW2HlGHULcF65QKvyK+T6/mj2vDvl3MgivnGkUw4pVxg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/@polkadot-api/merkleize-metadata": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/merkleize-metadata/-/merkleize-metadata-1.2.3.tgz", + "integrity": "sha512-WkPbz0p2XQ9c8yXagdnwCHEB70Gnm91okcsd6IXU393//3aPgkxKgb+/Efnz7C5/KQmg02P0zXo7q/n/W/yVCA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/@polkadot-api/metadata-builders": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-builders/-/metadata-builders-0.14.3.tgz", + "integrity": "sha512-m7CACsiqHzgVEh5WBZGkTV8AQ3CBQKR1YpPQMnlsJfCr/IkgKU0UyWM6WxCmBiReLFVkOfXMtGlpN8+GxpHmww==", + "license": "MIT", + "dependencies": { + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/@polkadot-api/metadata-compatibility": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-compatibility/-/metadata-compatibility-0.6.3.tgz", + "integrity": "sha512-/Y0uF8nDk60ijydp8Bd37YexPFdB8hBXJWwEgOJHsVlhiny8sVKXiMg+UkJ9BiEk2z+yMbZRCKmhNpTpizo7aw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/@polkadot-api/observable-client": { + "version": "0.18.7", + "resolved": "https://registry.npmjs.org/@polkadot-api/observable-client/-/observable-client-0.18.7.tgz", + "integrity": "sha512-/eQi3D8jbXLg/L1hZX4eX0/+nO3kTxkOfYeWEtFhtOvFdfqiiy2DdXG3Zg5QzgHmfG+11p4vSlLiPCC2UvS5Qw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/@polkadot-api/pjs-signer": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/pjs-signer/-/pjs-signer-0.7.3.tgz", + "integrity": "sha512-U7BLFZfnpFMxCh/scJoLXT6oSbfZtZgMTkiu+TbuWQljAb/1ttrQOfshub5VihNyD5rHajp/2Fq0ONZoL5N5PA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signers-common": "0.2.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/@polkadot-api/raw-client": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/raw-client/-/raw-client-0.3.0.tgz", + "integrity": "sha512-u/wM9W7ugIXxBSOEV8+zvQI43b5vgSI0pvE0Rg8PV0G65BTLK0SMc2o2SQL0HtS5+bi5sw/gg4reBJ/0a4U0cw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/@polkadot-api/signer": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/signer/-/signer-0.3.3.tgz", + "integrity": "sha512-bmEV65TwgwbMfiecl7ZQ3k5lfkCOx9FPwPYkVvALcPiqb/d3zYwT9LL/E00hj+EB6IKMlMelEQVuchcW5i/92w==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.2.0", + "@polkadot-api/merkleize-metadata": "1.2.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signers-common": "0.2.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/@polkadot-api/signers-common": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/signers-common/-/signers-common-0.2.3.tgz", + "integrity": "sha512-SzGLJMxug31Y1P8+0I809ICpDrayztUXFFh2TV02GuwjEnY6mDGnmX56wowZF5yCn+WteeRdcTJTK7Whk3AGYQ==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/@polkadot-api/sm-provider": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@polkadot-api/sm-provider/-/sm-provider-0.3.8.tgz", + "integrity": "sha512-jIvzBNsBsh6LIpCrUOa7miZdQPwLPns5IUQyz/8v84R3ON7URSIVbtywGSR8O83BZGyW/DmVkqa5lClLE2fkqw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/json-rpc-provider-proxy": "0.4.1" + }, + "peerDependencies": { + "@polkadot-api/smoldot": ">=0.3" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/@polkadot-api/smoldot": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@polkadot-api/smoldot/-/smoldot-0.4.6.tgz", + "integrity": "sha512-gOXMJ10fXOub0zPP1cGAYeyf29BA2fB+qukrjXGcGN9b0Ya+lDgV7A7Q7y9JOrlGLeKbd1YdrYLAe/F43V7b9Q==", + "license": "MIT", + "dependencies": { + "@types/node": "^25.9.4", + "smoldot": "~3.3.1" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/@polkadot-api/substrate-bindings": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-bindings/-/substrate-bindings-0.20.3.tgz", + "integrity": "sha512-9iqC71fx1ee9ld1NZV8PFime5vryi0kt1bKCSlvNgO6dqMc06sMZuZ8WPjOzWLCHiKHLuphdMs3rVBBaeCP3yg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.2.0", + "@polkadot-api/utils": "0.4.0", + "@scure/base": "^2.2.0", + "scale-ts": "^1.6.1" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/@polkadot-api/substrate-client": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-client/-/substrate-client-0.7.0.tgz", + "integrity": "sha512-TWCc4MAMa5SLVQXmomLHknbj+bztQ/Yclgwm8ENBhz8hR7c9rw9FBAkCa02jMBMCAygPhp3ayGRq+UFcF8KIxQ==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/raw-client": "0.3.0", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/@polkadot-api/utils": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/utils/-/utils-0.4.0.tgz", + "integrity": "sha512-9b/hwRM0UloLWV7SfpNaSD/4k8UQAHoaACAk7Xe+1MlfAm2JtnmPiB1GfGrfTyBlsrJVUIBCZpEmbmxVMaIqBA==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk-tx/node_modules/@polkadot-api/ws-provider": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/ws-provider/-/ws-provider-0.9.1.tgz", + "integrity": "sha512-Ft2QJEjLZgTyKiCEbz3urSOxNfMdqNkg+QKqJhRbOFB7JtR1qMTvLE3TjM+5d2mNlEpOc3j2KDK3APNSj7uQ2Q==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/json-rpc-provider-proxy": "0.4.1", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/@polkadot-labs/hdkd-helpers": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@polkadot-labs/hdkd-helpers/-/hdkd-helpers-0.0.30.tgz", + "integrity": "sha512-qWmmD6ayj14RenDuDFfjF3sHS7ObqPzwIIMPcSVoDeKFSeQV7RY0HwyhC5CG4i6FoguMzak2dbtjYpNN5XQiwQ==", + "license": "MIT", + "dependencies": { + "@noble/curves": "^2.2.0", + "@noble/hashes": "^2.2.0", + "@scure/base": "^2.2.0", + "@scure/sr25519": "^1.0.0", + "scale-ts": "^1.6.1" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/polkadot-api": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/polkadot-api/-/polkadot-api-2.2.1.tgz", + "integrity": "sha512-eiZVUhI1gY4ycBtsCo/yaDXyohUeuqnyKun+bwj90ADSn+jlfjZ4sajAqIhJFwPxbCT75+Zzbln4ZjuZ5PWNWA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/cli": "0.21.9", + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/known-chains": "0.12.1", + "@polkadot-api/logs-provider": "0.2.0", + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/observable-client": "0.18.7", + "@polkadot-api/pjs-signer": "0.7.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signer": "0.3.3", + "@polkadot-api/sm-provider": "0.3.8", + "@polkadot-api/smoldot": "0.4.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0", + "@polkadot-api/ws-middleware": "0.3.6", + "@polkadot-api/ws-provider": "0.9.1", + "@rx-state/core": "^0.1.4" + }, + "bin": { + "papi": "bin/cli.js", + "polkadot-api": "bin/cli.js" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/smoldot": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/smoldot/-/smoldot-3.3.2.tgz", + "integrity": "sha512-Zl4h/0gsw8cfTZzuJ7LV7mtR6QjxltwYjMY7MsVw0oXBXrLK8zyOS6DS9Vjsy57pX1vBMg6UVhHxjbH3W905zA==", + "license": "GPL-3.0-or-later WITH Classpath-exception-2.0", + "dependencies": { + "ws": "^8.8.1" + } + }, + "node_modules/@parity/product-sdk-tx/node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@parity/product-sdk/node_modules/@commander-js/extra-typings": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-15.0.0.tgz", + "integrity": "sha512-yeJlba62xqmkgELUsn7356MEnzLLu/fw2x4lofFqGnXh6YysRdEs2BaLeLtg1+KU0AXvMeqQvTTp+3hBEBK+EA==", + "license": "MIT", + "peerDependencies": { + "commander": "~15.0.0" + } + }, + "node_modules/@parity/product-sdk/node_modules/@polkadot-api/cli": { + "version": "0.21.9", + "resolved": "https://registry.npmjs.org/@polkadot-api/cli/-/cli-0.21.9.tgz", + "integrity": "sha512-9aVdF/ocF3DBb8b/gdkcicBIaQkak2kkU4EGW+TunXLIvbIj2X9hH5geOlx8uWNwt0O4O1DIHD/nkXvycG/AcA==", + "license": "MIT", + "dependencies": { + "@commander-js/extra-typings": "^15.0.0", + "@polkadot-api/codegen": "0.22.5", + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/known-chains": "0.12.1", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/observable-client": "0.18.7", + "@polkadot-api/sm-provider": "0.3.8", + "@polkadot-api/smoldot": "0.4.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0", + "@polkadot-api/wasm-executor": "^0.2.3", + "@polkadot-api/ws-middleware": "0.3.6", + "@polkadot-api/ws-provider": "0.9.1", + "@types/node": "^25.9.4", + "commander": "^15.0.0", + "execa": "^9.6.1", + "fs.promises.exists": "^1.1.4", + "ora": "^9.4.1", + "read-pkg": "^10.1.0", + "rollup": "^4.62.2", + "rollup-plugin-esbuild": "^6.2.1", + "rxjs": "^7.8.2", + "tsc-prog": "^2.3.0", + "typescript": "^6.0.3", + "write-package": "^7.2.0" + }, + "bin": { + "papi": "dist/main/src/main.js", + "polkadot-api": "dist/main/src/main.js" + } + }, + "node_modules/@parity/product-sdk/node_modules/@polkadot-api/codegen": { + "version": "0.22.5", + "resolved": "https://registry.npmjs.org/@polkadot-api/codegen/-/codegen-0.22.5.tgz", + "integrity": "sha512-zwZJAlviI211zhj5i6oXkrr0crrbO3GZjBd2vC9AshY1pRmyiNLV9DZsXw4E6l4JQwdAAyNWtPdnvvB9T3TpKg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk/node_modules/@polkadot-api/ink-contracts": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/ink-contracts/-/ink-contracts-0.6.3.tgz", + "integrity": "sha512-XqnM1VDzI5L62xgg+f8le2yEoz8QZbUKEfAfPnHMOgBj9tJiyF15FcOJmnLMO/vq3cGixqh18tzJekLG5YTxtA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk/node_modules/@polkadot-api/json-rpc-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.2.0.tgz", + "integrity": "sha512-lhkuBS/x06i3djQIN7p8jVkMnYuGsUAEMS6RhdWeEpz7X/8/APER4Wdih7MEBovCuwVSCTjOxl8f+alH7AZHZg==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk/node_modules/@polkadot-api/json-rpc-provider-proxy": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.4.1.tgz", + "integrity": "sha512-F1Hw01C60jn98KQ0vBbgcwAvEcMcKLsJ5kFzq600sgjc6Rnbn1VF/XjTdrjcIqvfpR9sXyGIrpImrov2Usbrsw==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk/node_modules/@polkadot-api/known-chains": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/known-chains/-/known-chains-0.12.1.tgz", + "integrity": "sha512-ZW9TYD2y5IhgNy28zi9sByMr0CaP2I9x8HcAI0pwZR2qDC0kysCG6brDlg1UoZKCjRO99SsKO/iU1o434JgMnQ==", + "license": "MIT" + }, + "node_modules/@parity/product-sdk/node_modules/@polkadot-api/logs-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/logs-provider/-/logs-provider-0.2.0.tgz", + "integrity": "sha512-BH9YdxZu+ZBPPAUwGrvqHPn1hQStL2Im3MmTwYkwXOWW2HlGHULcF65QKvyK+T6/mj2vDvl3MgivnGkUw4pVxg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0" + } + }, + "node_modules/@parity/product-sdk/node_modules/@polkadot-api/merkleize-metadata": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/merkleize-metadata/-/merkleize-metadata-1.2.3.tgz", + "integrity": "sha512-WkPbz0p2XQ9c8yXagdnwCHEB70Gnm91okcsd6IXU393//3aPgkxKgb+/Efnz7C5/KQmg02P0zXo7q/n/W/yVCA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk/node_modules/@polkadot-api/metadata-builders": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-builders/-/metadata-builders-0.14.3.tgz", + "integrity": "sha512-m7CACsiqHzgVEh5WBZGkTV8AQ3CBQKR1YpPQMnlsJfCr/IkgKU0UyWM6WxCmBiReLFVkOfXMtGlpN8+GxpHmww==", + "license": "MIT", + "dependencies": { + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk/node_modules/@polkadot-api/metadata-compatibility": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-compatibility/-/metadata-compatibility-0.6.3.tgz", + "integrity": "sha512-/Y0uF8nDk60ijydp8Bd37YexPFdB8hBXJWwEgOJHsVlhiny8sVKXiMg+UkJ9BiEk2z+yMbZRCKmhNpTpizo7aw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3" + } + }, + "node_modules/@parity/product-sdk/node_modules/@polkadot-api/observable-client": { + "version": "0.18.7", + "resolved": "https://registry.npmjs.org/@polkadot-api/observable-client/-/observable-client-0.18.7.tgz", + "integrity": "sha512-/eQi3D8jbXLg/L1hZX4eX0/+nO3kTxkOfYeWEtFhtOvFdfqiiy2DdXG3Zg5QzgHmfG+11p4vSlLiPCC2UvS5Qw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@parity/product-sdk/node_modules/@polkadot-api/pjs-signer": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/pjs-signer/-/pjs-signer-0.7.3.tgz", + "integrity": "sha512-U7BLFZfnpFMxCh/scJoLXT6oSbfZtZgMTkiu+TbuWQljAb/1ttrQOfshub5VihNyD5rHajp/2Fq0ONZoL5N5PA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signers-common": "0.2.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk/node_modules/@polkadot-api/raw-client": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/raw-client/-/raw-client-0.3.0.tgz", + "integrity": "sha512-u/wM9W7ugIXxBSOEV8+zvQI43b5vgSI0pvE0Rg8PV0G65BTLK0SMc2o2SQL0HtS5+bi5sw/gg4reBJ/0a4U0cw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0" + } + }, + "node_modules/@parity/product-sdk/node_modules/@polkadot-api/signer": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/signer/-/signer-0.3.3.tgz", + "integrity": "sha512-bmEV65TwgwbMfiecl7ZQ3k5lfkCOx9FPwPYkVvALcPiqb/d3zYwT9LL/E00hj+EB6IKMlMelEQVuchcW5i/92w==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.2.0", + "@polkadot-api/merkleize-metadata": "1.2.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signers-common": "0.2.3", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk/node_modules/@polkadot-api/signers-common": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/signers-common/-/signers-common-0.2.3.tgz", + "integrity": "sha512-SzGLJMxug31Y1P8+0I809ICpDrayztUXFFh2TV02GuwjEnY6mDGnmX56wowZF5yCn+WteeRdcTJTK7Whk3AGYQ==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@parity/product-sdk/node_modules/@polkadot-api/sm-provider": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@polkadot-api/sm-provider/-/sm-provider-0.3.8.tgz", + "integrity": "sha512-jIvzBNsBsh6LIpCrUOa7miZdQPwLPns5IUQyz/8v84R3ON7URSIVbtywGSR8O83BZGyW/DmVkqa5lClLE2fkqw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/json-rpc-provider-proxy": "0.4.1" + }, + "peerDependencies": { + "@polkadot-api/smoldot": ">=0.3" + } + }, + "node_modules/@parity/product-sdk/node_modules/@polkadot-api/smoldot": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@polkadot-api/smoldot/-/smoldot-0.4.6.tgz", + "integrity": "sha512-gOXMJ10fXOub0zPP1cGAYeyf29BA2fB+qukrjXGcGN9b0Ya+lDgV7A7Q7y9JOrlGLeKbd1YdrYLAe/F43V7b9Q==", + "license": "MIT", + "dependencies": { + "@types/node": "^25.9.4", + "smoldot": "~3.3.1" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "node_modules/@parity/product-sdk/node_modules/@polkadot-api/substrate-bindings": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-bindings/-/substrate-bindings-0.20.3.tgz", + "integrity": "sha512-9iqC71fx1ee9ld1NZV8PFime5vryi0kt1bKCSlvNgO6dqMc06sMZuZ8WPjOzWLCHiKHLuphdMs3rVBBaeCP3yg==", "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "@noble/hashes": "^2.2.0", + "@polkadot-api/utils": "0.4.0", + "@scure/base": "^2.2.0", + "scale-ts": "^1.6.1" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@parity/product-sdk/node_modules/@polkadot-api/substrate-client": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-client/-/substrate-client-0.7.0.tgz", + "integrity": "sha512-TWCc4MAMa5SLVQXmomLHknbj+bztQ/Yclgwm8ENBhz8hR7c9rw9FBAkCa02jMBMCAygPhp3ayGRq+UFcF8KIxQ==", "license": "MIT", - "engines": { - "node": ">=6.0.0" + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/raw-client": "0.3.0", + "@polkadot-api/utils": "0.4.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "node_modules/@parity/product-sdk/node_modules/@polkadot-api/utils": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/utils/-/utils-0.4.0.tgz", + "integrity": "sha512-9b/hwRM0UloLWV7SfpNaSD/4k8UQAHoaACAk7Xe+1MlfAm2JtnmPiB1GfGrfTyBlsrJVUIBCZpEmbmxVMaIqBA==", "license": "MIT" }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@parity/product-sdk/node_modules/@polkadot-api/ws-provider": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/ws-provider/-/ws-provider-0.9.1.tgz", + "integrity": "sha512-Ft2QJEjLZgTyKiCEbz3urSOxNfMdqNkg+QKqJhRbOFB7JtR1qMTvLE3TjM+5d2mNlEpOc3j2KDK3APNSj7uQ2Q==", "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/json-rpc-provider-proxy": "0.4.1", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" } }, - "node_modules/@noble/ciphers": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", - "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "node_modules/@parity/product-sdk/node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", "license": "MIT", "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "node": ">=22.12.0" } }, - "node_modules/@noble/curves": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.2.0.tgz", - "integrity": "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==", + "node_modules/@parity/product-sdk/node_modules/polkadot-api": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/polkadot-api/-/polkadot-api-2.2.1.tgz", + "integrity": "sha512-eiZVUhI1gY4ycBtsCo/yaDXyohUeuqnyKun+bwj90ADSn+jlfjZ4sajAqIhJFwPxbCT75+Zzbln4ZjuZ5PWNWA==", "license": "MIT", "dependencies": { - "@noble/hashes": "2.2.0" + "@polkadot-api/cli": "0.21.9", + "@polkadot-api/ink-contracts": "0.6.3", + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/known-chains": "0.12.1", + "@polkadot-api/logs-provider": "0.2.0", + "@polkadot-api/metadata-builders": "0.14.3", + "@polkadot-api/metadata-compatibility": "0.6.3", + "@polkadot-api/observable-client": "0.18.7", + "@polkadot-api/pjs-signer": "0.7.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signer": "0.3.3", + "@polkadot-api/sm-provider": "0.3.8", + "@polkadot-api/smoldot": "0.4.6", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0", + "@polkadot-api/ws-middleware": "0.3.6", + "@polkadot-api/ws-provider": "0.9.1", + "@rx-state/core": "^0.1.4" }, - "engines": { - "node": ">= 20.19.0" + "bin": { + "papi": "bin/cli.js", + "polkadot-api": "bin/cli.js" }, - "funding": { - "url": "https://paulmillr.com/funding/" + "peerDependencies": { + "rxjs": ">=7.8.0" } }, - "node_modules/@noble/hashes": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", - "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "node_modules/@parity/product-sdk/node_modules/smoldot": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/smoldot/-/smoldot-3.3.2.tgz", + "integrity": "sha512-Zl4h/0gsw8cfTZzuJ7LV7mtR6QjxltwYjMY7MsVw0oXBXrLK8zyOS6DS9Vjsy57pX1vBMg6UVhHxjbH3W905zA==", + "license": "GPL-3.0-or-later WITH Classpath-exception-2.0", + "dependencies": { + "ws": "^8.8.1" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "node_modules/@parity/product-sdk/node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": ">= 8" + "node": ">=14.17" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } + "node_modules/@parity/result": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@parity/result/-/result-0.2.0.tgz", + "integrity": "sha512-QCrhYPeVVaPIjnlsfBIk5GHPRqfuCjy6MjqKkoCP4kwS1LIo5YtJCSFeB5mGtvMG7hiaRNl4lHQcd5YqDfJ1tQ==", + "license": "Apache-2.0" }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, + "node_modules/@parity/truapi": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@parity/truapi/-/truapi-0.5.1.tgz", + "integrity": "sha512-5AV6YoqnUKXj2wJ/qt/J2i3jpFawhEVkY165V5fSDccGkgK2oVHxlSfLwscXpZp4DxwFTL1FmvkhwhuqPDIdtA==", "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" + "@noble/hashes": "^2.2.0", + "neverthrow": "^8.2.0", + "scale-ts": "^1.6.1" } }, "node_modules/@playwright/test": { @@ -1416,6 +4814,61 @@ "integrity": "sha512-B2h1o+Qlo9idpASaHvMSoViB2I5ko5OAfwfhYF8LQDkTADK0B+SeStzNj1Qn+FG34wqTuv7HzBCdjaUgzYINJQ==", "license": "MIT" }, + "node_modules/@polkadot-api/ws-middleware": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@polkadot-api/ws-middleware/-/ws-middleware-0.3.6.tgz", + "integrity": "sha512-IMoJB572DdSYPshCQa2JmmehUEzX2Uwg5vKQafubbTMEFacXbifc6LTTVvi9Ue67rBuDy2VOWXBAm3BBpfKpDA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/json-rpc-provider-proxy": "0.4.1", + "@polkadot-api/raw-client": "0.3.0", + "@polkadot-api/substrate-bindings": "0.20.3", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@polkadot-api/ws-middleware/node_modules/@polkadot-api/json-rpc-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.2.0.tgz", + "integrity": "sha512-lhkuBS/x06i3djQIN7p8jVkMnYuGsUAEMS6RhdWeEpz7X/8/APER4Wdih7MEBovCuwVSCTjOxl8f+alH7AZHZg==", + "license": "MIT" + }, + "node_modules/@polkadot-api/ws-middleware/node_modules/@polkadot-api/json-rpc-provider-proxy": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.4.1.tgz", + "integrity": "sha512-F1Hw01C60jn98KQ0vBbgcwAvEcMcKLsJ5kFzq600sgjc6Rnbn1VF/XjTdrjcIqvfpR9sXyGIrpImrov2Usbrsw==", + "license": "MIT" + }, + "node_modules/@polkadot-api/ws-middleware/node_modules/@polkadot-api/raw-client": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/raw-client/-/raw-client-0.3.0.tgz", + "integrity": "sha512-u/wM9W7ugIXxBSOEV8+zvQI43b5vgSI0pvE0Rg8PV0G65BTLK0SMc2o2SQL0HtS5+bi5sw/gg4reBJ/0a4U0cw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0" + } + }, + "node_modules/@polkadot-api/ws-middleware/node_modules/@polkadot-api/substrate-bindings": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-bindings/-/substrate-bindings-0.20.3.tgz", + "integrity": "sha512-9iqC71fx1ee9ld1NZV8PFime5vryi0kt1bKCSlvNgO6dqMc06sMZuZ8WPjOzWLCHiKHLuphdMs3rVBBaeCP3yg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.2.0", + "@polkadot-api/utils": "0.4.0", + "@scure/base": "^2.2.0", + "scale-ts": "^1.6.1" + } + }, + "node_modules/@polkadot-api/ws-middleware/node_modules/@polkadot-api/utils": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/utils/-/utils-0.4.0.tgz", + "integrity": "sha512-9b/hwRM0UloLWV7SfpNaSD/4k8UQAHoaACAk7Xe+1MlfAm2JtnmPiB1GfGrfTyBlsrJVUIBCZpEmbmxVMaIqBA==", + "license": "MIT" + }, "node_modules/@polkadot-api/ws-provider": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/@polkadot-api/ws-provider/-/ws-provider-0.7.5.tgz", @@ -1583,9 +5036,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", - "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.3.tgz", + "integrity": "sha512-c0wdcekXtQvvn5Tsrk/+op/gUArrbWaFduBnTLP2l1cKLSQs4diMWjJw3m6A0DdzT8dAAX95KpkJ3qynCePbmw==", "cpu": [ "arm" ], @@ -1596,9 +5049,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", - "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.3.tgz", + "integrity": "sha512-3YjElDdWN+qXAFbJ/CzPV+0wspLqh54k/I6GfdYtEJRqg7buSgc1yPM3B+93j1M4neobtkATHZTmxK2AMVGfnA==", "cpu": [ "arm64" ], @@ -1609,9 +5062,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", - "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.3.tgz", + "integrity": "sha512-Pch2pFNOxxz1hTjypIdPyRTR6riiwRl84+VcN9djS680fw+Co1nAJINrdpqp7KV0NvyuU8ilZXZCjd7ykJl1GQ==", "cpu": [ "arm64" ], @@ -1622,9 +5075,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", - "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.3.tgz", + "integrity": "sha512-LEuncFUHFiF8t4yZVZvvZA1wk0pjAscRnsrn1EfTEmN4HXotBi2YtcnLRyaK6UbuczW7xZS5ES+81Rdz8Z0T6g==", "cpu": [ "x64" ], @@ -1635,9 +5088,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", - "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.3.tgz", + "integrity": "sha512-zvBUvsQUpOWALdDsk6qbS8bXf2VxmPisuudNDrY7x0p0jBdsoZl8HsHczIOgkQiZldmcacMKtBzpoGVNeIe2bQ==", "cpu": [ "arm64" ], @@ -1648,9 +5101,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", - "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.3.tgz", + "integrity": "sha512-C2KmNrcSem/AMg984H/dev+si0lieQGdXdR/lYGJnuumXnFb9Y7QdiI62obFdLlxRYLBv4P0eUVIDbD4c1vVvw==", "cpu": [ "x64" ], @@ -1661,9 +5114,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", - "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.3.tgz", + "integrity": "sha512-ggXnsTAEzNQx74XpunRsiZ9aBZDsI7XIa0hm2nzR9f4WzH5/f/d73ZSDaC5ejJ8YLY4NW+V3wr0tjOaeCq8hqA==", "cpu": [ "arm" ], @@ -1674,9 +5127,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", - "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.3.tgz", + "integrity": "sha512-2vng+FlzNUhKZxtej3IUqJgbZoQk2M/dwQM20+ULV0R/E/8tr9/P6uEf2iiGIk4HL0zMKh5Jry7mUHdUOvyGgA==", "cpu": [ "arm" ], @@ -1687,9 +5140,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", - "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.3.tgz", + "integrity": "sha512-LLLFZKt4/Nraf9rxDkhiU8QVgLF4WmCkfr0L4fj0fPfIZFBib0DeiFk1hhaYKd03LFAFJcxHslhDFlNJLylf5Q==", "cpu": [ "arm64" ], @@ -1700,9 +5153,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", - "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.3.tgz", + "integrity": "sha512-WJkdQCvS9sWNOUBJZfQRKpZGFBztRzcowI+nndmflKgU4XY+3a420FgTOSKTsVqJbnzSxeT4vaJalpOaPo2YCQ==", "cpu": [ "arm64" ], @@ -1713,9 +5166,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", - "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.3.tgz", + "integrity": "sha512-PwHXCCS2n64/1Ot6rP1YEYA02MGYBcQlr8CSZZyrUG2O7NH6NklYmvr9v3Jy+5e/eDeNchc/ukmKJi9LuflMIQ==", "cpu": [ "loong64" ], @@ -1726,9 +5179,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", - "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.3.tgz", + "integrity": "sha512-vUjxINQu3RC8NZS3ykk1gN65gIz8pAopOq2HXuZhiIxHdx7TFvDG+jgrdSgInu1Eza4/Rfi2VzZgyIgEH4WOaw==", "cpu": [ "loong64" ], @@ -1739,9 +5192,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", - "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.3.tgz", + "integrity": "sha512-wzko4aJ13+0G3kGnviCg5gnXFKd40izKsrf2uOw12US4XqprkDrmwOpeW14aSNa37V8bfPcz5Fkob6LZ3BAPmA==", "cpu": [ "ppc64" ], @@ -1752,9 +5205,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", - "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.3.tgz", + "integrity": "sha512-8120ue0JUMSwy11stlwnfdX3pPd+WZYGCDBwEHWtIHi6pOpZmsEF5QKB7a/UN+XFdqvobxz98kv8RTqikyCEBw==", "cpu": [ "ppc64" ], @@ -1765,9 +5218,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", - "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.3.tgz", + "integrity": "sha512-XLFHnR3tXMjbOCh2vtVJHmxt+995uJsTERQyseFDRA0xxMxyTZPLa3OIUlyFaO4mF/Lu0FjmWHCuPXJT1n/IOg==", "cpu": [ "riscv64" ], @@ -1778,9 +5231,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", - "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.3.tgz", + "integrity": "sha512-se6yXvNGMIl0f+RQzyh7XAmia8/9kplQx424wnG2w0C1oi6XgO6Y8otKhdXFHbHs88Ihavzmvh1NWjuovE76BQ==", "cpu": [ "riscv64" ], @@ -1791,9 +5244,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", - "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.3.tgz", + "integrity": "sha512-gNoxRefktVIiGflpONuxWWXZAzIQG++z9qHO3xKwk4WdDMuQja3JHGfE1u0i3PfPDyvhypdk+WrgIJqLhGG7sg==", "cpu": [ "s390x" ], @@ -1804,9 +5257,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", - "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.3.tgz", + "integrity": "sha512-V4KtWtQfAFMU7+9/A/VDps/VI8CHd3cYz0L8sgJzz8qK7eY7wI4ruFD82UYIYvW9Z4DtlTfhQcsl4XyPHW5uSg==", "cpu": [ "x64" ], @@ -1817,9 +5270,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", - "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.3.tgz", + "integrity": "sha512-LBx9LYXvj2CBkMkjLdNAWLwH0MLMin7do2VcVo9kVPibGLkY0BQQut2fv7NVqkXqZ/CrAu9LqDHVV1xHCMpCPw==", "cpu": [ "x64" ], @@ -1830,9 +5283,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", - "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.3.tgz", + "integrity": "sha512-ABVf3Q0RCu7NcyCCOZQI0pJ3GuSdfSl8EXcy88QtdceIMIoCUdfhsJChZ64L9zVM2aJHjde1Bhn5uqSRcX9ySA==", "cpu": [ "x64" ], @@ -1843,9 +5296,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", - "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.3.tgz", + "integrity": "sha512-+2Cy/ldweGBLlPIKsQLF8U5N44a0KDdbrk1rAjHOM9M2K+kGdIVjHLmmrZIcx+9Ny3ke/1JomCsDI1ocb11+sg==", "cpu": [ "arm64" ], @@ -1856,9 +5309,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", - "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.3.tgz", + "integrity": "sha512-dtZvzc8BedpSaFNy75x6uiWwAGTH+aZHDtdrqP6qk+WcLJrfti6sGje1ZJ9UxyzDLF23d/mV+PaMwuC0hL7UVA==", "cpu": [ "arm64" ], @@ -1869,9 +5322,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", - "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.3.tgz", + "integrity": "sha512-Rj8Ra4noo+aYy7sKBggCx0407mws34kAb1ySyWuq5DAtFBQdkSwnsjCgPrhPe9cvgBKZIukpE+CVHvORCS93kQ==", "cpu": [ "ia32" ], @@ -1882,9 +5335,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", - "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.3.tgz", + "integrity": "sha512-vp7N084ew/odXn2gi/mzm9mUkQu9l6AiN6dt4IeUM2Uvm9o+cVmP+YkqbMOteLbiGgqBBlJZjIMYVCfOOIVbVQ==", "cpu": [ "x64" ], @@ -1895,9 +5348,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", - "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.3.tgz", + "integrity": "sha512-MOG/3gTOn4Fwf574RVOaY61I5o6P90legkFADiTyn1hyjNydT+cerU2rLUwPdZkKKyJ+iT+K9p7WXK4LM1Ka6g==", "cpu": [ "x64" ], @@ -2145,9 +5598,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "license": "MIT" }, "node_modules/@types/json-schema": { @@ -2158,12 +5611,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", - "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "version": "25.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", + "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", "license": "MIT", "dependencies": { - "undici-types": "~7.19.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/normalize-package-data": { @@ -2407,29 +5860,6 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -2831,11 +6261,14 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/base64id": { "version": "2.0.0", @@ -2872,14 +6305,16 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" } }, "node_modules/braces": { @@ -3203,23 +6638,16 @@ "node": ">=20" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/concurrently": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.3.tgz", - "integrity": "sha512-ihjs0E2SxvDgq/MK418hX6YycQgKhsqxpbZuZbHo0yKfqDWdymWMjWYIpCIzqDDLLKClHlXev8whW/8WXmJ0BA==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.4.tgz", + "integrity": "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==", "dev": true, "license": "MIT", "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", - "shell-quote": "1.8.4", + "shell-quote": "1.9.0", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" @@ -3474,7 +6902,6 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, "license": "MIT" }, "node_modules/esbuild": { @@ -4026,6 +7453,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4170,6 +7609,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ipfs-unixfs": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/ipfs-unixfs/-/ipfs-unixfs-12.0.2.tgz", + "integrity": "sha512-uZ3rutVVZZ+tw52P+sgDSgOSK6ztExJVlfCjKvSD+NIEVlWQPDeKgdSFm+Kxchmgp7t6g1h+dzir+NgY+VsQXg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "protons-runtime": "^6.0.1", + "uint8arraylist": "^2.4.8" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -4336,9 +7785,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, "funding": [ { @@ -4633,6 +8082,12 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/multiformats": { + "version": "13.4.2", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.2.tgz", + "integrity": "sha512-eh6eHCrRi1+POZ3dA+Dq1C6jhP1GNtr9CRINMb67OKzqW9I5DUuZM/3jLPlzhgpGeiNUlEGEbkCYChXMCc/8DQ==", + "license": "Apache-2.0 OR MIT" + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -4644,10 +8099,21 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nanoevents": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/nanoevents/-/nanoevents-9.1.0.tgz", + "integrity": "sha512-Jd0fILWG44a9luj8v5kED4WI+zfkkgwKyRQKItTtlPfEsh7Lznfi1kr8/iZ+XAIss4Qq5GqRB0qtWbaz9ceO/A==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "funding": [ { "type": "github", @@ -4678,6 +8144,18 @@ "node": ">= 0.6" } }, + "node_modules/neverthrow": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/neverthrow/-/neverthrow-8.2.0.tgz", + "integrity": "sha512-kOCT/1MCPAxY5iUV3wytNFUMUolzuwd/VF/1KCx7kf6CutrOsTie+84zTGTpgQycjvfLdBBdvBvFLqFD2c0wkQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "^4.24.0" + } + }, "node_modules/node-releases": { "version": "2.0.37", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", @@ -4801,9 +8279,9 @@ } }, "node_modules/ora": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-9.3.0.tgz", - "integrity": "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.1.tgz", + "integrity": "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==", "license": "MIT", "dependencies": { "chalk": "^5.6.2", @@ -4812,7 +8290,7 @@ "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", - "stdin-discarder": "^0.3.1", + "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" }, "engines": { @@ -5188,9 +8666,9 @@ } }, "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", "funding": [ { "type": "opencollective", @@ -5207,7 +8685,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5389,6 +8867,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/protons-runtime": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/protons-runtime/-/protons-runtime-6.0.2.tgz", + "integrity": "sha512-hiyjyANwGcgmzc+tXc1/ZcSZhKnl5MDjaVNWkISHBgadaU0sjTgKIKZMZ62d9J9zlSTyKHCs/osPkQ/3Z+7yeA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "uint8-varint": "^2.0.4", + "uint8arraylist": "^2.4.8", + "uint8arrays": "^5.1.0" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5724,6 +9213,15 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -5752,12 +9250,12 @@ } }, "node_modules/rollup": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", - "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.3.tgz", + "integrity": "sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==", "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@types/estree": "1.0.9" }, "bin": { "rollup": "dist/bin/rollup" @@ -5767,34 +9265,53 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.2", - "@rollup/rollup-android-arm64": "4.60.2", - "@rollup/rollup-darwin-arm64": "4.60.2", - "@rollup/rollup-darwin-x64": "4.60.2", - "@rollup/rollup-freebsd-arm64": "4.60.2", - "@rollup/rollup-freebsd-x64": "4.60.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", - "@rollup/rollup-linux-arm-musleabihf": "4.60.2", - "@rollup/rollup-linux-arm64-gnu": "4.60.2", - "@rollup/rollup-linux-arm64-musl": "4.60.2", - "@rollup/rollup-linux-loong64-gnu": "4.60.2", - "@rollup/rollup-linux-loong64-musl": "4.60.2", - "@rollup/rollup-linux-ppc64-gnu": "4.60.2", - "@rollup/rollup-linux-ppc64-musl": "4.60.2", - "@rollup/rollup-linux-riscv64-gnu": "4.60.2", - "@rollup/rollup-linux-riscv64-musl": "4.60.2", - "@rollup/rollup-linux-s390x-gnu": "4.60.2", - "@rollup/rollup-linux-x64-gnu": "4.60.2", - "@rollup/rollup-linux-x64-musl": "4.60.2", - "@rollup/rollup-openbsd-x64": "4.60.2", - "@rollup/rollup-openharmony-arm64": "4.60.2", - "@rollup/rollup-win32-arm64-msvc": "4.60.2", - "@rollup/rollup-win32-ia32-msvc": "4.60.2", - "@rollup/rollup-win32-x64-gnu": "4.60.2", - "@rollup/rollup-win32-x64-msvc": "4.60.2", + "@rollup/rollup-android-arm-eabi": "4.62.3", + "@rollup/rollup-android-arm64": "4.62.3", + "@rollup/rollup-darwin-arm64": "4.62.3", + "@rollup/rollup-darwin-x64": "4.62.3", + "@rollup/rollup-freebsd-arm64": "4.62.3", + "@rollup/rollup-freebsd-x64": "4.62.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.3", + "@rollup/rollup-linux-arm-musleabihf": "4.62.3", + "@rollup/rollup-linux-arm64-gnu": "4.62.3", + "@rollup/rollup-linux-arm64-musl": "4.62.3", + "@rollup/rollup-linux-loong64-gnu": "4.62.3", + "@rollup/rollup-linux-loong64-musl": "4.62.3", + "@rollup/rollup-linux-ppc64-gnu": "4.62.3", + "@rollup/rollup-linux-ppc64-musl": "4.62.3", + "@rollup/rollup-linux-riscv64-gnu": "4.62.3", + "@rollup/rollup-linux-riscv64-musl": "4.62.3", + "@rollup/rollup-linux-s390x-gnu": "4.62.3", + "@rollup/rollup-linux-x64-gnu": "4.62.3", + "@rollup/rollup-linux-x64-musl": "4.62.3", + "@rollup/rollup-openbsd-x64": "4.62.3", + "@rollup/rollup-openharmony-arm64": "4.62.3", + "@rollup/rollup-win32-arm64-msvc": "4.62.3", + "@rollup/rollup-win32-ia32-msvc": "4.62.3", + "@rollup/rollup-win32-x64-gnu": "4.62.3", + "@rollup/rollup-win32-x64-msvc": "4.62.3", "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-esbuild": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-esbuild/-/rollup-plugin-esbuild-6.2.1.tgz", + "integrity": "sha512-jTNOMGoMRhs0JuueJrJqbW8tOwxumaWYq+V5i+PD+8ecSCVkuX27tGW7BXqDgoULQ55rO7IdNxPcnsWtshz3AA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "get-tsconfig": "^4.10.0", + "unplugin-utils": "^0.2.4" + }, + "engines": { + "node": ">=14.18.0" + }, + "peerDependencies": { + "esbuild": ">=0.18.0", + "rollup": "^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5880,9 +9397,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", - "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", "dev": true, "license": "MIT", "engines": { @@ -6599,10 +10116,38 @@ "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", "license": "MIT" }, + "node_modules/uint8-varint": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/uint8-varint/-/uint8-varint-2.0.5.tgz", + "integrity": "sha512-jeFLbL/x30wBRnWjKE1qVBXeumG46r7XmYkpis955lTQ+blccGKFrOsSMHlxePwYB1pI7L8YPHz1t4jLxEs3nA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "uint8arraylist": "^2.0.0", + "uint8arrays": "^5.0.0" + } + }, + "node_modules/uint8arraylist": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/uint8arraylist/-/uint8arraylist-2.4.9.tgz", + "integrity": "sha512-KxWjyEFzchzik3aoQlK66oaoxIReoMo5bQRm1fcjBUZvE8xv/tyR3CTKhjh6K/faV8VaF6hd5pjr45CzbwuwkA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "uint8arrays": "^5.0.1" + } + }, + "node_modules/uint8arrays": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.1.1.tgz", + "integrity": "sha512-9muQwa4wZG4dKi9gMAIBtnk2Pw87SRpvWTH6lOGm19V2Uqxr4uomUf2PGqPnWc+qs06sN8owUU4jfcoWOcfwVQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "multiformats": "^13.0.0" + } + }, "node_modules/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "license": "MIT" }, "node_modules/unicorn-magic": { @@ -6617,6 +10162,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unplugin-utils": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.2.5.tgz", + "integrity": "sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==", + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/unplugin-utils/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -7277,6 +10850,23 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "license": "ISC" }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/web/package.json b/web/package.json index e221bc7..f073684 100644 --- a/web/package.json +++ b/web/package.json @@ -16,7 +16,9 @@ "dev:listen": "concurrently -n signal,web -c magenta,cyan \"npm:signal\" \"npm:dev\"", "build": "tsc -b && vite build", "build:bulletin": "tsc -b && vite build --config vite.bulletin.config.ts", + "build:product-devnet": "tsc -b && vite build --config vite.product.config.ts --mode product-devnet", "deploy:bulletin": "node scripts/deploy-bulletin.cjs", + "deploy:product-devnet": "npm run build:product-devnet && npx --yes --package @polkadot-community-foundation/polkadot-app-deploy@0.13.1 pad ./dist-product dotify.dot --env devnet --js-merkle --config ./polkadot-app-deploy.config.ts", "smoke:production-env": "node scripts/production-env-smoke.mjs", "smoke:signal": "node scripts/signaling-smoke.mjs", "lint": "eslint .", @@ -27,6 +29,7 @@ "preview": "vite preview --host 0.0.0.0 --port 4273" }, "dependencies": { + "@parity/product-sdk": "0.19.1", "@polkadot-api/descriptors": "file:.papi/descriptors", "@polkadot-apps/chain-client": "^2.0.5", "@polkadot-apps/descriptors": "^1.0.1", @@ -45,6 +48,7 @@ "viem": "^2.53.1" }, "overrides": { + "brace-expansion": "5.0.8", "ws": "8.21.0" }, "devDependencies": { diff --git a/web/polkadot-app-deploy.config.ts b/web/polkadot-app-deploy.config.ts new file mode 100644 index 0000000..b0dc1e8 --- /dev/null +++ b/web/polkadot-app-deploy.config.ts @@ -0,0 +1,16 @@ +export default { + domain: 'dotify.dot', + displayName: 'Dotify', + description: 'Shared musical presence with artist-owned access and value flows.', + icon: { + path: './product-icon.png', + format: 'png' + }, + executables: [ + { + kind: 'app', + path: './dist-product', + appVersion: [0, 1, 0] + } + ] +}; diff --git a/web/product-icon.png b/web/product-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..24c57db6a3da752d85996df78601b4f3dcd7fca7 GIT binary patch literal 23535 zcmXtfWk6g#6YdHw#oeJ;aS9Y?aW7h2i@UomPH`y3DT}v-;_fcRp?Gn3cf0$3_ugM; z_avD~CX-|`&vU-0ew4*TBSixM0GRS}QtAKz2>ufUK>6?CM=JRn0Kf*wONncGXCEyi z`fF;>0?sb&St)7XzQz96R@=9nhtK>spn+XA?bvY{v$SQB@SSRHBN4Nffdno}7$hoQ z^!_CAStmNod}k=YY3wykSrVlq{Daq6lP`M(6I%Mj3mExzvC3}ASmVGQI9BNKqmxl*4YNXRA<}I;z)TJK1b;iFAD!jLKyx4?uRi zAY?PFGVLjL5b}Jk3n*DS28LnhhYoMs&s6(lrH%@D+Rrs;-_8~LM0E|R+@ z0<+rpc5kspc22adnVxY5l&P8mc==a*J&bxj^n-!O`isz^)eK@N@mGLc#p`tg!z-s*G3ziCTOujiV!_2sM)-dIt~ib? zx@*%s0KdI~%W#?=W zRE>;WPVWX>`l)Ebx?PSN)xNF<(hCZr@xbIkcW%?Oc24b=5YIQ%Vc2LYp%2%VN{?k0 zm&Gu?>k{Zn#+7(Odk+^}YT`RN{^Ngcg9EuiU|S70gtgnp)7I0ZijJ%|C^TVzJ)e2a zR&uYJM=inMexw2v6cl6)JT?#N4&kFoeLs26lkdfU;e(nNsPZ{4}N+lic(TaW|_OVuGLuZSy&ws=kb8CW9u;`1ck;tY+N+kdwHgSMl+8Qy5JT><#z0l{1yloM(0d^ z+w*}6i;5c{f%_RWU+Lm@Dqe6#O9XeW)MZCwVK12y#YM@{@MJ*!>Az*Kvpw4ETUitu zw6_clWqMEgJxIE$lDr=Q_p32%QBMO>&1xJp zyiELzBN63VSQB_DcETv$A}D6?8>iEaqwT(%#R({}GpVkiq?KJ2ja@o#Mxf6+KRCw~ z&BlJGNP~3b5*_yhxF3?}*XX_*?$rK+pg?R)vdL(i0=icc4Ox9FR++;B3LDwnA7B)| z+>#`Eu8Ls2|FKexH@Z&&1SYxZAQ%V9KqRVyer6Y5e-GHr|2wA;pf|lT(k$&xNUyq zGZ+U)l;1+K$kEWU0VjRgCfdJP6cn`ruW`pqs{rHcaf7!&|MTnH*G4}OwK5a}ema3rknWv;}15tUT$ayr#Aaxxs-i^IJ86I9h9@UR` zKqc>^jG@y25X$~oapiOGNZw~HW3K6+k5}<}!qn_rmw}r#M3<3I80Gmi;D3H|4o+P3*}aUpWDpDK`yIf1e552?Ct zgaC==H3Cbh&g}zax?EjQXYL*QD~su^zbG;hYNBHV773@>qSfy6?@LB#+vA(sBh}@e zUX<|Au$Rf;2(QN_bLJ{+k~iE%8b|s;8@Dk^d$;E_ngL6E>`se~>rJa-uUgUCE9odG zHY4TEA0D#UDu^#LEA+Rdfljkq3sgES~r@Tia5ZgvVU17C&d08`IS(?5w; zuA^%4FrI@gU#}XJ)C{l}-n`$|a9A2d3r^_rBA-GVaCxkMRbv!6E<4S&LO8oA#DZ@~ z$A7EY6`8h^fhb37nkE5XQdH0nj)kCcf;noaQ_}*T@27;|X~jruz=BK@zr` z_&cIOOD)g8kBXNUeo|0HY1l6@EY&MCazc65ocUcnos{IF#4fX}5&#U!&%^~{QxeTb z4?`m6tsN9Hua~q579kxXK2lraKnYvA;D9NC-R2iQ5Vi3Khv|EhkCfDfzl=WTcnL4# zbprQ3e+;>OvzsjD-Re#7vn)_+*-&5W{mAPL#ghX({&525}M({#|fL%z>fvcL8iEt7|+AisS3mWoQ#kx;^a7Z$qjN6&mS ze*FMs)vrRrM$1xMH$F4^xD*gaNDa7+XkhVag{sb{=6nTcfhA1+dgo}AzkmvZ-RU!h zjo?a#Upoe<48Wa;fWqih^$pfr&NnTunMn}J^#=#C$d+qW9pVUP(dO}ecZAi_MgUav zJiUTWCP+DH;4!L#jvAkG>eRSlUFAnMs2dT0-!7wWfTg&)BZqpCNT%olRrP+?6UYg) zUw9y*#{aGl9<5pleKRQ8PCr2oKie$ku^6Dsel9^=XbJfkSbvcVV`zd|%P@nxHA!xC z<&$rY$C1LiC)CRc&^;l|asXL$0UT^JFzVymyBeR!3k%)lilGervH&FoLzZ1_v|C1z& z`=pxIMGZJf#3PbqoJP=d1Watth(Po|5&&A93aO_Zj}HG`|F&1?b7L)Y73s~f4el>B=Ls25SJWC&8uAJ zuslErp6V2>4}&|2oCsWU=2Prbb^rx6rm%&vG9?FmS-}VXZrF5Kfy9`D8Jy_*z3!Du zksk%+CmJ3=eU;(FJ9@*KjuEKd^P#)^!%-HHrp^=u$<*nmIH4_?>B>UysSgZ~%b z1klXt;E-nWhP=d4*0b09ZR;cUmiO!rCjEnqZ2fnS# zU)`Eq$!2yTKzZJB{jq;Bsot=UI|TCbD9;P6R2g(Mnpr&# z12b#D^VJ)%m1-vtQjgdaAp@W=QrTb~_eGV6i;eOK%6>$x;rrWeMSY!nt7=T#5qaNgKacpRfBahJ>O9#slQ5 zwSK0;|DiYq(PkcP&FDnlKw}STPi%pXvsn5=u`S^GfAuj*P;rHthZud1!1Lw=V?_<$ z^<3U%-}{$mbtxv}w887RnGjfWlw4s?`$l;z{*kZ!^bK2lrI&zx_d@&ZUVmPqY$a9+ zhzH=N;%41rD&qNum#J{BycO2(nk{+T6-?CIW9h%NVnTkuZU*)r2Ju#D+Q zM}0b~ie@9U-#R4jBP4n%!Uo^)_sIl8pUi1CnN6Q8)>te+^syDO=U+J#g8dn&x|}G@ z{s}dHg(hyp*9b};O8E5xjs%kg+!x`0EaKy4cw*G=cGN%r+E7v@H`lx8-yAX z6h0N`gC4B}@wfrhIIGg%qP;8H^T^laao+1{8eofOUy+*o6J4?wr zd&n~TyFhAY4~vld`+j%(X_$!jCbGn*QjURmbRJ3yYn|y9+BaO===ZHY{$r#sOt6D4 z1ZS73Xa^lmwvFQ{{XfvNBdh3+y5kInqdwI9 zm~I5sZ&jq-R_Yg~qa6FCi09w$ zOhEh{_zOM>9iw%@415j>YGYe9RBmP68$c8s4quKA(Fu&&^QOv6ilv>etXgIwSn)Lj z06}oZS;m^(ZFz$%qrj(@Xft?>%Co?mJC4Z82&G-J`_4WWtt|fjcxx*QG+H1&-%Ef* zOfo^j34Bua;(B(2U$NASVb7>v2wguL3S_eCo63{mXXu1y>>Uf}*&~qBIqiN_KllOZ zS+;j!ckJZ{GeBPz5|26jT7`&x1^KE*MKhI72I&zDeoYmsSn6)y-IbL(D=xF>DALKP>a-cwE6WQW|34RC;hqb2 zU|@_TAfvK)=jqmmeNHO^oF{OsZrBZYsh3&ilTIaJPf~9uy7?0eLXk$gXn*Vy;io(@ zLAbD_VzuN=-XuJaEF276m39XWCY!ZiV;r#-1?MFbl#qe=al&W^3}0p?3t2feH3Upy zI$k8X#@%-`>e7x}jqq4$fj~R)oLY5_Jt#{@x}y#`3}Z|4PUz`36a-&l0t7vNQAsTX zq7t%zh6nxD<73A;%Y+~l^3HpRUX1L#8bag1YP0tIK){SdNT*y?mrH-kHz8mW;{`4h zG=9fUaq2XnS`Y51BES7v3Fg3)os}v6SK{l}V*O51UP9cMdAi7%<&F;9l-3Syu((pA zt<4d=7Vz+vd8}NfLWa7qZbe;&tq*wBL^t94tAvQkw8WK`37EL|+ElWt%7t#tE38U| zU0n;0jv*|#2OxVp|_|d~yOA~ZtVWK zm-tZTl}k6HH)~`=>qf#~u){O|oR=+>ckFS@QH|u`BPa`p!6F~wAu6WRHf-R4#%AV>-!&ic}o+op>;ZJz{K$4agk@-pTc%V z%JP;k!*=o8#9bs94_l!`#9DJmO6ur6OVC&SWi4sK&^q zhw!s(v*lx%$G7w%TtERJnQ)*AK3WPkX34dqaB~TMC@@8M}|^z@ZKj- z7nTx|m22jc^(RFEm*|q+7kw&b=Up&yXNAiLt6>^@#)%KpC3m)qEk+~_PV<4OGqH?Mkk!@1utA9mfB<4;JhwH)=2?@S2Vn`z{VM zgFzW462(e@&FAB;e1$IU!Avtuh#G!BC0*~#I(ZfUTt&^=B9!I3u>S=v6N7+!_IRW8 zu!D=}v09Mq`xQ$2b_57Fo9AoXz|!5FpufBN*$JCD_QlzdBE%@Frkn|e4{llbYN8gm zeb<$L?-0Glc`?(ka()@b+xENC8u!9!-u`nw`H8SB>II{0gX(Q4LP*NJIHAS`qZ_st^FOj3e z&U9OBlm9jGAYd_0KS6XsR$L|)Xq-Qc+BKWj8N)K6m`+A84gZ$nSgdiuW$6V>f4 zRBrDDZ}{FH))Q0`urXC>8-&8paWgVH7mHx4%1coj+u#kNraY^AuNck`uO0X~V!P#4 zHa+?{g%!X_PL(z$OTVU-o4LV%($kPj=GF&*q53cnp(_kkt^Ypyf?FxowuBDXCi)pE zJy-y@^V4Uu%I0qdq6FW5`yf+kBP%|HL3f)tR{XA`y8@u1U9=Kim!I)l%P?5Cv4Urk znC&>hcyFOn3oA=+_j$;J&JazQxaB(q@$TjqG2mAVyAjidfXB(KXJ3OiwX`)pTYVOW zs1f5xY&(~o2S_oxF43r-qZ!md17<$#ksuF?nSa=KabhU_G+e<_k!zpY_>VQ**h|d% ziN5-}wbF`KaG9wOQq`3DrUfEebnY(qK)875H59QlGK%>D()7amgm z+i~h~{hk*U6ZdjoIz+Klhwzn}gU6={vy5!rtZ?D(#A6LQ{M)BHp>sZ_f@!LC|Ac0 z-_ZFX^JEGFM^T20G>4sDwIN*Q{39q`Urfr52uWL(Z+c2j(;atq4Z0p&s%#?cv7SH9 zgSFC%r99vl?!F%%0ZEll@TikV<#xE5L?55%>z_)(RsA9y@wE>B(@%K6AGG3lz@mKL zcu*&ZY;Ll2_3T!ES>Z1$c3njPO1iOl^<9p!mF5DdNBBQC?V0~OAt6n*5X*XU9cRz! zlpyBDrdIAQzK!0FMM+1b6(1>|5E?7~0K~wnF)$fH`{oFuK5Ts!^Zv{7i%{luB5{}R zIxgt;aDjw1@@e`m-Mk+MSum4XC+NL3J_=ac_cW&D1JHm$ugTKBi2jL2q~Z1b4e=NO z7Lz8Pap1upnt-n6yB|_hauANbSSQL{u-4qy4GE-L1^{BS{Jx2Y*oB`shxtreO!dnb zwBy#htGm*2as(||QdGJx?n<(g3)-u$U=g!X13R{nfy>fLfEk(QzEmn;tL3Xk z!iiXqOW|okNt?t>x$>n7W5;D+O@&MCnu(g|z(1i ztgbip)~PpSYxuY?W_C5LeGOB#j4ba176+UJj*|45zHAR|nf}TT$V)c9x3|LrZFHv6 zPwV`J_9IMm!+W|~Z?m#Wji+z# ze3nzyE~CFv#tdZ5q|t1}=MQfZTvPQ?_=K$a3y0Xu^F=)Lg?;`9&v&)#(On&H>c=`p1tmU#z)y>q#-{$1J^=T(L+j>PgS26Zi;8{aM-P&{9M zycP`+D(h^*1%`+1HdU*W@V&)qP=nL`|2>u`bK>HIRgJ9P}h0)+Oys| z+JVJdhQ>*e`#Ry8QsVlegQ3=1;d)g03PtNnl>Dr_%AVBHtjLjkB{@0Jz##c^=XGIb zG9+-mio4UA|JQbY^4W;?pbX|=P){Qc^jF;WaV9>L^Hr1ikC@DGvc}1yjv2oN5}BcXgK-C@5fx1wNz3VcYdgLf)0u@jfltIXv+t#yQ3 zUrqKmC*_IwY13Eti(x)8$_y>>vlZgBh)<4#S#pPXDb44TME`y$0?aqCMovYR>gh9$ z?#58d=m=Hij9zXfJq`hhJ$wmv>v3NI)=lEL>r`{<5W^ngpJmz-fL|CxH6+TJGH zb^6mhl7AHgGjx#vbu61`Jtt|uzIqoDFJh$%r~=*p&jpO3lYT@Ei)f~HjdcmlI5v-= zbS_h^WgQrr&(|R{y1|XET^zzQ_90DNtTn=9D8=v+OiORmamNWj&;Cmf-7F(gDvdf{ zIWqXM&#L8oOl0>G7Pi@_vJ$|>KzgfeBXKxmQcdg}>ehwY-+NAzTXdoWjt6K;_Iou5 z5K&mvJwI+PIt})(dccuwHh_&Jy!r;pVf(_pP`KjH8Ta(|VL1#~4S5y@QoTp{db8j*8)zq~?O$>^{=m~<^r2aDDHU2v zB%C>BXEeI)u~fX;WWY&)J6oNB3&V)p1@O6%rmCnKZr4*yQ0^)KG~MbnzJAY!Mh0FN z$VytQ8SPdd7dH#LQg~_Rot8V2?&8flBbwl5Ep%6v`}mR@(Jco7TVC_LcZ*8$3!HZo z0z)uAJ&HrqlFD61S1rE#na!$^%vvh}?3(CIkV>2Av|D!4xC19B4`acaZrX2-mrCsz zArDg}O{ttORpm{VLqNNx0tgi;UMJFtjsODGF{ZIKE4#yw*f9R4N|?ojs=gl{M;Q6~ zf~c1fvsg+8EG=rbptxrae|=+2OEt8R#wk$?yp$!$wbDc#+ZxhO)=S$6Wa z0R^!t2QisZYUtLPeh#`=wF7bhqSdRe@Pqz)^Oe`hM@M9Ho+)?Tl9g|YMpaI&t5vbz zyNAsgHu09;zzEv9iegb)t(MU6bq2RFou8ZKrqPYYWNAXJry?bIuSN=PCsZ#km$BMI zq6QW}<1fdgJ-su>@)-SN@dpaRbI`YL--FE&+iO7POal^xV6nXlLF& znB8_1FV;EA^aOAiKXYDgH;6%_2As2L^oeg==tJ`Zd0P#7jgP)mgc(A(er+m3lc?Ow zoXOl7psn8Eg1{WhC3K{&Q860t%lm;Y>y8oKtHJjbK*8`j>0q39#OI!K8@tDcQlac5 zS0U~tZm8kgRAW+p$ZkYl7lx%VsWCiznRcNTg2@4f`y!76L?)m{fCWP%Aa>7 zg8?2yL1gh&(n$=M5t%ecrxe%I_)A(8d?*2IN4|?ygd}17kc01BjQshFAo@Mh4Sq!QD9J|Xsi5~W2WU-R9!`%f9(f0kE9-RVFap{ zmkR*}fq~ss5vh9#mtwqLt77FJI3IGE-ckA;f~};3+!+c|L&iO_uSK_YmKYCqb^ZB# z9z%WQtRK<>j=}NUmS0{>@^LzCNL;VTRvdGR7O}reD&4!Ao{o11hq4-;>T_!m;=U&# zbPB9sH*0yNtZ?eB^Kw{r`Od6B4$Lkd{0w7CsLy1$-b#m~HU%Ke=@jS7Y4sEAjGMfm znOoMK81BgVi&Dc&IH>>*;U{aezp<_V)DPb97rm7=Jyl?*`cYQZvzSf%Y_}CGm*gq7ap7b9VisIYx66yC$-J;^{c--!8B5o2l5vWyE#t0l;EJu%3b}X z6|j36T^sgg9AL~`yK(*VY~2e3#rEHKfwAs4Ue3~F3ubVTMo3^$F|uY-o}3a;&Icb! zg7D>{w%ADP#OVggno@FuQaS${cRObK8{EU%Z(sG~zGNtTa_Wh*@8Ve!t@cE*v^@D< zoRCzE{x}q=xUPY3m3*Y66+-nTI`rv;tG_LcjUlfigln`PgWfv>iLIunm~;IkD=zwl zD)3yA06vy*RD2ALkm$3j)^)Y( zQGf4lYVUn*hAHekpd2CfYBmt#*zs!9b6cPFhRnffc7IoXC+wo>$`LSQ}>x?M9;I>jK}h)o?||+<*TvpV`yB~bvsEnDRHV< zWjBE`Cr~vEhaCDV(lmXkoDAHoU+ucQmCz2ve`mj?*@XbX@psZReqAw~o>qq#!`Tum z0_!9Y(@vR>1WJI~PDCha({=MCtVRDMsJamULsb}J8t_L5DwMDhpc-(^J>}*vklDp;^Ev6!*`; zLJY!m@>=}&@?iUet4;(6q5 zPy9XW=iH@Y|0g!;>osnG2z-n{+6L1Q*XM?)aI~@=k*|UB?yx3utGK|{nZFa#pfEEL= zLCq4u8;B`v@+!UUJx$)ZpNIgZBG0nARS&iw|6Dw{N zDu{)2_D>N#2qv85D8`y%hC4wwM`Dd8=L!(P?tUkJe_(7x!CK7zYAK8L>DP*4@DvGO zV1X%!law;5LfuwD{gMF_VrH>@LKT>SE@w4=7Z*V?Z_)@dsOt)73B4(PzRDUn1Q&83 z$pU10aG>jzc$(kX)9R7qn4>Sw)A(P_5XgbVn0V01GZEDyPlwFV{?szP$cA=e@P7>u zOn8(4M1*+9zw{X~d2a3-#K7DL|5rb8&l+hbqKEA&DUVEQ5D|wlco1d&4l@!JeqTXr zuk`%QoBNv4VU3s?_#Wf`_4owT41b>)+;7RI0~%!FM>K~^k1}%Lr38Anwo4B?zYOs6 z!$-yd2<4hbZH6xXl>a$Y2T^n*VjsAt0-J7rAcq&33Z$UhEax*@+rMlOKrzB%I%Rrj z(!1~xOUHAKFZ&m@psM&5=NEO**g^m|J_7)5tVU=t+!V zc;&cN>vRA{@tRE3rM%oE(6UhZs6;G0eM0CR^7ZtD52Zaw)pCKM#9Sql?gVt>l{r0nb znHy?r1nSygl>|ga!#sm0@PE}gBTSvC{n@jqp_amqSkwhcLn^H}BhK)FmB;2-0(sw-g6o&htk$ zQ268SBY)a>#d@s{VmVM1@c$O<9AJ##%#I(l#&3Vq4{r|kXCzKg!)J|lCG1G0gEFN> zl#Hl)Bode)d5LKN{@cd*{Rt~ndg25>vT-k9$4v+PfSyLyq@<(h#a{Jm_=&;h z%mDABB+_*FqznVB1YI?Ig!FAo82b99fZU*X_n+`{g7$8OfBNOfU}D6AVhO|@Li?Gn z1cd<*4F}&fJj)}70coE0@D-^5(1tSqSG^I#8P5B@fm#Y0PrXSzaRx&9;4>5g7r)pX z@WHy+@Nmbo1v9bTIaZNO3E-~w)F^qQ;HuQMWNw zvp)V&b_dNDov{`6YW8Ko_~C>Z)c&lVREp2Tm-DWbXw!_)u3%rfKBUp2-sV4ubkgh^ z^`R__-IT_Es200&5Td!Z9B_ZE9qCDnxGhxr_;paGh4MvU9$e1XI3>dnKYYdGF2!ILYRb>O2fKxnbH1-(fIhe$H9|Jw@G^7sVZNA>tE3^9CH7yx zk=1i-jrPRxtbyIK^#ss2d}BnoEiIn=*%s-cVU6j-95yY!%M6g_#OEu|?3QR7a9=Z= zRVt|9?h)9|U5J5yNO9R@h4R>ecazu!Oi#SWg*Yr+M>i-sF-NYf+5P`q09Yb9PimFi z`lIO0`@Yr18zsD(DL(PHk0M>@mpWxrKs!ErAe*5;Ee8W2Ad%tsU0dxpXpB}^DLAg; z_QUUrUU4}&^D;NE0K4B7MZR${U_&aC1kdAg?$pX_=3q1C5Gn5xKvMJ z!K@cxVfZEceJ`O67jgrur**yI!11}~9&dG{RB8Ltq#;W%s4mL`Tp<4)cfkxnFwc@ zCBxUjmM{`-=Gx+oUjqRY1^Ao0Mk!Ej!t=3K*e6nz$$ze+O!1ol8+k%7Pd0$s>A0jd zt*pH9pcvvdF#@Ngn8%YUbt9lcKc&lm|GwGF0OMjj>uPF^PFj`@E$JhUP;{GI^m^U!4Zn@Wa6jMH_c2<~e#)j-Pw~!xhD=V|`?;TIR4tFK`^Vn_Z#Y3%NBB-0;(3Do>d$G9ksTHU zyMEVP24$~VemaH=&<-Sd0NQsAM4d+axTS()+|RbIzx6A~Fy$PgS%*ZyALT6mOuDg@ z!^sJVl5i@D<7ERNdNKgT;L4sSJ9Srd8SzEDv@jAtx+024dL)y4Z9;c>X;q2v! zv?Jy&aRAqG$pVZka`~ofX`G~8w@)EtW>uIT#xZ60^lzGj6agklKJR#L*LcXIl)?az z0V>}fL_mQ%(#E@tZ4}}v(Nb=R1j|oVrgrd9bV6rIYxLxG_Zq5+Yu_9lzf6Q^bY}EY zc#4a(_h+L4Nv{&Ehyq;!58}jJkotLKN_pl4rS|*5e&6ApD-tp=n#2>|?kJ1OGWcdt z3R@lw?I!&9{L}1qwYZYX=kXsl?7R*REr`5Tup(ks1{8oXgS904%HUK7hi0~uVcIvK zsMDYf^ya&ZHf@b$JYYEEy!G|svyFD6lQ0-L3W?)h%M|}NFs>f~k{W~@eGUD*Ty(`D z0(M`beCMa&g01R&bcZE&m;_e;Y8P-jssE;xlN-a=^HshveTUNhX9^lV9WZXJ$b=LR zLRA$Hc}A@pQ$Ifa2lnt)qwl~ygY^2{_EbQK*s^#Z_HuTC;RH<(r5bC-Yk z_@n{NuWkBOW$mjk9V+Znl+tmG2e{Q51^z#HENN9ps z7b)=>-%#H&#cDMjBuFa&EkmAnQEv1#??A4l`X##MGzb4)LWAZoGtT^={Jwx4X#QuW zIVFP+>uw1X)wS=HM=Q9Y0#c z?`*G+C10aUa`Ce?v-#b9_tkNJk$P4w3H`hwEoiC)P8lv z=)}XxC>f2W9f$LIPq09EX4>tC^*>%#)1@)OIQP*TE?nz~D(Q2s;svInq?D~*vB1~K zea^MndU}>Qk-IHL#DVydnU5z6O&XD&Y?(YiRtsI8d%fTNQTbl#S>p&3sX9t8zD-Xi z_-ni@sa$)!YXDC%o_N{%wWukpdGU|RyX^lxux4ibcz)HoTu00!xngO~n_kyM9YVpv z>brl8^*v}KaKiA^G0jo;Ww@)=pOq?rRQGY_hVW2hDNu>xdHwJ2O$fh*#Rw!MY(iNkz@@kcz7%@V@t4$%ZZQG4dSJ+bEn6&9>YBK`lavOE#7EaG8 z%1_VN1BdzS>HTpalmu_Z!Qx6@7lJH%lDhGh74m% zyRzz!?$$0(f$&`DVnqaF$6(f;;k4tU06IL`9beFnVZdwgZZhFP5Nb)RYVL(2C0vc0 znBzyBD83cF8JoJhMs@2^=zME#&GfHB%CPnt>fu}0fT&mdl}A`avxyWVX>|8;^?IV? z+&`N}?sB^Jqk}&4KH(onB#g+k_B9zeM)KZQGie4HJR-$Y-s3TrdM&x!n;@40_Xt!wnF-Y)i**bC)ugDgNg*w-umz_oi-a*Zo^n* zAI?!I68mK`$*m(=bs2p9bS%XNZ3y^5jXySH5l=9+(9 z51;y@$SHRHe7!KsG)i*r=ukEjpkysc%qvfW8)ML7xGNfd3fr;TEEEqdsXr>TUS(aa z&%txGc!`#U(Ri63pFbpToLt-aC9W_vv3h8vz|jlx{>nt8006{@(4#31F|s0MA{x z;3(Uo-K!6AqHjo?!mMZHN+pCaykX~8_*kXProj^5*dKaF%8Lf%xM4yBA~5Z>IYjSx zt4S>IUq@Q~Fk2Es=2sR)7o6siCIC)x_9p zMH_-1)*`!+SQ=mLfN*R}qL79ww*Kqu*6~?oYLT1G8PPX9G9Csfg}qv5^I@k{Dz0Dn z%`N47SkznODA6bY_|Y@#<-Wy*i|{9z66%z<9NuThr9x>zZ-fWI0lShm4yyIrGWhxP zA2edW!)mlLiXA-M(7STa!lfrp`J4{Y}UkyDimSOVsIPLp{Z#?Z3!l6hCL1I66 zk&KS7+Vhr*>5VmcN9M|JWKvkq0C}m?HiVPyTfpFmYtr&asNQIH0PGT(;R_I?Kn!z|M*fF)AsmR)po}E zNt)#wQng)^J3eUQ*dTQ&e+IT;#eGQG&>Nl=t5^2HE)WEk?Zi`>US%cm{Aq-kP2}zo zD8@5Y4~<2&U0sC`v(aqUU|K9v!K?bU$`t0{5`%hs&g3w}l3s(y&PQyhYwI4GZRiZWlyuXN++vFEV_oQp=uGdu!1{@@tMr{#TwUA4 z?`cg4t$(CjXk!RXDj@y*yr4ED>tPnK)xHGy8QeOtwaw>j+5Ph@w5|iU$GI5AKv>nO zIPtml)U~pf2^%Ro<9&FjN#d&!1M35vUlHzSUrzR(m7W=gY3;^4fn3D%ZjY-&tkPlN zd-tX^V)#G;xlmx)*>A#ww-IM2* z8w(=x_lFMB)o+u@RUTd=81KF}f9vEPV-;;l;p)0U!+Q((EZ8xAfeK5L8jy~l)g==i zk)^NkB$~*BvzSO{<|<4eo+sQ$B529f;a7cTBe5`g&-vBM0bJd3t%w zsGp@oa3`7;t2;*URpq4Md58`UfyuM5v6ZG@O=?pGoQKX?oo6xrn3Al$dm0z5Hzy3j zR_f+Kg_{3`Q+-yhZxD3j4nO?=T)^Q0lbZHc=h;4t3!X$@RQU6UG3e^rYV}FeD9;O+ zjWWPAA+G>b^joSQVqFg6>=S@a2X5C4DFAe|$>*!h57=d&Pu*4bah^HSRFt9)C+g{c z*e)Dgi$oZS?e|o0Jsei#k?(SsphgDb$9{?EZ<8}He}_$Q<0GwIl zRcF+&Dsrr1YTv$%p3E^6`kCm8oL>@x?tZP>%l@xpBFNOVP0RRf9z4k#o%x=X(&z}6 zhGvd%wkr~_ILo@U-Mr7nDU$N?oKJ`l!jd-1*DOX5|J+HknRqpq(MN0 zrKRJJzwi0obARXlbI-Yd?(EF$JTuQuJ@3z}074AvlwkCs4ghmScfW(_PC3aHzm8!c zXy7fps|kF^hw53>gGb^P@G1jbYF8OyN^Cse9sc#I<-uYa4HgSTg}Tb;GW9DeDOXv$ zC_lbYJi0_(-F6ai$c4K+A{VwG6lGuE1m zY^b>TRlG(7u4Mte%y-_SJ7+eCW0wcnX@KcU&J^;P%};dMlzyer+7(2;R<$*3zS*O< zed9i@og4y2N3Z+dP{t}By0vc;!ac;vz&9KkN1C+7@+PTVlT-6-Xlvk&a&L~- zlLC%j%N3Bo#f43O&Yud?57B4|ACyOyyplH192vWIf(JZ^qB`qTvOrNtlc|Aj?`pEN zmmh0SYS>!UqgWNG*z~MRC*v;w~Z&I0~Nt#>G!<`zcb9$~-7dth^K0E_q|NQwA zOl!ijuxg@;nW2TFQ^*$tKH{538^Eq6*n6GA!Iy^?Gn_q&lQsGz1NOd7Qo%&W>8Qn| zw*OBE*v~H#fyd+c$BE9P{L$0GT)0pWm|^x9-5mQ_>J)zbG7+zmuJv1MaLJ`x-cL8T z@HMdZi+qDx_D{7bsnpFkPxu%spX;$us3sNkMA|q1>Mt}XvV=44FLlzRp4JX3pVS0x zYDT#ZxoCWsyMFrPL6R-{ArqCsR-Llh7bqnbJp*;LWt^p|}$y2mWR4Ez2-jXrv zdVlZngvbF1MxZHID*%rN8$dWG#+{5lZ01xj+al|c?^d)lzaR;l=;LPf;o$guHab@W z2t$b;n4$=-o{D1_Y1<zvSj~N-Y*MTSp&x##dYLk+{lq#iup!r4T~PJ=-=AcTvOFWKN>2x;}NF`OSG&k%3=)IJoP4x3!3Qi0!nBLh zdJat^$F{@}@oeQ$Gp?2>&5EqJlC7}DkhVn|S{OO=#)wqoPje+gep@^W(Jcs{C`DU>MivT}(Ew zt|$sybh*OIBbv}(thuCb%V#kMea21ppP2vIn=5kupa%@VxvgECO%r z=bFvn@_l|vI2%-Zda--J=>2r7E4fE+VVtD!sru6=y?rK@*t&=%oOGe`KZ@ZTOsKTe$JdRO{W;EGBui1yZ$aJeV~w4UOA6^a}`u&~_7cvcr+{DC^BTm5c4yCo#iS zf7s{vU6#_looAZ+`2$aPxIvleK4@+oR#!sfndAuu=c7|Ji~|LK@=pz;4~_&T-;Wt@ z9UXjdS}I~CYt(~4U4#gB;!g1f)zeY$nSeirSChMY4XPcU zbtKXyEW8Q}v4)N@Wy&Od6^q>!XBSJg6TN+XZ+Hq-f`z%c%ZX+=G6e#7G;E>94lVCs zK>n!_Is4V8An9`%-cz?K&joL7Q@Z9j@lpfF56?w}oUnYztpWp=_Nv;zKU()H;dsH?Mm(&=anAe4YYp_uwZ03KyiBH>nH?h%7yab|GvYFi9lR!wEU6_O zdX;%4aE$!V5+S@27p{47no~pdj&mqMv+ji@xwRSGG&`AgZGVe({FnI#Ukgg3ooFT z6vZ+aOzX>ig^K5RKl(E3JEHc~iEOd_D#iF?l+0y_o z{URmxG@i67gNEX7pVL4#WLyTmYcL@VMeCFWiO=vxC4sB`#jKd#C_idBB``w!--cf#%vQknnfqX6c9v3O*71{QT~^CldFgT|AuhplNSq_Zu9eWqHr>G|Ie4D`@77 z_vQz$b1TD%%quX19NO=WZRGE~PZV;Do4ZtklG3x;($x-INw%sVK&8`_%K61Y^DUQrchmg3`5#MiViTJn!3sww|k=xNb$D(b0u_D!>Q@|PbHhJID&<%n>>Q7K50Q}G6_J{ z#Z?5HgiY?Q4ITBkNzqUyDe7~hnaNMRdlAZn z=vWxazEzw51gcAPu$L6MqxuBLO=QP=zt!>@`RLaJd$h-l?i`I&7XEf!!bDj`lhMcz zx{}j3mm$cio2(JMTHocBRbb@pLvG?w*61@!b&@0QpEB>rFoM@Fpc@A72HG7Qko0@k z7l-<--W++_EULkWH}r*}8QdEuzlZt8wqzz@Am-`Mt;@}?KDx1aU`y!GtD)$A&Y?Mt zs_J>l7o*9m)5>@RT$@BUhk%e->$GL$5lo2El&ANi(&H1|D8fv(!&7_~p+{U>c*`3`b#EpH ze$m%#Bqe2KEpbNRhYxOh2lSh~R_)^ZrZZoK-Q%_gQqsI%MpVN*|4s9ZGI%#DKD3DbC-jb0whaT+Df^fK2P9 zZ7S{ieBHI(I(;?!ORK@n1={ja3KvTkYjyU}H?J2+U7FHgXLs0Rihho!AF&zs zc9R9dwKliww1k0q!q(}a?k%lLJuPm2uL|H6uGbfba)NKxGwYbfX31bhUVLas@pOCq zu`-yzvQWkEA>%;;Ke6khTRo{hcV7mFcruAyK2bS?SFHP?mGb?DS`*XSG>Qbm-I^jG2f&`e; zJTTjhR|>UnE?e}(nN2j;+xHG`+V2hmU<%G6VA%Gi)}`|2?}Z-H+U)IbvnCDVZVty> zBpe@J)w!vPS4TU;ojZ>EjXlTQ#&GzVR6v6?2IxPAhnD-!=(PL;*qfuZg1S*nk-YdA zvuAb{(95+=yZh6xY#VCw1wfQ5%7M~DN1g9eYs##wO4?(pA;hlk2$bRuJPda4;jja4rEO?c#S8d*7?SdW+0A*f#`- zomwqYwF_woS$+FC$I?~{M_JJcj|I>rN<`Q_HR{;?u4_g6{DrOad)khX(kVf#y`OLTnp;0F?BxD7?AwB_8zlFPJUH>%SY)$yOoy$BKMYjCb{E(;ToR^SCt z3bkpQxqzG%q_ro__{%-kYFtM?)ufF6`ZwmxE$OesWfl}(S`fVveqj47ReZ#m18Ta{ zAkCSz8hvpU_;5-L6FjUFumrcPtCzPj!Rjqu|q zJaklwK?ekSVQWeb`vBL*DM_G*f&kD4J%zK}-^kbD^B@fcqrtZTzR(Z!u^~?0W5}$Kr8-VFwwYGtJVk5 z67@>u&ttIk?=C`+);9oT@>h5bvS+|>Vn*TG#7C9p=*pqeQdMR@E`$ldG8y9=>tzn> zg$BI<@MA_|dvDM(W}3+FYy71cV9<0ma%#ylu~%Z!lIB#rlla-MMZ-{l0W^xX5hK&8 z4U;w@GEg}W*oeQ=aHIJpfD(h({LBgA`WOtl)j}Wi8W}zgPW=yLh%)VsCSV5HZEn6q z`FM0j;C^ndpQcy>ph5~qJG6NFO&{F^Q``-;^ts&x*s!S~_sl43PgL1VGL)YEq1g2H)a``rsF|58HPMyS=BR3Pj$S`i=wh#A*SPES5i zSXc;>UT3%d`VS_=D=3)ZJX=5rKta;|xc{uE=C^H)Dj!N-HPT8Mw2MVAf=v&(2M#Xw z{Tj|cp8>VZ+P0vUOAk%j@+w&pLqXoLi3y=Wp)ik+0gv0H671zSW8=b{pMGre{`&}H z;VpqN3RZYXSP;=|XwU;-CjyX?2F0b>JdOf+ZI}~`O(NA2nhyHWV2_R!zJp^~gvHPB zE-~^pqZ;+!gj3G4II8rzXm`ub5hjZLzFjaJWe;$Zv?XB!O1mne6INN3h~14g`SIL3!2nKnAb|?+h-C zINx7E2aGJ8#n~VyJ_g)QE1fa64AZe!QHAYy(#eMPcWFZga14YnPMafXCom8@$oj=P4L zB6OtbSo{hQ2~-LftG2be!FL^x353m{c!1m`a1NdIoVU{L!sdCPq|s#j6H6i31%pioC_rfxCr{%;0f;7^wA!bxMGRP}9k;7k zh(O2hQ6CWrN=lRus^hEV9?b~p6YAWc&{H3PIFtQUMdK}Y-ID=9G#$5mK_vJC0+aeq zl56-f!r@hB^#$F4*3xX z3LRf8XJt4>O4FJH_NXE()!;j>OrR6$Ia5gt0ux^}t^}l{4n|#b?xa-I%YtsW=YTT_!$&0PP5<*sbA9(K#5jyj;i1@kwZ^soSNt<+ z@QvXV{WvX3XUTI|4_TTLk&sNm$MulwiA%B1Y&=m;0~8lA=YKlZXn7TsX$ENLo24qe ze;cksCls1hZuLvPrP<*->1yEpiXVLM4a8zD zCkV-d7?9z&BN(098!4PQbJG~?Su4fB%kz6{TY>hOUXhKGkg z)V1HrOeq3BB46~R{3t_4URwJ4S2uV#2OUA$aU}5cb9!YW9Gb?=)MNc!aka)pQAOn} zJUFiu4fY>&e)0*TkPK zgV$xM33$Om#!BEjx`y`MOenKZ%$? zUf7f89?cU?lx#Oy#F@_P`4gQ?CHC#=8-YE;OF@hif1Gijz4h4Mx z8vs-ZDzuW4g(?lxGm}iR=_?N;?2+G7c%^c@^tkN-PV|g{@p^BfP*30*^}F@Qa`mbM zd`UNQ27uz7){cL7IP=Nz54^oa8f4ID(ouLb~66c%NOB?VDfz{NhsN9;1QVSvTW5jz&7!DI+a7&mwy1V5o>#(F^^ zZHOi{vftcc?lWTQ(ufHxXAdZWfe$nC!1*-fs5k)>^@myrm(tH!QIQ>J`?Qu;MX#Yi z`@#H2?l4s#*5I_*?%A78jZZuNcT;~9C+9S{jh{`P-)1S-XYN)Q3z?taR8<%~Lj}I2 zdNox(6fG+-6MYXVHOoxQNNf*WA6;~5Ap?H5Hzj@np z&PU4q%&N!%;@OA>Zy>b?bJ=&f3AhAmdA!5shOE^kB~8=Y-ikSnzp>%e3y??$MC9#l zFEIdt<_9{)mrwG*n?kTEkgX|dUe1wK)(2)iaP@@qa)?0y=a&uwIqt!@=y*AwcOT^6 zVxpEu)CO7Oq84&lX|f+N7xzAdCbYT2&%3e!0F6=1dS=9Fo}o;$2gBL8@z-R#bEJ1g z*CUl=(0F~o;eF5z9?-*|KDUAH1Yb~vvUf#!6jWTjjSxa$?;*@xkDS= zSNUp2NQ+6=?|HzEworS(1c4qNGI&qyK_C#u-+OLUkg=Cy9SLJzIxq#KD61w@DQ)rQ FzW|VN&M^Q0 literal 0 HcmV?d00001 diff --git a/web/product-icon.svg b/web/product-icon.svg new file mode 100644 index 0000000..5827921 --- /dev/null +++ b/web/product-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/web/src/app/providers/SessionProvider.tsx b/web/src/app/providers/SessionProvider.tsx index 49c01fa..968b4ca 100644 --- a/web/src/app/providers/SessionProvider.tsx +++ b/web/src/app/providers/SessionProvider.tsx @@ -13,19 +13,21 @@ import { useNavigation } from './NavigationProvider'; import { useCatalogContext } from './CatalogProvider'; const signalUrl = import.meta.env.VITE_SIGNAL_URL ?? `${window.location.protocol}//${window.location.hostname}:8788`; +const publicAppUrl = import.meta.env.VITE_PUBLIC_APP_URL?.trim() || null; type SessionValue = ReturnType; const SessionContext = createContext(null); export function SessionProvider({ children }: { children: ReactNode }) { - const { listenerEvmAddress } = useWalletContext(); + const { activeIdentityAddress } = useWalletContext(); const { navigateToView } = useNavigation(); const catalog = useCatalogContext(); const session = useSession({ signalUrl, - identityAddress: listenerEvmAddress, + publicAppUrl, + identityAddress: activeIdentityAddress, audioSource: catalog.audioSource, trackInfo: catalog.trackInfo, setTrackInfo: catalog.setTrackInfo, @@ -50,7 +52,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { useEffect(() => { const initialRoomCode = getInitialRoomCode(); if (!initialRoomCode || session.roomId) return; - const remembered = getStoredDisplayName(listenerEvmAddress); + const remembered = getStoredDisplayName(activeIdentityAddress); if (!remembered) return; session.setDisplayName(remembered); session.joinRoom(initialRoomCode, { displayName: remembered }); @@ -68,11 +70,11 @@ export function SessionProvider({ children }: { children: ReactNode }) { // write a partial name to storage on every keystroke. const setDisplayName = session.setDisplayName; useEffect(() => { - const stored = getStoredDisplayName(listenerEvmAddress); + const stored = getStoredDisplayName(activeIdentityAddress); if (stored) setDisplayName(stored); // Re-run only when the connected address changes. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [listenerEvmAddress]); + }, [activeIdentityAddress]); return {children}; } diff --git a/web/src/app/providers/WalletProvider.tsx b/web/src/app/providers/WalletProvider.tsx index aef0e7d..9be0100 100644 --- a/web/src/app/providers/WalletProvider.tsx +++ b/web/src/app/providers/WalletProvider.tsx @@ -18,6 +18,7 @@ import { devAccounts, type DevAccount } from '../../hooks/useDevAccounts'; import { getDefaultEthRpcUrl } from '../../shared/config/network'; import { resolveEvmChain, getWalletClient } from '../../shared/config/contracts'; import { chainMismatchMessage } from '../../features/wallet/network'; +import type { ProductHostMode, ProductHostStatus } from '../../features/productHost/productHost'; import { useUiFeedback } from './UiFeedbackProvider'; type WalletContextValue = { @@ -25,6 +26,7 @@ type WalletContextValue = { connectedWallet: ConnectedWallet | null; activeEvmAddress: `0x${string}`; listenerEvmAddress: `0x${string}` | null; + activeIdentityAddress: string | null; activeSubstrateAddress: string | null; activeSubstrateSigner: PolkadotSigner | null; currentBulletinAccount: DevAccount; @@ -37,10 +39,13 @@ type WalletContextValue = { switchNetwork: () => Promise; connectPasskey: () => Promise; connectExtension: () => Promise; + connectProductHost: () => Promise; disconnect: () => void; forgetPasskey: () => void; hasPrfSupport: boolean; hasStoredPasskey: boolean; + productHostMode: ProductHostMode; + productHostStatus: ProductHostStatus; }; const WalletContext = createContext(null); @@ -51,11 +56,14 @@ export function WalletProvider({ children }: { children: ReactNode }) { state: walletState, connectPasskey, connectExtension, + connectProductHost, switchExtensionNetwork, disconnect: disconnectWalletOnly, hasPrfSupport, hasStoredPasskey, - forgetPasskey + forgetPasskey, + productHostMode, + productHostStatus } = useWallet(); const [ethRpcUrl] = useState(getDefaultEthRpcUrl); @@ -68,7 +76,7 @@ export function WalletProvider({ children }: { children: ReactNode }) { // Disconnecting the wallet also signs out of the Dotify session (ticket 24 // P2): revoke the server-side token and forget the stored one, so a shared // machine does not keep listening rights after the wallet leaves. - const connectedAddress = connectedWallet?.evmAddress; + const connectedAddress = connectedWallet?.createEvmClient ? connectedWallet.evmAddress : undefined; const lastConnectedAddressRef = useRef<`0x${string}` | null>(null); const disconnect = useCallback(() => { if (connectedAddress) void signOutOfDotifySession(connectedAddress); @@ -88,7 +96,10 @@ export function WalletProvider({ children }: { children: ReactNode }) { const currentBulletinAccount = devAccounts[bulletinAccountIndex]; const activeEvmAddress = connectedWallet?.evmAddress ?? zeroAddress; - const listenerEvmAddress = connectedWallet?.evmAddress ?? null; + const listenerEvmAddress = connectedWallet?.createEvmClient ? connectedWallet.evmAddress : null; + // Local room-name persistence lowercases its key, so use the H160 identity + // for both EVM wallets and Product accounts rather than case-sensitive SS58. + const activeIdentityAddress = connectedWallet?.evmAddress ?? null; const devBulletinFallback = import.meta.env.DEV ? currentBulletinAccount : null; const activeSubstrateAddress = connectedWallet ? (connectedWallet.substrateAddress ?? null) : (devBulletinFallback?.address ?? null); const activeSubstrateSigner = connectedWallet ? (connectedWallet.substrateSigner ?? null) : (devBulletinFallback?.signer ?? null); @@ -97,6 +108,9 @@ export function WalletProvider({ children }: { children: ReactNode }) { if (!connectedWallet) { throw new Error('Connect a wallet before signing this transaction.'); } + if (!connectedWallet.createEvmClient) { + throw new Error('This action still requires a passkey or EVM wallet while Dotify contracts are being ported to the Product DevNet host signer.'); + } const chain = await resolveEvmChain(ethRpcUrl); if (connectedWallet.chainId !== undefined && connectedWallet.chainId !== chain.id) { throw new Error(chainMismatchMessage(chain.id, connectedWallet.chainId)); @@ -166,6 +180,7 @@ export function WalletProvider({ children }: { children: ReactNode }) { connectedWallet, activeEvmAddress, listenerEvmAddress, + activeIdentityAddress, activeSubstrateAddress, activeSubstrateSigner, currentBulletinAccount, @@ -178,16 +193,20 @@ export function WalletProvider({ children }: { children: ReactNode }) { switchNetwork, connectPasskey, connectExtension, + connectProductHost, disconnect, forgetPasskey, hasPrfSupport, - hasStoredPasskey + hasStoredPasskey, + productHostMode, + productHostStatus }), [ walletState, connectedWallet, activeEvmAddress, listenerEvmAddress, + activeIdentityAddress, activeSubstrateAddress, activeSubstrateSigner, currentBulletinAccount, @@ -199,10 +218,13 @@ export function WalletProvider({ children }: { children: ReactNode }) { switchNetwork, connectPasskey, connectExtension, + connectProductHost, disconnect, forgetPasskey, hasPrfSupport, - hasStoredPasskey + hasStoredPasskey, + productHostMode, + productHostStatus ] ); diff --git a/web/src/components/WalletModal.tsx b/web/src/components/WalletModal.tsx index 989d2e1..cae1226 100644 --- a/web/src/components/WalletModal.tsx +++ b/web/src/components/WalletModal.tsx @@ -1,4 +1,4 @@ -import { ExternalLink, KeyRound, LockKeyhole, Music2, Power, RefreshCw, Users, Wallet, X } from 'lucide-react'; +import { Box, ExternalLink, KeyRound, LockKeyhole, Music2, Power, RefreshCw, Users, Wallet, X } from 'lucide-react'; import { Dialog } from './Dialog'; import type { WalletState } from '../hooks/useWallet'; import type { CatalogTrack } from '../shared/types'; @@ -58,6 +58,9 @@ export function WalletModal({ isSwitchingNetwork, connectPasskey, connectExtension, + connectProductHost, + productHostMode, + productHostStatus, switchNetwork, forgetPasskey: onForgetPasskey, disconnect: onDisconnect @@ -69,6 +72,7 @@ export function WalletModal({ const onClose = () => setShowWalletModal(false); const onPasskey = () => void connectPasskey(); const onExtension = () => void connectExtension(); + const onProductHost = () => void connectProductHost(); const onSwitchNetwork = () => void switchNetwork(); if (state.status === 'connected') { @@ -95,7 +99,7 @@ export function WalletModal({
    {wallet.label} - {wallet.evmAddress ? ( + {wallet.method !== 'product-host' ? ( {shortenAddress(identityAddress)} @@ -108,7 +112,15 @@ export function WalletModal({
    Connection - {walletChainMismatch ? 'Needs attention' : wallet.method === 'passkey' ? 'This device' : 'Wallet app'} + + {walletChainMismatch + ? 'Needs attention' + : wallet.method === 'passkey' + ? 'This device' + : wallet.method === 'product-host' + ? 'Product host' + : 'Wallet app'} + {walletChainMismatch && Choose the right network to continue} {walletChainMismatch && wallet.method === 'extension' && onSwitchNetwork && (
    + {wallet.method === 'product-host' && ( + <> +

    + Your app-scoped Polkadot identity is active for presence and rooms. Paid access, protected playback, and artist publishing still require an EVM + signer during the contract port. +

    +
    + {hasPrfSupport && ( + + )} + +
    + + )} +
    {unlockedCount} @@ -254,15 +297,39 @@ export function WalletModal({ {state.status === 'error' &&

    {state.message}

    } {state.status === 'connecting' && ( -

    {state.via === 'passkey' ? 'Check your browser prompt to continue.' : 'Check your wallet to approve the connection.'}

    +

    + {state.via === 'passkey' + ? 'Check your browser prompt to continue.' + : state.via === 'product-host' + ? 'Check the Polkadot Product host to continue.' + : 'Check your wallet to approve the connection.'} +

    )} {state.status === 'needs-reconnect' && state.via === 'passkey' && (

    Your saved passkey is ready. Use passkey to reconnect when you are ready.

    )}
    + {productHostMode !== 'off' && ( + + )} + {hasPrfSupport && ( -
  • @@ -1607,7 +1608,7 @@

    Explore the project

    Product DevNet architecture - What the Product host adapts now, what remains on Fly, and how contract signing ports next. + What the Product host adapts now, what remains on Fly, and how runtime ports prepare CDM/PAPI. diff --git a/web/src/features/runtime/runtimePorts.ts b/web/src/features/runtime/runtimePorts.ts new file mode 100644 index 0000000..b79e9e3 --- /dev/null +++ b/web/src/features/runtime/runtimePorts.ts @@ -0,0 +1,69 @@ +import type { Address, Hash } from 'viem'; +import type { OnchainTrackRecord, RoyaltySplit } from '../../shared/types'; + +export type RuntimeDirectoryEntry = { + artist: Address; + runtime: Address; +}; + +export type RuntimeTrackSnapshot = { + hash: Hash; + record: OnchainTrackRecord; + royaltySplits: Array>; +}; + +export type RuntimeRoyaltyPaymentLog = { + trackHash: Hash; + listener: Address; + amountWei: bigint; + paidAtMs: number | null; + transactionHash: Hash; + blockNumber: bigint; + logIndex: number; +}; + +export type RuntimeTrackRegistration = { + contentHash: Hash; + title: string; + artistName: string; + description: string; + imageRef: string; + audioRef: string; + metadataRef: string; + artistContractRef: string; + accessMode: number; + pricePlanck: bigint; + requiredPersonhood: number; + royaltyRecipients: Address[]; + royaltyShares: number[]; +}; + +export type RuntimeAccessPolicyUpdate = { + contentHash: Hash; + accessMode: number; + pricePlanck: bigint; + requiredPersonhood: number; +}; + +export interface RuntimeReadPort { + ensureContract(address: Address): Promise; + resolveArtistRuntime(directoryAddress: Address, artistAddress: Address): Promise
    ; + getArtistCount(directoryAddress: Address): Promise; + listArtistRuntimes(directoryAddress: Address, artistCount: bigint): Promise; + listRuntimeTracks(runtimeAddress: Address): Promise; + canAccess(runtimeAddress: Address, contentHash: Hash, listenerAddress: Address): Promise; + hasPaid(runtimeAddress: Address, contentHash: Hash, listenerAddress: Address): Promise; + pendingRuntimeOf(factoryAddress: Address, artistAddress: Address): Promise
    ; + pendingRuntimeStageOf(factoryAddress: Address, artistAddress: Address): Promise; + listRoyaltyPaymentLogs(runtimeAddress: Address): Promise; +} + +export interface RuntimeWritePort { + createRuntime(factoryAddress: Address): Promise; + installRuntimeStep(factoryAddress: Address): Promise; + registerTrack(runtimeAddress: Address, registration: RuntimeTrackRegistration): Promise; + payForAccess(runtimeAddress: Address, contentHash: Hash, value: bigint): Promise; + setAccessMode(runtimeAddress: Address, update: RuntimeAccessPolicyUpdate): Promise; + setReleaseActive(runtimeAddress: Address, contentHash: Hash, active: boolean): Promise; + waitForTransaction(txHash: Hash): Promise; +} diff --git a/web/src/features/runtime/viemRuntimeAdapter.test.ts b/web/src/features/runtime/viemRuntimeAdapter.test.ts new file mode 100644 index 0000000..d4b2c6c --- /dev/null +++ b/web/src/features/runtime/viemRuntimeAdapter.test.ts @@ -0,0 +1,161 @@ +import { describe, expect, it, vi } from 'vitest'; +import { createViemRuntimeReader, createViemRuntimeWriter } from './viemRuntimeAdapter'; +import type { OnchainTrackRecord } from '../../shared/types'; + +const directory = '0x1000000000000000000000000000000000000000' as const; +const factory = '0x2000000000000000000000000000000000000000' as const; +const runtime = '0x3000000000000000000000000000000000000000' as const; +const artist = '0x4000000000000000000000000000000000000000' as const; +const listener = '0x5000000000000000000000000000000000000000' as const; +const splitRecipient = '0x6000000000000000000000000000000000000000' as const; +const hash = `0x${'ab'.repeat(32)}` as const; +const txHash = `0x${'cd'.repeat(32)}` as const; + +function baseTrackRecord(patch: Partial = {}): OnchainTrackRecord { + return { + artist, + tokenId: 1n, + title: 'Runtime song', + artistName: 'Runtime artist', + description: 'On-chain description', + imageRef: 'ipfs://cover', + audioRef: 'dotify.audio.v2:audio', + metadataRef: 'ipfs://metadata', + artistContractRef: 'dotify:self-certified', + royaltyBps: 9000, + accessMode: 1, + pricePlanck: 1_000_000_000_000_000_000n, + requiredPersonhood: 0, + registeredAtBlock: 12n, + active: true, + ...patch + }; +} + +describe('createViemRuntimeReader', () => { + it('paginates directory runtimes and filters zero-address entries', async () => { + const readContract = vi.fn(async ({ functionName, args }: { functionName: string; args?: unknown[] }) => { + if (functionName !== 'artistsPage') throw new Error(`unexpected ${functionName}`); + const offset = args?.[0]; + if (offset === 0n) { + return [[artist], [runtime]]; + } + return [[listener], ['0x0000000000000000000000000000000000000000']]; + }); + const reader = createViemRuntimeReader({ + ethRpcUrl: 'http://localhost:8545', + publicClient: { readContract } as never + }); + + await expect(reader.listArtistRuntimes(directory, 51n)).resolves.toEqual([{ artist, runtime }]); + expect(readContract).toHaveBeenCalledTimes(2); + }); + + it('returns runtime track snapshots with royalty splits', async () => { + const record = baseTrackRecord(); + const readContract = vi.fn(async ({ functionName }: { functionName: string }) => { + switch (functionName) { + case 'musicRegTrackCount': + return 1n; + case 'musicRegTrackHashAtIndex': + return hash; + case 'musicRegGetTrack': + return [record, runtime]; + case 'musicRoySplitCount': + return 1n; + case 'musicRoySplitAt': + return [splitRecipient, 2500]; + default: + throw new Error(`unexpected ${functionName}`); + } + }); + const reader = createViemRuntimeReader({ + ethRpcUrl: 'http://localhost:8545', + publicClient: { readContract } as never + }); + + await expect(reader.listRuntimeTracks(runtime)).resolves.toEqual([ + { + hash, + record, + royaltySplits: [{ recipient: splitRecipient, bps: 2500 }] + } + ]); + }); + + it('normalizes royalty payment logs with block timestamps', async () => { + const getLogs = vi.fn(async () => [ + { + args: { contentHash: hash, listener, amount: 2_000_000_000_000_000_000n }, + transactionHash: txHash, + blockNumber: 7n, + logIndex: 3 + } + ]); + const getBlock = vi.fn(async () => ({ timestamp: 123n })); + const reader = createViemRuntimeReader({ + ethRpcUrl: 'http://localhost:8545', + publicClient: { getLogs, getBlock } as never + }); + + await expect(reader.listRoyaltyPaymentLogs(runtime)).resolves.toEqual([ + { + trackHash: hash, + listener, + amountWei: 2_000_000_000_000_000_000n, + paidAtMs: 123_000, + transactionHash: txHash, + blockNumber: 7n, + logIndex: 3 + } + ]); + }); +}); + +describe('createViemRuntimeWriter', () => { + it('routes runtime writes through named contract calls and waits for receipts', async () => { + const writeContract = vi.fn(async ({ functionName }: { functionName: string }) => { + return `${txHash}:${functionName}` as `0x${string}`; + }); + const waitForTransactionReceipt = vi.fn(async () => ({ status: 'success' })); + const writer = createViemRuntimeWriter({ + ethRpcUrl: 'http://localhost:8545', + walletClient: { writeContract } as never, + publicClient: { waitForTransactionReceipt } as never + }); + + await expect(writer.createRuntime(factory)).resolves.toBe(`${txHash}:createRuntime`); + await expect(writer.installRuntimeStep(factory)).resolves.toBe(`${txHash}:installRuntimeStep`); + await expect(writer.payForAccess(runtime, hash, 1n)).resolves.toBe(`${txHash}:musicRoyPayAccess`); + await expect( + writer.registerTrack(runtime, { + contentHash: hash, + title: 'Runtime song', + artistName: 'Runtime artist', + description: 'On-chain description', + imageRef: 'ipfs://cover', + audioRef: 'dotify.audio.v2:audio', + metadataRef: 'ipfs://metadata', + artistContractRef: 'dotify:self-certified', + accessMode: 1, + pricePlanck: 1n, + requiredPersonhood: 0, + royaltyRecipients: [artist], + royaltyShares: [10_000] + }) + ).resolves.toBe(`${txHash}:musicRegRegister`); + await expect( + writer.setAccessMode(runtime, { + contentHash: hash, + accessMode: 2, + pricePlanck: 0n, + requiredPersonhood: 1 + }) + ).resolves.toBe(`${txHash}:musicRegSetAccessMode`); + await expect(writer.setReleaseActive(runtime, hash, false)).resolves.toBe(`${txHash}:musicRegDeactivate`); + + await writer.waitForTransaction(txHash); + expect(waitForTransactionReceipt).toHaveBeenCalledWith({ hash: txHash }); + expect(writeContract).toHaveBeenCalledTimes(6); + }); +}); diff --git a/web/src/features/runtime/viemRuntimeAdapter.ts b/web/src/features/runtime/viemRuntimeAdapter.ts new file mode 100644 index 0000000..39300fa --- /dev/null +++ b/web/src/features/runtime/viemRuntimeAdapter.ts @@ -0,0 +1,310 @@ +import { parseAbiItem, zeroAddress, type Address, type Hash } from 'viem'; +import { + artistDirectoryAbi, + artistRuntimeFactoryAbi, + getPublicClient, + getWalletClient, + musicAccessAbi, + musicRegistryAbi, + musicRoyaltiesAbi +} from '../../shared/config/contracts'; +import type { + RuntimeAccessPolicyUpdate, + RuntimeDirectoryEntry, + RuntimeReadPort, + RuntimeRoyaltyPaymentLog, + RuntimeTrackSnapshot, + RuntimeWritePort +} from './runtimePorts'; +import type { OnchainTrackRecord } from '../../shared/types'; + +type ViemPublicClient = ReturnType; +type ViemWalletClient = Awaited>; + +const musicRoyAccessPaidEvent = parseAbiItem('event MusicRoyAccessPaid(bytes32 indexed contentHash, address indexed listener, uint256 amount)'); + +export type ViemRuntimeReaderDeps = { + ethRpcUrl: string; + publicClient?: ViemPublicClient; +}; + +export type ViemRuntimeWriterDeps = ViemRuntimeReaderDeps & { + walletClient: ViemWalletClient; +}; + +function resolvePublicClient(deps: ViemRuntimeReaderDeps): ViemPublicClient { + return deps.publicClient ?? getPublicClient(deps.ethRpcUrl); +} + +async function blockTimestampMs(client: ViemPublicClient, blockNumber: bigint): Promise { + const block = await client.getBlock({ blockNumber }); + return Number(block.timestamp) * 1000; +} + +export function createViemRuntimeReader(deps: ViemRuntimeReaderDeps): RuntimeReadPort { + const client = () => resolvePublicClient(deps); + + return { + ensureContract(address) { + return client() + .getCode({ address }) + .then(code => Boolean(code && code !== '0x')); + }, + + async resolveArtistRuntime(directoryAddress, artistAddress) { + const runtimeAddress = (await client().readContract({ + address: directoryAddress, + abi: artistDirectoryAbi, + functionName: 'runtimeOf', + args: [artistAddress] + })) as Address; + return runtimeAddress === zeroAddress ? null : runtimeAddress; + }, + + async getArtistCount(directoryAddress) { + return (await client().readContract({ + address: directoryAddress, + abi: artistDirectoryAbi, + functionName: 'artistCount' + })) as bigint; + }, + + async listArtistRuntimes(directoryAddress, artistCount) { + const pageSize = 50n; + const entries: RuntimeDirectoryEntry[] = []; + + for (let offset = 0n; offset < artistCount; offset += pageSize) { + const limit = artistCount - offset > pageSize ? pageSize : artistCount - offset; + const [artists, runtimes] = (await client().readContract({ + address: directoryAddress, + abi: artistDirectoryAbi, + functionName: 'artistsPage', + args: [offset, limit] + })) as [Address[], Address[]]; + + for (let index = 0; index < artists.length; index += 1) { + const artist = artists[index]; + const runtime = runtimes[index]; + if (!artist || !runtime || runtime === zeroAddress) continue; + entries.push({ artist, runtime }); + } + } + + return entries; + }, + + async listRuntimeTracks(runtimeAddress) { + const trackCount = (await client().readContract({ + address: runtimeAddress, + abi: musicRegistryAbi, + functionName: 'musicRegTrackCount' + })) as bigint; + + return Promise.all( + Array.from({ length: Number(trackCount) }, async (_, index): Promise => { + const hash = (await client().readContract({ + address: runtimeAddress, + abi: musicRegistryAbi, + functionName: 'musicRegTrackHashAtIndex', + args: [BigInt(index)] + })) as Hash; + + const [record] = (await client().readContract({ + address: runtimeAddress, + abi: musicRegistryAbi, + functionName: 'musicRegGetTrack', + args: [hash] + })) as [OnchainTrackRecord, Address]; + + const splitCount = (await client() + .readContract({ + address: runtimeAddress, + abi: musicRoyaltiesAbi, + functionName: 'musicRoySplitCount', + args: [hash] + }) + .catch(() => 0n)) as bigint; + + const royaltySplits = ( + await Promise.all( + Array.from({ length: Number(splitCount) }, async (_, splitIndex) => { + try { + const [recipient, bps] = (await client().readContract({ + address: runtimeAddress, + abi: musicRoyaltiesAbi, + functionName: 'musicRoySplitAt', + args: [hash, BigInt(splitIndex)] + })) as [Address, number]; + return { recipient, bps: Number(bps) }; + } catch { + return null; + } + }) + ) + ).filter((split): split is { recipient: Address; bps: number } => Boolean(split)); + + return { hash, record, royaltySplits }; + }) + ); + }, + + async canAccess(runtimeAddress, contentHash, listenerAddress) { + return (await client().readContract({ + address: runtimeAddress, + abi: musicAccessAbi, + functionName: 'musicAccCanAccess', + args: [contentHash, listenerAddress] + })) as boolean; + }, + + async hasPaid(runtimeAddress, contentHash, listenerAddress) { + return (await client().readContract({ + address: runtimeAddress, + abi: musicAccessAbi, + functionName: 'musicAccHasPaid', + args: [contentHash, listenerAddress] + })) as boolean; + }, + + async pendingRuntimeOf(factoryAddress, artistAddress) { + const pendingRuntime = (await client().readContract({ + address: factoryAddress, + abi: artistRuntimeFactoryAbi, + functionName: 'pendingRuntimeOf', + args: [artistAddress] + })) as Address; + return pendingRuntime === zeroAddress ? null : pendingRuntime; + }, + + async pendingRuntimeStageOf(factoryAddress, artistAddress) { + return Number( + await client().readContract({ + address: factoryAddress, + abi: artistRuntimeFactoryAbi, + functionName: 'pendingRuntimeStageOf', + args: [artistAddress] + }) + ); + }, + + async listRoyaltyPaymentLogs(runtimeAddress) { + const logs = await client().getLogs({ + address: runtimeAddress, + event: musicRoyAccessPaidEvent, + fromBlock: 0n, + toBlock: 'latest' + }); + const timestampsByBlock = new Map(); + await Promise.all( + Array.from(new Set(logs.map(log => log.blockNumber.toString()))).map(async blockNumber => { + timestampsByBlock.set(blockNumber, await blockTimestampMs(client(), BigInt(blockNumber))); + }) + ); + + return logs + .map((log): RuntimeRoyaltyPaymentLog | null => { + const trackHash = log.args.contentHash; + const listener = log.args.listener; + const amountWei = log.args.amount; + if (!trackHash || !listener || amountWei === undefined) return null; + return { + trackHash, + listener, + amountWei, + paidAtMs: timestampsByBlock.get(log.blockNumber.toString()) ?? null, + transactionHash: log.transactionHash, + blockNumber: log.blockNumber, + logIndex: log.logIndex + }; + }) + .filter((payment): payment is RuntimeRoyaltyPaymentLog => Boolean(payment)); + } + }; +} + +export function createViemRuntimeWriter(deps: ViemRuntimeWriterDeps): RuntimeWritePort { + const client = () => resolvePublicClient(deps); + const { walletClient } = deps; + + return { + createRuntime(factoryAddress) { + return walletClient.writeContract({ + address: factoryAddress, + abi: artistRuntimeFactoryAbi, + functionName: 'createRuntime' + }); + }, + + installRuntimeStep(factoryAddress) { + return walletClient.writeContract({ + address: factoryAddress, + abi: artistRuntimeFactoryAbi, + functionName: 'installRuntimeStep' + }); + }, + + registerTrack(runtimeAddress, registration) { + return walletClient.writeContract({ + address: runtimeAddress, + abi: musicRegistryAbi, + functionName: 'musicRegRegister', + args: [ + { + contentHash: registration.contentHash, + title: registration.title, + artistName: registration.artistName, + description: registration.description, + imageRef: registration.imageRef, + audioRef: registration.audioRef, + metadataRef: registration.metadataRef, + artistContractRef: registration.artistContractRef, + accessMode: registration.accessMode, + pricePlanck: registration.pricePlanck, + requiredPersonhood: registration.requiredPersonhood + }, + registration.royaltyRecipients, + registration.royaltyShares + ] + }); + }, + + payForAccess(runtimeAddress, contentHash, value) { + return walletClient.writeContract({ + address: runtimeAddress, + abi: musicRoyaltiesAbi, + functionName: 'musicRoyPayAccess', + args: [contentHash], + value + }); + }, + + setAccessMode(runtimeAddress, update: RuntimeAccessPolicyUpdate) { + return walletClient.writeContract({ + address: runtimeAddress, + abi: musicRegistryAbi, + functionName: 'musicRegSetAccessMode', + args: [update.contentHash, update.accessMode, update.pricePlanck, update.requiredPersonhood] + }); + }, + + setReleaseActive(runtimeAddress, contentHash, active) { + return active + ? walletClient.writeContract({ + address: runtimeAddress, + abi: musicRegistryAbi, + functionName: 'musicRegReactivate', + args: [contentHash] + }) + : walletClient.writeContract({ + address: runtimeAddress, + abi: musicRegistryAbi, + functionName: 'musicRegDeactivate', + args: [contentHash] + }); + }, + + async waitForTransaction(txHash) { + await client().waitForTransactionReceipt({ hash: txHash }); + } + }; +} diff --git a/web/src/hooks/useArtistConsole.ts b/web/src/hooks/useArtistConsole.ts index 8e06d74..56420ba 100644 --- a/web/src/hooks/useArtistConsole.ts +++ b/web/src/hooks/useArtistConsole.ts @@ -1,14 +1,6 @@ import { useState } from 'react'; -import { getAddress, isAddress, parseAbiItem } from 'viem'; -import { - ensureContract, - getPublicClient, - getWalletClient, - resolveEvmChain, - artistRuntimeFactoryAbi, - artistDirectoryAbi, - musicRegistryAbi -} from '../shared/config/contracts'; +import { getAddress, isAddress } from 'viem'; +import { getWalletClient, resolveEvmChain } from '../shared/config/contracts'; import { checkBulletinAuthorization, encodeBulletinJson, uploadToBulletin } from './useBulletin'; import { protectedAudioUploadToCID, @@ -21,8 +13,9 @@ import { import { chainMismatchMessage } from '../features/wallet/network'; import { localAudioRef, priceDotForAccessMode, runtimeAddressFromTrackId } from '../features/catalog/trackModel'; import { encodeAccessMode, encodeRequiredPersonhood, manifestRequiredPersonhood } from '../features/runtime/accessEncoding'; +import { createViemRuntimeReader, createViemRuntimeWriter } from '../features/runtime/viemRuntimeAdapter'; import { resolveConfiguredArtistPublicationSafety } from '../shared/config/deploymentSafety'; -import { describeArtistRegistrationError, formatBlockTimestampMs, formatWeiAsDot, shorten, dotToPlanck } from '../shared/utils/format'; +import { describeArtistRegistrationError, formatWeiAsDot, shorten, dotToPlanck } from '../shared/utils/format'; import { createArtistPublishE2eTrack, E2E_ARTIST_PROFILE_TX_HASH, @@ -41,8 +34,6 @@ import type { AccessMode, CatalogTrack, PersonhoodLevel, ReleaseRoyaltySplitDraf import type { ConnectedWallet } from './useWallet'; import type { PolkadotSigner } from 'polkadot-api'; -const zeroAddress = '0x0000000000000000000000000000000000000000' as const; -const musicRoyAccessPaidEvent = parseAbiItem('event MusicRoyAccessPaid(bytes32 indexed contentHash, address indexed listener, uint256 amount)'); const runtimeBootstrapSteps = [ { label: 'Claim your artist space', @@ -241,6 +232,7 @@ export function useArtistConsole(deps: UseArtistConsoleDeps) { coverUploadRef } = deps; + const runtimeReader = createViemRuntimeReader({ ethRpcUrl }); const [artistRuntimeAddress, setArtistRuntimeAddress] = useState<`0x${string}` | null>(null); const [artistRegistrationStatus, setArtistRegistrationStatus] = useState('Checking artist registration'); const [isRegisteringArtist, setIsRegisteringArtist] = useState(false); @@ -314,21 +306,16 @@ export function useArtistConsole(deps: UseArtistConsoleDeps) { setArtistRegistrationStatus('Checking artist runtime'); try { - const directoryExists = await ensureContract(directoryAddress!, ethRpcUrl); + const directoryExists = await runtimeReader.ensureContract(directoryAddress!); if (!directoryExists) { setArtistRuntimeAddress(null); setArtistRegistrationStatus('Artist directory unavailable'); return null; } - const runtimeAddress = (await getPublicClient(ethRpcUrl).readContract({ - address: directoryAddress!, - abi: artistDirectoryAbi, - functionName: 'runtimeOf', - args: [activeEvmAddress] - })) as `0x${string}`; + const runtimeAddress = await runtimeReader.resolveArtistRuntime(directoryAddress!, activeEvmAddress); - if (runtimeAddress === zeroAddress) { + if (!runtimeAddress) { setArtistRuntimeAddress(null); setArtistRegistrationStatus(artistPublicationQuarantined ? artistPublicationSafety.reason : 'Artist not registered yet'); return null; @@ -420,7 +407,7 @@ export function useArtistConsole(deps: UseArtistConsoleDeps) { return; } - const factoryExists = await ensureContract(factoryAddress!, ethRpcUrl); + const factoryExists = await runtimeReader.ensureContract(factoryAddress!); if (!factoryExists) { setTransactionFeedback({ tone: 'error', @@ -431,19 +418,14 @@ export function useArtistConsole(deps: UseArtistConsoleDeps) { } const walletClient = await getActiveWalletClient(); - const publicClient = getPublicClient(ethRpcUrl); + const runtimeWriter = createViemRuntimeWriter({ ethRpcUrl, walletClient }); - let pendingRuntime = (await publicClient.readContract({ - address: factoryAddress!, - abi: artistRuntimeFactoryAbi, - functionName: 'pendingRuntimeOf', - args: [activeEvmAddress] - })) as `0x${string}`; + let pendingRuntime = await runtimeReader.pendingRuntimeOf(factoryAddress!, activeEvmAddress); let txHash: `0x${string}` | undefined; const confirmedBootstrapTxHashes: Partial> = {}; - if (pendingRuntime === zeroAddress) { + if (!pendingRuntime) { setArtistRegistrationStatus(runtimeBootstrapSteps[0].label); setTransactionFeedback({ tone: 'pending', @@ -452,11 +434,7 @@ export function useArtistConsole(deps: UseArtistConsoleDeps) { steps: artistRuntimeBootstrapRoadmap(0, 'active') }); - txHash = await walletClient.writeContract({ - address: factoryAddress!, - abi: artistRuntimeFactoryAbi, - functionName: 'createRuntime' - }); + txHash = await runtimeWriter.createRuntime(factoryAddress!); setTransactionFeedback({ tone: 'pending', @@ -466,17 +444,10 @@ export function useArtistConsole(deps: UseArtistConsoleDeps) { steps: artistRuntimeBootstrapRoadmap(0, 'submitted', confirmedBootstrapTxHashes) }); - await publicClient.waitForTransactionReceipt({ hash: txHash }); + await runtimeWriter.waitForTransaction(txHash); confirmedBootstrapTxHashes[0] = txHash; } else { - const pendingStage = Number( - await publicClient.readContract({ - address: factoryAddress!, - abi: artistRuntimeFactoryAbi, - functionName: 'pendingRuntimeStageOf', - args: [activeEvmAddress] - }) - ); + const pendingStage = await runtimeReader.pendingRuntimeStageOf(factoryAddress!, activeEvmAddress); const pendingStepIndex = Math.max(1, Math.min(runtimeBootstrapSteps.length - 1, pendingStage)); setTransactionFeedback({ tone: 'pending', @@ -486,22 +457,10 @@ export function useArtistConsole(deps: UseArtistConsoleDeps) { }); } - pendingRuntime = (await publicClient.readContract({ - address: factoryAddress!, - abi: artistRuntimeFactoryAbi, - functionName: 'pendingRuntimeOf', - args: [activeEvmAddress] - })) as `0x${string}`; - - while (pendingRuntime !== zeroAddress) { - const currentStage = Number( - await publicClient.readContract({ - address: factoryAddress!, - abi: artistRuntimeFactoryAbi, - functionName: 'pendingRuntimeStageOf', - args: [activeEvmAddress] - }) - ); + pendingRuntime = await runtimeReader.pendingRuntimeOf(factoryAddress!, activeEvmAddress); + + while (pendingRuntime) { + const currentStage = await runtimeReader.pendingRuntimeStageOf(factoryAddress!, activeEvmAddress); const stepIndex = Math.max(1, Math.min(runtimeBootstrapSteps.length - 1, currentStage)); const step = runtimeBootstrapSteps[stepIndex]; @@ -513,11 +472,7 @@ export function useArtistConsole(deps: UseArtistConsoleDeps) { steps: artistRuntimeBootstrapRoadmap(stepIndex, 'active', confirmedBootstrapTxHashes) }); - txHash = await walletClient.writeContract({ - address: factoryAddress!, - abi: artistRuntimeFactoryAbi, - functionName: 'installRuntimeStep' - }); + txHash = await runtimeWriter.installRuntimeStep(factoryAddress!); setTransactionFeedback({ tone: 'pending', @@ -527,15 +482,10 @@ export function useArtistConsole(deps: UseArtistConsoleDeps) { steps: artistRuntimeBootstrapRoadmap(stepIndex, 'submitted', confirmedBootstrapTxHashes) }); - await publicClient.waitForTransactionReceipt({ hash: txHash }); + await runtimeWriter.waitForTransaction(txHash); confirmedBootstrapTxHashes[stepIndex] = txHash; - pendingRuntime = (await publicClient.readContract({ - address: factoryAddress!, - abi: artistRuntimeFactoryAbi, - functionName: 'pendingRuntimeOf', - args: [activeEvmAddress] - })) as `0x${string}`; + pendingRuntime = await runtimeReader.pendingRuntimeOf(factoryAddress!, activeEvmAddress); } const runtimeAddress = await refreshArtistRuntime(); @@ -579,42 +529,20 @@ export function useArtistConsole(deps: UseArtistConsoleDeps) { setRoyaltyStatus('Reading artist runtime payments'); try { - const client = getPublicClient(ethRpcUrl); const trackByHash = new Map(artistTracks.map(track => [track.hash.toLowerCase(), track])); - const logs = await client.getLogs({ - address: artistRuntimeAddress, - event: musicRoyAccessPaidEvent, - fromBlock: 0n, - toBlock: 'latest' - }); - const blockTimestampsByNumber = new Map(); - await Promise.all( - Array.from(new Set(logs.map(log => log.blockNumber.toString()))).map(async blockNumber => { - const block = await client.getBlock({ blockNumber: BigInt(blockNumber) }); - blockTimestampsByNumber.set(blockNumber, block.timestamp); - }) - ); - + const logs = await runtimeReader.listRoyaltyPaymentLogs(artistRuntimeAddress); const payments = logs .map(log => { - const trackHash = log.args.contentHash; - const listener = log.args.listener; - const amountWei = log.args.amount; - - if (!trackHash || !listener || amountWei === undefined) { - return null; - } - - const track = trackByHash.get(trackHash.toLowerCase()); + const track = trackByHash.get(log.trackHash.toLowerCase()); return { id: `${log.transactionHash}-${log.logIndex}`, - trackHash, - trackTitle: track?.title ?? shorten(trackHash, 14), - listener, - amountWei, - amountDot: formatWeiAsDot(amountWei), - paidAtMs: formatBlockTimestampMs(blockTimestampsByNumber.get(log.blockNumber.toString())), + trackHash: log.trackHash, + trackTitle: track?.title ?? shorten(log.trackHash, 14), + listener: log.listener, + amountWei: log.amountWei, + amountDot: formatWeiAsDot(log.amountWei), + paidAtMs: log.paidAtMs, transactionHash: log.transactionHash, blockNumber: log.blockNumber, logIndex: log.logIndex @@ -862,7 +790,7 @@ export function useArtistConsole(deps: UseArtistConsoleDeps) { title: 'Checking factory', message: 'Verifying that the ArtistRuntimeFactory is reachable before submission.' }); - const factoryExists = await ensureContract(factoryAddress, ethRpcUrl); + const factoryExists = await runtimeReader.ensureContract(factoryAddress); if (!factoryExists) { setRightsStatus('Factory not found'); setTransactionFeedback({ tone: 'error', title: 'Factory unavailable', message: 'ArtistRuntimeFactory not found at the configured address.' }); @@ -880,6 +808,7 @@ export function useArtistConsole(deps: UseArtistConsoleDeps) { } const walletClient = await getActiveWalletClient(); + const runtimeWriter = createViemRuntimeWriter({ ethRpcUrl, walletClient }); const ipfsAudioRef = resolvedAudioRef || localAudioRef(fileHash); const ipfsCoverRef = resolvedCoverCID ? `ipfs://${resolvedCoverCID}` : `dotify:cover:${fileHash}`; @@ -890,27 +819,20 @@ export function useArtistConsole(deps: UseArtistConsoleDeps) { message: 'Sending the registration to your SmartRuntime.' }); - const txHash = await walletClient.writeContract({ - address: runtimeAddress, - abi: musicRegistryAbi, - functionName: 'musicRegRegister', - args: [ - { - contentHash: fileHash, - title, - artistName, - description, - imageRef: ipfsCoverRef, - audioRef: ipfsAudioRef, - metadataRef: ipfsMetadataRef, - artistContractRef: `dotify:self-certified:${fileHash}`, - accessMode: encodeAccessMode(accessMode), - pricePlanck: dotToPlanck(priceDotForAccessMode(accessMode, priceDot)), - requiredPersonhood: encodeRequiredPersonhood(accessMode, personhoodLevel) - }, - royaltyRecipients, - royaltyShares - ] + const txHash = await runtimeWriter.registerTrack(runtimeAddress, { + contentHash: fileHash, + title, + artistName, + description, + imageRef: ipfsCoverRef, + audioRef: ipfsAudioRef, + metadataRef: ipfsMetadataRef, + artistContractRef: `dotify:self-certified:${fileHash}`, + accessMode: encodeAccessMode(accessMode), + pricePlanck: dotToPlanck(priceDotForAccessMode(accessMode, priceDot)), + requiredPersonhood: encodeRequiredPersonhood(accessMode, personhoodLevel), + royaltyRecipients, + royaltyShares }); setRightsStatus('Waiting for transaction confirmation'); @@ -920,7 +842,7 @@ export function useArtistConsole(deps: UseArtistConsoleDeps) { message: 'Transaction submitted. Waiting for the final receipt on the EVM network.', txHash }); - await getPublicClient(ethRpcUrl).waitForTransactionReceipt({ hash: txHash }); + await runtimeWriter.waitForTransaction(txHash); setRightsStatus('Rights registered'); setTransactionFeedback({ tone: 'success', @@ -973,24 +895,20 @@ export function useArtistConsole(deps: UseArtistConsoleDeps) { setReleaseActionId(`${track.id}:access`); try { const walletClient = await getActiveWalletClient(); + const runtimeWriter = createViemRuntimeWriter({ ethRpcUrl, walletClient }); setTransactionFeedback({ tone: 'pending', title: 'Updating access', message: `Changing "${track.title}" access policy.` }); - const txHash = await walletClient.writeContract({ - address: runtimeAddress, - abi: musicRegistryAbi, - functionName: 'musicRegSetAccessMode', - args: [ - track.hash, - encodeAccessMode(nextAccessMode), - dotToPlanck(priceDotForAccessMode(nextAccessMode, nextPriceDot)), - encodeRequiredPersonhood(nextAccessMode, nextPersonhoodLevel) - ] + const txHash = await runtimeWriter.setAccessMode(runtimeAddress, { + contentHash: track.hash, + accessMode: encodeAccessMode(nextAccessMode), + pricePlanck: dotToPlanck(priceDotForAccessMode(nextAccessMode, nextPriceDot)), + requiredPersonhood: encodeRequiredPersonhood(nextAccessMode, nextPersonhoodLevel) }); setTransactionFeedback({ tone: 'pending', title: 'Awaiting confirmation', message: 'Access update submitted.', txHash }); - await getPublicClient(ethRpcUrl).waitForTransactionReceipt({ hash: txHash }); + await runtimeWriter.waitForTransaction(txHash); await refreshCatalogFromRegistry(track.hash); setTransactionFeedback({ tone: 'success', @@ -1029,26 +947,15 @@ export function useArtistConsole(deps: UseArtistConsoleDeps) { setReleaseActionId(`${track.id}:active`); try { const walletClient = await getActiveWalletClient(); + const runtimeWriter = createViemRuntimeWriter({ ethRpcUrl, walletClient }); setTransactionFeedback({ tone: 'pending', title: active ? 'Reactivating release' : 'Deactivating release', message: `${active ? 'Reactivating' : 'Deactivating'} "${track.title}".` }); - const txHash = active - ? await walletClient.writeContract({ - address: runtimeAddress, - abi: musicRegistryAbi, - functionName: 'musicRegReactivate', - args: [track.hash] - }) - : await walletClient.writeContract({ - address: runtimeAddress, - abi: musicRegistryAbi, - functionName: 'musicRegDeactivate', - args: [track.hash] - }); + const txHash = await runtimeWriter.setReleaseActive(runtimeAddress, track.hash, active); setTransactionFeedback({ tone: 'pending', title: 'Awaiting confirmation', message: 'Release status update submitted.', txHash }); - await getPublicClient(ethRpcUrl).waitForTransactionReceipt({ hash: txHash }); + await runtimeWriter.waitForTransaction(txHash); await refreshCatalogFromRegistry(track.hash); setTransactionFeedback({ tone: 'success', diff --git a/web/src/hooks/useCatalog.ts b/web/src/hooks/useCatalog.ts index e403caa..5fed5e8 100644 --- a/web/src/hooks/useCatalog.ts +++ b/web/src/hooks/useCatalog.ts @@ -1,6 +1,6 @@ import { useRef, useState } from 'react'; import { fetchAssetRef, fetchIpfsCid, getGatewayUrl } from '../services/pinata'; -import { ensureContract, getPublicClient, artistDirectoryAbi, musicRegistryAbi, musicAccessAbi, musicRoyaltiesAbi } from '../shared/config/contracts'; +import { getPublicClient } from '../shared/config/contracts'; import { decryptAudio, hexToBytes } from '../shared/utils/crypto'; import { formatWeiAsDot } from '../shared/utils/format'; import { isKeyServiceConfigured, requestContentKey, requestFreeContentKey, type KeyRequestPurpose } from '../services/keyService'; @@ -24,6 +24,8 @@ import { pumpAudioV2ReadAhead } from '../features/catalog/audioV2Pipeline'; import { AudioV2ChunkAuthenticationError, routeAudioV2MseFailure } from '../features/catalog/audioV2Recovery'; import { runtimeAddressFromTrackId } from '../features/catalog/trackModel'; import { decodeAccessMode, decodePersonhood } from '../features/runtime/accessEncoding'; +import { createViemRuntimeReader, createViemRuntimeWriter } from '../features/runtime/viemRuntimeAdapter'; +import type { RuntimeReadPort, RuntimeTrackSnapshot } from '../features/runtime/runtimePorts'; import { fetchCatalog, isCatalogApiConfigured, readCachedCatalog, type CatalogApiRelease } from '../services/catalog'; import { E2E_CLASSIC_AUDIO_URL, @@ -49,12 +51,10 @@ import type { AccessGate, AccessMode, CatalogTrack, - OnchainTrackRecord, PersonhoodLevel, PlayerState, RegistryCatalogTrack, RoomPlaybackMode, - RoyaltySplit, TrackInfo, TransactionFeedback } from '../shared/types'; @@ -272,6 +272,7 @@ export function useCatalog(deps: UseCatalogDeps) { setDescription } = deps; + const runtimeReader = createViemRuntimeReader({ ethRpcUrl }); const usesCatalogApi = isCatalogApiConfigured() && !isClassicUnlockE2e && !isArtistPublishE2e && !isRoomJoinE2e; const [initialCatalog] = useState(() => { const cached = usesCatalogApi ? readCachedCatalog() : null; @@ -379,12 +380,7 @@ export function useCatalog(deps: UseCatalogDeps) { // a buyer, or personhood-verified, so the read answers true only when // the track's current mode grants access to everyone (Free). This is // what lets a walletless visitor play Free tracks (access model v2). - return (await getPublicClient(ethRpcUrl).readContract({ - address: runtimeAddress, - abi: musicAccessAbi, - functionName: 'musicAccCanAccess', - args: [track.hash, listenerAddress ?? zeroAddress] - })) as boolean; + return await runtimeReader.canAccess(runtimeAddress, track.hash, listenerAddress ?? zeroAddress); } catch { return false; } @@ -405,12 +401,7 @@ export function useCatalog(deps: UseCatalogDeps) { const runtimeAddress = runtimeAddressFromTrackId(track); if (!runtimeAddress) return false; try { - return (await getPublicClient(ethRpcUrl).readContract({ - address: runtimeAddress, - abi: musicAccessAbi, - functionName: 'musicAccHasPaid', - args: [track.hash, listenerAddress] - })) as boolean; + return await runtimeReader.hasPaid(runtimeAddress, track.hash, listenerAddress); } catch { return false; } @@ -1008,7 +999,6 @@ export function useCatalog(deps: UseCatalogDeps) { const runtimeAddress = runtimeAddressFromTrackId(track); if (!runtimeAddress) return; - const { musicRoyaltiesAbi, getPublicClient: getClient } = await import('../shared/config/contracts'); const { dotToPlanck } = await import('../shared/utils/format'); const priceWei = dotToPlanck(track.priceDot); @@ -1022,15 +1012,10 @@ export function useCatalog(deps: UseCatalogDeps) { try { const walletClient = await getActiveWalletClient(); - const txHash = await walletClient.writeContract({ - address: runtimeAddress, - abi: musicRoyaltiesAbi, - functionName: 'musicRoyPayAccess', - args: [track.hash], - value: priceWei - }); + const runtimeWriter = createViemRuntimeWriter({ ethRpcUrl, walletClient }); + const txHash = await runtimeWriter.payForAccess(runtimeAddress, track.hash, priceWei); setTransactionFeedback({ tone: 'pending', title: 'Awaiting confirmation', message: 'Payment submitted.', txHash }); - await getClient(ethRpcUrl).waitForTransactionReceipt({ hash: txHash }); + await runtimeWriter.waitForTransaction(txHash); setCatalogAccessByTrackId(previous => ({ ...previous, [track.id]: true })); setCatalogPaidAccessByTrackId(previous => ({ ...previous, [track.id]: true })); @@ -1047,117 +1032,45 @@ export function useCatalog(deps: UseCatalogDeps) { } } - async function fetchDirectoryEntries(client: ReturnType, registryAddress: `0x${string}`, artistCount: bigint) { - const pageSize = 50n; - const entries: Array<{ artist: `0x${string}`; runtime: `0x${string}` }> = []; - - for (let offset = 0n; offset < artistCount; offset += pageSize) { - const limit = artistCount - offset > pageSize ? pageSize : artistCount - offset; - const [artists, runtimes] = (await client.readContract({ - address: registryAddress, - abi: artistDirectoryAbi, - functionName: 'artistsPage', - args: [offset, limit] - })) as [`0x${string}`[], `0x${string}`[]]; - - for (let index = 0; index < artists.length; index += 1) { - const artist = artists[index]; - const runtime = runtimes[index]; - if (!artist || !runtime || runtime === zeroAddress) continue; - entries.push({ artist, runtime }); - } - } + async function fetchRuntimeCatalog(reader: RuntimeReadPort, artistAddress: `0x${string}`, runtimeAddress: `0x${string}`): Promise { + const snapshots = await reader.listRuntimeTracks(runtimeAddress); + const tracks = snapshots.map((snapshot: RuntimeTrackSnapshot): RegistryCatalogTrack => { + const { hash, record: track } = snapshot; + const imageRef = resolveVisualAssetRef(track.imageRef, track.title); + const encrypted = isEncryptedAudioRef(track.audioRef); + const localUrl = resolveAudioAssetRef(track.audioRef); - return entries; - } - - async function fetchRuntimeCatalog( - client: ReturnType, - artistAddress: `0x${string}`, - runtimeAddress: `0x${string}` - ): Promise { - const trackCount = (await client.readContract({ - address: runtimeAddress, - abi: musicRegistryAbi, - functionName: 'musicRegTrackCount' - })) as bigint; - - const tracks: Array = await Promise.all( - Array.from({ length: Number(trackCount) }, async (_, index) => { - const hash = (await client.readContract({ - address: runtimeAddress, - abi: musicRegistryAbi, - functionName: 'musicRegTrackHashAtIndex', - args: [BigInt(index)] - })) as `0x${string}`; - - const [track] = (await client.readContract({ - address: runtimeAddress, - abi: musicRegistryAbi, - functionName: 'musicRegGetTrack', - args: [hash] - })) as [OnchainTrackRecord, `0x${string}`]; - - const imageRef = resolveVisualAssetRef(track.imageRef, track.title); - const encrypted = isEncryptedAudioRef(track.audioRef); - const localUrl = resolveAudioAssetRef(track.audioRef); - const splitCount = (await client - .readContract({ - address: runtimeAddress, - abi: musicRoyaltiesAbi, - functionName: 'musicRoySplitCount', - args: [hash] - }) - .catch(() => 0n)) as bigint; - const royaltySplits = await Promise.all( - Array.from({ length: Number(splitCount) }, async (_, splitIndex): Promise => { - try { - const [recipient, bps] = (await client.readContract({ - address: runtimeAddress, - abi: musicRoyaltiesAbi, - functionName: 'musicRoySplitAt', - args: [hash, BigInt(splitIndex)] - })) as [`0x${string}`, number]; - return { - label: splitIndex === 0 ? 'Primary recipient' : `Split ${splitIndex + 1}`, - recipient, - bps: Number(bps) - }; - } catch { - return null; - } - }) - ); - - return { - id: `${runtimeAddress}:${hash}`, - hash, - title: track.title, - artist: track.artistName, - artistAddress: track.artist || artistAddress, - audioRef: track.audioRef, - imageRef, - priceDot: formatWeiAsDot(track.pricePlanck), - localUrl, - description: track.description, - bulletinRef: track.metadataRef.startsWith('paseo-bulletin:') ? track.metadataRef : '', - metadataRef: track.metadataRef, - royaltyBps: Number(track.royaltyBps), - txHash: undefined, - durationLabel: 'ready', - accessMode: decodeAccessMode(Number(track.accessMode)), - active: track.active, - source: 'artist' as const, - royaltySplits: royaltySplits.filter((split): split is RoyaltySplit => Boolean(split)), - personhoodLevel: decodePersonhood(Number(track.requiredPersonhood)), - zone: 'Registry', - encrypted, - registeredAtBlock: Number(track.registeredAtBlock) - }; - }) - ); + return { + id: `${runtimeAddress}:${hash}`, + hash, + title: track.title, + artist: track.artistName, + artistAddress: track.artist || artistAddress, + audioRef: track.audioRef, + imageRef, + priceDot: formatWeiAsDot(track.pricePlanck), + localUrl, + description: track.description, + bulletinRef: track.metadataRef.startsWith('paseo-bulletin:') ? track.metadataRef : '', + metadataRef: track.metadataRef, + royaltyBps: Number(track.royaltyBps), + txHash: undefined, + durationLabel: 'ready', + accessMode: decodeAccessMode(Number(track.accessMode)), + active: track.active, + source: 'artist' as const, + royaltySplits: snapshot.royaltySplits.map((split, splitIndex) => ({ + label: splitIndex === 0 ? 'Primary recipient' : `Split ${splitIndex + 1}`, + ...split + })), + personhoodLevel: decodePersonhood(Number(track.requiredPersonhood)), + zone: 'Registry', + encrypted, + registeredAtBlock: Number(track.registeredAtBlock) + }; + }); - return tracks.flatMap(track => (track ? [track] : [])); + return tracks; } function commitCatalog(allTracks: CatalogTrack[], preferredTrackHash: `0x${string}` | undefined, status: string): CatalogTrack[] { @@ -1227,7 +1140,7 @@ export function useCatalog(deps: UseCatalogDeps) { setCatalogStatus('Loading registry catalog'); try { - const directoryExists = await ensureContract(directoryAddress, ethRpcUrl); + const directoryExists = await runtimeReader.ensureContract(directoryAddress); if (!directoryExists) { setCatalogTracks([]); setAllCatalogTracks([]); @@ -1236,12 +1149,7 @@ export function useCatalog(deps: UseCatalogDeps) { return []; } - const client = getPublicClient(ethRpcUrl); - const artistCount = (await client.readContract({ - address: directoryAddress, - abi: artistDirectoryAbi, - functionName: 'artistCount' - })) as bigint; + const artistCount = await runtimeReader.getArtistCount(directoryAddress); if (artistCount === 0n) { setCatalogTracks([]); @@ -1251,11 +1159,11 @@ export function useCatalog(deps: UseCatalogDeps) { return []; } - const entries = await fetchDirectoryEntries(client, directoryAddress, artistCount); + const entries = await runtimeReader.listArtistRuntimes(directoryAddress, artistCount); const runtimeCatalogs = await Promise.all( entries.map(async entry => { try { - return await fetchRuntimeCatalog(client, entry.artist, entry.runtime); + return await fetchRuntimeCatalog(runtimeReader, entry.artist, entry.runtime); } catch (runtimeError) { console.warn(`Failed to load runtime catalog for ${entry.runtime}`, runtimeError); return []; @@ -1341,7 +1249,6 @@ export function useCatalog(deps: UseCatalogDeps) { payForTrackAccess, fetchAndDecryptAudio, refreshCatalogFromRegistry, - fetchDirectoryEntries, fetchRuntimeCatalog, clearObjectUrls }; From 00f1d93db4e36462c16d5ecab7a0b14d5b3bb939 Mon Sep 17 00:00:00 2001 From: Kevin Nzeng Essimengane Date: Sun, 26 Jul 2026 23:40:55 +0200 Subject: [PATCH 04/22] feat: add Product CDM runtime adapter --- README.md | 9 +- docs/backlog/README.md | 13 +- ...oduct-readiness-and-killer-dapp-roadmap.md | 6 +- docs/explanation/architecture-overview.md | 4 +- .../product-devnet-architecture.md | 29 +- docs/index.html | 8 +- docs/operations/product-devnet-deployment.md | 5 +- spec.md | 9 +- .../runtime/productCdmRuntimeAdapter.test.ts | 286 +++++++++++++++ .../runtime/productCdmRuntimeAdapter.ts | 345 ++++++++++++++++++ 10 files changed, 689 insertions(+), 25 deletions(-) create mode 100644 web/src/features/runtime/productCdmRuntimeAdapter.test.ts create mode 100644 web/src/features/runtime/productCdmRuntimeAdapter.ts diff --git a/README.md b/README.md index 2a23937..91ca92a 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,12 @@ iteration can move signaling to statement-store style infrastructure. **Product SDK direction**: Dotify now has an adaptive Product DevNet build for `dotify-test01.dot`. It keeps standalone link-first rooms and Free listening intact, adds explicit app-scoped Product identity, and publishes through -Bulletin/DotNS. Product host signing is not yet accepted for contract writes or -protected key delivery; those boundaries remain passkey/EVM until the CDM/PAPI -and backend signature adapters are proven. See +Bulletin/DotNS. The runtime hooks now sit behind typed ports with the current +viem implementation and an experimental Product CDM/PAPI adapter boundary. +Product host signing is not yet accepted for contract writes or protected key +delivery; those boundaries remain passkey/EVM until CDM-installed runtime +packages, host-signed transaction evidence, and backend signature adapters are +proven. See [`docs/explanation/product-devnet-architecture.md`](docs/explanation/product-devnet-architecture.md) and the [`Product roadmap`](docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md). diff --git a/docs/backlog/README.md b/docs/backlog/README.md index 6702155..5e32be6 100644 --- a/docs/backlog/README.md +++ b/docs/backlog/README.md @@ -107,9 +107,11 @@ Product SDK direction. The plan is now dual-mode: standalone web remains the first public listening path, while the Product DevNet build adds `dotify-test01.dot`, explicit Host detection, app-scoped Product identity, and canonical -Product-origin room links. It does not imply Product-signed contract writes, -protected key access, Statement Store rooms, or Humanity decisions; those -remain gated until their adapters prove the current API and security boundary. +Product-origin room links. The typed runtime ports and experimental +Product CDM/PAPI adapter boundary are implementation preparation only; they do +not imply Product-signed contract writes, protected key access, Statement Store +rooms, or Humanity decisions. Those remain gated until their adapters prove the +current API and security boundary. The Product SDK evidence snapshot used for this replanning is `@parity/product-sdk` 0.19.1 and @@ -162,8 +164,9 @@ on `main`. The remaining order is: 3. Improve room resilience and shared-listening depth only where it preserves the link-first guest doctrine. 4. Validate the delivered Product host/account and Bulletin/DotNS baseline, - then port writes through CDM/PAPI, add backend Product-signature - verification, and run bounded resource-allocation/Statement Store spikes. + then wire real CDM-installed runtime packages through the Product CDM/PAPI + adapter, add backend Product-signature verification, and run bounded + resource-allocation/Statement Store spikes. 5. Build live Humanity / Individuality only after the research ticket proves a privacy-preserving source, proof shape, address-binding story, and fallback UX. diff --git a/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md b/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md index 1d2a4b5..e3a9b9d 100644 --- a/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md +++ b/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md @@ -164,7 +164,11 @@ Goal: deepen the delivered Product mode one adapter at a time. - Delivered on the follow-up branch: extract typed runtime read/write ports and move the current viem runtime implementation behind `RuntimeReadPort` / `RuntimeWritePort`. -- Next: add a generated CDM/PAPI adapter behind those ports. +- Delivered on the next follow-up branch: add an experimental CDM/PAPI adapter + behind those ports. It is not selected by default until Dotify has + CDM-installed Product runtime packages and host-signed transaction evidence. +- Next: wire a generated CDM manifest/types into Product mode and run real host + transaction smoke tests. - Next: add a backend signature-scheme adapter that verifies Product account signatures and address binding before key delivery. - Keep backend key delivery authoritative unless a Product-host design proves a diff --git a/docs/explanation/architecture-overview.md b/docs/explanation/architecture-overview.md index 58f4dad..d3439a1 100644 --- a/docs/explanation/architecture-overview.md +++ b/docs/explanation/architecture-overview.md @@ -104,7 +104,9 @@ src/ │ └── useWallet.ts # Wallet tiers: passkey → EIP-6963 extension ├── features/runtime/ │ ├── runtimePorts.ts # RuntimeReadPort / RuntimeWritePort contracts -│ └── viemRuntimeAdapter.ts # Current EVM implementation behind the ports +│ ├── viemRuntimeAdapter.ts # Current EVM implementation behind the ports +│ └── productCdmRuntimeAdapter.ts +│ # Experimental Product CDM/PAPI adapter ├── views/ # One file per screen / tab │ ├── ListenView.tsx │ ├── PlayerView.tsx diff --git a/docs/explanation/product-devnet-architecture.md b/docs/explanation/product-devnet-architecture.md index 8ceea6b..160fa98 100644 --- a/docs/explanation/product-devnet-architecture.md +++ b/docs/explanation/product-devnet-architecture.md @@ -149,17 +149,28 @@ Adapters: - `ViemRuntimeAdapter`: current standalone EVM implementation behind the typed ports; -- `ProductRuntimeAdapter`: next adapter, generated from CDM/ABI bindings and - submitted with the host PAPI signer; +- `ProductCdmRuntimeAdapter`: experimental CDM/PAPI implementation behind the + same ports. It maps the Dotify runtime method surface to Product SDK contract + handles, but remains opt-in until Dotify has CDM-installed Product runtime + packages and host signing evidence; - `CatalogApiAdapter`: the existing server-side read model, shared by both frontends. -The remaining Product contract work is adapter work, not UI rewiring. The -backend authentication protocol must still gain an explicit signature scheme -field. A Product signature is accepted only after the server can bind the -signed payload, Product account public key, derived H160, chain, nonce, -purpose, and expiry. EIP-191 remains supported for standalone clients. Unknown -schemes fail closed. +The CDM adapter has one deliberate gap: royalty payment history is not read +through Product contract handles because the current SDK surface exposes +method queries and transactions, not the viem-style historical log query used +by the artist console. Product mode must use the backend catalog/read-model +indexer, or a future Product event/indexer API, for that history. + +The remaining Product contract work is integration and evidence work, not UI +rewiring. Operators still need CDM-deployed Dotify runtime packages, +`cdm.json`/generated contract types, `pallet-revive` account mapping, and real +host-signed transaction smoke evidence before Product writes can replace the +EVM wallet path. The backend authentication protocol must also gain an +explicit signature scheme field. A Product signature is accepted only after the +server can bind the signed payload, Product account public key, derived H160, +chain, nonce, purpose, and expiry. EIP-191 remains supported for standalone +clients. Unknown schemes fail closed. This avoids a second frontend business model and allows Product mode to replace one infrastructure adapter at a time. @@ -209,6 +220,8 @@ For every SDK or deploy-tool upgrade: - [Product documentation](https://docs.polkadotcommunity.foundation/) - [Build and publish guide](https://docs.polkadotcommunity.foundation/guides/build-and-publish/) +- [Deploy and register contracts with CDM](https://docs.polkadotcommunity.foundation/guides/deploy-contracts-cdm/) +- [Smart contracts and CDM](https://docs.polkadotcommunity.foundation/architecture/contracts/) - [Platform Services SDK guide](https://docs.polkadotcommunity.foundation/guides/platform-services-sdk/) - [Product network reference](https://docs.polkadotcommunity.foundation/reference/networks/) - [Product identity architecture](https://docs.polkadotcommunity.foundation/architecture/identity/) diff --git a/docs/index.html b/docs/index.html index 175a953..87f46e7 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1541,10 +1541,10 @@

    Operate the spine and validate first sound

    Product DevNet now, sovereignty adapters next

    Dotify now has a publishable dotify-test01.dot build, explicit app-scoped Product - identity, public room links that preserve wallet-free entry, and typed runtime ports around - the current viem implementation. CDM/PAPI contract writes, Product-signed key requests, - Humanity/Individuality proofs, consented provenance, and ambassador mechanics remain - sequenced behind verified security and privacy boundaries. + identity, public room links that preserve wallet-free entry, typed runtime ports around the + current viem implementation, and an experimental CDM/PAPI adapter boundary. Product contract + writes, Product-signed key requests, Humanity/Individuality proofs, consented provenance, and + ambassador mechanics remain sequenced behind verified security and privacy boundaries.

    diff --git a/docs/operations/product-devnet-deployment.md b/docs/operations/product-devnet-deployment.md index 84270a4..4c1fe1d 100644 --- a/docs/operations/product-devnet-deployment.md +++ b/docs/operations/product-devnet-deployment.md @@ -171,7 +171,10 @@ active. ## Known Limits - Product account signing is identity/presence only in this phase. -- Contract writes and key requests still require passkey/EVM signing. +- Contract writes and key requests still require passkey/EVM signing in the + shipped UI. The experimental Product CDM/PAPI runtime adapter is present in + code, but it is not selected until Dotify has CDM-installed runtime packages, + generated contract types, and real host-signed transaction evidence. - Rooms still depend on one in-memory Fly signaling machine. - Product-host cloud storage does not hold Dotify audio or content keys. - Product personhood is not yet an access decision source. diff --git a/spec.md b/spec.md index c2692df..bcb9bda 100644 --- a/spec.md +++ b/spec.md @@ -119,6 +119,10 @@ host, and requests an app-scoped account only after explicit user action. That account is currently an identity/presence capability: Classic payments, artist publication, and protected key requests still require the passkey/EVM path until CDM/PAPI writes and backend Product-signature verification are delivered. +The runtime hooks now depend on typed read/write ports; the current viem +adapter remains active, and the Product CDM/PAPI adapter remains experimental +until Dotify has CDM-installed runtime packages and host-signed transaction +evidence. ## 5. Repository Layout @@ -668,8 +672,9 @@ Priority improvements: 4. Finish security hardening for publish intents, auth chain binding, durable revocation, realtime reconnect, and short-lived TURN credentials. 5. Validate the delivered Product host/account and Bulletin/DotNS deployment - baseline, then implement CDM/PAPI contract portability, backend Product - signature verification, and a bounded Statement Store presence spike. + baseline, then wire real CDM-installed runtime packages through the + experimental Product CDM/PAPI adapter, add backend Product signature + verification, and run a bounded Statement Store presence spike. 6. Move the large catalog, session, artist, and player workflows behind domain ports and application use cases. 7. Validate the cacheable catalog API's warm/cold p75 budgets under public seed diff --git a/web/src/features/runtime/productCdmRuntimeAdapter.test.ts b/web/src/features/runtime/productCdmRuntimeAdapter.test.ts new file mode 100644 index 0000000..3ea0a23 --- /dev/null +++ b/web/src/features/runtime/productCdmRuntimeAdapter.test.ts @@ -0,0 +1,286 @@ +import { describe, expect, it, vi } from 'vitest'; +import { + ProductCdmRuntimeError, + ProductCdmRuntimeUnsupportedOperationError, + createProductCdmRuntimeContractResolver, + createProductCdmRuntimeReader, + createProductCdmRuntimeWriter, + type ProductCdmContractHandle +} from './productCdmRuntimeAdapter'; +import type { OnchainTrackRecord } from '../../shared/types'; + +const directory = '0x1000000000000000000000000000000000000000' as const; +const factory = '0x2000000000000000000000000000000000000000' as const; +const runtime = '0x3000000000000000000000000000000000000000' as const; +const artist = '0x4000000000000000000000000000000000000000' as const; +const listener = '0x5000000000000000000000000000000000000000' as const; +const splitRecipient = '0x6000000000000000000000000000000000000000' as const; +const hash = `0x${'ab'.repeat(32)}` as const; +const txHash = `0x${'cd'.repeat(32)}` as const; + +function baseTrackRecord(patch: Partial = {}): OnchainTrackRecord { + return { + artist, + tokenId: 1n, + title: 'Product runtime song', + artistName: 'Product runtime artist', + description: 'CDM-backed description', + imageRef: 'ipfs://cover', + audioRef: 'dotify.audio.v2:audio', + metadataRef: 'ipfs://metadata', + artistContractRef: 'dotify:self-certified', + royaltyBps: 9000, + accessMode: 1, + pricePlanck: 1_000_000_000_000_000_000n, + requiredPersonhood: 0, + registeredAtBlock: 12n, + active: true, + ...patch + }; +} + +function queryMethod(value: unknown) { + return { + query: vi.fn(async () => ({ success: true as const, value })) + }; +} + +function txMethod() { + return { + tx: vi.fn(async () => ({ + ok: true as const, + value: { + txHash, + ok: true + } + })) + }; +} + +describe('createProductCdmRuntimeContractResolver', () => { + it('resolves static CDM packages and requires an explicit runtime instance factory', async () => { + const directoryContract = {}; + const factoryContract = {}; + const manager = { + getContract: vi.fn((packageName: string) => { + if (packageName === '@dotify/directory') return directoryContract; + if (packageName === '@dotify/factory') return factoryContract; + throw new Error(`unexpected package ${packageName}`); + }), + getAddress: vi.fn((packageName: string) => { + if (packageName === '@dotify/directory') return directory; + if (packageName === '@dotify/factory') return factory; + return runtime; + }) + }; + const resolver = createProductCdmRuntimeContractResolver({ + manager, + packages: { + directory: '@dotify/directory', + factory: '@dotify/factory', + runtime: '@dotify/runtime' + } + }); + + expect(await resolver.hasContract?.(directory)).toBe(true); + expect(await resolver.hasContract?.(runtime)).toBe(false); + expect(resolver.getDirectoryContract(directory)).toBe(directoryContract); + expect(resolver.getFactoryContract(factory)).toBe(factoryContract); + expect(() => resolver.getRuntimeContract(runtime)).toThrow(ProductCdmRuntimeUnsupportedOperationError); + }); + + it('fails closed when a configured package address does not match the requested address', () => { + const resolver = createProductCdmRuntimeContractResolver({ + manager: { + getContract: vi.fn(() => ({})), + getAddress: vi.fn(() => factory) + }, + packages: { + directory: '@dotify/directory', + factory: '@dotify/factory', + runtime: '@dotify/runtime' + } + }); + + expect(() => resolver.getDirectoryContract(directory)).toThrow(ProductCdmRuntimeError); + }); +}); + +describe('createProductCdmRuntimeReader', () => { + it('paginates directory runtimes and filters zero-address entries', async () => { + const artistsPage = { + query: vi.fn(async (offset: unknown) => { + if (offset === 0n) { + return { success: true as const, value: [[artist], [runtime]] }; + } + return { success: true as const, value: [[listener], ['0x0000000000000000000000000000000000000000']] }; + }) + }; + const reader = createProductCdmRuntimeReader({ + directoryPageSize: 50n, + contracts: { + getDirectoryContract: () => ({ artistsPage }), + getFactoryContract: () => ({}), + getRuntimeContract: () => ({}) + } + }); + + await expect(reader.listArtistRuntimes(directory, 51n)).resolves.toEqual([{ artist, runtime }]); + expect(artistsPage.query).toHaveBeenCalledTimes(2); + }); + + it('returns runtime track snapshots with royalty splits and skips unreadable splits', async () => { + const record = baseTrackRecord(); + const musicRoySplitAt = { + query: vi.fn(async (_contentHash: unknown, splitIndex: unknown) => { + if (splitIndex === 1n) { + return { success: false as const, value: 'missing split' }; + } + return { success: true as const, value: [splitRecipient, 2500n] }; + }) + }; + const runtimeContract: ProductCdmContractHandle = { + musicRegTrackCount: queryMethod(1n), + musicRegTrackHashAtIndex: queryMethod(hash), + musicRegGetTrack: queryMethod([record, runtime]), + musicRoySplitCount: queryMethod(2n), + musicRoySplitAt + }; + const reader = createProductCdmRuntimeReader({ + contracts: { + getDirectoryContract: () => ({}), + getFactoryContract: () => ({}), + getRuntimeContract: () => runtimeContract + } + }); + + await expect(reader.listRuntimeTracks(runtime)).resolves.toEqual([ + { + hash, + record, + royaltySplits: [{ recipient: splitRecipient, bps: 2500 }] + } + ]); + }); + + it('normalizes zero-address runtime lookups and rejects failed queries', async () => { + const reader = createProductCdmRuntimeReader({ + contracts: { + getDirectoryContract: () => ({ + runtimeOf: queryMethod('0x0000000000000000000000000000000000000000'), + artistCount: { + query: vi.fn(async () => ({ success: false as const, value: 'registry unavailable' })) + } + }), + getFactoryContract: () => ({}), + getRuntimeContract: () => ({}) + } + }); + + await expect(reader.resolveArtistRuntime(directory, artist)).resolves.toBeNull(); + await expect(reader.getArtistCount(directory)).rejects.toThrow(ProductCdmRuntimeError); + }); + + it('marks royalty payment history unsupported until Product events are indexed', async () => { + const reader = createProductCdmRuntimeReader({ + contracts: { + getDirectoryContract: () => ({}), + getFactoryContract: () => ({}), + getRuntimeContract: () => ({}) + } + }); + + await expect(reader.listRoyaltyPaymentLogs(runtime)).rejects.toThrow(ProductCdmRuntimeUnsupportedOperationError); + }); +}); + +describe('createProductCdmRuntimeWriter', () => { + it('routes runtime writes through Product CDM contract tx methods', async () => { + const createRuntime = txMethod(); + const installRuntimeStep = txMethod(); + const musicRegRegister = txMethod(); + const musicRoyPayAccess = txMethod(); + const musicRegSetAccessMode = txMethod(); + const musicRegDeactivate = txMethod(); + const writer = createProductCdmRuntimeWriter({ + contracts: { + getDirectoryContract: () => ({}), + getFactoryContract: () => ({ createRuntime, installRuntimeStep }), + getRuntimeContract: () => ({ + musicRegRegister, + musicRoyPayAccess, + musicRegSetAccessMode, + musicRegDeactivate + }) + } + }); + + await expect(writer.createRuntime(factory)).resolves.toBe(txHash); + await expect(writer.installRuntimeStep(factory)).resolves.toBe(txHash); + await expect( + writer.registerTrack(runtime, { + contentHash: hash, + title: 'Product song', + artistName: 'Product artist', + description: 'Published through CDM', + imageRef: 'ipfs://cover', + audioRef: 'dotify.audio.v2:audio', + metadataRef: 'ipfs://metadata', + artistContractRef: 'dotify:self-certified', + accessMode: 1, + pricePlanck: 1n, + requiredPersonhood: 0, + royaltyRecipients: [artist], + royaltyShares: [10_000] + }) + ).resolves.toBe(txHash); + await expect(writer.payForAccess(runtime, hash, 3n)).resolves.toBe(txHash); + await expect( + writer.setAccessMode(runtime, { + contentHash: hash, + accessMode: 2, + pricePlanck: 0n, + requiredPersonhood: 1 + }) + ).resolves.toBe(txHash); + await expect(writer.setReleaseActive(runtime, hash, false)).resolves.toBe(txHash); + await expect(writer.waitForTransaction(txHash)).resolves.toBeUndefined(); + + expect(musicRoyPayAccess.tx).toHaveBeenCalledWith(hash, { value: 3n }); + expect(musicRegSetAccessMode.tx).toHaveBeenCalledWith(hash, 2, 0n, 1); + expect(musicRegDeactivate.tx).toHaveBeenCalledWith(hash); + }); + + it('throws when Product tx submission returns an error or invalid hash', async () => { + const failedTx = { + tx: vi.fn(async () => ({ ok: false as const, error: 'no signer' })) + }; + const badHashTx = { + tx: vi.fn(async () => ({ + ok: true as const, + value: { + txHash: '0x123', + ok: true + } + })) + }; + + const failedWriter = createProductCdmRuntimeWriter({ + contracts: { + getDirectoryContract: () => ({}), + getFactoryContract: () => ({ createRuntime: failedTx }), + getRuntimeContract: () => ({}) + } + }); + const badHashWriter = createProductCdmRuntimeWriter({ + contracts: { + getDirectoryContract: () => ({}), + getFactoryContract: () => ({ createRuntime: badHashTx }), + getRuntimeContract: () => ({}) + } + }); + + await expect(failedWriter.createRuntime(factory)).rejects.toThrow(ProductCdmRuntimeError); + await expect(badHashWriter.createRuntime(factory)).rejects.toThrow(ProductCdmRuntimeError); + }); +}); diff --git a/web/src/features/runtime/productCdmRuntimeAdapter.ts b/web/src/features/runtime/productCdmRuntimeAdapter.ts new file mode 100644 index 0000000..df38821 --- /dev/null +++ b/web/src/features/runtime/productCdmRuntimeAdapter.ts @@ -0,0 +1,345 @@ +import { zeroAddress, type Address, type Hash } from 'viem'; +import type { + RuntimeAccessPolicyUpdate, + RuntimeDirectoryEntry, + RuntimeReadPort, + RuntimeRoyaltyPaymentLog, + RuntimeTrackRegistration, + RuntimeTrackSnapshot, + RuntimeWritePort +} from './runtimePorts'; +import type { OnchainTrackRecord } from '../../shared/types'; + +export type ProductCdmQueryResult = + | { + success: true; + value: T; + gasRequired?: unknown; + } + | { + success: false; + value: unknown; + gasRequired?: unknown; + }; + +export type ProductCdmTxResult = { + txHash: string; + ok: boolean; + dispatchError?: unknown; +}; + +export type ProductCdmResult = + | { + ok: true; + value: T; + } + | { + ok: false; + error: unknown; + }; + +export type ProductCdmContractMethod = { + query?: (...args: unknown[]) => Promise; + tx?: (...args: unknown[]) => Promise>; +}; + +// Structural mirror of @parity/product-sdk-contracts handles. Keeping this +// local lets unit tests exercise the adapter without opening a Product host. +export type ProductCdmContractHandle = Record; + +export type ProductCdmRuntimePackages = { + directory: string; + factory: string; + runtime: string; +}; + +export type ProductCdmContractManagerLike = { + getContract(packageName: string): ProductCdmContractHandle; + getAddress?: (packageName: string) => Address; +}; + +export type ProductCdmRuntimeContractFactory = (runtimeAddress: Address, runtimePackage: string) => ProductCdmContractHandle; + +export type ProductCdmRuntimeContractResolver = { + hasContract?: (address: Address) => Promise; + getDirectoryContract(directoryAddress: Address): ProductCdmContractHandle; + getFactoryContract(factoryAddress: Address): ProductCdmContractHandle; + getRuntimeContract(runtimeAddress: Address): ProductCdmContractHandle; +}; + +export type ProductCdmRuntimeAdapterDeps = { + contracts: ProductCdmRuntimeContractResolver; + directoryPageSize?: bigint; +}; + +export class ProductCdmRuntimeError extends Error { + constructor(message: string) { + super(message); + this.name = 'ProductCdmRuntimeError'; + } +} + +export class ProductCdmRuntimeUnsupportedOperationError extends ProductCdmRuntimeError { + constructor(message: string) { + super(message); + this.name = 'ProductCdmRuntimeUnsupportedOperationError'; + } +} + +export function createProductCdmRuntimeContractResolver(input: { + manager: ProductCdmContractManagerLike; + packages: ProductCdmRuntimePackages; + runtimeContractFactory?: ProductCdmRuntimeContractFactory; + hasContract?: (address: Address) => Promise; +}): ProductCdmRuntimeContractResolver { + const { manager, packages, runtimeContractFactory } = input; + + return { + async hasContract(address) { + if (input.hasContract) return input.hasContract(address); + return [packages.directory, packages.factory].some(packageName => { + try { + return sameAddress(manager.getAddress?.(packageName), address); + } catch { + return false; + } + }); + }, + + getDirectoryContract(directoryAddress) { + assertPackageAddress(manager, packages.directory, directoryAddress, 'ArtistDirectory'); + return manager.getContract(packages.directory); + }, + + getFactoryContract(factoryAddress) { + assertPackageAddress(manager, packages.factory, factoryAddress, 'ArtistRuntimeFactory'); + return manager.getContract(packages.factory); + }, + + getRuntimeContract(runtimeAddress) { + if (!runtimeContractFactory) { + throw new ProductCdmRuntimeUnsupportedOperationError( + 'Product CDM runtime instance resolution is not configured. Run cdm install for the SmartRuntime ABI and pass a runtimeContractFactory that binds that ABI to the artist runtime address.' + ); + } + return runtimeContractFactory(runtimeAddress, packages.runtime); + } + }; +} + +export function createProductCdmRuntimeReader(deps: ProductCdmRuntimeAdapterDeps): RuntimeReadPort { + const pageSize = deps.directoryPageSize ?? 50n; + + return { + async ensureContract(address) { + return deps.contracts.hasContract ? deps.contracts.hasContract(address) : false; + }, + + async resolveArtistRuntime(directoryAddress, artistAddress) { + const runtimeAddress = await queryContract
    (deps.contracts.getDirectoryContract(directoryAddress), 'runtimeOf', [artistAddress]); + return runtimeAddress === zeroAddress ? null : runtimeAddress; + }, + + async getArtistCount(directoryAddress) { + return toBigInt(await queryContract(deps.contracts.getDirectoryContract(directoryAddress), 'artistCount')); + }, + + async listArtistRuntimes(directoryAddress, artistCount) { + const directory = deps.contracts.getDirectoryContract(directoryAddress); + const entries: RuntimeDirectoryEntry[] = []; + + for (let offset = 0n; offset < artistCount; offset += pageSize) { + const limit = artistCount - offset > pageSize ? pageSize : artistCount - offset; + const [artists, runtimes] = await queryContract<[Address[], Address[]]>(directory, 'artistsPage', [offset, limit]); + + for (let index = 0; index < artists.length; index += 1) { + const artist = artists[index]; + const runtime = runtimes[index]; + if (!artist || !runtime || runtime === zeroAddress) continue; + entries.push({ artist, runtime }); + } + } + + return entries; + }, + + async listRuntimeTracks(runtimeAddress) { + const runtime = deps.contracts.getRuntimeContract(runtimeAddress); + const trackCount = toBigInt(await queryContract(runtime, 'musicRegTrackCount')); + + return Promise.all( + Array.from({ length: Number(trackCount) }, async (_, index): Promise => { + const hash = await queryContract(runtime, 'musicRegTrackHashAtIndex', [BigInt(index)]); + const trackResult = await queryContract(runtime, 'musicRegGetTrack', [hash]); + const record = Array.isArray(trackResult) ? trackResult[0] : trackResult; + const splitCount = await queryContract(runtime, 'musicRoySplitCount', [hash]).catch(() => 0n); + const royaltySplits = ( + await Promise.all( + Array.from({ length: Number(splitCount) }, async (_, splitIndex) => { + try { + const [recipient, bps] = await queryContract<[Address, bigint | number | string]>(runtime, 'musicRoySplitAt', [hash, BigInt(splitIndex)]); + return { recipient, bps: toNumber(bps) }; + } catch { + return null; + } + }) + ) + ).filter((split): split is { recipient: Address; bps: number } => Boolean(split)); + + return { hash, record, royaltySplits }; + }) + ); + }, + + canAccess(runtimeAddress, contentHash, listenerAddress) { + return queryContract(deps.contracts.getRuntimeContract(runtimeAddress), 'musicAccCanAccess', [contentHash, listenerAddress]); + }, + + hasPaid(runtimeAddress, contentHash, listenerAddress) { + return queryContract(deps.contracts.getRuntimeContract(runtimeAddress), 'musicAccHasPaid', [contentHash, listenerAddress]); + }, + + async pendingRuntimeOf(factoryAddress, artistAddress) { + const runtimeAddress = await queryContract
    (deps.contracts.getFactoryContract(factoryAddress), 'pendingRuntimeOf', [artistAddress]); + return runtimeAddress === zeroAddress ? null : runtimeAddress; + }, + + async pendingRuntimeStageOf(factoryAddress, artistAddress) { + return toNumber( + await queryContract(deps.contracts.getFactoryContract(factoryAddress), 'pendingRuntimeStageOf', [artistAddress]) + ); + }, + + async listRoyaltyPaymentLogs(): Promise { + throw new ProductCdmRuntimeUnsupportedOperationError( + 'Product CDM runtime payment history is not available through the current contract handle API. Use the catalog/read-model indexer until a Product event API or backend indexer is wired.' + ); + } + }; +} + +export function createProductCdmRuntimeWriter(deps: ProductCdmRuntimeAdapterDeps): RuntimeWritePort { + return { + createRuntime(factoryAddress) { + return txContract(deps.contracts.getFactoryContract(factoryAddress), 'createRuntime'); + }, + + installRuntimeStep(factoryAddress) { + return txContract(deps.contracts.getFactoryContract(factoryAddress), 'installRuntimeStep'); + }, + + registerTrack(runtimeAddress, registration: RuntimeTrackRegistration) { + return txContract(deps.contracts.getRuntimeContract(runtimeAddress), 'musicRegRegister', [ + { + contentHash: registration.contentHash, + title: registration.title, + artistName: registration.artistName, + description: registration.description, + imageRef: registration.imageRef, + audioRef: registration.audioRef, + metadataRef: registration.metadataRef, + artistContractRef: registration.artistContractRef, + accessMode: registration.accessMode, + pricePlanck: registration.pricePlanck, + requiredPersonhood: registration.requiredPersonhood + }, + registration.royaltyRecipients, + registration.royaltyShares + ]); + }, + + payForAccess(runtimeAddress, contentHash, value) { + return txContract(deps.contracts.getRuntimeContract(runtimeAddress), 'musicRoyPayAccess', [contentHash, { value }]); + }, + + setAccessMode(runtimeAddress, update: RuntimeAccessPolicyUpdate) { + return txContract(deps.contracts.getRuntimeContract(runtimeAddress), 'musicRegSetAccessMode', [ + update.contentHash, + update.accessMode, + update.pricePlanck, + update.requiredPersonhood + ]); + }, + + setReleaseActive(runtimeAddress, contentHash, active) { + return txContract(deps.contracts.getRuntimeContract(runtimeAddress), active ? 'musicRegReactivate' : 'musicRegDeactivate', [contentHash]); + }, + + async waitForTransaction() { + return; + } + }; +} + +function assertPackageAddress(manager: ProductCdmContractManagerLike, packageName: string, requestedAddress: Address, label: string): void { + const resolvedAddress = manager.getAddress?.(packageName); + if (resolvedAddress && !sameAddress(resolvedAddress, requestedAddress)) { + throw new ProductCdmRuntimeError(`${label} address ${requestedAddress} does not match CDM package ${packageName} at ${resolvedAddress}.`); + } +} + +function sameAddress(left: Address | undefined, right: Address): boolean { + return Boolean(left && left.toLowerCase() === right.toLowerCase()); +} + +function getMethod(contract: ProductCdmContractHandle, methodName: string): ProductCdmContractMethod { + const method = contract[methodName]; + if (!method) { + throw new ProductCdmRuntimeError(`Product CDM contract method "${methodName}" is missing from the installed ABI.`); + } + return method; +} + +async function queryContract(contract: ProductCdmContractHandle, methodName: string, args: unknown[] = []): Promise { + const method = getMethod(contract, methodName); + if (!method.query) { + throw new ProductCdmRuntimeError(`Product CDM contract method "${methodName}" does not support query.`); + } + + const result = await method.query(...args); + if (!result.success) { + throw new ProductCdmRuntimeError(`Product CDM query "${methodName}" failed: ${formatUnknown(result.value)}`); + } + return result.value as T; +} + +async function txContract(contract: ProductCdmContractHandle, methodName: string, args: unknown[] = []): Promise { + const method = getMethod(contract, methodName); + if (!method.tx) { + throw new ProductCdmRuntimeError(`Product CDM contract method "${methodName}" does not support transactions.`); + } + + const result = await method.tx(...args); + if (!result.ok) { + throw new ProductCdmRuntimeError(`Product CDM transaction "${methodName}" failed: ${formatUnknown(result.error)}`); + } + if (!result.value.ok) { + throw new ProductCdmRuntimeError(`Product CDM transaction "${methodName}" was rejected by the runtime: ${formatUnknown(result.value.dispatchError)}`); + } + return asHash(result.value.txHash, methodName); +} + +function asHash(value: string, methodName: string): Hash { + if (/^0x[0-9a-fA-F]{64}$/.test(value)) return value as Hash; + throw new ProductCdmRuntimeError(`Product CDM transaction "${methodName}" returned an invalid transaction hash.`); +} + +function toBigInt(value: bigint | number | string): bigint { + if (typeof value === 'bigint') return value; + if (typeof value === 'number') return BigInt(value); + return BigInt(value); +} + +function toNumber(value: bigint | number | string): number { + return Number(value); +} + +function formatUnknown(value: unknown): string { + if (value instanceof Error) return value.message; + if (typeof value === 'string') return value; + try { + return JSON.stringify(value) ?? String(value); + } catch { + return String(value); + } +} From 5e41931e61b7ad5678b60f9d483a710686135c72 Mon Sep 17 00:00:00 2001 From: Kevin Nzeng Essimengane Date: Tue, 28 Jul 2026 15:27:33 +0200 Subject: [PATCH 05/22] feat: add Product signature scheme --- README.md | 18 +- docs/backlog/README.md | 9 +- ...oduct-readiness-and-killer-dapp-roadmap.md | 8 +- docs/context/dotify-technical-memory.md | 12 +- .../product-devnet-architecture.md | 16 +- docs/index.html | 5 +- docs/operations/deployment-configuration.md | 18 +- docs/operations/product-devnet-deployment.md | 13 +- docs/product/ux-signature-flows.md | 15 ++ docs/reference/environment-variables.md | 6 +- services/api/.env.example | 7 +- services/api/package-lock.json | 44 ++++ services/api/package.json | 1 + services/api/src/cors.test.ts | 5 +- services/api/src/routes/auth.test.ts | 53 +++++ services/api/src/routes/auth.ts | 46 +++- services/api/src/routes/keys.test.ts | 74 +++++++ services/api/src/routes/keys.ts | 63 ++++-- services/api/src/services/signatures.test.ts | 109 ++++++++++ services/api/src/services/signatures.ts | 205 ++++++++++++++---- spec.md | 16 +- web/README.md | 13 +- 22 files changed, 650 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index 91ca92a..7d05f32 100644 --- a/README.md +++ b/README.md @@ -47,10 +47,12 @@ iteration can move signaling to statement-store style infrastructure. adds explicit app-scoped Product identity, and publishes through Bulletin/DotNS. The runtime hooks now sit behind typed ports with the current viem implementation and an experimental Product CDM/PAPI adapter boundary. -Product host signing is not yet accepted for contract writes or protected key -delivery; those boundaries remain passkey/EVM until CDM-installed runtime -packages, host-signed transaction evidence, and backend signature adapters are -proven. See +The backend key-delivery protocol now has an explicit Product sr25519 +signature scheme that binds the Product account public key to the derived H160 +requester before access checks. The shipped Product frontend still uses the +passkey/EVM path for protected playback until host-signed key/session requests +are wired; contract writes also remain passkey/EVM until CDM-installed runtime +packages and host-signed transaction evidence are proven. See [`docs/explanation/product-devnet-architecture.md`](docs/explanation/product-devnet-architecture.md) and the [`Product roadmap`](docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md). @@ -305,6 +307,10 @@ releasing it. Gated tracks use a signed session or signed key request; the backend verifies the requester, resolves the artist runtime, and calls `musicAccCanAccess` before releasing a per-track key. If access is denied, the UI shows the action needed to unlock the track and plays no protected audio. +Standalone clients sign with the default `eip191` scheme. Product-host clients +can use `product-sr25519-v1` by signing the same canonical Dotify message bytes +with the app-scoped Product account and sending `productPublicKey`; the backend +derives the H160 requester from that public key before any nonce is consumed. For registered artist tracks, users without a connected wallet can play Free tracks. For gated tracks, they see a sign-in/unlock gate. Dev-account fallback @@ -438,8 +444,8 @@ handle: 4. Keep demo-mode browser-exposed Pinata/content secrets out of public deployments. 5. Validate the Product host/account and Bulletin/DotNS deployment baseline, - then implement Product signature verification, resource allocation, and - PolkaVM/CDM contract portability. + then wire frontend Product-signed key/session requests, resource allocation, + and PolkaVM/CDM contract portability. 6. Add a production artist dashboard on `/artists`: release drafts, edit metadata, royalty analytics, and profile verification state. 7. Deploy and monitor a public signaling server for DotNS / Bulletin builds. diff --git a/docs/backlog/README.md b/docs/backlog/README.md index 5e32be6..4d45a60 100644 --- a/docs/backlog/README.md +++ b/docs/backlog/README.md @@ -109,9 +109,10 @@ first public listening path, while the Product DevNet build adds explicit Host detection, app-scoped Product identity, and canonical Product-origin room links. The typed runtime ports and experimental Product CDM/PAPI adapter boundary are implementation preparation only; they do -not imply Product-signed contract writes, protected key access, Statement Store -rooms, or Humanity decisions. Those remain gated until their adapters prove the -current API and security boundary. +not imply Product-signed contract writes, Statement Store rooms, or Humanity +decisions. API-side Product-signed key/session verification now exists through +`product-sr25519-v1`, but the Product frontend still needs to send host-signed +requests before protected playback can use that identity path. The Product SDK evidence snapshot used for this replanning is `@parity/product-sdk` 0.19.1 and @@ -165,7 +166,7 @@ on `main`. The remaining order is: the link-first guest doctrine. 4. Validate the delivered Product host/account and Bulletin/DotNS baseline, then wire real CDM-installed runtime packages through the Product CDM/PAPI - adapter, add backend Product-signature verification, and run bounded + adapter, wire frontend Product-signed key/session requests, and run bounded resource-allocation/Statement Store spikes. 5. Build live Humanity / Individuality only after the research ticket proves a privacy-preserving source, proof shape, address-binding story, and fallback diff --git a/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md b/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md index e3a9b9d..9b742cd 100644 --- a/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md +++ b/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md @@ -167,10 +167,14 @@ Goal: deepen the delivered Product mode one adapter at a time. - Delivered on the next follow-up branch: add an experimental CDM/PAPI adapter behind those ports. It is not selected by default until Dotify has CDM-installed Product runtime packages and host-signed transaction evidence. +- Delivered on the next follow-up branch: add an API-side Product sr25519 + signature scheme for key delivery and session sign-in. It binds the Product + account public key to the derived H160 requester before nonce consumption and + access checks. - Next: wire a generated CDM manifest/types into Product mode and run real host transaction smoke tests. -- Next: add a backend signature-scheme adapter that verifies Product account - signatures and address binding before key delivery. +- Next: wire frontend Product-host key/session requests to the API signature + scheme and run real host smoke tests. - Keep backend key delivery authoritative unless a Product-host design proves a stronger key-custody boundary. - Keep `.dot`/Playground deployment separate from access enforcement. diff --git a/docs/context/dotify-technical-memory.md b/docs/context/dotify-technical-memory.md index a341a1b..5f41deb 100644 --- a/docs/context/dotify-technical-memory.md +++ b/docs/context/dotify-technical-memory.md @@ -57,8 +57,9 @@ Wallet-gated onboarding, runtime creation, upload, encryption, IPFS publication, - Product SDK / Playground / Humanity integration is a progressive enhancement track. The current verified Product SDK snapshot is prototype/reference/ unaudited, Host APIs require a compatible container, contracts target - `pallet-revive` / PolkaVM CDM flows, and Statement Store is constrained to - small signed ephemeral data. + `pallet-revive` / PolkaVM CDM flows, Product sr25519 key/session signatures + are API-supported but not yet wired into the shipped Product frontend, and + Statement Store is constrained to small signed ephemeral data. ## Production spine @@ -87,7 +88,8 @@ Introduce a lean backend service for: - Pinata uploads; - content-key custody and delivery; -- wallet signature verification; +- wallet signature verification, including EIP-191 and Product sr25519 + request schemes; - nonce/replay protection; - access checks against SmartRuntime; - room host key requests; @@ -211,7 +213,9 @@ Contracts already have meaningful tests; frontend and e2e must catch up. - No dev fallback signer in public flows. - Access checks must fail closed. - Backend must not trust frontend-provided access results. -- Wallet signatures must include nonce, chain ID, content hash, requester address, request purpose, and expiration. +- Wallet signatures must include nonce, chain ID, content hash, requester + address, request purpose, and expiration; Product signatures must also bind + the Product account public key to the derived H160 requester. - Replay protection is mandatory for key requests. - Room listeners must never receive content keys or encrypted source files. - Logs must never expose secrets, keys, or raw uploaded contents. diff --git a/docs/explanation/product-devnet-architecture.md b/docs/explanation/product-devnet-architecture.md index 160fa98..2ea8121 100644 --- a/docs/explanation/product-devnet-architecture.md +++ b/docs/explanation/product-devnet-architecture.md @@ -84,7 +84,7 @@ cross-origin catalog reads, key requests, Socket.IO, and WebRTC signaling. | Host room | Socket.IO + WebRTC | Same | Keep until a multiparty replacement proves equivalent UX | | Product identity | Not applicable | App-scoped SS58/H160 | Host identity with explicit capability grants | | Classic payment | Passkey/EVM wallet | Passkey/EVM wallet | CDM/PAPI write adapter | -| Protected key request | EIP-191 | EIP-191 | Backend-verified Product signature scheme | +| Protected key request | EIP-191 or session token | EIP-191 or session token in shipped UI; API accepts `product-sr25519-v1` | Frontend-host signed Product key/session requests | | Artist publication | viem/EVM | viem/EVM | Generated CDM contract adapter | | Personhood | Current on-chain policy source | No new claim | Privacy-preserving Product proof after verification | | Static delivery | Netlify | Bulletin + DotNS | Bulletin + DotNS | @@ -166,11 +166,15 @@ The remaining Product contract work is integration and evidence work, not UI rewiring. Operators still need CDM-deployed Dotify runtime packages, `cdm.json`/generated contract types, `pallet-revive` account mapping, and real host-signed transaction smoke evidence before Product writes can replace the -EVM wallet path. The backend authentication protocol must also gain an -explicit signature scheme field. A Product signature is accepted only after the -server can bind the signed payload, Product account public key, derived H160, -chain, nonce, purpose, and expiry. EIP-191 remains supported for standalone -clients. Unknown schemes fail closed. +EVM wallet path. + +The backend authentication protocol now has an explicit signature scheme field. +Standalone clients use the default `eip191` scheme. Product-host clients can +use `product-sr25519-v1` after signing the same canonical Dotify message bytes +with the app-scoped Product account; the server binds the signature to the +Product public key, derived H160 requester, chain, nonce, purpose, and expiry +before consuming the nonce or running access checks. Unknown schemes fail +closed. The shipped Product frontend does not yet send this Product proof shape. This avoids a second frontend business model and allows Product mode to replace one infrastructure adapter at a time. diff --git a/docs/index.html b/docs/index.html index 87f46e7..ae701bb 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1542,8 +1542,9 @@

    Product DevNet now, sovereignty adapters next

    Dotify now has a publishable dotify-test01.dot build, explicit app-scoped Product identity, public room links that preserve wallet-free entry, typed runtime ports around the - current viem implementation, and an experimental CDM/PAPI adapter boundary. Product contract - writes, Product-signed key requests, Humanity/Individuality proofs, consented provenance, and + current viem implementation, an experimental CDM/PAPI adapter boundary, and API-side Product + sr25519 verification for protected key/session requests. Product contract writes, frontend + host-signed protected playback, Humanity/Individuality proofs, consented provenance, and ambassador mechanics remain sequenced behind verified security and privacy boundaries.

    diff --git a/docs/operations/deployment-configuration.md b/docs/operations/deployment-configuration.md index 075ce0a..63f1e2f 100644 --- a/docs/operations/deployment-configuration.md +++ b/docs/operations/deployment-configuration.md @@ -210,6 +210,22 @@ For production-grade catalog evidence: - keep at least one machine warm while measuring catalog p75 performance, then record whether the trace was warm or cold. +### Backend Signature Schemes + +No Netlify or Fly dashboard variable enables Product signatures. The API +accepts two explicit schemes on session sign-in and protected key requests: + +| Scheme | Client | Required proof fields | Backend binding | +| --- | --- | --- | --- | +| `eip191` | Standalone EVM/passkey wallet path | `signature` | `viem.verifyMessage` against the requester H160 | +| `product-sr25519-v1` | Product-host app-scoped account path | `signature`, `productPublicKey` | sr25519 signature over the canonical Dotify message bytes, then Product public-key-to-H160 derivation matching the requester | + +Unknown schemes fail at the API schema boundary. Product requests must still +pass the same nonce, chain, purpose, expiry, and `musicAccCanAccess` checks as +standalone requests. The shipped Product frontend does not yet submit this +Product proof shape; when it does, validate it through Product host smoke tests +before treating Product identity as a protected-playback account. + ## Fly Signaling Open app `dotify-signal`. @@ -269,7 +285,7 @@ npm run build:product-devnet checks in [`docs/operations/product-devnet-deployment.md`](product-devnet-deployment.md). -6. For explicit origin rejection evidence, include a denied origin: +7. For explicit origin rejection evidence, include a denied origin: ```bash cd web diff --git a/docs/operations/product-devnet-deployment.md b/docs/operations/product-devnet-deployment.md index 4c1fe1d..d77c3f4 100644 --- a/docs/operations/product-devnet-deployment.md +++ b/docs/operations/product-devnet-deployment.md @@ -170,11 +170,14 @@ active. ## Known Limits -- Product account signing is identity/presence only in this phase. -- Contract writes and key requests still require passkey/EVM signing in the - shipped UI. The experimental Product CDM/PAPI runtime adapter is present in - code, but it is not selected until Dotify has CDM-installed runtime packages, - generated contract types, and real host-signed transaction evidence. +- Product account signing is accepted by the API only through the explicit + `product-sr25519-v1` session/key-request scheme. The shipped Product UI has + not yet been wired to submit that proof shape, so protected playback still + uses passkey/EVM signing in the current frontend. +- Contract writes still require passkey/EVM signing in the shipped UI. The + experimental Product CDM/PAPI runtime adapter is present in code, but it is + not selected until Dotify has CDM-installed runtime packages, generated + contract types, and real host-signed transaction evidence. - Rooms still depend on one in-memory Fly signaling machine. - Product-host cloud storage does not hold Dotify audio or content keys. - Product personhood is not yet an access decision source. diff --git a/docs/product/ux-signature-flows.md b/docs/product/ux-signature-flows.md index 8f6e892..95b0549 100644 --- a/docs/product/ux-signature-flows.md +++ b/docs/product/ux-signature-flows.md @@ -25,6 +25,21 @@ Dotify must avoid wallet pop-up fatigue. Wallet prompts should appear only when | Human Free unlock | Yes | Maybe session signature | No, unless proving/linking personhood requires one | | Artist publishing | Yes | Yes/transaction depending on step | Yes for runtime/register actions | +## Backend signature schemes + +Signed session and protected key requests carry an explicit `signatureScheme`. +If the field is omitted, the backend treats the request as `eip191` for +backward compatibility. + +| Scheme | Signer | Extra fields | Verification | +| --- | --- | --- | --- | +| `eip191` | Standalone EVM/passkey wallet | `signature` | Verify the canonical Dotify message with the requester H160 address. | +| `product-sr25519-v1` | App-scoped Product account | `signature`, `productPublicKey` | Verify sr25519 over the same canonical message bytes, derive H160 from the Product public key, and require it to match the requester. | + +Unknown schemes and Product public-key mismatches fail closed before nonce +consumption. Every successful signature path still runs the runtime access +check before the backend releases a content key. + ## Individual playback flow ```mermaid diff --git a/docs/reference/environment-variables.md b/docs/reference/environment-variables.md index 4391111..beef044 100644 --- a/docs/reference/environment-variables.md +++ b/docs/reference/environment-variables.md @@ -126,8 +126,10 @@ Production deployments must use a publicly reachable HTTPS endpoint. Backend API base URL. When set, audio, cover, and metadata uploads go through the backend. Full-track playback can request content keys with wallet-signed -requests. When unset, the web app falls back to local/demo browser-side Pinata -upload and `VITE_CONTENT_SECRET` encryption. +requests. The backend accepts the default `eip191` signature scheme and the +Product-host `product-sr25519-v1` scheme without an additional env flag. When +unset, the web app falls back to local/demo browser-side Pinata upload and +`VITE_CONTENT_SECRET` encryption. --- diff --git a/services/api/.env.example b/services/api/.env.example index a6cb595..bcdf622 100644 --- a/services/api/.env.example +++ b/services/api/.env.example @@ -32,9 +32,10 @@ CATALOG_CONFIRMATIONS=2 # Master secret for per-track content-key derivation. Used by BOTH # /api/uploads/audio (server-side AES-256-GCM encryption before pinning) and -# /api/tracks/:contentHash/key-request (key delivery after a wallet-signed, -# on-chain-verified access check). Must be at least 32 random bytes encoded as -# hex. Never expose this value; rotating it re-keys every track at once. +# /api/tracks/:contentHash/key-request (key delivery after an eip191 or +# product-sr25519-v1 signed, on-chain-verified access check). Must be at least +# 32 random bytes encoded as hex. Never expose this value; rotating it re-keys +# every track at once. # Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" CONTENT_KEY_MASTER_SECRET= diff --git a/services/api/package-lock.json b/services/api/package-lock.json index 9a9f4ca..7fa0eb8 100644 --- a/services/api/package-lock.json +++ b/services/api/package-lock.json @@ -12,6 +12,7 @@ "@fastify/multipart": "^10.0.0", "@fastify/rate-limit": "^10.3.0", "@noble/hashes": "1.8.0", + "@scure/sr25519": "^1.0.0", "fastify": "^5.8.5", "viem": "^2.52.2", "zod": "^3.23.8" @@ -757,6 +758,49 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/sr25519": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@scure/sr25519/-/sr25519-1.0.0.tgz", + "integrity": "sha512-b+uhK5akMINXZP95F3gJGcb5CMKYxf+q55fwMl0GoBwZDbWolmGNi1FrBSwuaZX5AhqS2byHiAueZgtDNpot2A==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~2.0.0", + "@noble/hashes": "~2.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/sr25519/node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/sr25519/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@types/node": { "version": "22.19.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", diff --git a/services/api/package.json b/services/api/package.json index 72eefec..01515c5 100644 --- a/services/api/package.json +++ b/services/api/package.json @@ -19,6 +19,7 @@ "@fastify/multipart": "^10.0.0", "@fastify/rate-limit": "^10.3.0", "@noble/hashes": "1.8.0", + "@scure/sr25519": "^1.0.0", "fastify": "^5.8.5", "viem": "^2.52.2", "zod": "^3.23.8" diff --git a/services/api/src/cors.test.ts b/services/api/src/cors.test.ts index 578357a..22e3894 100644 --- a/services/api/src/cors.test.ts +++ b/services/api/src/cors.test.ts @@ -16,9 +16,10 @@ describe('frontend origin boundary', () => { logging: false, apiOrigins: ['https://muzinga.netlify.app', 'https://dotify-test01.dev-dot.li'], }); + const server = app; for (const origin of ['https://muzinga.netlify.app', 'https://dotify-test01.dev-dot.li']) { - const response = await app.inject({ + const response = await server.inject({ method: 'GET', url: '/health', headers: { origin }, @@ -26,7 +27,7 @@ describe('frontend origin boundary', () => { assert.equal(response.headers['access-control-allow-origin'], origin); } - const unrelated = await app.inject({ + const unrelated = await server.inject({ method: 'GET', url: '/health', headers: { origin: 'https://unrelated.example' }, diff --git a/services/api/src/routes/auth.test.ts b/services/api/src/routes/auth.test.ts index e3490d8..8bf4c13 100644 --- a/services/api/src/routes/auth.test.ts +++ b/services/api/src/routes/auth.test.ts @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'; import { afterEach, describe, it } from 'node:test'; import Fastify, { type FastifyInstance } from 'fastify'; import { createAuthRoutes, type AuthRouteDeps } from './auth.js'; +import { PRODUCT_SR25519_SIGNATURE_SCHEME, type SignInRequest } from '../services/signatures.js'; const ADDRESS = '0x1111111111111111111111111111111111111111'; @@ -80,6 +81,34 @@ describe('POST /api/auth/session', () => { assert.equal(body.address, ADDRESS); }); + it('passes Product sr25519 proof fields to sign-in verification', async () => { + let verifiedRequest: SignInRequest | null = null; + const server = await buildApp({ + verifySignInRequest: async request => { + verifiedRequest = request; + return { valid: true }; + } + }); + const productPublicKey = `0x${'22'.repeat(32)}`; + const signature = `0x${'33'.repeat(64)}`; + const response = await server.inject({ + method: 'POST', + url: '/api/auth/session', + payload: sessionBody({ + signatureScheme: PRODUCT_SR25519_SIGNATURE_SCHEME, + productPublicKey, + signature + }) + }); + + assert.equal(response.statusCode, 200); + const productRequest = verifiedRequest as Extract | null; + assert.ok(productRequest); + assert.equal(productRequest.signatureScheme, PRODUCT_SR25519_SIGNATURE_SCHEME); + assert.equal(productRequest.productPublicKey, productPublicKey); + assert.equal(productRequest.signature, signature); + }); + it('rejects an invalid signature with 401 and the verification code', async () => { const server = await buildApp({ verifySignInRequest: async () => ({ valid: false, code: 'SIGNATURE_INVALID', reason: 'bad signature' }) @@ -106,6 +135,30 @@ describe('POST /api/auth/session', () => { assert.equal(response.statusCode, 400); }); + it('rejects unknown sign-in signature schemes before verification or token issuance', async () => { + let verificationCalled = false; + let issuanceCalled = false; + const server = await buildApp({ + verifySignInRequest: async () => { + verificationCalled = true; + return { valid: true }; + }, + issueSessionToken: () => { + issuanceCalled = true; + return { ok: true, token: 'payload.signature', expiresAt: new Date(Date.now() + 1000).toISOString() }; + } + }); + const response = await server.inject({ + method: 'POST', + url: '/api/auth/session', + payload: sessionBody({ signatureScheme: 'product-unknown-v1' }) + }); + + assert.equal(response.statusCode, 400); + assert.equal(verificationCalled, false); + assert.equal(issuanceCalled, false); + }); + it('rejects a different-chain sign-in before verification or token issuance', async () => { let verificationCalled = false; let issuanceCalled = false; diff --git a/services/api/src/routes/auth.ts b/services/api/src/routes/auth.ts index 90d0845..07839bb 100644 --- a/services/api/src/routes/auth.ts +++ b/services/api/src/routes/auth.ts @@ -8,6 +8,8 @@ import { z } from 'zod'; import { config } from '../config.js'; import { checkDotifyChainId } from '../services/chainDomain.js'; import { + EIP191_SIGNATURE_SCHEME, + PRODUCT_SR25519_SIGNATURE_SCHEME, createWalletNonceChallenge, verifySignInRequest as defaultVerifySignInRequest, type SignInRequest, @@ -25,14 +27,26 @@ const nonceRequestSchema = z.object({ chainId: z.number().int().positive().optional() }); -const sessionRequestSchema = z.object({ +const sessionBaseRequestSchema = z.object({ address: z.string().regex(/^0x[0-9a-fA-F]{40}$/, 'Invalid EVM address'), - signature: z.string().regex(/^0x[0-9a-fA-F]+$/, 'Invalid signature'), nonce: z.string().min(16, 'Nonce is required'), chainId: z.number().int().positive(), expiresAt: z.string().datetime() }); +const eip191SessionRequestSchema = sessionBaseRequestSchema.extend({ + signatureScheme: z.literal(EIP191_SIGNATURE_SCHEME).optional(), + signature: z.string().regex(/^0x[0-9a-fA-F]+$/, 'Invalid signature') +}); + +const productSr25519SessionRequestSchema = sessionBaseRequestSchema.extend({ + signatureScheme: z.literal(PRODUCT_SR25519_SIGNATURE_SCHEME), + signature: z.string().regex(/^0x[0-9a-fA-F]{128}$/, 'Invalid Product sr25519 signature'), + productPublicKey: z.string().regex(/^0x[0-9a-fA-F]{64}$/, 'Invalid Product account public key') +}); + +const sessionRequestSchema = z.union([productSr25519SessionRequestSchema, eip191SessionRequestSchema]); + const logoutRequestSchema = z.object({ sessionToken: z.string().min(16, 'Session token is required') }); @@ -103,13 +117,27 @@ export function createAuthRoutes(deps: AuthRouteDeps = defaultDeps) { return reply.status(400).send({ error: domain.reason, code: domain.code }); } - const verification = await deps.verifySignInRequest({ - requester: parsed.data.address, - chainId: parsed.data.chainId, - nonce: parsed.data.nonce, - expiresAt: parsed.data.expiresAt, - signature: parsed.data.signature - }); + const signInRequest: SignInRequest = + parsed.data.signatureScheme === PRODUCT_SR25519_SIGNATURE_SCHEME + ? { + requester: parsed.data.address, + chainId: parsed.data.chainId, + nonce: parsed.data.nonce, + expiresAt: parsed.data.expiresAt, + signature: parsed.data.signature, + signatureScheme: PRODUCT_SR25519_SIGNATURE_SCHEME, + productPublicKey: parsed.data.productPublicKey + } + : { + requester: parsed.data.address, + chainId: parsed.data.chainId, + nonce: parsed.data.nonce, + expiresAt: parsed.data.expiresAt, + signature: parsed.data.signature, + signatureScheme: EIP191_SIGNATURE_SCHEME + }; + + const verification = await deps.verifySignInRequest(signInRequest); if (!verification.valid) { return reply.status(401).send({ error: verification.reason, code: verification.code }); } diff --git a/services/api/src/routes/keys.test.ts b/services/api/src/routes/keys.test.ts index 210abf8..23abf04 100644 --- a/services/api/src/routes/keys.test.ts +++ b/services/api/src/routes/keys.test.ts @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'; import { afterEach, describe, it } from 'node:test'; import Fastify, { type FastifyInstance } from 'fastify'; import { createKeyRoutes, type KeyRouteDeps } from './keys.js'; +import { PRODUCT_SR25519_SIGNATURE_SCHEME, type KeySignatureRequest } from '../services/signatures.js'; const CONTENT_HASH = `0x${'ab'.repeat(32)}`; const REQUESTER = '0x1111111111111111111111111111111111111111'; @@ -85,6 +86,79 @@ describe('POST /api/tracks/:contentHash/key-request', () => { assert.equal(response.json().code, 'SIGNATURE_INVALID'); }); + it('passes Product sr25519 proof fields to signature verification', async () => { + let verifiedRequest: KeySignatureRequest | null = null; + const server = await buildApp({ + verifySignedRequest: async request => { + verifiedRequest = request; + return { valid: true }; + } + }); + const productPublicKey = `0x${'22'.repeat(32)}`; + const signature = `0x${'33'.repeat(64)}`; + const response = await server.inject({ + method: 'POST', + url: `/api/tracks/${CONTENT_HASH}/key-request`, + payload: baseBody({ + signatureScheme: PRODUCT_SR25519_SIGNATURE_SCHEME, + productPublicKey, + signature + }) + }); + + assert.equal(response.statusCode, 200); + const productRequest = verifiedRequest as Extract | null; + assert.ok(productRequest); + assert.equal(productRequest.signatureScheme, PRODUCT_SR25519_SIGNATURE_SCHEME); + assert.equal(productRequest.productPublicKey, productPublicKey); + assert.equal(productRequest.signature, signature); + }); + + it('rejects unknown signature schemes before verification or access checks', async () => { + let verificationCalled = false; + let accessChecked = false; + const server = await buildApp({ + verifySignedRequest: async () => { + verificationCalled = true; + return { valid: true }; + }, + checkTrackAccess: async () => { + accessChecked = true; + return { allowed: true, runtime: RUNTIME }; + } + }); + const response = await server.inject({ + method: 'POST', + url: `/api/tracks/${CONTENT_HASH}/key-request`, + payload: baseBody({ signatureScheme: 'product-unknown-v1' }) + }); + + assert.equal(response.statusCode, 400); + assert.equal(verificationCalled, false); + assert.equal(accessChecked, false); + }); + + it('requires Product public key for Product sr25519 requests', async () => { + let verificationCalled = false; + const server = await buildApp({ + verifySignedRequest: async () => { + verificationCalled = true; + return { valid: true }; + } + }); + const response = await server.inject({ + method: 'POST', + url: `/api/tracks/${CONTENT_HASH}/key-request`, + payload: baseBody({ + signatureScheme: PRODUCT_SR25519_SIGNATURE_SCHEME, + signature: `0x${'33'.repeat(64)}` + }) + }); + + assert.equal(response.statusCode, 400); + assert.equal(verificationCalled, false); + }); + it('answers a denied individual listener with an unlock CTA, never a key or a preview mode', async () => { const server = await buildApp({ checkTrackAccess: async () => ({ allowed: false, code: 'LISTENER_ACCESS_REQUIRED', reason: 'no access' }) diff --git a/services/api/src/routes/keys.ts b/services/api/src/routes/keys.ts index a4e56e4..fde1bdc 100644 --- a/services/api/src/routes/keys.ts +++ b/services/api/src/routes/keys.ts @@ -21,26 +21,44 @@ import { type TrackAccessResult } from '../services/chainAccess.js'; import { deriveContentKey as defaultDeriveContentKey, type ContentKeyResult } from '../services/keyVault.js'; -import { verifySignedRequest as defaultVerifySignedRequest, type KeySignatureRequest, type SignatureVerification } from '../services/signatures.js'; +import { + EIP191_SIGNATURE_SCHEME, + PRODUCT_SR25519_SIGNATURE_SCHEME, + verifySignedRequest as defaultVerifySignedRequest, + type KeySignatureRequest, + type SignatureVerification +} from '../services/signatures.js'; import { verifySessionToken as defaultVerifySessionToken, type SessionVerification } from '../services/sessionTokens.js'; const paramsSchema = z.object({ contentHash: z.string().regex(/^0x[0-9a-fA-F]{64}$/, 'Invalid content hash') }); -const signedBodySchema = z.object({ +const signedBaseBodySchema = z.object({ requester: z.string().regex(/^0x[0-9a-fA-F]{40}$/, 'Invalid EVM address'), - signature: z.string().regex(/^0x[0-9a-fA-F]+$/, 'Invalid signature'), nonce: z.string().min(16, 'Nonce is required'), chainId: z.number().int().positive(), expiresAt: z.string().datetime() }); // 'room_listener' is intentionally not accepted; room listeners never get keys. -const keyRequestBodySchema = signedBodySchema.extend({ +const keyRequestPurposeSchema = z.object({ purpose: z.enum(['individual', 'room_host']) }); +const eip191KeyRequestBodySchema = signedBaseBodySchema.merge(keyRequestPurposeSchema).extend({ + signatureScheme: z.literal(EIP191_SIGNATURE_SCHEME).optional(), + signature: z.string().regex(/^0x[0-9a-fA-F]+$/, 'Invalid signature') +}); + +const productSr25519KeyRequestBodySchema = signedBaseBodySchema.merge(keyRequestPurposeSchema).extend({ + signatureScheme: z.literal(PRODUCT_SR25519_SIGNATURE_SCHEME), + signature: z.string().regex(/^0x[0-9a-fA-F]{128}$/, 'Invalid Product sr25519 signature'), + productPublicKey: z.string().regex(/^0x[0-9a-fA-F]{64}$/, 'Invalid Product account public key') +}); + +const keyRequestBodySchema = z.union([productSr25519KeyRequestBodySchema, eip191KeyRequestBodySchema]); + // Session path (ticket 24 P2): after the one-per-session sign-in, a key // request carries the bearer token instead of a fresh wallet signature. The // on-chain access check still runs on every request. @@ -145,16 +163,33 @@ export function createKeyRoutes(deps: KeyRouteDeps = defaultDeps) { return reply.status(401).send({ error: domain.reason, code: domain.code }); } - const signature = await deps.verifySignedRequest({ - action: 'REQUEST_CONTENT_KEY', - purpose: body.data.purpose, - contentHash: params.data.contentHash, - requester: body.data.requester, - chainId: body.data.chainId, - nonce: body.data.nonce, - expiresAt: body.data.expiresAt, - signature: body.data.signature - }); + const signatureRequest: KeySignatureRequest = + body.data.signatureScheme === PRODUCT_SR25519_SIGNATURE_SCHEME + ? { + action: 'REQUEST_CONTENT_KEY', + purpose: body.data.purpose, + contentHash: params.data.contentHash, + requester: body.data.requester, + chainId: body.data.chainId, + nonce: body.data.nonce, + expiresAt: body.data.expiresAt, + signature: body.data.signature, + signatureScheme: PRODUCT_SR25519_SIGNATURE_SCHEME, + productPublicKey: body.data.productPublicKey + } + : { + action: 'REQUEST_CONTENT_KEY', + purpose: body.data.purpose, + contentHash: params.data.contentHash, + requester: body.data.requester, + chainId: body.data.chainId, + nonce: body.data.nonce, + expiresAt: body.data.expiresAt, + signature: body.data.signature, + signatureScheme: EIP191_SIGNATURE_SCHEME + }; + + const signature = await deps.verifySignedRequest(signatureRequest); if (!signature.valid) { return reply.status(401).send({ error: signature.reason, code: signature.code }); diff --git a/services/api/src/services/signatures.test.ts b/services/api/src/services/signatures.test.ts index 373066b..197b7e2 100644 --- a/services/api/src/services/signatures.test.ts +++ b/services/api/src/services/signatures.test.ts @@ -1,11 +1,14 @@ import assert from 'node:assert/strict'; import { beforeEach, describe, it } from 'node:test'; +import { getPublicKey, secretFromSeed, sign as signSr25519 } from '@scure/sr25519'; import { privateKeyToAccount } from 'viem/accounts'; import { resetNonceStore } from './replayProtection.js'; import { + PRODUCT_SR25519_SIGNATURE_SCHEME, buildSignedRequestMessage, buildSignInMessage, createWalletNonceChallenge, + deriveProductAccountH160, verifySignInRequest, verifySignedRequest, type SignInPayload, @@ -17,6 +20,15 @@ const signer = privateKeyToAccount('0xac0974bec39a37e36980911eda47a06fcd4ee8d3a8 const CONTENT_HASH = `0x${'ab'.repeat(32)}` as const; const CHAIN_ID = 420420417; +const productSecretKey = secretFromSeed(new Uint8Array(32).fill(7)); +const productPublicKey = getPublicKey(productSecretKey); +const productPublicKeyHex = `0x${bytesToHex(productPublicKey)}` as const; + +function bytesToHex(bytes: Uint8Array): string { + return Array.from(bytes) + .map(byte => byte.toString(16).padStart(2, '0')) + .join(''); +} async function signedPayload(overrides: Partial = {}) { const challenge = createWalletNonceChallenge({ address: signer.address, chainId: CHAIN_ID }); @@ -34,6 +46,23 @@ async function signedPayload(overrides: Partial = {}) { return { payload, signature }; } +async function productSignedPayload(overrides: Partial = {}) { + const requester = overrides.requester ?? deriveProductAccountH160(productPublicKey); + const challenge = createWalletNonceChallenge({ address: requester, chainId: CHAIN_ID }); + const payload: SignedRequestPayload = { + action: 'REQUEST_CONTENT_KEY', + purpose: 'individual', + contentHash: CONTENT_HASH, + requester, + chainId: CHAIN_ID, + nonce: challenge.nonce, + expiresAt: challenge.expiresAt, + ...overrides + }; + const signature = `0x${bytesToHex(signSr25519(productSecretKey, new TextEncoder().encode(buildSignedRequestMessage(payload))))}`; + return { payload, signature, productPublicKey: productPublicKeyHex }; +} + describe('verifySignedRequest', () => { beforeEach(() => { resetNonceStore(); @@ -45,6 +74,17 @@ describe('verifySignedRequest', () => { assert.equal(result.valid, true); }); + it('accepts a Product sr25519 request bound to the derived H160 requester', async () => { + const { payload, signature, productPublicKey } = await productSignedPayload(); + const result = await verifySignedRequest({ + ...payload, + signatureScheme: PRODUCT_SR25519_SIGNATURE_SCHEME, + signature, + productPublicKey + }); + assert.equal(result.valid, true); + }); + it('rejects a replayed nonce', async () => { const { payload, signature } = await signedPayload(); const first = await verifySignedRequest({ ...payload, signature }); @@ -63,6 +103,45 @@ describe('verifySignedRequest', () => { assert.equal(!result.valid && result.code, 'SIGNATURE_INVALID'); }); + it('rejects a Product signature when the payload changes', async () => { + const { payload, signature, productPublicKey } = await productSignedPayload(); + const result = await verifySignedRequest({ + ...payload, + contentHash: `0x${'cd'.repeat(32)}`, + signatureScheme: PRODUCT_SR25519_SIGNATURE_SCHEME, + signature, + productPublicKey + }); + assert.equal(result.valid, false); + assert.equal(!result.valid && result.code, 'SIGNATURE_INVALID'); + }); + + it('rejects a Product public key that does not derive to the requester H160', async () => { + const { payload, signature, productPublicKey } = await productSignedPayload({ + requester: '0x1111111111111111111111111111111111111111' + }); + const result = await verifySignedRequest({ + ...payload, + signatureScheme: PRODUCT_SR25519_SIGNATURE_SCHEME, + signature, + productPublicKey + }); + assert.equal(result.valid, false); + assert.equal(!result.valid && result.code, 'PRODUCT_ADDRESS_MISMATCH'); + }); + + it('rejects malformed Product proof bytes before nonce consumption', async () => { + const { payload, signature } = await productSignedPayload(); + const result = await verifySignedRequest({ + ...payload, + signatureScheme: PRODUCT_SR25519_SIGNATURE_SCHEME, + signature, + productPublicKey: '0x1234' + }); + assert.equal(result.valid, false); + assert.equal(!result.valid && result.code, 'PRODUCT_SIGNATURE_INVALID'); + }); + it('rejects a signature over a different purpose', async () => { const { payload, signature } = await signedPayload({ purpose: 'individual' }); const result = await verifySignedRequest({ ...payload, purpose: 'room_host', signature }); @@ -124,4 +203,34 @@ describe('verifySignedRequest', () => { assert.equal(result.valid, false); assert.equal(!result.valid && result.code, 'CHAIN_ID_MISMATCH'); }); + + it('accepts a Product sr25519 sign-in bound to the derived H160 requester', async () => { + const requester = deriveProductAccountH160(productPublicKey); + const challenge = createWalletNonceChallenge({ address: requester, chainId: CHAIN_ID }); + const payload: SignInPayload = { + requester, + chainId: CHAIN_ID, + nonce: challenge.nonce, + expiresAt: challenge.expiresAt + }; + const signature = `0x${bytesToHex(signSr25519(productSecretKey, new TextEncoder().encode(buildSignInMessage(payload))))}`; + const result = await verifySignInRequest({ + ...payload, + signatureScheme: PRODUCT_SR25519_SIGNATURE_SCHEME, + signature, + productPublicKey: productPublicKeyHex + }); + + assert.equal(result.valid, true); + }); + + it('matches the Product SDK H160 derivation vector for a native Substrate public key', () => { + const alicePublicKey = new Uint8Array([ + 0xd4, 0x35, 0x93, 0xc7, 0x15, 0xfd, 0xd3, 0x1c, 0x61, 0x14, 0x1a, 0xbd, 0x04, 0xa9, 0x9f, + 0xd6, 0x82, 0x2c, 0x85, 0x58, 0x85, 0x4c, 0xcd, 0xe3, 0x9a, 0x56, 0x84, 0xe7, 0xa5, 0x6d, + 0xa2, 0x7d + ]); + + assert.equal(deriveProductAccountH160(alicePublicKey), '0x9621dde636de098b43efb0fa9b61facfe328f99d'); + }); }); diff --git a/services/api/src/services/signatures.ts b/services/api/src/services/signatures.ts index 34c9105..fac3749 100644 --- a/services/api/src/services/signatures.ts +++ b/services/api/src/services/signatures.ts @@ -1,4 +1,4 @@ -// Wallet-signed request verification (EIP-191 personal_sign). +// Wallet-signed request verification. // // The signed payload is a structured, domain-bound text message that binds: // app, action, purpose, content hash, requester address, chain ID, nonce, @@ -6,11 +6,16 @@ // (web/src/services/keyService.ts); any drift between the two breaks // verification, which fails closed. // -// Security boundary: EIP-191 is used instead of EIP-712 for the first -// production spine because it is supported uniformly across the wallets we -// target. The message is structured and domain-bound, so it cannot be -// replayed against another app, chain, purpose, or track. +// Security boundary: standalone wallets use EIP-191 instead of EIP-712 for +// the first production spine because it is supported uniformly across the +// wallets we target. Product-host requests sign the same canonical message +// bytes with the app-scoped Product account and must prove that account's +// public key derives to the requester H160 used by runtime access checks. +// The message is structured and domain-bound, so it cannot be replayed +// against another app, chain, purpose, or track. +import { keccak_256 } from '@noble/hashes/sha3'; +import { verify as verifySr25519Signature } from '@scure/sr25519'; import { verifyMessage } from 'viem'; import { config } from '../config.js'; import { checkDotifyChainId } from './chainDomain.js'; @@ -20,6 +25,14 @@ import { consumeNonce, issueNonce } from './replayProtection.js'; // content keys, they only receive the host's ephemeral WebRTC stream. export type KeyRequestPurpose = 'individual' | 'room_host'; export type SignedAction = 'REQUEST_CONTENT_KEY' | 'SIGN_IN'; +export const EIP191_SIGNATURE_SCHEME = 'eip191'; +export const PRODUCT_SR25519_SIGNATURE_SCHEME = 'product-sr25519-v1'; +export type SignatureScheme = typeof EIP191_SIGNATURE_SCHEME | typeof PRODUCT_SR25519_SIGNATURE_SCHEME; + +const PRODUCT_PUBLIC_KEY_BYTES = 32; +const PRODUCT_SR25519_SIGNATURE_BYTES = 64; +const H160_BYTES = 20; +const EVM_DERIVED_MARKER = 0xee; export type NonceChallengeRequest = { address: string; @@ -42,10 +55,19 @@ export type SignedRequestPayload = { expiresAt: string; }; -export type KeySignatureRequest = SignedRequestPayload & { +export type Eip191SignatureFields = { + signatureScheme?: typeof EIP191_SIGNATURE_SCHEME; signature: string; }; +export type ProductSr25519SignatureFields = { + signatureScheme: typeof PRODUCT_SR25519_SIGNATURE_SCHEME; + signature: string; + productPublicKey: string; +}; + +export type SignatureFields = Eip191SignatureFields | ProductSr25519SignatureFields; +export type KeySignatureRequest = SignedRequestPayload & SignatureFields; export type SignatureVerification = { valid: true } | { valid: false; code: string; reason: string }; /** @@ -73,9 +95,7 @@ export type SignInPayload = { expiresAt: string; }; -export type SignInRequest = SignInPayload & { - signature: string; -}; +export type SignInRequest = SignInPayload & SignatureFields; /** * Canonical EIP-191 message for the one-per-session Dotify sign-in @@ -96,6 +116,131 @@ export function buildSignInMessage(payload: SignInPayload): string { ].join('\n'); } +function bytesToHex(bytes: Uint8Array): string { + return Array.from(bytes) + .map(byte => byte.toString(16).padStart(2, '0')) + .join(''); +} + +function fixedHexToBytes(hex: string, expectedBytes: number): Uint8Array { + const clean = hex.startsWith('0x') ? hex.slice(2) : hex; + if (clean.length !== expectedBytes * 2 || !/^[0-9a-fA-F]+$/.test(clean)) { + throw new Error(`Expected ${expectedBytes} bytes of hex`); + } + return new Uint8Array(Buffer.from(clean, 'hex')); +} + +/** + * Match Product SDK / pallet-revive AccountId32 -> H160 derivation: + * native Substrate accounts use keccak256(publicKey), last 20 bytes; accounts + * already derived from H160 strip the trailing 0xee padding. + */ +export function deriveProductAccountH160(publicKey: Uint8Array): `0x${string}` { + if (publicKey.length !== PRODUCT_PUBLIC_KEY_BYTES) { + throw new Error(`Expected ${PRODUCT_PUBLIC_KEY_BYTES}-byte Product public key`); + } + + const evmDerived = publicKey.slice(H160_BYTES).every(byte => byte === EVM_DERIVED_MARKER); + const addressBytes = evmDerived + ? publicKey.slice(0, H160_BYTES) + : keccak_256(publicKey).slice(PRODUCT_PUBLIC_KEY_BYTES - H160_BYTES); + return `0x${bytesToHex(addressBytes)}`; +} + +function verifyProductSr25519Payload(args: { + requester: string; + message: string; + signature: string; + productPublicKey: string | undefined; + invalidSignatureReason: string; +}): SignatureVerification { + if (!args.productPublicKey) { + return { + valid: false, + code: 'PRODUCT_PUBLIC_KEY_REQUIRED', + reason: 'Product signed requests must include the Product account public key.' + }; + } + + let publicKey: Uint8Array; + let signature: Uint8Array; + try { + publicKey = fixedHexToBytes(args.productPublicKey, PRODUCT_PUBLIC_KEY_BYTES); + signature = fixedHexToBytes(args.signature, PRODUCT_SR25519_SIGNATURE_BYTES); + } catch { + return { + valid: false, + code: 'PRODUCT_SIGNATURE_INVALID', + reason: 'Product signature payload is malformed.' + }; + } + + const derivedRequester = deriveProductAccountH160(publicKey); + if (derivedRequester.toLowerCase() !== args.requester.toLowerCase()) { + return { + valid: false, + code: 'PRODUCT_ADDRESS_MISMATCH', + reason: 'Product account public key does not derive to the requester H160 address.' + }; + } + + let signatureValid = false; + try { + signatureValid = verifySr25519Signature(new TextEncoder().encode(args.message), signature, publicKey); + } catch { + signatureValid = false; + } + + if (!signatureValid) { + return { valid: false, code: 'SIGNATURE_INVALID', reason: args.invalidSignatureReason }; + } + + return { valid: true }; +} + +async function verifySignatureEnvelope( + request: SignatureFields & { requester: string }, + message: string, + invalidSignatureReason: string +): Promise { + const signatureScheme = request.signatureScheme ?? EIP191_SIGNATURE_SCHEME; + + if (signatureScheme === EIP191_SIGNATURE_SCHEME) { + let signatureValid = false; + try { + signatureValid = await verifyMessage({ + address: request.requester as `0x${string}`, + message, + signature: request.signature as `0x${string}` + }); + } catch { + signatureValid = false; + } + + if (!signatureValid) { + return { valid: false, code: 'SIGNATURE_INVALID', reason: invalidSignatureReason }; + } + + return { valid: true }; + } + + if (signatureScheme === PRODUCT_SR25519_SIGNATURE_SCHEME) { + return verifyProductSr25519Payload({ + requester: request.requester, + message, + signature: request.signature, + productPublicKey: 'productPublicKey' in request ? request.productPublicKey : undefined, + invalidSignatureReason + }); + } + + return { + valid: false, + code: 'SIGNATURE_SCHEME_UNSUPPORTED', + reason: 'Signature scheme is not supported for Dotify key delivery.' + }; +} + /** * Verify a sign-in request: expiry, signature, then nonce consumption - * the same fail-closed order as verifySignedRequest. @@ -111,19 +256,13 @@ export async function verifySignInRequest(request: SignInRequest): Promise Date: Tue, 28 Jul 2026 16:20:44 +0200 Subject: [PATCH 06/22] feat: wire Product key signatures in frontend --- README.md | 8 +- ...oduct-readiness-and-killer-dapp-roadmap.md | 7 +- docs/context/dotify-technical-memory.md | 4 +- .../product-devnet-architecture.md | 10 +- docs/index.html | 7 +- docs/operations/deployment-configuration.md | 9 +- docs/operations/product-devnet-deployment.md | 7 +- web/src/app/providers/WalletProvider.tsx | 8 +- .../features/productHost/productHost.test.ts | 42 ++++++- web/src/features/productHost/productHost.ts | 41 ++++++- web/src/hooks/useCatalog.ts | 8 +- web/src/hooks/useWallet.ts | 11 +- web/src/services/keyService.test.ts | 89 ++++++++++++++ web/src/services/keyService.ts | 111 +++++++++++++----- 14 files changed, 301 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 7d05f32..0875db9 100644 --- a/README.md +++ b/README.md @@ -49,10 +49,10 @@ Bulletin/DotNS. The runtime hooks now sit behind typed ports with the current viem implementation and an experimental Product CDM/PAPI adapter boundary. The backend key-delivery protocol now has an explicit Product sr25519 signature scheme that binds the Product account public key to the derived H160 -requester before access checks. The shipped Product frontend still uses the -passkey/EVM path for protected playback until host-signed key/session requests -are wired; contract writes also remain passkey/EVM until CDM-installed runtime -packages and host-signed transaction evidence are proven. See +requester before access checks. The Product frontend can now submit that +Product proof after explicit host-account connection; contract writes remain +passkey/EVM until CDM-installed runtime packages and host-signed transaction +evidence are proven. See [`docs/explanation/product-devnet-architecture.md`](docs/explanation/product-devnet-architecture.md) and the [`Product roadmap`](docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md). diff --git a/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md b/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md index 9b742cd..3129299 100644 --- a/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md +++ b/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md @@ -171,10 +171,13 @@ Goal: deepen the delivered Product mode one adapter at a time. signature scheme for key delivery and session sign-in. It binds the Product account public key to the derived H160 requester before nonce consumption and access checks. +- Delivered on the next follow-up branch: wire Product-host frontend key and + session requests to that signature scheme, while keeping contract writes on + the standalone EVM/passkey signer path. - Next: wire a generated CDM manifest/types into Product mode and run real host transaction smoke tests. -- Next: wire frontend Product-host key/session requests to the API signature - scheme and run real host smoke tests. +- Next: run real Product host smoke tests for protected playback and capture the + Product sr25519 request evidence. - Keep backend key delivery authoritative unless a Product-host design proves a stronger key-custody boundary. - Keep `.dot`/Playground deployment separate from access enforcement. diff --git a/docs/context/dotify-technical-memory.md b/docs/context/dotify-technical-memory.md index 5f41deb..11d139b 100644 --- a/docs/context/dotify-technical-memory.md +++ b/docs/context/dotify-technical-memory.md @@ -58,8 +58,8 @@ Wallet-gated onboarding, runtime creation, upload, encryption, IPFS publication, track. The current verified Product SDK snapshot is prototype/reference/ unaudited, Host APIs require a compatible container, contracts target `pallet-revive` / PolkaVM CDM flows, Product sr25519 key/session signatures - are API-supported but not yet wired into the shipped Product frontend, and - Statement Store is constrained to small signed ephemeral data. + are wired for Product frontend protected playback after explicit host-account + connection, and Statement Store is constrained to small signed ephemeral data. ## Production spine diff --git a/docs/explanation/product-devnet-architecture.md b/docs/explanation/product-devnet-architecture.md index 2ea8121..3f86482 100644 --- a/docs/explanation/product-devnet-architecture.md +++ b/docs/explanation/product-devnet-architecture.md @@ -174,7 +174,10 @@ use `product-sr25519-v1` after signing the same canonical Dotify message bytes with the app-scoped Product account; the server binds the signature to the Product public key, derived H160 requester, chain, nonce, purpose, and expiry before consuming the nonce or running access checks. Unknown schemes fail -closed. The shipped Product frontend does not yet send this Product proof shape. +closed. The Product frontend now sends this proof shape after explicit +Product-host account connection; real Host smoke evidence is still required +for each Product publication before gated listening is treated as +production-ready. This avoids a second frontend business model and allows Product mode to replace one infrastructure adapter at a time. @@ -187,8 +190,9 @@ one infrastructure adapter at a time. permission, or personhood proof before value is visible. 3. If the host is absent, catalog browsing, Free playback, and room links still work. The wallet modal explains why the Product account is unavailable. -4. A Product account without an EVM signing adapter is not a protected - listener. Dotify passes no requester address to the key service. +4. A Product account without an EVM signing adapter can request protected keys + only through `product-sr25519-v1`; contract writes still require a + passkey/EVM signer until Product CDM transaction evidence lands. 5. A denied key, RPC failure, or unsupported signature never falls back to a browser content secret. 6. The Product SDK and deploy tooling are prototype/reference dependencies. diff --git a/docs/index.html b/docs/index.html index ae701bb..aeeade6 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1543,9 +1543,10 @@

    Product DevNet now, sovereignty adapters next

    Dotify now has a publishable dotify-test01.dot build, explicit app-scoped Product identity, public room links that preserve wallet-free entry, typed runtime ports around the current viem implementation, an experimental CDM/PAPI adapter boundary, and API-side Product - sr25519 verification for protected key/session requests. Product contract writes, frontend - host-signed protected playback, Humanity/Individuality proofs, consented provenance, and - ambassador mechanics remain sequenced behind verified security and privacy boundaries. + sr25519 verification with frontend Product proof submission for protected key/session requests. + Product contract writes, real Host smoke evidence for gated playback, Humanity/Individuality + proofs, consented provenance, and ambassador mechanics remain sequenced behind verified security + and privacy boundaries.

    diff --git a/docs/operations/deployment-configuration.md b/docs/operations/deployment-configuration.md index 63f1e2f..f4d7973 100644 --- a/docs/operations/deployment-configuration.md +++ b/docs/operations/deployment-configuration.md @@ -222,9 +222,12 @@ accepts two explicit schemes on session sign-in and protected key requests: Unknown schemes fail at the API schema boundary. Product requests must still pass the same nonce, chain, purpose, expiry, and `musicAccCanAccess` checks as -standalone requests. The shipped Product frontend does not yet submit this -Product proof shape; when it does, validate it through Product host smoke tests -before treating Product identity as a protected-playback account. +standalone requests. The Product frontend submits this proof shape only after +an explicit Product-host account connection; contract writes remain on the +standalone EVM/passkey signer path until the Product CDM transaction adapter has +real host-signed transaction evidence. Validate Product protected playback +through host smoke tests after each Product publication before treating Product +identity as production-ready for gated listening. ## Fly Signaling diff --git a/docs/operations/product-devnet-deployment.md b/docs/operations/product-devnet-deployment.md index d77c3f4..f558670 100644 --- a/docs/operations/product-devnet-deployment.md +++ b/docs/operations/product-devnet-deployment.md @@ -171,9 +171,10 @@ active. ## Known Limits - Product account signing is accepted by the API only through the explicit - `product-sr25519-v1` session/key-request scheme. The shipped Product UI has - not yet been wired to submit that proof shape, so protected playback still - uses passkey/EVM signing in the current frontend. + `product-sr25519-v1` session/key-request scheme. The Product UI now submits + that proof shape after an explicit host-account connection, but each published + Product build still needs real Host smoke evidence before gated playback is + considered production-ready on Product DevNet. - Contract writes still require passkey/EVM signing in the shipped UI. The experimental Product CDM/PAPI runtime adapter is present in code, but it is not selected until Dotify has CDM-installed runtime packages, generated diff --git a/web/src/app/providers/WalletProvider.tsx b/web/src/app/providers/WalletProvider.tsx index 9be0100..1d08dfb 100644 --- a/web/src/app/providers/WalletProvider.tsx +++ b/web/src/app/providers/WalletProvider.tsx @@ -50,6 +50,10 @@ type WalletContextValue = { const WalletContext = createContext(null); +function canRequestProtectedPlayback(wallet: ConnectedWallet | null): boolean { + return Boolean(wallet?.createEvmClient || wallet?.keyRequestSigner); +} + export function WalletProvider({ children }: { children: ReactNode }) { const { setTransactionFeedback, setShowWalletModal } = useUiFeedback(); const { @@ -76,7 +80,7 @@ export function WalletProvider({ children }: { children: ReactNode }) { // Disconnecting the wallet also signs out of the Dotify session (ticket 24 // P2): revoke the server-side token and forget the stored one, so a shared // machine does not keep listening rights after the wallet leaves. - const connectedAddress = connectedWallet?.createEvmClient ? connectedWallet.evmAddress : undefined; + const connectedAddress = canRequestProtectedPlayback(connectedWallet) ? connectedWallet?.evmAddress : undefined; const lastConnectedAddressRef = useRef<`0x${string}` | null>(null); const disconnect = useCallback(() => { if (connectedAddress) void signOutOfDotifySession(connectedAddress); @@ -96,7 +100,7 @@ export function WalletProvider({ children }: { children: ReactNode }) { const currentBulletinAccount = devAccounts[bulletinAccountIndex]; const activeEvmAddress = connectedWallet?.evmAddress ?? zeroAddress; - const listenerEvmAddress = connectedWallet?.createEvmClient ? connectedWallet.evmAddress : null; + const listenerEvmAddress = canRequestProtectedPlayback(connectedWallet) ? connectedWallet?.evmAddress ?? null : null; // Local room-name persistence lowercases its key, so use the H160 identity // for both EVM wallets and Product accounts rather than case-sensitive SS58. const activeIdentityAddress = connectedWallet?.evmAddress ?? null; diff --git a/web/src/features/productHost/productHost.test.ts b/web/src/features/productHost/productHost.test.ts index e6c6474..051f8ee 100644 --- a/web/src/features/productHost/productHost.test.ts +++ b/web/src/features/productHost/productHost.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'vitest'; -import { probeProductHost, resolveProductHostConfig } from './productHost'; +import { describe, expect, it, vi } from 'vitest'; +import { connectProductHostIdentity, probeProductHost, resolveProductHostConfig } from './productHost'; describe('resolveProductHostConfig', () => { it('keeps ordinary browser builds independent from the Product host', () => { @@ -51,3 +51,41 @@ describe('probeProductHost', () => { ).resolves.toBe('unavailable'); }); }); + +describe('connectProductHostIdentity', () => { + it('exposes the Product account identity and message signer', async () => { + const publicKey = new Uint8Array(32).fill(0x22); + const signature = new Uint8Array(64).fill(0x33); + const signBytes = vi.fn(async () => signature); + const account = { + dotNsIdentifier: 'dotify-test01.dot', + derivationIndex: 0, + publicKey + }; + const provider = { + getProductAccount: vi.fn(() => ({ + match: async (onOk: (value: typeof account) => T) => onOk(account) + })), + getProductAccountSigner: vi.fn(() => ({ signBytes })) + }; + + const identity = await connectProductHostIdentity( + { mode: 'required', productId: 'dotify-test01.dot' }, + { + getAccountsProvider: async () => provider, + deriveH160: () => '0x1111111111111111111111111111111111111111', + ss58Encode: () => '5ProductAccount' + } + ); + + await expect(identity.signMessage('Dotify sign-in')).resolves.toBe(`0x${'33'.repeat(64)}`); + expect(identity).toMatchObject({ + evmAddress: '0x1111111111111111111111111111111111111111', + substrateAddress: '5ProductAccount', + productPublicKey: `0x${'22'.repeat(32)}` + }); + expect(provider.getProductAccount).toHaveBeenCalledWith('dotify-test01.dot', 0); + expect(provider.getProductAccountSigner).toHaveBeenCalledWith(account); + expect(signBytes).toHaveBeenCalledWith(new TextEncoder().encode('Dotify sign-in')); + }); +}); diff --git a/web/src/features/productHost/productHost.ts b/web/src/features/productHost/productHost.ts index e154261..3e23a4c 100644 --- a/web/src/features/productHost/productHost.ts +++ b/web/src/features/productHost/productHost.ts @@ -1,3 +1,5 @@ +import { bytesToHex } from '@polkadot-apps/utils'; + export type ProductHostMode = 'off' | 'auto' | 'required'; export type ProductHostStatus = 'off' | 'checking' | 'available' | 'unavailable'; @@ -9,9 +11,31 @@ export type ProductHostConfig = { export type ProductHostIdentity = { evmAddress: `0x${string}`; substrateAddress: string; + productPublicKey: `0x${string}`; + signMessage: (message: string) => Promise<`0x${string}`>; }; type EnvironmentLike = Record; +type ProductAccount = { + dotNsIdentifier: string; + derivationIndex: number; + publicKey: Uint8Array; +}; +type ProductAccountResult = { + match: (onOk: (value: ProductAccount) => T, onErr: (error: unknown) => E) => Promise; +}; +type ProductAccountSigner = { + signBytes: (data: Uint8Array) => Promise; +}; +type ProductAccountsProvider = { + getProductAccount: (dotNsIdentifier: string, derivationIndex?: number) => ProductAccountResult; + getProductAccountSigner: (account: ProductAccount) => ProductAccountSigner; +}; +type ProductHostIdentityDeps = { + getAccountsProvider: () => Promise; + deriveH160: (publicKey: Uint8Array) => `0x${string}`; + ss58Encode: (publicKey: Uint8Array) => string; +}; function envValue(env: EnvironmentLike, key: string): string { const value = env[key]; @@ -43,6 +67,11 @@ export async function probeProductHost(mode: ProductHostMode, probe: () => Promi } } +async function loadProductHostIdentityDeps(): Promise { + const [{ getAccountsProvider }, { deriveH160, ss58Encode }] = await Promise.all([import('@parity/product-sdk/host'), import('@parity/product-sdk/address')]); + return { getAccountsProvider, deriveH160, ss58Encode }; +} + function describeHostError(error: unknown): string { if (error instanceof Error) return error.message; if (typeof error === 'object' && error !== null && 'reason' in error) { @@ -51,12 +80,12 @@ function describeHostError(error: unknown): string { return String(error); } -export async function connectProductHostIdentity(config: ProductHostConfig): Promise { +export async function connectProductHostIdentity(config: ProductHostConfig, deps?: ProductHostIdentityDeps): Promise { if (config.mode === 'off') { throw new Error('This Dotify build does not use the Polkadot Product host.'); } - const [{ getAccountsProvider }, { deriveH160, ss58Encode }] = await Promise.all([import('@parity/product-sdk/host'), import('@parity/product-sdk/address')]); + const { getAccountsProvider, deriveH160, ss58Encode } = deps ?? (await loadProductHostIdentityDeps()); const provider = await getAccountsProvider(); if (!provider) { throw new Error('Open this build inside the Polkadot Product host, then try again.'); @@ -68,9 +97,15 @@ export async function connectProductHostIdentity(config: ProductHostConfig): Pro throw new Error(`The Product host could not provide the Dotify account: ${describeHostError(error)}`); } ); + const signer = provider.getProductAccountSigner(account); return { substrateAddress: ss58Encode(account.publicKey), - evmAddress: deriveH160(account.publicKey) + evmAddress: deriveH160(account.publicKey), + productPublicKey: `0x${bytesToHex(account.publicKey)}` as `0x${string}`, + signMessage: async message => { + const signature = await signer.signBytes(new TextEncoder().encode(message)); + return `0x${bytesToHex(signature)}` as `0x${string}`; + } }; } diff --git a/web/src/hooks/useCatalog.ts b/web/src/hooks/useCatalog.ts index 5fed5e8..2ae26af 100644 --- a/web/src/hooks/useCatalog.ts +++ b/web/src/hooks/useCatalog.ts @@ -472,15 +472,15 @@ export function useCatalog(deps: UseCatalogDeps) { const cacheKey = contentHash.toLowerCase(); const cached = contentKeysRef.current.get(cacheKey); if (cached) return cached; - if (!isKeyServiceConfigured() || !connectedWallet?.createEvmClient) return null; + if (!isKeyServiceConfigured() || !connectedWallet || (!connectedWallet.createEvmClient && !connectedWallet.keyRequestSigner)) return null; try { - const walletClient = await getActiveWalletClient(); - const chainId = walletClient.chain?.id ?? (await getPublicClient(ethRpcUrl).getChainId()); + const walletClient = connectedWallet.keyRequestSigner ? null : await getActiveWalletClient(); + const chainId = walletClient?.chain?.id ?? (await getPublicClient(ethRpcUrl).getChainId()); const response = await requestContentKey({ contentHash, purpose: keyRequestPurposeRef.current, - walletClient, + ...(connectedWallet.keyRequestSigner ? { signer: connectedWallet.keyRequestSigner } : { walletClient: walletClient! }), chainId }); if (response.access !== 'allowed') return null; diff --git a/web/src/hooks/useWallet.ts b/web/src/hooks/useWallet.ts index 0de2e53..54dc9a3 100644 --- a/web/src/hooks/useWallet.ts +++ b/web/src/hooks/useWallet.ts @@ -28,6 +28,7 @@ import { import { connectProductHostIdentity, probeProductHost, resolveProductHostConfig, type ProductHostStatus } from '../features/productHost/productHost'; import { isRoomJoinE2eContext } from '../e2e/roomJoinMock'; import { getProviderErrorCode, parseChainId, toEip155ChainId } from '../features/wallet/network'; +import { PRODUCT_SR25519_SIGNATURE_SCHEME, type KeyRequestSigner } from '../services/keyService'; // ── Constants ──────────────────────────────────────────────────────────────── @@ -53,6 +54,8 @@ export type ConnectedWallet = { evmAddress: `0x${string}`; /** EIP-1193 chain id when the connected wallet reports one */ chainId?: number; + /** Optional identity signer for backend key/session requests. Product-host accounts use this without gaining EVM tx authority. */ + keyRequestSigner?: KeyRequestSigner; /** Build the right viem WalletClient for this connection type */ createEvmClient?: (chain: Chain, rpcUrl: string) => WalletClient; }; @@ -326,7 +329,13 @@ export function useWallet() { method: 'product-host', label: 'Polkadot app', substrateAddress: identity.substrateAddress, - evmAddress: identity.evmAddress + evmAddress: identity.evmAddress, + keyRequestSigner: { + signatureScheme: PRODUCT_SR25519_SIGNATURE_SCHEME, + address: identity.evmAddress, + productPublicKey: identity.productPublicKey, + signMessage: identity.signMessage + } } }); } catch (error) { diff --git a/web/src/services/keyService.test.ts b/web/src/services/keyService.test.ts index 543cccf..aac9001 100644 --- a/web/src/services/keyService.test.ts +++ b/web/src/services/keyService.test.ts @@ -5,6 +5,8 @@ const ADDRESS = '0x1111111111111111111111111111111111111111' as const; const CONTENT_HASH = `0x${'ab'.repeat(32)}` as const; const CONTENT_KEY = `0x${'cd'.repeat(32)}` as const; const RUNTIME = '0x2222222222222222222222222222222222222222' as const; +const PRODUCT_PUBLIC_KEY = `0x${'22'.repeat(32)}` as const; +const PRODUCT_SIGNATURE = `0x${'33'.repeat(64)}` as const; function jsonResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { @@ -44,6 +46,15 @@ function walletClient(signMessage = vi.fn(async () => `0x${'11'.repeat(65)}`)): return { account: { address: ADDRESS }, signMessage } as unknown as WalletClient; } +function productSigner(signMessage = vi.fn(async () => PRODUCT_SIGNATURE)) { + return { + signatureScheme: 'product-sr25519-v1' as const, + address: ADDRESS, + productPublicKey: PRODUCT_PUBLIC_KEY, + signMessage + }; +} + function keyRequestResponse() { return jsonResponse({ access: 'allowed', @@ -147,4 +158,82 @@ describe('keyService sessions', () => { expect(signMessage).toHaveBeenCalledTimes(1); expect(fetchMock).not.toHaveBeenCalledWith('https://api.test/api/auth/session', expect.objectContaining({ method: 'POST' })); }); + + it('opens a Product-signed session and then requests the key with the session token', async () => { + installLocalStorage(); + const signMessage = vi.fn(async (message: string) => { + expect(message).toContain('Action: SIGN_IN'); + return PRODUCT_SIGNATURE; + }); + const sessionExpiresAt = new Date(Date.now() + 3_600_000).toISOString(); + const fetchMock = vi.fn(async (url: string, init?: RequestInit) => { + if (url === 'https://api.test/api/auth/session' && init?.method === 'GET') return jsonResponse({ available: true }); + if (url === 'https://api.test/api/auth/nonce') { + return jsonResponse({ nonce: 'c'.repeat(48), expiresAt: new Date(Date.now() + 60_000).toISOString() }); + } + if (url === 'https://api.test/api/auth/session' && init?.method === 'POST') { + expect(JSON.parse(String(init.body))).toMatchObject({ + address: ADDRESS, + signature: PRODUCT_SIGNATURE, + signatureScheme: 'product-sr25519-v1', + productPublicKey: PRODUCT_PUBLIC_KEY + }); + return jsonResponse({ sessionToken: 'product-session-token', expiresAt: sessionExpiresAt }); + } + if (url === `https://api.test/api/tracks/${CONTENT_HASH}/key-request`) { + expect(JSON.parse(String(init?.body))).toEqual({ sessionToken: 'product-session-token', purpose: 'room_host' }); + return keyRequestResponse(); + } + throw new Error(`Unexpected request: ${init?.method ?? 'GET'} ${url}`); + }); + vi.stubGlobal('fetch', fetchMock); + const { requestContentKey } = await loadKeyService(); + + const response = await requestContentKey({ + contentHash: CONTENT_HASH, + purpose: 'room_host', + signer: productSigner(signMessage), + chainId: 420420417 + }); + + expect(response.access).toBe('allowed'); + expect(signMessage).toHaveBeenCalledTimes(1); + }); + + it('submits Product signature fields on the per-request fallback path', async () => { + installLocalStorage(); + const signMessage = vi.fn(async (message: string) => { + expect(message).toContain('Action: REQUEST_CONTENT_KEY'); + expect(message).toContain(`Requester: ${ADDRESS}`); + return PRODUCT_SIGNATURE; + }); + const fetchMock = vi.fn(async (url: string, init?: RequestInit) => { + if (url === 'https://api.test/api/auth/session' && init?.method === 'GET') return jsonResponse({ error: 'not found' }, 404); + if (url === 'https://api.test/api/auth/nonce') { + return jsonResponse({ nonce: 'd'.repeat(48), expiresAt: new Date(Date.now() + 60_000).toISOString() }); + } + if (url === `https://api.test/api/tracks/${CONTENT_HASH}/key-request`) { + expect(JSON.parse(String(init?.body))).toMatchObject({ + requester: ADDRESS, + signature: PRODUCT_SIGNATURE, + signatureScheme: 'product-sr25519-v1', + productPublicKey: PRODUCT_PUBLIC_KEY, + purpose: 'individual' + }); + return keyRequestResponse(); + } + throw new Error(`Unexpected request: ${init?.method ?? 'GET'} ${url}`); + }); + vi.stubGlobal('fetch', fetchMock); + const { requestContentKey } = await loadKeyService(); + + await requestContentKey({ + contentHash: CONTENT_HASH, + purpose: 'individual', + signer: productSigner(signMessage), + chainId: 420420417 + }); + + expect(signMessage).toHaveBeenCalledTimes(1); + }); }); diff --git a/web/src/services/keyService.ts b/web/src/services/keyService.ts index 11af85e..888a1b9 100644 --- a/web/src/services/keyService.ts +++ b/web/src/services/keyService.ts @@ -1,8 +1,8 @@ // Wallet-signed content-key client (Sprint 0, Ticket 03). // -// Flow: request a single-use nonce, sign a structured EIP-191 message with -// the connected wallet, exchange the signature for the per-track content key. -// The backend independently re-checks the on-chain access policy; nothing the +// Flow: request a single-use nonce, sign a structured Dotify message with the +// connected identity, exchange the signature for the per-track content key. The +// backend independently re-checks the on-chain access policy; nothing the // frontend sends is trusted as an access decision. // // The canonical message format below MUST stay byte-identical with the @@ -14,6 +14,7 @@ import type { WalletClient } from 'viem'; const API_URL = (import.meta.env.VITE_DOTIFY_API_URL as string | undefined)?.replace(/\/$/, ''); export type KeyRequestPurpose = 'individual' | 'room_host'; +export const PRODUCT_SR25519_SIGNATURE_SCHEME = 'product-sr25519-v1'; // Access model v2 (ticket 24 P1): a denial names the reason and the action the // listener can take. There is no degraded playback mode - the preview doctrine @@ -47,6 +48,23 @@ export function isKeyServiceConfigured(): boolean { return Boolean(API_URL); } +export type DotifySignatureHex = `0x${string}`; + +export type Eip191KeyRequestSigner = { + signatureScheme?: 'eip191'; + address: `0x${string}`; + signMessage: (message: string) => Promise; +}; + +export type ProductKeyRequestSigner = { + signatureScheme: typeof PRODUCT_SR25519_SIGNATURE_SCHEME; + address: `0x${string}`; + productPublicKey: `0x${string}`; + signMessage: (message: string) => Promise; +}; + +export type KeyRequestSigner = Eip191KeyRequestSigner | ProductKeyRequestSigner; + type SignedRequestPayload = { action: 'REQUEST_CONTENT_KEY'; purpose: KeyRequestPurpose; @@ -96,7 +114,8 @@ async function requestNonce(address: string, chainId: number): Promise<{ nonce: export type ContentKeyRequest = { contentHash: `0x${string}`; purpose: KeyRequestPurpose; - walletClient: WalletClient; + walletClient?: WalletClient; + signer?: KeyRequestSigner; chainId: number; }; @@ -146,6 +165,35 @@ function storeSession(address: string, session: StoredSession): void { } } +function toWalletSigner(walletClient: WalletClient): KeyRequestSigner | null { + const account = walletClient.account; + if (!account) return null; + + return { + address: account.address, + signMessage: message => walletClient.signMessage({ account, message }) + }; +} + +function resolveRequestSigner(request: ContentKeyRequest): KeyRequestSigner | null { + if (request.signer) return request.signer; + if (request.walletClient) return toWalletSigner(request.walletClient); + return null; +} + +type ProductSignatureRequestFields = { + signatureScheme: typeof PRODUCT_SR25519_SIGNATURE_SCHEME; + productPublicKey: `0x${string}`; +}; + +function productSignatureFields(signer: KeyRequestSigner): Partial { + if (signer.signatureScheme !== PRODUCT_SR25519_SIGNATURE_SCHEME) return {}; + return { + signatureScheme: PRODUCT_SR25519_SIGNATURE_SCHEME, + productPublicKey: signer.productPublicKey + }; +} + export function clearStoredSession(address: string): void { try { window.localStorage.removeItem(sessionStorageKey(address)); @@ -195,25 +243,27 @@ function buildSignInMessage(payload: { requester: string; chainId: number; nonce * Returns null when the backend does not support sessions (older deployment * or unconfigured), so callers fall back to per-request signing. */ -export async function ensureDotifySession(walletClient: WalletClient, chainId: number): Promise { +async function ensureDotifySessionForSigner(signer: KeyRequestSigner, chainId: number): Promise { if (!API_URL) return null; - const account = walletClient.account; - if (!account) return null; - const stored = getStoredSession(account.address); + const stored = getStoredSession(signer.address); if (stored) return stored.token; if (!(await isDotifySessionAvailable())) return null; - const { nonce, expiresAt } = await requestNonce(account.address, chainId); - const signature = await walletClient.signMessage({ - account, - message: buildSignInMessage({ requester: account.address, chainId, nonce, expiresAt }) - }); + const { nonce, expiresAt } = await requestNonce(signer.address, chainId); + const signature = await signer.signMessage(buildSignInMessage({ requester: signer.address, chainId, nonce, expiresAt })); const res = await fetch(`${API_URL}/api/auth/session`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ address: account.address, signature, nonce, chainId, expiresAt }) + body: JSON.stringify({ + address: signer.address, + signature, + nonce, + chainId, + expiresAt, + ...productSignatureFields(signer) + }) }); // 404 (older backend) or 503 (session auth unconfigured): fall back to the @@ -228,10 +278,15 @@ export async function ensureDotifySession(walletClient: WalletClient, chainId: n } const body = (await res.json()) as { sessionToken: string; expiresAt: string }; - storeSession(account.address, { token: body.sessionToken, expiresAt: body.expiresAt }); + storeSession(signer.address, { token: body.sessionToken, expiresAt: body.expiresAt }); return body.sessionToken; } +export async function ensureDotifySession(walletClient: WalletClient, chainId: number): Promise { + const signer = toWalletSigner(walletClient); + return signer ? ensureDotifySessionForSigner(signer, chainId) : null; +} + /** Sign out: revoke the session server-side and forget the stored token. */ export async function signOutOfDotifySession(address: string): Promise { const stored = readStoredSession(address, { requireFresh: false }); @@ -267,18 +322,18 @@ export async function requestContentKey(request: ContentKeyRequest): Promise Date: Wed, 29 Jul 2026 02:48:05 +0200 Subject: [PATCH 07/22] fix: accept Product host signing envelopes and reject EVM-derived keys The Host signRaw wire format is not pinned by the SDK: HostSignPayloadResponse carries an untagged signature, and a Substrate host may sign a raw payload verbatim or inside the conventional envelope. Verification assumed one shape, so a wrong guess would have failed every Product key request with an error indistinguishable from a wrong signer. Accept a bounded set instead: the canonical message verbatim or -wrapped, and a bare 64-byte or MultiSignature-tagged 65-byte sr25519 signature. Every variant carries the identical domain-bound message, so this adds no replay, cross-app, cross-chain, or cross-track surface; a non-sr25519 tag still fails closed. Route schemas widen to 128 or 130 hex so the tag is checked by the verifier rather than rejected before it. Reject EVM-derived account ids for product-sr25519-v1. A 20-byte H160 padded with 0xee derives back to the H160 it contains, so accepting that shape let a caller name any paying EVM listener as the requester and rested the boundary on the curve check alone. A real Product account is a native AccountId32. A key that parses and derives to the requester but verifies under no variant now returns PRODUCT_SIGNATURE_REJECTED, kept distinct from SIGNATURE_INVALID so an envelope problem is separable from a wrong-account problem in logs. Pin @scure/sr25519 exactly, matching @noble/hashes. Co-Authored-By: Claude Opus 5 (1M context) --- services/api/package-lock.json | 2 +- services/api/package.json | 2 +- services/api/src/routes/auth.ts | 4 +- services/api/src/routes/keys.test.ts | 26 +++++ services/api/src/routes/keys.ts | 4 +- services/api/src/services/signatures.test.ts | 98 +++++++++++++++++- services/api/src/services/signatures.ts | 101 ++++++++++++++++--- 7 files changed, 216 insertions(+), 21 deletions(-) diff --git a/services/api/package-lock.json b/services/api/package-lock.json index 7fa0eb8..25f9f9b 100644 --- a/services/api/package-lock.json +++ b/services/api/package-lock.json @@ -12,7 +12,7 @@ "@fastify/multipart": "^10.0.0", "@fastify/rate-limit": "^10.3.0", "@noble/hashes": "1.8.0", - "@scure/sr25519": "^1.0.0", + "@scure/sr25519": "1.0.0", "fastify": "^5.8.5", "viem": "^2.52.2", "zod": "^3.23.8" diff --git a/services/api/package.json b/services/api/package.json index 01515c5..4fa05fc 100644 --- a/services/api/package.json +++ b/services/api/package.json @@ -19,7 +19,7 @@ "@fastify/multipart": "^10.0.0", "@fastify/rate-limit": "^10.3.0", "@noble/hashes": "1.8.0", - "@scure/sr25519": "^1.0.0", + "@scure/sr25519": "1.0.0", "fastify": "^5.8.5", "viem": "^2.52.2", "zod": "^3.23.8" diff --git a/services/api/src/routes/auth.ts b/services/api/src/routes/auth.ts index 07839bb..9089c1e 100644 --- a/services/api/src/routes/auth.ts +++ b/services/api/src/routes/auth.ts @@ -39,9 +39,11 @@ const eip191SessionRequestSchema = sessionBaseRequestSchema.extend({ signature: z.string().regex(/^0x[0-9a-fA-F]+$/, 'Invalid signature') }); +// 128 hex = bare 64-byte sr25519; 130 hex = MultiSignature-tagged 65-byte +// value. The tag itself is validated in verifySignInRequest, not here. const productSr25519SessionRequestSchema = sessionBaseRequestSchema.extend({ signatureScheme: z.literal(PRODUCT_SR25519_SIGNATURE_SCHEME), - signature: z.string().regex(/^0x[0-9a-fA-F]{128}$/, 'Invalid Product sr25519 signature'), + signature: z.string().regex(/^0x([0-9a-fA-F]{128}|[0-9a-fA-F]{130})$/, 'Invalid Product sr25519 signature'), productPublicKey: z.string().regex(/^0x[0-9a-fA-F]{64}$/, 'Invalid Product account public key') }); diff --git a/services/api/src/routes/keys.test.ts b/services/api/src/routes/keys.test.ts index 23abf04..95c8fc4 100644 --- a/services/api/src/routes/keys.test.ts +++ b/services/api/src/routes/keys.test.ts @@ -138,6 +138,32 @@ describe('POST /api/tracks/:contentHash/key-request', () => { assert.equal(accessChecked, false); }); + it('forwards a MultiSignature-tagged Product signature to verification', async () => { + // 65-byte tagged signatures are a legitimate Substrate signRaw shape; the + // route must not reject them at the schema before the verifier can check + // the tag. + let verifiedRequest: KeySignatureRequest | null = null; + const server = await buildApp({ + verifySignedRequest: async request => { + verifiedRequest = request; + return { valid: true }; + } + }); + const signature = `0x01${'33'.repeat(64)}`; + const response = await server.inject({ + method: 'POST', + url: `/api/tracks/${CONTENT_HASH}/key-request`, + payload: baseBody({ + signatureScheme: PRODUCT_SR25519_SIGNATURE_SCHEME, + productPublicKey: `0x${'22'.repeat(32)}`, + signature + }) + }); + + assert.equal(response.statusCode, 200); + assert.equal((verifiedRequest as KeySignatureRequest | null)?.signature, signature); + }); + it('requires Product public key for Product sr25519 requests', async () => { let verificationCalled = false; const server = await buildApp({ diff --git a/services/api/src/routes/keys.ts b/services/api/src/routes/keys.ts index fde1bdc..79a5677 100644 --- a/services/api/src/routes/keys.ts +++ b/services/api/src/routes/keys.ts @@ -51,9 +51,11 @@ const eip191KeyRequestBodySchema = signedBaseBodySchema.merge(keyRequestPurposeS signature: z.string().regex(/^0x[0-9a-fA-F]+$/, 'Invalid signature') }); +// 128 hex = bare 64-byte sr25519; 130 hex = MultiSignature-tagged 65-byte +// value. The tag itself is validated in verifySignedRequest, not here. const productSr25519KeyRequestBodySchema = signedBaseBodySchema.merge(keyRequestPurposeSchema).extend({ signatureScheme: z.literal(PRODUCT_SR25519_SIGNATURE_SCHEME), - signature: z.string().regex(/^0x[0-9a-fA-F]{128}$/, 'Invalid Product sr25519 signature'), + signature: z.string().regex(/^0x([0-9a-fA-F]{128}|[0-9a-fA-F]{130})$/, 'Invalid Product sr25519 signature'), productPublicKey: z.string().regex(/^0x[0-9a-fA-F]{64}$/, 'Invalid Product account public key') }); diff --git a/services/api/src/services/signatures.test.ts b/services/api/src/services/signatures.test.ts index 197b7e2..3c8943f 100644 --- a/services/api/src/services/signatures.test.ts +++ b/services/api/src/services/signatures.test.ts @@ -46,7 +46,24 @@ async function signedPayload(overrides: Partial = {}) { return { payload, signature }; } -async function productSignedPayload(overrides: Partial = {}) { +// The Host may sign the canonical message verbatim or inside the conventional +// Substrate `` envelope, and may return the signature bare or with a +// MultiSignature tag. Tests cover every shape the verifier accepts. +type ProductEnvelope = 'raw' | 'bytes-wrapped'; +type ProductSignatureShape = 'bare' | 'multisignature'; + +function encodeProductPayload(message: string, envelope: ProductEnvelope): Uint8Array { + return new TextEncoder().encode(envelope === 'bytes-wrapped' ? `${message}` : message); +} + +function encodeProductSignature(raw: Uint8Array, shape: ProductSignatureShape): string { + return shape === 'multisignature' ? `0x01${bytesToHex(raw)}` : `0x${bytesToHex(raw)}`; +} + +async function productSignedPayload( + overrides: Partial = {}, + options: { envelope?: ProductEnvelope; shape?: ProductSignatureShape } = {}, +) { const requester = overrides.requester ?? deriveProductAccountH160(productPublicKey); const challenge = createWalletNonceChallenge({ address: requester, chainId: CHAIN_ID }); const payload: SignedRequestPayload = { @@ -59,8 +76,8 @@ async function productSignedPayload(overrides: Partial = { expiresAt: challenge.expiresAt, ...overrides }; - const signature = `0x${bytesToHex(signSr25519(productSecretKey, new TextEncoder().encode(buildSignedRequestMessage(payload))))}`; - return { payload, signature, productPublicKey: productPublicKeyHex }; + const raw = signSr25519(productSecretKey, encodeProductPayload(buildSignedRequestMessage(payload), options.envelope ?? 'raw')); + return { payload, signature: encodeProductSignature(raw, options.shape ?? 'bare'), productPublicKey: productPublicKeyHex }; } describe('verifySignedRequest', () => { @@ -103,6 +120,79 @@ describe('verifySignedRequest', () => { assert.equal(!result.valid && result.code, 'SIGNATURE_INVALID'); }); + it('accepts a Product signature made over the envelope', async () => { + const { payload, signature, productPublicKey } = await productSignedPayload({}, { envelope: 'bytes-wrapped' }); + const result = await verifySignedRequest({ + ...payload, + signatureScheme: PRODUCT_SR25519_SIGNATURE_SCHEME, + signature, + productPublicKey + }); + assert.equal(result.valid, true); + }); + + it('accepts a MultiSignature-tagged Product signature in either envelope', async () => { + for (const envelope of ['raw', 'bytes-wrapped'] as const) { + resetNonceStore(); + const { payload, signature, productPublicKey } = await productSignedPayload({}, { envelope, shape: 'multisignature' }); + assert.equal(signature.length, 2 + 130, 'expected a 65-byte tagged signature'); + const result = await verifySignedRequest({ + ...payload, + signatureScheme: PRODUCT_SR25519_SIGNATURE_SCHEME, + signature, + productPublicKey + }); + assert.equal(result.valid, true, `envelope ${envelope} should verify`); + } + }); + + it('rejects a 65-byte signature whose MultiSignature tag is not sr25519', async () => { + const { payload, signature, productPublicKey } = await productSignedPayload(); + const ed25519Tagged = `0x00${signature.slice(2)}`; + const result = await verifySignedRequest({ + ...payload, + signatureScheme: PRODUCT_SR25519_SIGNATURE_SCHEME, + signature: ed25519Tagged, + productPublicKey + }); + assert.equal(result.valid, false); + assert.equal(!result.valid && result.code, 'PRODUCT_SIGNATURE_INVALID'); + }); + + it('rejects an EVM-derived account id claiming an arbitrary requester H160', async () => { + // 20-byte H160 padded with 0xee derives straight back to that H160, so + // without this guard a caller could name any paying EVM listener. + const victim = '742d35cc6634c0532925a3b844bc9e7595f0beb0'; + const forgedKey = `0x${victim}${'ee'.repeat(12)}`; + const { payload, signature } = await productSignedPayload({ requester: `0x${victim}` }); + + const result = await verifySignedRequest({ + ...payload, + signatureScheme: PRODUCT_SR25519_SIGNATURE_SCHEME, + signature, + productPublicKey: forgedKey + }); + + assert.equal(result.valid, false); + assert.equal(!result.valid && result.code, 'PRODUCT_KEY_NOT_NATIVE'); + }); + + it('reports an envelope/account failure distinctly from a malformed request', async () => { + const { payload, productPublicKey } = await productSignedPayload(); + const wrongKey = secretFromSeed(new Uint8Array(32).fill(9)); + const signature = `0x${bytesToHex(signSr25519(wrongKey, new TextEncoder().encode(buildSignedRequestMessage(payload))))}`; + + const result = await verifySignedRequest({ + ...payload, + signatureScheme: PRODUCT_SR25519_SIGNATURE_SCHEME, + signature, + productPublicKey + }); + + assert.equal(result.valid, false); + assert.equal(!result.valid && result.code, 'PRODUCT_SIGNATURE_REJECTED'); + }); + it('rejects a Product signature when the payload changes', async () => { const { payload, signature, productPublicKey } = await productSignedPayload(); const result = await verifySignedRequest({ @@ -113,7 +203,7 @@ describe('verifySignedRequest', () => { productPublicKey }); assert.equal(result.valid, false); - assert.equal(!result.valid && result.code, 'SIGNATURE_INVALID'); + assert.equal(!result.valid && result.code, 'PRODUCT_SIGNATURE_REJECTED'); }); it('rejects a Product public key that does not derive to the requester H160', async () => { diff --git a/services/api/src/services/signatures.ts b/services/api/src/services/signatures.ts index fac3749..a34901b 100644 --- a/services/api/src/services/signatures.ts +++ b/services/api/src/services/signatures.ts @@ -13,6 +13,14 @@ // public key derives to the requester H160 used by runtime access checks. // The message is structured and domain-bound, so it cannot be replayed // against another app, chain, purpose, or track. +// +// The Product Host `signRaw` wire format is not pinned by the SDK: the +// response signature is untagged, and a Substrate host may sign a raw payload +// verbatim or inside the conventional `` envelope. Rather than guess +// one shape and fail every request on a wrong guess, verification accepts the +// bounded set of shapes below. Each still carries the identical domain-bound +// message, so tolerance costs no security - it only removes an unverifiable +// assumption. See docs/explanation/product-devnet-architecture.md. import { keccak_256 } from '@noble/hashes/sha3'; import { verify as verifySr25519Signature } from '@scure/sr25519'; @@ -33,6 +41,8 @@ const PRODUCT_PUBLIC_KEY_BYTES = 32; const PRODUCT_SR25519_SIGNATURE_BYTES = 64; const H160_BYTES = 20; const EVM_DERIVED_MARKER = 0xee; +// Substrate MultiSignature enum tag for sr25519 (0 = ed25519, 1 = sr25519). +const MULTISIGNATURE_SR25519_TAG = 0x01; export type NonceChallengeRequest = { address: string; @@ -130,6 +140,54 @@ function fixedHexToBytes(hex: string, expectedBytes: number): Uint8Array { return new Uint8Array(Buffer.from(clean, 'hex')); } +function hexToBytes(hex: string): Uint8Array { + const clean = hex.startsWith('0x') ? hex.slice(2) : hex; + if (clean.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(clean)) { + throw new Error('Expected an even-length hex string'); + } + return new Uint8Array(Buffer.from(clean, 'hex')); +} + +/** + * The Host `signRaw` response carries an opaque signature with no scheme tag + * (truapi `HostSignPayloadResponse.signature`). Accept the two shapes a + * Substrate signer can return for sr25519 - a bare 64-byte signature, or a + * 65-byte MultiSignature-tagged value - and reject everything else. The tag is + * checked, not skipped, so an ed25519 or ECDSA signature still fails closed. + */ +function parseProductSignatureBytes(hex: string): Uint8Array { + const bytes = hexToBytes(hex); + if (bytes.length === PRODUCT_SR25519_SIGNATURE_BYTES) { + return bytes; + } + if (bytes.length === PRODUCT_SR25519_SIGNATURE_BYTES + 1 && bytes[0] === MULTISIGNATURE_SR25519_TAG) { + return bytes.slice(1); + } + throw new Error('Unsupported Product signature length'); +} + +/** + * A Substrate host may sign a raw payload either verbatim or wrapped in the + * conventional `...` envelope. Both variants carry the same + * canonical Dotify message, which is already bound to app, action, purpose, + * content hash, requester, chain, nonce, and expiry - so accepting either + * envelope adds no replay surface, it only removes a guess about host + * behaviour. Nothing outside these two shapes is accepted. + */ +function productSignedMessageVariants(message: string): Uint8Array[] { + const encoder = new TextEncoder(); + return [encoder.encode(message), encoder.encode(`${message}`)]; +} + +/** + * True when the 32-byte account id is a pallet-revive EVM-derived account + * (a 20-byte H160 padded with 0xee). Such an account is not a native + * sr25519 keypair, so it can never legitimately produce a Product signature. + */ +function isEvmDerivedAccountId(publicKey: Uint8Array): boolean { + return publicKey.slice(H160_BYTES).every(byte => byte === EVM_DERIVED_MARKER); +} + /** * Match Product SDK / pallet-revive AccountId32 -> H160 derivation: * native Substrate accounts use keccak256(publicKey), last 20 bytes; accounts @@ -140,8 +198,7 @@ export function deriveProductAccountH160(publicKey: Uint8Array): `0x${string}` { throw new Error(`Expected ${PRODUCT_PUBLIC_KEY_BYTES}-byte Product public key`); } - const evmDerived = publicKey.slice(H160_BYTES).every(byte => byte === EVM_DERIVED_MARKER); - const addressBytes = evmDerived + const addressBytes = isEvmDerivedAccountId(publicKey) ? publicKey.slice(0, H160_BYTES) : keccak_256(publicKey).slice(PRODUCT_PUBLIC_KEY_BYTES - H160_BYTES); return `0x${bytesToHex(addressBytes)}`; @@ -152,7 +209,6 @@ function verifyProductSr25519Payload(args: { message: string; signature: string; productPublicKey: string | undefined; - invalidSignatureReason: string; }): SignatureVerification { if (!args.productPublicKey) { return { @@ -166,7 +222,7 @@ function verifyProductSr25519Payload(args: { let signature: Uint8Array; try { publicKey = fixedHexToBytes(args.productPublicKey, PRODUCT_PUBLIC_KEY_BYTES); - signature = fixedHexToBytes(args.signature, PRODUCT_SR25519_SIGNATURE_BYTES); + signature = parseProductSignatureBytes(args.signature); } catch { return { valid: false, @@ -175,6 +231,18 @@ function verifyProductSr25519Payload(args: { }; } + // An EVM-derived account id would let a caller name any H160 as the + // requester and lean entirely on the curve check to stop the takeover. + // A real Product account is a native AccountId32, so reject that shape + // before deriving anything from it. + if (isEvmDerivedAccountId(publicKey)) { + return { + valid: false, + code: 'PRODUCT_KEY_NOT_NATIVE', + reason: 'Product signed requests require a native Product account key, not an EVM-derived account id.' + }; + } + const derivedRequester = deriveProductAccountH160(publicKey); if (derivedRequester.toLowerCase() !== args.requester.toLowerCase()) { return { @@ -184,15 +252,23 @@ function verifyProductSr25519Payload(args: { }; } - let signatureValid = false; - try { - signatureValid = verifySr25519Signature(new TextEncoder().encode(args.message), signature, publicKey); - } catch { - signatureValid = false; - } + const signatureValid = productSignedMessageVariants(args.message).some(payload => { + try { + return verifySr25519Signature(payload, signature, publicKey); + } catch { + return false; + } + }); if (!signatureValid) { - return { valid: false, code: 'SIGNATURE_INVALID', reason: args.invalidSignatureReason }; + // Deliberately distinct from SIGNATURE_INVALID: the key parsed and derives + // to the requester, so this is a signing-envelope or wrong-account problem, + // not a malformed request. Operators need those apart in Fly logs. + return { + valid: false, + code: 'PRODUCT_SIGNATURE_REJECTED', + reason: 'Product host signature did not verify against the Dotify request payload in any supported signing envelope.' + }; } return { valid: true }; @@ -229,8 +305,7 @@ async function verifySignatureEnvelope( requester: request.requester, message, signature: request.signature, - productPublicKey: 'productPublicKey' in request ? request.productPublicKey : undefined, - invalidSignatureReason + productPublicKey: 'productPublicKey' in request ? request.productPublicKey : undefined }); } From 65e7650e19b15c884a1f467e888168d8e61b14d2 Mon Sep 17 00:00:00 2001 From: Kevin Nzeng Essimengane Date: Wed, 29 Jul 2026 02:48:21 +0200 Subject: [PATCH 08/22] docs: correct Product protected-playback claims Wiring the Product key signatures changed the behaviour but three places still described the old one. The wallet modal told Product-host users that protected playback required an EVM signer, the runbook asked operators to confirm no key is released through the Product identity, and the architecture matrix said the shipped UI used EIP-191 or a session token - contradicted by its own prose two sections later. All three now describe what ships: a connected Product account requests protected keys through product-sr25519-v1, and paid access plus artist publishing remain on the EVM signer. This matters beyond tidiness - the stale runbook step would have had an operator sign off on a denial as correct behaviour, hiding a real signing failure. Record the signing envelope decision and the EVM-derived key rejection, and turn the runbook step into an evidence capture that names which envelope the live host actually produced. Co-Authored-By: Claude Opus 5 (1M context) --- .../product-devnet-architecture.md | 32 ++++++++++++++++++- docs/operations/product-devnet-deployment.md | 21 ++++++++++-- web/src/components/WalletModal.tsx | 8 ++--- 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/docs/explanation/product-devnet-architecture.md b/docs/explanation/product-devnet-architecture.md index 3f86482..ab0beae 100644 --- a/docs/explanation/product-devnet-architecture.md +++ b/docs/explanation/product-devnet-architecture.md @@ -84,7 +84,7 @@ cross-origin catalog reads, key requests, Socket.IO, and WebRTC signaling. | Host room | Socket.IO + WebRTC | Same | Keep until a multiparty replacement proves equivalent UX | | Product identity | Not applicable | App-scoped SS58/H160 | Host identity with explicit capability grants | | Classic payment | Passkey/EVM wallet | Passkey/EVM wallet | CDM/PAPI write adapter | -| Protected key request | EIP-191 or session token | EIP-191 or session token in shipped UI; API accepts `product-sr25519-v1` | Frontend-host signed Product key/session requests | +| Protected key request | EIP-191 or session token | `product-sr25519-v1` when a Product account is connected; EIP-191 or session token otherwise | Frontend-host signed Product key/session requests, with captured host signing evidence | | Artist publication | viem/EVM | viem/EVM | Generated CDM contract adapter | | Personhood | Current on-chain policy source | No new claim | Privacy-preserving Product proof after verification | | Static delivery | Netlify | Bulletin + DotNS | Bulletin + DotNS | @@ -179,6 +179,36 @@ Product-host account connection; real Host smoke evidence is still required for each Product publication before gated listening is treated as production-ready. +### Host Signing Envelope + +The SDK does not pin the `signRaw` wire format. `HostSignPayloadResponse` +carries an untagged signature, and a Substrate host may sign a raw payload +verbatim or inside the conventional `...` envelope. Guessing one +shape would make every Product key request fail on a wrong guess, and the +failure would be indistinguishable from a wrong signer. + +Verification therefore accepts a bounded set: + +- the canonical message verbatim, or wrapped in ``; +- a bare 64-byte sr25519 signature, or a 65-byte value carrying the + MultiSignature sr25519 tag `0x01`. + +This is not a weakening. Every accepted variant carries the identical +domain-bound message, so no new replay, cross-app, cross-chain, or cross-track +surface is created; an ed25519 or ECDSA tag is still rejected. A request whose +key parses and derives to the requester but verifies under no variant returns +`PRODUCT_SIGNATURE_REJECTED`, kept distinct from `SIGNATURE_INVALID` so +operators can separate an envelope problem from a wrong-account problem. + +`product-sr25519-v1` additionally rejects EVM-derived account ids - a 20-byte +H160 padded with `0xee`. Such a value derives straight back to the H160 it +contains, so accepting it would let a caller name any paying EVM listener as +the requester and rest the whole boundary on the curve check alone. A real +Product account is a native `AccountId32`, so that shape is refused outright. + +Once live host evidence records which envelope the host actually produces, the +accepted set can be narrowed to it. + This avoids a second frontend business model and allows Product mode to replace one infrastructure adapter at a time. diff --git a/docs/operations/product-devnet-deployment.md b/docs/operations/product-devnet-deployment.md index f558670..eaf3fad 100644 --- a/docs/operations/product-devnet-deployment.md +++ b/docs/operations/product-devnet-deployment.md @@ -143,8 +143,18 @@ Then verify in the Product host: 2. Free playback starts without connecting an account. 3. **Use Polkadot app** connects an app-scoped Product account only after the button is selected. -4. A protected track asks for a passkey/EVM wallet; it does not release a key - through the Product identity. +4. A protected track requests its key through the Product identity using + `product-sr25519-v1`. Record which happened: + - accepted, and playback starts: capture the request/response pair as the + Product signing evidence this build needs; + - denied with `PRODUCT_SIGNATURE_REJECTED`: the key and requester bound + correctly but the host signing envelope is not one this API accepts. + Capture the Fly log line and the raw host signature length before + changing anything; + - denied with any other code: treat as a normal fail-closed denial. + + In every rejected case, playback must stop and offer a passkey/EVM wallet. + No path may release a key without a verified signature. 5. A Product-origin host creates a room and copies a `https://dotify-test01.dev-dot.li/#/rooms/` link. 6. A wallet-free browser joins that link from outside the Product host. @@ -175,6 +185,13 @@ active. that proof shape after an explicit host-account connection, but each published Product build still needs real Host smoke evidence before gated playback is considered production-ready on Product DevNet. +- The Host `signRaw` wire format is not pinned by the SDK: the response + signature is untagged, and a Substrate host may sign the payload verbatim or + inside a `` envelope. The API accepts both envelopes and both a bare + 64-byte and a MultiSignature-tagged 65-byte sr25519 signature, so a correct + host signature verifies regardless of which shape it uses. Step 6.4 above + records which shape the live host actually produced - that observation is the + evidence, and until it is captured the accepted set stays deliberately wide. - Contract writes still require passkey/EVM signing in the shipped UI. The experimental Product CDM/PAPI runtime adapter is present in code, but it is not selected until Dotify has CDM-installed runtime packages, generated diff --git a/web/src/components/WalletModal.tsx b/web/src/components/WalletModal.tsx index cae1226..58416bc 100644 --- a/web/src/components/WalletModal.tsx +++ b/web/src/components/WalletModal.tsx @@ -133,8 +133,8 @@ export function WalletModal({ {wallet.method === 'product-host' && ( <>

    - Your app-scoped Polkadot identity is active for presence and rooms. Paid access, protected playback, and artist publishing still require an EVM - signer during the contract port. + Your app-scoped Polkadot identity is active for presence, rooms, and protected playback. If the host signature is rejected, protected playback + fails closed - add a passkey or EVM wallet below. Paying for access and artist publishing still require an EVM signer during the contract port.

    {hasPrfSupport && ( @@ -144,7 +144,7 @@ export function WalletModal({ {hasStoredPasskey ? 'Use passkey' : 'Create passkey'} - Enable protected and paid actions. + Enable paid access and publishing. )} @@ -154,7 +154,7 @@ export function WalletModal({ Use EVM wallet - Enable protected and paid actions. + Enable paid access and publishing.
    From 9164b2c6a38a128375addae850f7cfd55a020df7 Mon Sep 17 00:00:00 2001 From: Kevin Nzeng Essimengane Date: Wed, 29 Jul 2026 02:48:21 +0200 Subject: [PATCH 09/22] chore: bound runtime catalog reads and record CDM adapter gaps Track and split counts come from contract storage and the directory enumerates runtimes Dotify does not control, so Array.from({ length: Number(count) }) allocated before anything could reject a malformed or hostile value. Both adapters now validate counts first and throw rather than truncate, since a silent cap would present a partial catalog as complete. The catalog loader already isolates per-runtime failures, so one bad runtime degrades to a missing artist. Mark the two unverified spots in the CDM adapter that must be settled before it can be selected: waitForTransaction returns immediately where the viem writer awaits a receipt, and the payForAccess value-transfer shape is inferred rather than confirmed against generated contract types. Co-Authored-By: Claude Opus 5 (1M context) --- web/src/app/providers/WalletProvider.tsx | 2 +- .../runtime/productCdmRuntimeAdapter.ts | 37 +++++++++++++----- web/src/features/runtime/runtimePorts.ts | 21 ++++++++++ .../runtime/viemRuntimeAdapter.test.ts | 39 +++++++++++++++++++ .../features/runtime/viemRuntimeAdapter.ts | 24 +++++++----- 5 files changed, 103 insertions(+), 20 deletions(-) diff --git a/web/src/app/providers/WalletProvider.tsx b/web/src/app/providers/WalletProvider.tsx index 1d08dfb..5619848 100644 --- a/web/src/app/providers/WalletProvider.tsx +++ b/web/src/app/providers/WalletProvider.tsx @@ -100,7 +100,7 @@ export function WalletProvider({ children }: { children: ReactNode }) { const currentBulletinAccount = devAccounts[bulletinAccountIndex]; const activeEvmAddress = connectedWallet?.evmAddress ?? zeroAddress; - const listenerEvmAddress = canRequestProtectedPlayback(connectedWallet) ? connectedWallet?.evmAddress ?? null : null; + const listenerEvmAddress = canRequestProtectedPlayback(connectedWallet) ? (connectedWallet?.evmAddress ?? null) : null; // Local room-name persistence lowercases its key, so use the H160 identity // for both EVM wallets and Product accounts rather than case-sensitive SS58. const activeIdentityAddress = connectedWallet?.evmAddress ?? null; diff --git a/web/src/features/runtime/productCdmRuntimeAdapter.ts b/web/src/features/runtime/productCdmRuntimeAdapter.ts index df38821..d1384fb 100644 --- a/web/src/features/runtime/productCdmRuntimeAdapter.ts +++ b/web/src/features/runtime/productCdmRuntimeAdapter.ts @@ -1,12 +1,15 @@ import { zeroAddress, type Address, type Hash } from 'viem'; -import type { - RuntimeAccessPolicyUpdate, - RuntimeDirectoryEntry, - RuntimeReadPort, - RuntimeRoyaltyPaymentLog, - RuntimeTrackRegistration, - RuntimeTrackSnapshot, - RuntimeWritePort +import { + MAX_ROYALTY_SPLITS, + MAX_RUNTIME_TRACKS, + assertBoundedCount, + type RuntimeAccessPolicyUpdate, + type RuntimeDirectoryEntry, + type RuntimeReadPort, + type RuntimeRoyaltyPaymentLog, + type RuntimeTrackRegistration, + type RuntimeTrackSnapshot, + type RuntimeWritePort } from './runtimePorts'; import type { OnchainTrackRecord } from '../../shared/types'; @@ -166,16 +169,18 @@ export function createProductCdmRuntimeReader(deps: ProductCdmRuntimeAdapterDeps async listRuntimeTracks(runtimeAddress) { const runtime = deps.contracts.getRuntimeContract(runtimeAddress); const trackCount = toBigInt(await queryContract(runtime, 'musicRegTrackCount')); + const trackTotal = assertBoundedCount(trackCount, MAX_RUNTIME_TRACKS, `Runtime ${runtimeAddress}`); return Promise.all( - Array.from({ length: Number(trackCount) }, async (_, index): Promise => { + Array.from({ length: trackTotal }, async (_, index): Promise => { const hash = await queryContract(runtime, 'musicRegTrackHashAtIndex', [BigInt(index)]); const trackResult = await queryContract(runtime, 'musicRegGetTrack', [hash]); const record = Array.isArray(trackResult) ? trackResult[0] : trackResult; const splitCount = await queryContract(runtime, 'musicRoySplitCount', [hash]).catch(() => 0n); + const splitTotal = assertBoundedCount(toBigInt(splitCount), MAX_ROYALTY_SPLITS, `Track ${hash} royalty splits`); const royaltySplits = ( await Promise.all( - Array.from({ length: Number(splitCount) }, async (_, splitIndex) => { + Array.from({ length: splitTotal }, async (_, splitIndex) => { try { const [recipient, bps] = await queryContract<[Address, bigint | number | string]>(runtime, 'musicRoySplitAt', [hash, BigInt(splitIndex)]); return { recipient, bps: toNumber(bps) }; @@ -248,6 +253,12 @@ export function createProductCdmRuntimeWriter(deps: ProductCdmRuntimeAdapterDeps ]); }, + // UNVERIFIED: the value-transfer argument shape is inferred, not confirmed + // against @parity/product-sdk-contracts. The viem writer passes `value` as + // a sibling of `args`; this assumes the CDM handle takes it as a trailing + // options object. Confirm against generated contract types before this + // adapter is selected - a wrong shape sends a zero-value call, which the + // runtime would reject rather than silently underpay. payForAccess(runtimeAddress, contentHash, value) { return txContract(deps.contracts.getRuntimeContract(runtimeAddress), 'musicRoyPayAccess', [contentHash, { value }]); }, @@ -265,6 +276,12 @@ export function createProductCdmRuntimeWriter(deps: ProductCdmRuntimeAdapterDeps return txContract(deps.contracts.getRuntimeContract(runtimeAddress), active ? 'musicRegReactivate' : 'musicRegDeactivate', [contentHash]); }, + // NOT IMPLEMENTED: the viem writer awaits a receipt here, so callers that + // write and then re-read (the artist console does) rely on this settling. + // Returning immediately is only safe if `txContract` already blocks until + // inclusion, which the SDK surface does not state. Until that is confirmed + // against a real host, this adapter must not be selected for writes - + // a caller would read pre-inclusion state and report a phantom failure. async waitForTransaction() { return; } diff --git a/web/src/features/runtime/runtimePorts.ts b/web/src/features/runtime/runtimePorts.ts index b79e9e3..5fd7622 100644 --- a/web/src/features/runtime/runtimePorts.ts +++ b/web/src/features/runtime/runtimePorts.ts @@ -45,6 +45,27 @@ export type RuntimeAccessPolicyUpdate = { requiredPersonhood: number; }; +/** + * Track and split counts are read from contract storage, and the artist + * directory enumerates runtimes Dotify does not control. `Array.from({ length: + * Number(count) })` allocates before any later check can reject the value, so a + * malformed or hostile count has to be refused before it is materialised. + * + * Exceeding a bound throws rather than truncating: a silent cap would present a + * partial catalog as complete. The catalog loader already isolates per-runtime + * failures, so one bad runtime degrades to a missing artist, not a dead + * catalog. + */ +export const MAX_RUNTIME_TRACKS = 2_000; +export const MAX_ROYALTY_SPLITS = 128; + +export function assertBoundedCount(count: bigint, max: number, label: string): number { + if (count < 0n || count > BigInt(max)) { + throw new Error(`${label} reports ${count} entries, above the supported maximum of ${max}.`); + } + return Number(count); +} + export interface RuntimeReadPort { ensureContract(address: Address): Promise; resolveArtistRuntime(directoryAddress: Address, artistAddress: Address): Promise
    ; diff --git a/web/src/features/runtime/viemRuntimeAdapter.test.ts b/web/src/features/runtime/viemRuntimeAdapter.test.ts index d4b2c6c..e05ef4c 100644 --- a/web/src/features/runtime/viemRuntimeAdapter.test.ts +++ b/web/src/features/runtime/viemRuntimeAdapter.test.ts @@ -83,6 +83,45 @@ describe('createViemRuntimeReader', () => { ]); }); + it('refuses an implausible track count instead of allocating for it', async () => { + // A hostile or malformed runtime must not reach Array.from({ length: n }). + const readContract = vi.fn(async ({ functionName }: { functionName: string }) => { + if (functionName === 'musicRegTrackCount') return 2n ** 64n; + throw new Error(`unexpected ${functionName}`); + }); + const reader = createViemRuntimeReader({ + ethRpcUrl: 'http://localhost:8545', + publicClient: { readContract } as never + }); + + await expect(reader.listRuntimeTracks(runtime)).rejects.toThrow(/above the supported maximum/); + expect(readContract).toHaveBeenCalledTimes(1); + }); + + it('refuses an implausible royalty split count for a single track', async () => { + const record = baseTrackRecord(); + const readContract = vi.fn(async ({ functionName }: { functionName: string }) => { + switch (functionName) { + case 'musicRegTrackCount': + return 1n; + case 'musicRegTrackHashAtIndex': + return hash; + case 'musicRegGetTrack': + return [record, runtime]; + case 'musicRoySplitCount': + return 10_000n; + default: + throw new Error(`unexpected ${functionName}`); + } + }); + const reader = createViemRuntimeReader({ + ethRpcUrl: 'http://localhost:8545', + publicClient: { readContract } as never + }); + + await expect(reader.listRuntimeTracks(runtime)).rejects.toThrow(/above the supported maximum/); + }); + it('normalizes royalty payment logs with block timestamps', async () => { const getLogs = vi.fn(async () => [ { diff --git a/web/src/features/runtime/viemRuntimeAdapter.ts b/web/src/features/runtime/viemRuntimeAdapter.ts index 39300fa..a3dc284 100644 --- a/web/src/features/runtime/viemRuntimeAdapter.ts +++ b/web/src/features/runtime/viemRuntimeAdapter.ts @@ -8,13 +8,16 @@ import { musicRegistryAbi, musicRoyaltiesAbi } from '../../shared/config/contracts'; -import type { - RuntimeAccessPolicyUpdate, - RuntimeDirectoryEntry, - RuntimeReadPort, - RuntimeRoyaltyPaymentLog, - RuntimeTrackSnapshot, - RuntimeWritePort +import { + MAX_ROYALTY_SPLITS, + MAX_RUNTIME_TRACKS, + assertBoundedCount, + type RuntimeAccessPolicyUpdate, + type RuntimeDirectoryEntry, + type RuntimeReadPort, + type RuntimeRoyaltyPaymentLog, + type RuntimeTrackSnapshot, + type RuntimeWritePort } from './runtimePorts'; import type { OnchainTrackRecord } from '../../shared/types'; @@ -100,8 +103,10 @@ export function createViemRuntimeReader(deps: ViemRuntimeReaderDeps): RuntimeRea functionName: 'musicRegTrackCount' })) as bigint; + const trackTotal = assertBoundedCount(trackCount, MAX_RUNTIME_TRACKS, `Runtime ${runtimeAddress}`); + return Promise.all( - Array.from({ length: Number(trackCount) }, async (_, index): Promise => { + Array.from({ length: trackTotal }, async (_, index): Promise => { const hash = (await client().readContract({ address: runtimeAddress, abi: musicRegistryAbi, @@ -124,10 +129,11 @@ export function createViemRuntimeReader(deps: ViemRuntimeReaderDeps): RuntimeRea args: [hash] }) .catch(() => 0n)) as bigint; + const splitTotal = assertBoundedCount(splitCount, MAX_ROYALTY_SPLITS, `Track ${hash} royalty splits`); const royaltySplits = ( await Promise.all( - Array.from({ length: Number(splitCount) }, async (_, splitIndex) => { + Array.from({ length: splitTotal }, async (_, splitIndex) => { try { const [recipient, bps] = (await client().readContract({ address: runtimeAddress, From 1ef3117adbe3343d0437e7fd000e9e83e0de34ff Mon Sep 17 00:00:00 2001 From: Kevin Nzeng Essimengane Date: Wed, 29 Jul 2026 03:18:17 +0200 Subject: [PATCH 10/22] feat: generate the Product CDM manifest and contract types Dotify has no CDM-registered packages and no `cdm install`, but it does not need them. Its Solidity contracts are deployed through Asset Hub's eth-rpc, which is a compatibility layer over pallet-revive - the same pallet the Product SDK contract helpers target - so the deployed H160 addresses are already reachable without PolkaVM recompilation or a registry entry. CdmJsonContract needs only version, address, and abi for getContract(), and `new ContractManager(...)` is documented as snapshot-only. The generator emits exactly that snapshot from the same Hardhat artifacts the viem bindings come from, so the two adapters cannot disagree about an ABI. Artist runtimes are deliberately absent from the manifest: a diamond is deployed per artist, so its address is known at call time, not build time, and a placeholder would misrepresent the deployment. Their merged facet ABI is emitted separately and bound to an address by createContract. The generator lives in web/ because it needs the SDK's codegen, which is a frontend dependency; adding the Product SDK tree to contracts/evm just to emit types would be a worse trade. Unnamed Solidity getter params are named positionally for codegen only - generateContractTypes interpolates the name into a tuple label and emits `args: [: HexString]`, which does not parse. The manifest ABI stays byte-faithful to the artifact. Co-Authored-By: Claude Opus 5 (1M context) --- web/package-lock.json | 1 + web/package.json | 2 + web/scripts/generate-cdm-manifest.mjs | 157 +++ web/src/generated/contracts/cdm.d.ts | 72 ++ web/src/generated/contracts/cdm.json | 508 +++++++++ web/src/generated/contracts/smartRuntime.ts | 1094 +++++++++++++++++++ 6 files changed, 1834 insertions(+) create mode 100644 web/scripts/generate-cdm-manifest.mjs create mode 100644 web/src/generated/contracts/cdm.d.ts create mode 100644 web/src/generated/contracts/cdm.json create mode 100644 web/src/generated/contracts/smartRuntime.ts diff --git a/web/package-lock.json b/web/package-lock.json index ea7aaf1..d246673 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@parity/product-sdk": "0.19.1", + "@parity/product-sdk-descriptors": "0.8.0", "@polkadot-api/descriptors": "file:.papi/descriptors", "@polkadot-apps/chain-client": "^2.0.5", "@polkadot-apps/descriptors": "^1.0.1", diff --git a/web/package.json b/web/package.json index 4c5137e..75edb7f 100644 --- a/web/package.json +++ b/web/package.json @@ -24,12 +24,14 @@ "lint": "eslint .", "fmt": "prettier --write 'src/**/*.{ts,tsx}' 'server/**/*.mjs' 'scripts/**/*.mjs' README.md", "fmt:check": "prettier --check 'src/**/*.{ts,tsx}' 'server/**/*.mjs' 'scripts/**/*.mjs' README.md", + "generate:cdm": "node scripts/generate-cdm-manifest.mjs", "update-types": "papi update", "codegen": "papi generate", "preview": "vite preview --host 0.0.0.0 --port 4273" }, "dependencies": { "@parity/product-sdk": "0.19.1", + "@parity/product-sdk-descriptors": "0.8.0", "@polkadot-api/descriptors": "file:.papi/descriptors", "@polkadot-apps/chain-client": "^2.0.5", "@polkadot-apps/descriptors": "^1.0.1", diff --git a/web/scripts/generate-cdm-manifest.mjs b/web/scripts/generate-cdm-manifest.mjs new file mode 100644 index 0000000..dfe5469 --- /dev/null +++ b/web/scripts/generate-cdm-manifest.mjs @@ -0,0 +1,157 @@ +// Generate the Product CDM manifest and typed contract augmentation for the +// frontend from Hardhat artifacts + deployments.json. +// +// Run via `npm run generate:cdm` from web/. Plain Node ESM, like +// contracts/evm/scripts/generate-abis.mjs: this only reads JSON and writes +// JSON/TS, so it needs no Hardhat runtime. +// +// It lives in web/ rather than next to generate-abis.mjs because it needs +// @parity/product-sdk-contracts/codegen, which is a frontend dependency. +// Adding the Product SDK tree to contracts/evm just to emit types would be a +// worse trade. Compile first: `cd contracts/evm && npm run compile`. +// +// Why a hand-generated manifest instead of `cdm install`: +// +// Dotify's contracts are Solidity deployed through Asset Hub's eth-rpc, which +// is a compatibility layer over pallet-revive - the same pallet the Product SDK +// contract helpers target. So the deployed H160 addresses are already +// addressable through @parity/product-sdk-contracts without recompiling to +// PolkaVM or registering CDM packages. `CdmJsonContract` only needs `version`, +// `address`, and `abi` for getContract(), and `new ContractManager(...)` is +// documented as snapshot-only. This produces exactly that snapshot, from the +// same artifacts the viem bindings come from, so the two adapters can never +// disagree about an ABI. +// +// The manifest carries only fixed-address contracts. Artist runtimes are +// diamonds deployed per artist, so their address is known at call time, not +// build time; their merged facet ABI is emitted separately for +// createContract(runtime, artistRuntimeAddress, abi). + +import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { generateContractTypes } from '@parity/product-sdk-contracts/codegen'; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(scriptDir, '../..'); +const artifactsRoot = resolve(repoRoot, 'contracts/evm/artifacts/contracts'); +const outDir = resolve(repoRoot, 'web/src/generated/contracts'); +const deploymentsPath = resolve(repoRoot, 'deployments.json'); + +const HEADER = '// Auto-generated by web/scripts/generate-cdm-manifest.mjs. Do not edit manually.'; + +// CDM library name -> { deployments.json key, artifact path }. +const FIXED_CONTRACTS = [ + { library: '@dotify/artist-directory', deployment: 'directory', artifact: 'ArtistDirectory.sol/ArtistDirectory.json' }, + { library: '@dotify/artist-runtime-factory', deployment: 'factory', artifact: 'ArtistRuntimeFactory.sol/ArtistRuntimeFactory.json' } +]; + +// Facets whose selectors are installed on every artist runtime diamond. Merged +// into one ABI so a runtime address can be called through a single handle, the +// same way the viem adapter calls facet ABIs at the runtime address. +const RUNTIME_FACETS = [ + 'pallets/MusicRegistryPallet.sol/MusicRegistryPallet.json', + 'pallets/MusicRoyaltiesPallet.sol/MusicRoyaltiesPallet.json', + 'pallets/MusicAccessPallet.sol/MusicAccessPallet.json', + 'pallets/MusicNFTPallet.sol/MusicNFTPallet.json' +]; + +const RUNTIME_LIBRARY = '@dotify/smart-runtime'; + +function readArtifactAbi(artifact) { + const artifactPath = resolve(artifactsRoot, artifact); + let parsed; + try { + parsed = JSON.parse(readFileSync(artifactPath, 'utf8')); + } catch { + throw new Error(`Missing artifact ${artifact}. Run "cd contracts/evm && npm run compile" first.`); + } + if (!Array.isArray(parsed.abi)) { + throw new Error(`Artifact ${artifact} has no abi array.`); + } + return parsed.abi; +} + +/** Stable identity for an ABI entry: selector-equivalent for functions, plus type and name. */ +function entryKey(entry) { + const inputs = (entry.inputs ?? []).map(input => input.type).join(','); + return `${entry.type}:${entry.name ?? ''}(${inputs})`; +} + +/** + * Merge facet ABIs into one runtime ABI. A diamond cannot install two facets + * with the same selector, so a collision here means the facet set is wrong - + * fail loudly rather than silently keeping whichever came first. + */ +function mergeRuntimeAbi(facets) { + const merged = new Map(); + for (const { artifact, abi } of facets) { + for (const entry of abi) { + const key = entryKey(entry); + const existing = merged.get(key); + if (!existing) { + merged.set(key, { entry, artifact }); + continue; + } + // Shared constructors/events across facets are expected and identical. + if (JSON.stringify(existing.entry) === JSON.stringify(entry)) continue; + throw new Error(`Runtime facet collision on ${key}: ${existing.artifact} and ${artifact} disagree. Check the diamond facet set.`); + } + } + return Array.from(merged.values(), ({ entry }) => entry); +} + +/** + * Solidity leaves auto-generated getter params unnamed, and + * generateContractTypes interpolates the name straight into a tuple label - + * emitting `args: [: HexString]`, which does not parse. Name them positionally + * for codegen only. The manifest ABI stays byte-faithful to the artifact: + * argument encoding is positional, so names there are cosmetic, and rewriting + * them would make cdm.json diverge from the compiled contract. + */ +function withNamedParams(abi) { + return abi.map(entry => ({ + ...entry, + inputs: (entry.inputs ?? []).map((input, index) => ({ ...input, name: input.name || `arg${index}` })) + })); +} + +function requireAddress(deployments, key) { + const address = deployments[key]; + if (typeof address !== 'string' || !/^0x[0-9a-fA-F]{40}$/.test(address)) { + throw new Error(`deployments.json has no valid "${key}" address. Deploy the contracts before generating the CDM manifest.`); + } + return address.toLowerCase(); +} + +const deployments = JSON.parse(readFileSync(deploymentsPath, 'utf8')); + +const contracts = {}; +const typeInputs = []; + +for (const { library, deployment, artifact } of FIXED_CONTRACTS) { + const abi = readArtifactAbi(artifact); + contracts[library] = { version: 1, address: requireAddress(deployments, deployment), abi }; + typeInputs.push({ library, abi: withNamedParams(abi) }); +} + +const runtimeAbi = mergeRuntimeAbi(RUNTIME_FACETS.map(artifact => ({ artifact, abi: readArtifactAbi(artifact) }))); +typeInputs.push({ library: RUNTIME_LIBRARY, abi: withNamedParams(runtimeAbi) }); + +mkdirSync(outDir, { recursive: true }); + +// The manifest deliberately omits the artist runtime: it has no build-time +// address, and inventing a placeholder one would misrepresent the deployment. +const manifest = { dependencies: Object.fromEntries(Object.keys(contracts).map(library => [library, 1])), contracts }; +writeFileSync(resolve(outDir, 'cdm.json'), `${JSON.stringify(manifest, null, 2)}\n`); + +writeFileSync( + resolve(outDir, 'smartRuntime.ts'), + `${HEADER}\n// Merged artist-runtime diamond facet ABI. Bound to a per-artist address at\n// call time via createContract(), so it carries no address of its own.\n// Source facets:\n${RUNTIME_FACETS.map(artifact => `// contracts/evm/artifacts/contracts/${artifact}`).join('\n')}\n\nexport const SMART_RUNTIME_LIBRARY = '${RUNTIME_LIBRARY}';\n\nexport const smartRuntimeAbi = ${JSON.stringify(runtimeAbi, null, 2)} as const;\n` +); + +writeFileSync(resolve(outDir, 'cdm.d.ts'), `${HEADER}\n\n${generateContractTypes(typeInputs)}`); + +console.log( + `Generated cdm.json (${Object.keys(contracts).length} fixed contracts), smartRuntime.ts (${runtimeAbi.length} merged entries), and cdm.d.ts into web/src/generated/contracts/` +); diff --git a/web/src/generated/contracts/cdm.d.ts b/web/src/generated/contracts/cdm.d.ts new file mode 100644 index 0000000..d975bce --- /dev/null +++ b/web/src/generated/contracts/cdm.d.ts @@ -0,0 +1,72 @@ +// Auto-generated by web/scripts/generate-cdm-manifest.mjs. Do not edit manually. + +// Auto-generated by cdm install — do not edit +import type { HexString, SizedHex } from "polkadot-api"; + +declare module "@parity/product-sdk-contracts" { + interface Contracts { + "@dotify/artist-directory": { + methods: { + artistAtIndex: { args: [index: bigint]; response: HexString }; + artistCount: { args: []; response: bigint }; + artistsPage: { args: [offset: bigint, limit: bigint]; response: { artists: HexString[]; runtimes: HexString[] } }; + deployer: { args: []; response: HexString }; + factory: { args: []; response: HexString }; + register: { args: [artist: HexString, runtime: HexString]; response: undefined }; + runtimeOf: { args: [arg0: HexString]; response: HexString }; + setFactory: { args: [_factory: HexString]; response: undefined }; + }; + }; + "@dotify/artist-runtime-factory": { + methods: { + accessPallet: { args: []; response: HexString }; + createRuntime: { args: []; response: HexString }; + cutPallet: { args: []; response: HexString }; + directory: { args: []; response: HexString }; + initContract: { args: []; response: HexString }; + installRuntimeStep: { args: []; response: number }; + loupePallet: { args: []; response: HexString }; + nftPallet: { args: []; response: HexString }; + ownershipPallet: { args: []; response: HexString }; + pendingRuntimeOf: { args: [arg0: HexString]; response: HexString }; + pendingRuntimeStageOf: { args: [arg0: HexString]; response: number }; + registryPallet: { args: []; response: HexString }; + royaltiesPallet: { args: []; response: HexString }; + runtimeOf: { args: [artist: HexString]; response: HexString }; + }; + }; + "@dotify/smart-runtime": { + methods: { + musicRegDeactivate: { args: [contentHash: SizedHex<32>]; response: undefined }; + musicRegGetTrack: { args: [contentHash: SizedHex<32>]; response: { track: { artist: HexString; tokenId: bigint; title: string; artistName: string; description: string; imageRef: string; audioRef: string; metadataRef: string; artistContractRef: string; royaltyBps: number; accessMode: number; pricePlanck: bigint; requiredPersonhood: number; registeredAtBlock: bigint; active: boolean }; tokenOwner: HexString } }; + musicRegGetTrackByTokenId: { args: [tokenId: bigint]; response: { track: { artist: HexString; tokenId: bigint; title: string; artistName: string; description: string; imageRef: string; audioRef: string; metadataRef: string; artistContractRef: string; royaltyBps: number; accessMode: number; pricePlanck: bigint; requiredPersonhood: number; registeredAtBlock: bigint; active: boolean }; tokenOwner: HexString } }; + musicRegIsActive: { args: [contentHash: SizedHex<32>]; response: boolean }; + musicRegIsRegistered: { args: [contentHash: SizedHex<32>]; response: boolean }; + musicRegReactivate: { args: [contentHash: SizedHex<32>]; response: undefined }; + musicRegRegister: { args: [reg: { contentHash: SizedHex<32>; title: string; artistName: string; description: string; imageRef: string; audioRef: string; metadataRef: string; artistContractRef: string; accessMode: number; pricePlanck: bigint; requiredPersonhood: number }, recipients: HexString[], bps: number[]]; response: undefined }; + musicRegSetAccessMode: { args: [contentHash: SizedHex<32>, accessMode: number, pricePlanck: bigint, requiredPersonhood: number]; response: undefined }; + musicRegTrackCount: { args: []; response: bigint }; + musicRegTrackHashAtIndex: { args: [index: bigint]; response: SizedHex<32> }; + musicRoyPayAccess: { args: [contentHash: SizedHex<32>]; response: undefined }; + musicRoyRecordListen: { args: [contentHash: SizedHex<32>]; response: undefined }; + musicRoySplitAt: { args: [contentHash: SizedHex<32>, index: bigint]; response: { recipient: HexString; bps: number } }; + musicRoySplitCount: { args: [contentHash: SizedHex<32>]; response: bigint }; + musicRoyTotalBps: { args: [contentHash: SizedHex<32>]; response: number }; + musicAccCanAccess: { args: [contentHash: SizedHex<32>, listener: HexString]; response: boolean }; + musicAccGetRegistrar: { args: []; response: HexString }; + musicAccHasPaid: { args: [contentHash: SizedHex<32>, listener: HexString]; response: boolean }; + musicAccHasPersonhood: { args: [account: HexString, required: number]; response: boolean }; + musicAccPersonhoodLevel: { args: [account: HexString]; response: number }; + musicAccSetPersonhoodLevel: { args: [account: HexString, level: number]; response: undefined }; + setPersonhoodRegistrar: { args: [registrar: HexString]; response: undefined }; + musicNFTApprove: { args: [to: HexString, tokenId: bigint]; response: undefined }; + musicNFTBalanceOf: { args: [owner: HexString]; response: bigint }; + musicNFTGetApproved: { args: [tokenId: bigint]; response: HexString }; + musicNFTIsApprovedForAll: { args: [owner: HexString, operator: HexString]; response: boolean }; + musicNFTOwnerOf: { args: [tokenId: bigint]; response: HexString }; + musicNFTSetApprovalForAll: { args: [operator: HexString, approved: boolean]; response: undefined }; + musicNFTTransfer: { args: [tokenId: bigint, to: HexString]; response: undefined }; + }; + }; + } +} diff --git a/web/src/generated/contracts/cdm.json b/web/src/generated/contracts/cdm.json new file mode 100644 index 0000000..e7aa044 --- /dev/null +++ b/web/src/generated/contracts/cdm.json @@ -0,0 +1,508 @@ +{ + "dependencies": { + "@dotify/artist-directory": 1, + "@dotify/artist-runtime-factory": 1 + }, + "contracts": { + "@dotify/artist-directory": { + "version": 1, + "address": "0xcf1534c6e2b0e43b9436c1e86a076466dc0f2108", + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "artist", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "runtime", + "type": "address" + } + ], + "name": "ArtistRegistered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "factory", + "type": "address" + } + ], + "name": "FactorySet", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "artistAtIndex", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "artistCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "offset", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "limit", + "type": "uint256" + } + ], + "name": "artistsPage", + "outputs": [ + { + "internalType": "address[]", + "name": "artists", + "type": "address[]" + }, + { + "internalType": "address[]", + "name": "runtimes", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "deployer", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "artist", + "type": "address" + }, + { + "internalType": "address", + "name": "runtime", + "type": "address" + } + ], + "name": "register", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "runtimeOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_factory", + "type": "address" + } + ], + "name": "setFactory", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ] + }, + "@dotify/artist-runtime-factory": { + "version": 1, + "address": "0xbd1a11cfce8b5ef7a37e507bc5109895f8f42a72", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_directory", + "type": "address" + }, + { + "internalType": "address", + "name": "_initContract", + "type": "address" + }, + { + "internalType": "address", + "name": "_cutPallet", + "type": "address" + }, + { + "internalType": "address", + "name": "_loupePallet", + "type": "address" + }, + { + "internalType": "address", + "name": "_ownershipPallet", + "type": "address" + }, + { + "internalType": "address", + "name": "_registryPallet", + "type": "address" + }, + { + "internalType": "address", + "name": "_nftPallet", + "type": "address" + }, + { + "internalType": "address", + "name": "_royaltiesPallet", + "type": "address" + }, + { + "internalType": "address", + "name": "_accessPallet", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "artist", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "runtime", + "type": "address" + } + ], + "name": "ArtistRuntimeBootstrapStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "artist", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "runtime", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint8", + "name": "completedStage", + "type": "uint8" + } + ], + "name": "ArtistRuntimeBootstrapStep", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "artist", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "runtime", + "type": "address" + } + ], + "name": "ArtistRuntimeCreated", + "type": "event" + }, + { + "inputs": [], + "name": "accessPallet", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "createRuntime", + "outputs": [ + { + "internalType": "address", + "name": "runtime", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "cutPallet", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "directory", + "outputs": [ + { + "internalType": "contract ArtistDirectory", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "initContract", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "installRuntimeStep", + "outputs": [ + { + "internalType": "uint8", + "name": "completedStage", + "type": "uint8" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "loupePallet", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nftPallet", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "ownershipPallet", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "pendingRuntimeOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "pendingRuntimeStageOf", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "registryPallet", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "royaltiesPallet", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "artist", + "type": "address" + } + ], + "name": "runtimeOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } + ] + } + } +} diff --git a/web/src/generated/contracts/smartRuntime.ts b/web/src/generated/contracts/smartRuntime.ts new file mode 100644 index 0000000..62904ac --- /dev/null +++ b/web/src/generated/contracts/smartRuntime.ts @@ -0,0 +1,1094 @@ +// Auto-generated by web/scripts/generate-cdm-manifest.mjs. Do not edit manually. +// Merged artist-runtime diamond facet ABI. Bound to a per-artist address at +// call time via createContract(), so it carries no address of its own. +// Source facets: +// contracts/evm/artifacts/contracts/pallets/MusicRegistryPallet.sol/MusicRegistryPallet.json +// contracts/evm/artifacts/contracts/pallets/MusicRoyaltiesPallet.sol/MusicRoyaltiesPallet.json +// contracts/evm/artifacts/contracts/pallets/MusicAccessPallet.sol/MusicAccessPallet.json +// contracts/evm/artifacts/contracts/pallets/MusicNFTPallet.sol/MusicNFTPallet.json + +export const SMART_RUNTIME_LIBRARY = '@dotify/smart-runtime'; + +export const smartRuntimeAbi = [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "contentHash", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "artist", + "type": "address" + }, + { + "indexed": false, + "internalType": "enum LibMusicRegistry.AccessMode", + "name": "accessMode", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "pricePlanck", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "enum LibMusicRegistry.PersonhoodLevel", + "name": "requiredPersonhood", + "type": "uint8" + } + ], + "name": "TrackAccessModeChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "contentHash", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "artist", + "type": "address" + } + ], + "name": "TrackDeactivated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "contentHash", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "artist", + "type": "address" + } + ], + "name": "TrackReactivated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "contentHash", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "artist", + "type": "address" + }, + { + "indexed": false, + "internalType": "string", + "name": "title", + "type": "string" + }, + { + "indexed": false, + "internalType": "enum LibMusicRegistry.AccessMode", + "name": "accessMode", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "pricePlanck", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "enum LibMusicRegistry.PersonhoodLevel", + "name": "requiredPersonhood", + "type": "uint8" + } + ], + "name": "TrackRegistered", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "contentHash", + "type": "bytes32" + } + ], + "name": "musicRegDeactivate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "contentHash", + "type": "bytes32" + } + ], + "name": "musicRegGetTrack", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "artist", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "string", + "name": "title", + "type": "string" + }, + { + "internalType": "string", + "name": "artistName", + "type": "string" + }, + { + "internalType": "string", + "name": "description", + "type": "string" + }, + { + "internalType": "string", + "name": "imageRef", + "type": "string" + }, + { + "internalType": "string", + "name": "audioRef", + "type": "string" + }, + { + "internalType": "string", + "name": "metadataRef", + "type": "string" + }, + { + "internalType": "string", + "name": "artistContractRef", + "type": "string" + }, + { + "internalType": "uint16", + "name": "royaltyBps", + "type": "uint16" + }, + { + "internalType": "enum LibMusicRegistry.AccessMode", + "name": "accessMode", + "type": "uint8" + }, + { + "internalType": "uint128", + "name": "pricePlanck", + "type": "uint128" + }, + { + "internalType": "enum LibMusicRegistry.PersonhoodLevel", + "name": "requiredPersonhood", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "registeredAtBlock", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + } + ], + "internalType": "struct LibMusicRegistry.TrackRecord", + "name": "track", + "type": "tuple" + }, + { + "internalType": "address", + "name": "tokenOwner", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "musicRegGetTrackByTokenId", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "artist", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "string", + "name": "title", + "type": "string" + }, + { + "internalType": "string", + "name": "artistName", + "type": "string" + }, + { + "internalType": "string", + "name": "description", + "type": "string" + }, + { + "internalType": "string", + "name": "imageRef", + "type": "string" + }, + { + "internalType": "string", + "name": "audioRef", + "type": "string" + }, + { + "internalType": "string", + "name": "metadataRef", + "type": "string" + }, + { + "internalType": "string", + "name": "artistContractRef", + "type": "string" + }, + { + "internalType": "uint16", + "name": "royaltyBps", + "type": "uint16" + }, + { + "internalType": "enum LibMusicRegistry.AccessMode", + "name": "accessMode", + "type": "uint8" + }, + { + "internalType": "uint128", + "name": "pricePlanck", + "type": "uint128" + }, + { + "internalType": "enum LibMusicRegistry.PersonhoodLevel", + "name": "requiredPersonhood", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "registeredAtBlock", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + } + ], + "internalType": "struct LibMusicRegistry.TrackRecord", + "name": "track", + "type": "tuple" + }, + { + "internalType": "address", + "name": "tokenOwner", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "contentHash", + "type": "bytes32" + } + ], + "name": "musicRegIsActive", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "contentHash", + "type": "bytes32" + } + ], + "name": "musicRegIsRegistered", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "contentHash", + "type": "bytes32" + } + ], + "name": "musicRegReactivate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "contentHash", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "title", + "type": "string" + }, + { + "internalType": "string", + "name": "artistName", + "type": "string" + }, + { + "internalType": "string", + "name": "description", + "type": "string" + }, + { + "internalType": "string", + "name": "imageRef", + "type": "string" + }, + { + "internalType": "string", + "name": "audioRef", + "type": "string" + }, + { + "internalType": "string", + "name": "metadataRef", + "type": "string" + }, + { + "internalType": "string", + "name": "artistContractRef", + "type": "string" + }, + { + "internalType": "enum LibMusicRegistry.AccessMode", + "name": "accessMode", + "type": "uint8" + }, + { + "internalType": "uint128", + "name": "pricePlanck", + "type": "uint128" + }, + { + "internalType": "enum LibMusicRegistry.PersonhoodLevel", + "name": "requiredPersonhood", + "type": "uint8" + } + ], + "internalType": "struct MusicRegistryPallet.TrackRegistration", + "name": "reg", + "type": "tuple" + }, + { + "internalType": "address[]", + "name": "recipients", + "type": "address[]" + }, + { + "internalType": "uint16[]", + "name": "bps", + "type": "uint16[]" + } + ], + "name": "musicRegRegister", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "contentHash", + "type": "bytes32" + }, + { + "internalType": "enum LibMusicRegistry.AccessMode", + "name": "accessMode", + "type": "uint8" + }, + { + "internalType": "uint128", + "name": "pricePlanck", + "type": "uint128" + }, + { + "internalType": "enum LibMusicRegistry.PersonhoodLevel", + "name": "requiredPersonhood", + "type": "uint8" + } + ], + "name": "musicRegSetAccessMode", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "musicRegTrackCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "musicRegTrackHashAtIndex", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "contentHash", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "listener", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "MusicRoyAccessPaid", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "contentHash", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "listener", + "type": "address" + }, + { + "indexed": false, + "internalType": "enum LibMusicRegistry.PersonhoodLevel", + "name": "requiredPersonhood", + "type": "uint8" + } + ], + "name": "MusicRoyListenRecorded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "contentHash", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "listener", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "MusicRoyRefunded", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "contentHash", + "type": "bytes32" + } + ], + "name": "musicRoyPayAccess", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "contentHash", + "type": "bytes32" + } + ], + "name": "musicRoyRecordListen", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "contentHash", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "musicRoySplitAt", + "outputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint16", + "name": "bps", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "contentHash", + "type": "bytes32" + } + ], + "name": "musicRoySplitCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "contentHash", + "type": "bytes32" + } + ], + "name": "musicRoyTotalBps", + "outputs": [ + { + "internalType": "uint16", + "name": "total", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": false, + "internalType": "enum LibMusicRegistry.PersonhoodLevel", + "name": "level", + "type": "uint8" + } + ], + "name": "MusicAccPersonhoodLevelSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previous", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "next", + "type": "address" + } + ], + "name": "MusicAccPersonhoodRegistrarSet", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "contentHash", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "listener", + "type": "address" + } + ], + "name": "musicAccCanAccess", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "musicAccGetRegistrar", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "contentHash", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "listener", + "type": "address" + } + ], + "name": "musicAccHasPaid", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "enum LibMusicRegistry.PersonhoodLevel", + "name": "required", + "type": "uint8" + } + ], + "name": "musicAccHasPersonhood", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "musicAccPersonhoodLevel", + "outputs": [ + { + "internalType": "enum LibMusicRegistry.PersonhoodLevel", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "enum LibMusicRegistry.PersonhoodLevel", + "name": "level", + "type": "uint8" + } + ], + "name": "musicAccSetPersonhoodLevel", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "registrar", + "type": "address" + } + ], + "name": "setPersonhoodRegistrar", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "MusicNFTApproval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "MusicNFTApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "MusicNFTTransfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "musicNFTApprove", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "musicNFTBalanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "musicNFTGetApproved", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "musicNFTIsApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "musicNFTOwnerOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "musicNFTSetApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "musicNFTTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] as const; From 8bd9220faaa6d7c804aee50fc2e9216d7a7e280d Mon Sep 17 00:00:00 2001 From: Kevin Nzeng Essimengane Date: Wed, 29 Jul 2026 03:18:37 +0200 Subject: [PATCH 11/22] feat: wire the Product CDM runtime adapter behind a build-time flag Implements the contract resolver the CDM adapter was missing. Fixed-address contracts resolve from the generated manifest through ContractManager; per-artist runtime diamonds bind the merged facet ABI to their call-time address through createContract. Two SDK constraints shape this. First, createChainClient routes exclusively through the Product host provider with no direct-WebSocket fallback, so Product mode cannot work in a standalone build - validateProductionEnvironment now rejects product-cdm unless the host mode is enabled. Second, the host decides which chain an environment resolves to, and Dotify's runtimes live on Polkadot Hub TestNet, reached through the `paseo` preset rather than `devnet`. verifyDeployment queries the directory before any catalog read so a wrong-chain connection fails with a named error instead of looking like artists with no releases. Selection is build-time rather than runtime, for two reasons. Switching the authority for access policy is a deployment decision made with evidence, not something a page should flip. And Vite inlines the value, so a viem build tree-shakes the whole Product graph away: 4.4 MB against 10 MB when opted in. The difference is @parity/product-sdk-descriptors, whose shared descriptors module references every chain's metadata - only one chunk is ever fetched, but all are published, and Bulletin storage is a finite quota. Both shipped builds stay at 4.4 MB. Reads only. Writes stay on the viem signer path in every mode, because routing a payment or a publication through a signer with no host transaction evidence is not a reasonable default. A failed Product setup rejects every read rather than falling back to viem: the adapter in use must never be ambiguous. Co-Authored-By: Claude Opus 5 (1M context) --- .../runtime/productCdmContracts.test.ts | 89 ++++++++ .../features/runtime/productCdmContracts.ts | 199 ++++++++++++++++++ .../runtime/productCdmRuntimeAdapter.ts | 24 +-- .../runtime/runtimeAdapterConfig.test.ts | 29 +++ .../features/runtime/runtimeAdapterConfig.ts | 45 ++++ .../runtime/runtimeReaderProvider.test.ts | 62 ++++++ .../features/runtime/runtimeReaderProvider.ts | 79 +++++++ web/src/hooks/useArtistConsole.ts | 5 +- web/src/hooks/useCatalog.ts | 5 +- .../shared/config/deploymentSafety.test.ts | 44 ++++ web/src/shared/config/deploymentSafety.ts | 19 ++ 11 files changed, 584 insertions(+), 16 deletions(-) create mode 100644 web/src/features/runtime/productCdmContracts.test.ts create mode 100644 web/src/features/runtime/productCdmContracts.ts create mode 100644 web/src/features/runtime/runtimeAdapterConfig.test.ts create mode 100644 web/src/features/runtime/runtimeAdapterConfig.ts create mode 100644 web/src/features/runtime/runtimeReaderProvider.test.ts create mode 100644 web/src/features/runtime/runtimeReaderProvider.ts diff --git a/web/src/features/runtime/productCdmContracts.test.ts b/web/src/features/runtime/productCdmContracts.test.ts new file mode 100644 index 0000000..10f915a --- /dev/null +++ b/web/src/features/runtime/productCdmContracts.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, it, vi } from 'vitest'; +import { DOTIFY_CDM_PACKAGES, createProductCdmContracts, type ProductCdmContractsDeps } from './productCdmContracts'; +import cdmManifest from '../../generated/contracts/cdm.json'; + +const DIRECTORY = cdmManifest.contracts['@dotify/artist-directory'].address as `0x${string}`; +const FACTORY = cdmManifest.contracts['@dotify/artist-runtime-factory'].address as `0x${string}`; +const RUNTIME = '0x00000000000000000000000000000000000000aa' as const; + +type Handles = { artistCountSuccess?: boolean }; + +function buildDeps(overrides: Handles = {}, spies: Record> = {}): ProductCdmContractsDeps { + const artistCountQuery = vi.fn(async () => ({ success: overrides.artistCountSuccess ?? true, value: 3n, gasRequired: {} })); + const managerContract = { artistCount: { query: artistCountQuery } }; + + const createContract = spies.createContract ?? vi.fn(() => ({ musicAccCanAccess: { query: vi.fn() } })); + const destroy = spies.destroy ?? vi.fn(); + + class ContractManager { + getAddress(library: string) { + if (library === DOTIFY_CDM_PACKAGES.directory) return DIRECTORY; + if (library === DOTIFY_CDM_PACKAGES.factory) return FACTORY; + throw new Error(`unknown package ${library}`); + } + getContract() { + return managerContract; + } + } + + return { + loadContracts: async () => + ({ + ContractManager, + createContractRuntimeFromClient: vi.fn(() => ({ runtime: true })), + createContract + }) as never, + loadChain: async () => ({ createChainClient: spies.createChainClient ?? vi.fn(async () => ({ raw: { assetHub: {} }, destroy })) }) as never, + loadDescriptor: async () => ({ descriptor: true }) + }; +} + +describe('createProductCdmContracts', () => { + it('resolves the manifest contracts by their deployed addresses', async () => { + const { resolver } = await createProductCdmContracts({ environment: 'paseo' }, buildDeps()); + + expect(() => resolver.getDirectoryContract(DIRECTORY)).not.toThrow(); + expect(() => resolver.getFactoryContract(FACTORY)).not.toThrow(); + await expect(resolver.hasContract?.(DIRECTORY)).resolves.toBe(true); + await expect(resolver.hasContract?.(RUNTIME)).resolves.toBe(false); + }); + + it('refuses a directory address that does not match the manifest', async () => { + const { resolver } = await createProductCdmContracts({ environment: 'paseo' }, buildDeps()); + + expect(() => resolver.getDirectoryContract(RUNTIME)).toThrow(/does not match CDM package/); + }); + + it('binds the merged facet ABI to a per-artist runtime address', async () => { + const createContract = vi.fn((..._args: unknown[]) => ({ musicAccCanAccess: { query: vi.fn() } })); + const { resolver } = await createProductCdmContracts({ environment: 'paseo' }, buildDeps({}, { createContract })); + + resolver.getRuntimeContract(RUNTIME); + + expect(createContract).toHaveBeenCalledTimes(1); + const [, address, abi] = createContract.mock.calls[0]; + expect(address).toBe(RUNTIME); + expect(Array.isArray(abi)).toBe(true); + }); + + it('names the missing host connection instead of reporting an RPC failure', async () => { + const createChainClient = vi.fn(async () => { + throw new Error('no host provider'); + }); + + await expect(createProductCdmContracts({ environment: 'paseo' }, buildDeps({}, { createChainClient }))).rejects.toThrow(/Polkadot Product host connection/); + }); + + it('verifyDeployment rejects a chain that does not answer for the directory', async () => { + // A wrong-chain connection otherwise looks like an artist with no releases. + const { verifyDeployment } = await createProductCdmContracts({ environment: 'devnet' }, buildDeps({ artistCountSuccess: false })); + + await expect(verifyDeployment()).rejects.toThrow(/did not answer on the "devnet" chain/); + }); + + it('verifyDeployment passes when the directory answers', async () => { + const { verifyDeployment } = await createProductCdmContracts({ environment: 'paseo' }, buildDeps()); + + await expect(verifyDeployment()).resolves.toBeUndefined(); + }); +}); diff --git a/web/src/features/runtime/productCdmContracts.ts b/web/src/features/runtime/productCdmContracts.ts new file mode 100644 index 0000000..39e0c15 --- /dev/null +++ b/web/src/features/runtime/productCdmContracts.ts @@ -0,0 +1,199 @@ +// Real Product CDM contract wiring for the runtime ports. +// +// This closes the gap `createProductCdmRuntimeContractResolver` documented: the +// adapter knew how to map Dotify's runtime surface onto Product contract +// handles, but nothing could actually produce those handles. +// +// Two resolution paths, because Dotify's contracts have two shapes: +// +// ArtistDirectory / ArtistRuntimeFactory - one fixed address each, so they +// come from the generated snapshot manifest through `ContractManager`. +// +// Artist runtimes - a diamond deployed per artist, so the address is only +// known at call time. Those bind the merged facet ABI to that address with +// `createContract`, which needs no manifest entry. +// +// Two hard constraints shape this file: +// +// 1. `createChainClient`/`getChainAPI` route exclusively through the Product +// host provider. There is no direct-WebSocket fallback, so this path +// cannot work in the standalone build and must fail closed outside a host +// container rather than appear to work. +// +// 2. The host decides which chain an environment resolves to. Dotify's +// runtimes are deployed on Polkadot Hub TestNet (EVM chain 420420417); +// if the host connects an environment that does not hold them, every +// manifest address resolves to an account with no code. `verifyDeployment` +// turns that into an explicit error instead of an empty catalog. +// +// Everything Product-specific loads through dynamic imports so a standalone +// build never pulls the PAPI/contract tree into its entry chunk, matching how +// productHost.ts loads the host SDK. + +import type { Address } from 'viem'; +import cdmManifest from '../../generated/contracts/cdm.json'; +import { SMART_RUNTIME_LIBRARY, smartRuntimeAbi } from '../../generated/contracts/smartRuntime'; +import { + ProductCdmRuntimeError, + type ProductCdmContractHandle, + type ProductCdmRuntimeContractResolver, + type ProductCdmRuntimePackages +} from './productCdmRuntimeAdapter'; + +export const DOTIFY_CDM_PACKAGES: ProductCdmRuntimePackages = { + directory: '@dotify/artist-directory', + factory: '@dotify/artist-runtime-factory', + runtime: SMART_RUNTIME_LIBRARY +}; + +/** + * Product chain environments Dotify can target. + * + * Deliberately not the SDK's full preset list. Each descriptor is a ~850 kB + * metadata chunk that ships with the Bulletin publication whether or not it is + * fetched, and Bulletin storage is a finite quota. Dotify has no deployment on + * Polkadot or Kusama Asset Hub, so carrying their metadata would be dead + * weight. Add one here only when Dotify actually deploys there. + */ +export type ProductChainEnvironment = 'paseo' | 'devnet'; + +export type ProductCdmContractsOptions = { + environment: ProductChainEnvironment; + /** Signer manager from @parity/product-sdk-signer, when transactions are in scope. */ + signerManager?: unknown; +}; + +type ContractsModule = typeof import('@parity/product-sdk/contracts'); +type ChainModule = typeof import('@parity/product-sdk/chain'); + +export type ProductCdmContractsDeps = { + loadContracts: () => Promise; + loadChain: () => Promise; + loadDescriptor: (environment: ProductChainEnvironment) => Promise; +}; + +const DESCRIPTOR_LOADERS: Record Promise> = { + paseo: async () => (await import('@parity/product-sdk-descriptors/paseo-asset-hub')).paseo_asset_hub, + devnet: async () => (await import('@parity/product-sdk-descriptors/devnet-asset-hub')).devnet_asset_hub +}; + +const defaultDeps: ProductCdmContractsDeps = { + loadContracts: () => import('@parity/product-sdk/contracts'), + loadChain: () => import('@parity/product-sdk/chain'), + loadDescriptor: environment => DESCRIPTOR_LOADERS[environment]() +}; + +export type ProductCdmContracts = { + resolver: ProductCdmRuntimeContractResolver; + /** + * Confirm the connected chain actually holds Dotify's contracts. Call before + * serving catalog reads: a wrong-chain connection otherwise looks like an + * artist with no releases rather than a misconfiguration. + */ + verifyDeployment: () => Promise; + /** Close the chain connection opened for this resolver. */ + destroy: () => void; +}; + +/** + * Build a live Product CDM contract resolver. + * + * Fails loudly rather than degrading: a caller that cannot reach the chain must + * not silently fall back to another data source, because the artist runtime is + * the authority on access policy. + */ +export async function createProductCdmContracts( + options: ProductCdmContractsOptions, + deps: ProductCdmContractsDeps = defaultDeps +): Promise { + const [contracts, chain, descriptor] = await Promise.all([deps.loadContracts(), deps.loadChain(), deps.loadDescriptor(options.environment)]); + + // createChainClient, not getChainAPI: the zero-config preset table statically + // references every environment's assetHub, bulletin, and individuality + // descriptors, which pulled ~5 MB of chain metadata into the published + // bundle. Passing the one descriptor we resolved keeps that to a single + // lazily-fetched chunk. + let client: Awaited>; + try { + client = await chain.createChainClient({ chains: { assetHub: descriptor } } as never); + } catch (error) { + // The SDK throws here when no host provider is present. Name that, because + // "connection failed" would send an operator hunting for an RPC problem. + throw new ProductCdmRuntimeError( + `Product CDM mode needs a Polkadot Product host connection for the "${options.environment}" environment. Open Dotify inside the Product host, or keep the viem runtime adapter selected. Cause: ${describe(error)}` + ); + } + + const runtime = contracts.createContractRuntimeFromClient(client.raw.assetHub, descriptor); + const manager = new contracts.ContractManager(cdmManifest as never, runtime, { + signerManager: options.signerManager as never + }); + + function manifestAddress(packageName: string): Address { + return manager.getAddress(packageName) as Address; + } + + function requireManifestAddress(packageName: string, requested: Address, label: string): void { + const resolved = manifestAddress(packageName); + if (resolved.toLowerCase() !== requested.toLowerCase()) { + throw new ProductCdmRuntimeError( + `${label} address ${requested} does not match CDM package ${packageName} at ${resolved}. Regenerate the manifest after a redeploy (npm run generate:cdm).` + ); + } + } + + const resolver: ProductCdmRuntimeContractResolver = { + async hasContract(address) { + // Only the manifest's fixed contracts are knowable from a snapshot. A + // per-artist runtime address is not, so callers read false as "not one of + // the manifest contracts", never as "not deployed". + return [DOTIFY_CDM_PACKAGES.directory, DOTIFY_CDM_PACKAGES.factory].some(packageName => { + try { + return manifestAddress(packageName).toLowerCase() === address.toLowerCase(); + } catch { + return false; + } + }); + }, + + getDirectoryContract(directoryAddress) { + requireManifestAddress(DOTIFY_CDM_PACKAGES.directory, directoryAddress, 'ArtistDirectory'); + return manager.getContract(DOTIFY_CDM_PACKAGES.directory) as unknown as ProductCdmContractHandle; + }, + + getFactoryContract(factoryAddress) { + requireManifestAddress(DOTIFY_CDM_PACKAGES.factory, factoryAddress, 'ArtistRuntimeFactory'); + return manager.getContract(DOTIFY_CDM_PACKAGES.factory) as unknown as ProductCdmContractHandle; + }, + + getRuntimeContract(runtimeAddress) { + // Per-artist diamond: bind the merged facet ABI to this address. + return contracts.createContract(runtime, runtimeAddress, smartRuntimeAbi as never, { + signerManager: options.signerManager as never + }) as unknown as ProductCdmContractHandle; + } + }; + + async function verifyDeployment(): Promise { + const directory = manager.getContract(DOTIFY_CDM_PACKAGES.directory) as unknown as ProductCdmContractHandle; + const artistCount = directory.artistCount; + if (!artistCount?.query) { + throw new ProductCdmRuntimeError( + 'Generated CDM manifest has no artistCount query on the ArtistDirectory package. Regenerate it with npm run generate:cdm.' + ); + } + + const result = await artistCount.query(); + if (!result.success) { + throw new ProductCdmRuntimeError( + `ArtistDirectory at ${manifestAddress(DOTIFY_CDM_PACKAGES.directory)} did not answer on the "${options.environment}" chain. Dotify's runtimes are deployed on Polkadot Hub TestNet; confirm the Product host connects that chain before enabling Product CDM mode.` + ); + } + } + + return { resolver, verifyDeployment, destroy: () => client.destroy() }; +} + +function describe(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} diff --git a/web/src/features/runtime/productCdmRuntimeAdapter.ts b/web/src/features/runtime/productCdmRuntimeAdapter.ts index d1384fb..0ea0956 100644 --- a/web/src/features/runtime/productCdmRuntimeAdapter.ts +++ b/web/src/features/runtime/productCdmRuntimeAdapter.ts @@ -253,12 +253,11 @@ export function createProductCdmRuntimeWriter(deps: ProductCdmRuntimeAdapterDeps ]); }, - // UNVERIFIED: the value-transfer argument shape is inferred, not confirmed - // against @parity/product-sdk-contracts. The viem writer passes `value` as - // a sibling of `args`; this assumes the CDM handle takes it as a trailing - // options object. Confirm against generated contract types before this - // adapter is selected - a wrong shape sends a zero-value call, which the - // runtime would reject rather than silently underpay. + // Verified against @parity/product-sdk-contracts: contract methods take + // positional args followed by an optional options object, and `TxOptions` + // carries `value?: bigint`. txContract spreads this array, so the call is + // `musicRoyPayAccess.tx(contentHash, { value })` - the CDM equivalent of + // the viem writer's sibling `value` field. payForAccess(runtimeAddress, contentHash, value) { return txContract(deps.contracts.getRuntimeContract(runtimeAddress), 'musicRoyPayAccess', [contentHash, { value }]); }, @@ -276,12 +275,13 @@ export function createProductCdmRuntimeWriter(deps: ProductCdmRuntimeAdapterDeps return txContract(deps.contracts.getRuntimeContract(runtimeAddress), active ? 'musicRegReactivate' : 'musicRegDeactivate', [contentHash]); }, - // NOT IMPLEMENTED: the viem writer awaits a receipt here, so callers that - // write and then re-read (the artist console does) rely on this settling. - // Returning immediately is only safe if `txContract` already blocks until - // inclusion, which the SDK surface does not state. Until that is confirmed - // against a real host, this adapter must not be selected for writes - - // a caller would read pre-inclusion state and report a phantom failure. + // Intentionally a no-op, and safe by construction: `.tx()` resolves at + // best-block by default and its `TxResult` carries the including block, so + // txContract has already awaited inclusion by the time it returns a hash. + // That is the same point viem's waitForTransactionReceipt resolves at, so + // a caller that writes and then re-reads (the artist console does) sees + // post-inclusion state on both adapters. Verified against + // @parity/product-sdk-tx `SubmitOptions.waitFor` and `TxResult`. async waitForTransaction() { return; } diff --git a/web/src/features/runtime/runtimeAdapterConfig.test.ts b/web/src/features/runtime/runtimeAdapterConfig.test.ts new file mode 100644 index 0000000..e11ef0d --- /dev/null +++ b/web/src/features/runtime/runtimeAdapterConfig.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; +import { resolveRuntimeAdapterConfig } from './runtimeAdapterConfig'; + +describe('resolveRuntimeAdapterConfig', () => { + it('defaults to the viem adapter when nothing is configured', () => { + expect(resolveRuntimeAdapterConfig({})).toEqual({ kind: 'viem', productEnvironment: 'paseo' }); + }); + + it('selects the Product CDM adapter only on an exact opt-in', () => { + expect(resolveRuntimeAdapterConfig({ VITE_DOTIFY_RUNTIME_ADAPTER: 'product-cdm' }).kind).toBe('product-cdm'); + expect(resolveRuntimeAdapterConfig({ VITE_DOTIFY_RUNTIME_ADAPTER: 'PRODUCT-CDM' }).kind).toBe('product-cdm'); + }); + + it('fails closed to viem for an unknown adapter rather than disabling reads', () => { + expect(resolveRuntimeAdapterConfig({ VITE_DOTIFY_RUNTIME_ADAPTER: 'cdm' }).kind).toBe('viem'); + expect(resolveRuntimeAdapterConfig({ VITE_DOTIFY_RUNTIME_ADAPTER: '' }).kind).toBe('viem'); + }); + + it('defaults the Product chain to the preset holding Dotify runtimes', () => { + // Dotify's contracts are on Polkadot Hub TestNet, reached via the paseo + // preset - defaulting to devnet would resolve addresses holding no code. + expect(resolveRuntimeAdapterConfig({ VITE_DOTIFY_RUNTIME_ADAPTER: 'product-cdm' }).productEnvironment).toBe('paseo'); + expect(resolveRuntimeAdapterConfig({ VITE_DOTIFY_PRODUCT_CHAIN: 'nowhere' }).productEnvironment).toBe('paseo'); + }); + + it('accepts an explicit supported Product chain', () => { + expect(resolveRuntimeAdapterConfig({ VITE_DOTIFY_PRODUCT_CHAIN: 'devnet' }).productEnvironment).toBe('devnet'); + }); +}); diff --git a/web/src/features/runtime/runtimeAdapterConfig.ts b/web/src/features/runtime/runtimeAdapterConfig.ts new file mode 100644 index 0000000..9120a5c --- /dev/null +++ b/web/src/features/runtime/runtimeAdapterConfig.ts @@ -0,0 +1,45 @@ +// Which runtime adapter backs the contract ports. +// +// `viem` is the default and the only path with production evidence. `product-cdm` +// is opt-in and additionally requires a Product host container, because the +// Product chain client has no direct-WebSocket fallback. +// +// Selection is deliberately a build-time environment value rather than a +// runtime toggle: switching the authority for access policy is a deployment +// decision an operator makes with evidence, not something a page should be able +// to flip. + +export type RuntimeAdapterKind = 'viem' | 'product-cdm'; + +export type RuntimeAdapterConfig = { + kind: RuntimeAdapterKind; + /** Product chain environment used only when kind is 'product-cdm'. */ + productEnvironment: 'paseo' | 'devnet'; +}; + +type EnvironmentLike = Record; + +const PRODUCT_ENVIRONMENTS = ['paseo', 'devnet'] as const; + +function envValue(env: EnvironmentLike, key: string): string { + const value = env[key]; + return value === null || value === undefined ? '' : String(value).trim().toLowerCase(); +} + +/** + * Resolve the adapter selection, failing closed to `viem` for any unknown + * value. An unrecognised adapter name must not silently disable contract reads. + */ +export function resolveRuntimeAdapterConfig(env: EnvironmentLike): RuntimeAdapterConfig { + const requested = envValue(env, 'VITE_DOTIFY_RUNTIME_ADAPTER'); + const kind: RuntimeAdapterKind = requested === 'product-cdm' ? 'product-cdm' : 'viem'; + + const requestedEnvironment = envValue(env, 'VITE_DOTIFY_PRODUCT_CHAIN'); + // Dotify's runtimes are deployed on Polkadot Hub TestNet, which the Product + // chain client reaches through its `paseo` preset - not `devnet`. + const productEnvironment = (PRODUCT_ENVIRONMENTS as readonly string[]).includes(requestedEnvironment) + ? (requestedEnvironment as RuntimeAdapterConfig['productEnvironment']) + : 'paseo'; + + return { kind, productEnvironment }; +} diff --git a/web/src/features/runtime/runtimeReaderProvider.test.ts b/web/src/features/runtime/runtimeReaderProvider.test.ts new file mode 100644 index 0000000..a505fe2 --- /dev/null +++ b/web/src/features/runtime/runtimeReaderProvider.test.ts @@ -0,0 +1,62 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('./viemRuntimeAdapter', () => ({ + createViemRuntimeReader: vi.fn(() => ({ kind: 'viem', getArtistCount: async () => 7n })), + createViemRuntimeWriter: vi.fn() +})); + +afterEach(() => { + vi.unstubAllEnvs(); + vi.resetModules(); + vi.doUnmock('./productCdmContracts'); +}); + +async function loadProvider() { + return (await import('./runtimeReaderProvider')).createRuntimeReader; +} + +describe('createRuntimeReader', () => { + it('uses the viem reader by default', async () => { + const createRuntimeReader = await loadProvider(); + const reader = createRuntimeReader({ ethRpcUrl: 'https://rpc.example', config: { kind: 'viem', productEnvironment: 'paseo' } }); + + await expect(reader.getArtistCount('0x1' as never)).resolves.toBe(7n); + }); + + it('explains that the Product graph is absent when the build did not opt in', async () => { + // The adapter is selected at build time so an unopted build can tree-shake + // ~5.6 MB of chain metadata away. Asking for it anyway must say exactly + // that, not surface a confusing connection error. + vi.stubEnv('VITE_DOTIFY_RUNTIME_ADAPTER', 'viem'); + vi.resetModules(); + const createRuntimeReader = await loadProvider(); + + const reader = createRuntimeReader({ + ethRpcUrl: 'https://rpc.example', + config: { kind: 'product-cdm', productEnvironment: 'paseo' } + }); + + await expect(reader.getArtistCount('0x1' as never)).rejects.toThrow(/not bundled/); + }); + + it('surfaces a failed Product setup on every read instead of falling back to viem', async () => { + // Silently degrading would leave the adapter in use ambiguous, and the + // artist runtime is the authority on access policy. + vi.stubEnv('VITE_DOTIFY_RUNTIME_ADAPTER', 'product-cdm'); + vi.doMock('./productCdmContracts', () => ({ + createProductCdmContracts: async () => { + throw new Error('no host provider'); + } + })); + vi.resetModules(); + const createRuntimeReader = await loadProvider(); + + const reader = createRuntimeReader({ + ethRpcUrl: 'https://rpc.example', + config: { kind: 'product-cdm', productEnvironment: 'paseo' } + }); + + await expect(reader.getArtistCount('0x1' as never)).rejects.toThrow(/no host provider/); + await expect(reader.ensureContract('0x1' as never)).rejects.toThrow(/no host provider/); + }); +}); diff --git a/web/src/features/runtime/runtimeReaderProvider.ts b/web/src/features/runtime/runtimeReaderProvider.ts new file mode 100644 index 0000000..8c331aa --- /dev/null +++ b/web/src/features/runtime/runtimeReaderProvider.ts @@ -0,0 +1,79 @@ +// Selects the runtime read adapter and hands callers a plain RuntimeReadPort. +// +// Scope note: this switches READS only. Contract writes stay on the viem +// signer path in every mode, because the Product write path still has no +// host-signed transaction evidence and a payment or publication is not +// something to route through an unproven signer. `RuntimeWritePort` therefore +// has no equivalent provider on purpose. +// +// Every RuntimeReadPort method already returns a promise, so the Product +// adapter's asynchronous setup (dynamic import, host connection, deployment +// check) hides behind a facade instead of turning ~16 call sites into +// double-awaits. The underlying port resolves once and is shared. + +import { createViemRuntimeReader } from './viemRuntimeAdapter'; +import { createProductCdmRuntimeReader } from './productCdmRuntimeAdapter'; +import { resolveRuntimeAdapterConfig, type RuntimeAdapterConfig } from './runtimeAdapterConfig'; +import type { RuntimeReadPort } from './runtimePorts'; + +export type RuntimeReaderDeps = { + ethRpcUrl: string; + config?: RuntimeAdapterConfig; +}; + +// Build-time constant, not a runtime check. Vite inlines the env value, so a +// build that did not opt in folds this to `false` and Rollup drops the import +// below along with the whole Product contract graph. +// +// That matters more than it looks: @parity/product-sdk-descriptors keeps a +// shared descriptors module that references every chain's metadata, so pulling +// in a single Asset Hub descriptor drags ~5.6 MB of chain metadata into the +// output. Shipping that to a viem build would inflate every Bulletin +// publication - a finite quota - for code that build can never execute. +const PRODUCT_CDM_ENABLED = import.meta.env.VITE_DOTIFY_RUNTIME_ADAPTER === 'product-cdm'; + +async function createProductCdmReader(config: RuntimeAdapterConfig): Promise { + if (!PRODUCT_CDM_ENABLED) { + throw new Error( + 'This Dotify build was not built with VITE_DOTIFY_RUNTIME_ADAPTER=product-cdm, so the Product contract adapter is not bundled. Rebuild with that flag to use it.' + ); + } + const { createProductCdmContracts } = await import('./productCdmContracts'); + const { resolver, verifyDeployment } = await createProductCdmContracts({ environment: config.productEnvironment }); + // Confirm the host connected a chain that actually holds Dotify's contracts + // before any catalog read runs. Skipping this would surface a wrong-chain + // connection as an empty catalog. + await verifyDeployment(); + return createProductCdmRuntimeReader({ contracts: resolver }); +} + +/** + * Build the configured read port. + * + * The returned object is usable immediately; each call awaits the underlying + * port. A failed Product setup rejects every read with that error rather than + * silently falling back to viem - the adapter in use must never be ambiguous, + * because the artist runtime is the authority on access policy. + */ +export function createRuntimeReader(deps: RuntimeReaderDeps): RuntimeReadPort { + const config = deps.config ?? resolveRuntimeAdapterConfig(import.meta.env); + + if (config.kind === 'viem') { + return createViemRuntimeReader({ ethRpcUrl: deps.ethRpcUrl }); + } + + const portPromise = createProductCdmReader(config); + + return { + ensureContract: (...args) => portPromise.then(port => port.ensureContract(...args)), + resolveArtistRuntime: (...args) => portPromise.then(port => port.resolveArtistRuntime(...args)), + getArtistCount: (...args) => portPromise.then(port => port.getArtistCount(...args)), + listArtistRuntimes: (...args) => portPromise.then(port => port.listArtistRuntimes(...args)), + listRuntimeTracks: (...args) => portPromise.then(port => port.listRuntimeTracks(...args)), + canAccess: (...args) => portPromise.then(port => port.canAccess(...args)), + hasPaid: (...args) => portPromise.then(port => port.hasPaid(...args)), + pendingRuntimeOf: (...args) => portPromise.then(port => port.pendingRuntimeOf(...args)), + pendingRuntimeStageOf: (...args) => portPromise.then(port => port.pendingRuntimeStageOf(...args)), + listRoyaltyPaymentLogs: (...args) => portPromise.then(port => port.listRoyaltyPaymentLogs(...args)) + }; +} diff --git a/web/src/hooks/useArtistConsole.ts b/web/src/hooks/useArtistConsole.ts index 56420ba..228b6db 100644 --- a/web/src/hooks/useArtistConsole.ts +++ b/web/src/hooks/useArtistConsole.ts @@ -13,7 +13,8 @@ import { import { chainMismatchMessage } from '../features/wallet/network'; import { localAudioRef, priceDotForAccessMode, runtimeAddressFromTrackId } from '../features/catalog/trackModel'; import { encodeAccessMode, encodeRequiredPersonhood, manifestRequiredPersonhood } from '../features/runtime/accessEncoding'; -import { createViemRuntimeReader, createViemRuntimeWriter } from '../features/runtime/viemRuntimeAdapter'; +import { createViemRuntimeWriter } from '../features/runtime/viemRuntimeAdapter'; +import { createRuntimeReader } from '../features/runtime/runtimeReaderProvider'; import { resolveConfiguredArtistPublicationSafety } from '../shared/config/deploymentSafety'; import { describeArtistRegistrationError, formatWeiAsDot, shorten, dotToPlanck } from '../shared/utils/format'; import { @@ -232,7 +233,7 @@ export function useArtistConsole(deps: UseArtistConsoleDeps) { coverUploadRef } = deps; - const runtimeReader = createViemRuntimeReader({ ethRpcUrl }); + const runtimeReader = createRuntimeReader({ ethRpcUrl }); const [artistRuntimeAddress, setArtistRuntimeAddress] = useState<`0x${string}` | null>(null); const [artistRegistrationStatus, setArtistRegistrationStatus] = useState('Checking artist registration'); const [isRegisteringArtist, setIsRegisteringArtist] = useState(false); diff --git a/web/src/hooks/useCatalog.ts b/web/src/hooks/useCatalog.ts index 2ae26af..5b35258 100644 --- a/web/src/hooks/useCatalog.ts +++ b/web/src/hooks/useCatalog.ts @@ -24,7 +24,8 @@ import { pumpAudioV2ReadAhead } from '../features/catalog/audioV2Pipeline'; import { AudioV2ChunkAuthenticationError, routeAudioV2MseFailure } from '../features/catalog/audioV2Recovery'; import { runtimeAddressFromTrackId } from '../features/catalog/trackModel'; import { decodeAccessMode, decodePersonhood } from '../features/runtime/accessEncoding'; -import { createViemRuntimeReader, createViemRuntimeWriter } from '../features/runtime/viemRuntimeAdapter'; +import { createViemRuntimeWriter } from '../features/runtime/viemRuntimeAdapter'; +import { createRuntimeReader } from '../features/runtime/runtimeReaderProvider'; import type { RuntimeReadPort, RuntimeTrackSnapshot } from '../features/runtime/runtimePorts'; import { fetchCatalog, isCatalogApiConfigured, readCachedCatalog, type CatalogApiRelease } from '../services/catalog'; import { @@ -272,7 +273,7 @@ export function useCatalog(deps: UseCatalogDeps) { setDescription } = deps; - const runtimeReader = createViemRuntimeReader({ ethRpcUrl }); + const runtimeReader = createRuntimeReader({ ethRpcUrl }); const usesCatalogApi = isCatalogApiConfigured() && !isClassicUnlockE2e && !isArtistPublishE2e && !isRoomJoinE2e; const [initialCatalog] = useState(() => { const cached = usesCatalogApi ? readCachedCatalog() : null; diff --git a/web/src/shared/config/deploymentSafety.test.ts b/web/src/shared/config/deploymentSafety.test.ts index 82ff3fd..96c8e1b 100644 --- a/web/src/shared/config/deploymentSafety.test.ts +++ b/web/src/shared/config/deploymentSafety.test.ts @@ -219,6 +219,50 @@ describe('validateProductionEnvironment', () => { }); }); + it('rejects the Product CDM adapter without a Product host, which cannot reach the chain', () => { + expect( + validateProductionEnvironment({ + ...validProductionEnv, + VITE_DOTIFY_RUNTIME_ADAPTER: 'product-cdm' + }).errors + ).toEqual([ + 'VITE_DOTIFY_RUNTIME_ADAPTER=product-cdm requires VITE_DOTIFY_HOST_MODE to be auto or required: Product contract calls route only through the Product host.' + ]); + }); + + it('rejects an unknown runtime adapter and Product chain preset', () => { + expect( + validateProductionEnvironment({ + ...validProductionEnv, + VITE_DOTIFY_RUNTIME_ADAPTER: 'cdm' + }).errors + ).toEqual(['VITE_DOTIFY_RUNTIME_ADAPTER must be one of viem or product-cdm.']); + + expect( + validateProductionEnvironment({ + ...validProductionEnv, + VITE_DOTIFY_RUNTIME_ADAPTER: 'product-cdm', + VITE_DOTIFY_HOST_MODE: 'required', + VITE_DOTIFY_PRODUCT_ID: 'dotify-test01.dot', + VITE_PUBLIC_APP_URL: 'https://dotify-test01.dev-dot.li', + VITE_DOTIFY_PRODUCT_CHAIN: 'nowhere' + }).errors + ).toEqual(['VITE_DOTIFY_PRODUCT_CHAIN must be one of paseo or devnet.']); + }); + + it('accepts the Product CDM adapter alongside an enabled Product host', () => { + expect( + validateProductionEnvironment({ + ...validProductionEnv, + VITE_DOTIFY_RUNTIME_ADAPTER: 'product-cdm', + VITE_DOTIFY_PRODUCT_CHAIN: 'paseo', + VITE_DOTIFY_HOST_MODE: 'required', + VITE_DOTIFY_PRODUCT_ID: 'dotify-test01.dot', + VITE_PUBLIC_APP_URL: 'https://dotify-test01.dev-dot.li' + }) + ).toEqual({ mode: 'production', errors: [], warnings: [] }); + }); + it('accepts an explicit production environment that keeps secrets server-side', () => { expect(validateProductionEnvironment(validProductionEnv)).toEqual({ mode: 'production', diff --git a/web/src/shared/config/deploymentSafety.ts b/web/src/shared/config/deploymentSafety.ts index ed56209..002703f 100644 --- a/web/src/shared/config/deploymentSafety.ts +++ b/web/src/shared/config/deploymentSafety.ts @@ -259,6 +259,25 @@ export function validateProductionEnvironment(env: EnvironmentLike): ProductionE errors.push('VITE_CONTENT_SECRET is bundled into the browser and must not be set for production builds. Use backend CONTENT_KEY_MASTER_SECRET.'); } + const runtimeAdapter = readEnvironmentValue(env, 'VITE_DOTIFY_RUNTIME_ADAPTER').toLowerCase() || 'viem'; + if (!['viem', 'product-cdm'].includes(runtimeAdapter)) { + errors.push('VITE_DOTIFY_RUNTIME_ADAPTER must be one of viem or product-cdm.'); + } + if (runtimeAdapter === 'product-cdm') { + const productChain = readEnvironmentValue(env, 'VITE_DOTIFY_PRODUCT_CHAIN').toLowerCase() || 'paseo'; + if (!['paseo', 'devnet'].includes(productChain)) { + errors.push('VITE_DOTIFY_PRODUCT_CHAIN must be one of paseo or devnet.'); + } + // The Product chain client only connects through a host container, so a + // production build selecting this adapter without the host would ship a + // frontend that cannot read the catalog at all. + if ((readEnvironmentValue(env, 'VITE_DOTIFY_HOST_MODE').toLowerCase() || 'off') === 'off') { + errors.push( + 'VITE_DOTIFY_RUNTIME_ADAPTER=product-cdm requires VITE_DOTIFY_HOST_MODE to be auto or required: Product contract calls route only through the Product host.' + ); + } + } + const productHostMode = readEnvironmentValue(env, 'VITE_DOTIFY_HOST_MODE').toLowerCase() || 'off'; if (!['off', 'auto', 'required'].includes(productHostMode)) { errors.push('VITE_DOTIFY_HOST_MODE must be one of off, auto, or required.'); From ed701e6d94643165830b11bdaa52f09eda4af9bf Mon Sep 17 00:00:00 2001 From: Kevin Nzeng Essimengane Date: Wed, 29 Jul 2026 03:18:37 +0200 Subject: [PATCH 12/22] docs: record the CDM manifest slice and its remaining gate The blocking unknown for Product contract mode is no longer the manifest or generated types - both now exist and are wired. It is whether the Product host serves a chain that holds Dotify's runtimes, since the host controls that mapping and the contracts are on Polkadot Hub TestNet rather than Product DevNet Asset Hub. Also records the measured build-size trade-off, so an operator weighs it against the Bulletin quota before enabling product-cdm for a .dot deployment. Co-Authored-By: Claude Opus 5 (1M context) --- ...oduct-readiness-and-killer-dapp-roadmap.md | 13 +++- .../product-devnet-architecture.md | 72 ++++++++++++++++--- docs/reference/environment-variables.md | 62 ++++++++++++++++ 3 files changed, 136 insertions(+), 11 deletions(-) diff --git a/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md b/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md index 3129299..5d0f02b 100644 --- a/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md +++ b/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md @@ -174,8 +174,17 @@ Goal: deepen the delivered Product mode one adapter at a time. - Delivered on the next follow-up branch: wire Product-host frontend key and session requests to that signature scheme, while keeping contract writes on the standalone EVM/passkey signer path. -- Next: wire a generated CDM manifest/types into Product mode and run real host - transaction smoke tests. +- Delivered on the next follow-up branch: generate the CDM manifest and typed + contract augmentation from the same Hardhat artifacts as the viem bindings, + and implement the real Product contract resolver behind the runtime ports. + Selection stays opt-in behind `VITE_DOTIFY_RUNTIME_ADAPTER=product-cdm`. +- Next: confirm the Product host serves a chain that holds Dotify's runtimes. + `createChainClient`/`getChainAPI` connect only through the host container, and + the host decides which chain each environment resolves to. Dotify's contracts + are on Polkadot Hub TestNet (EVM chain 420420417), reached through the `paseo` + preset - not `devnet`. This, not the manifest, is now the blocking unknown. +- Next: `pallet-revive` account mapping plus real host-signed transaction smoke + tests before Product writes can replace the EVM wallet path. - Next: run real Product host smoke tests for protected playback and capture the Product sr25519 request evidence. - Keep backend key delivery authoritative unless a Product-host design proves a diff --git a/docs/explanation/product-devnet-architecture.md b/docs/explanation/product-devnet-architecture.md index ab0beae..b9c30d5 100644 --- a/docs/explanation/product-devnet-architecture.md +++ b/docs/explanation/product-devnet-architecture.md @@ -149,10 +149,11 @@ Adapters: - `ViemRuntimeAdapter`: current standalone EVM implementation behind the typed ports; -- `ProductCdmRuntimeAdapter`: experimental CDM/PAPI implementation behind the - same ports. It maps the Dotify runtime method surface to Product SDK contract - handles, but remains opt-in until Dotify has CDM-installed Product runtime - packages and host signing evidence; +- `ProductCdmRuntimeAdapter`: CDM/PAPI implementation behind the same ports, + now backed by a real contract resolver (`productCdmContracts.ts`) over a + generated snapshot manifest. It remains opt-in behind + `VITE_DOTIFY_RUNTIME_ADAPTER=product-cdm` until host transaction evidence + exists; - `CatalogApiAdapter`: the existing server-side read model, shared by both frontends. @@ -162,11 +163,64 @@ method queries and transactions, not the viem-style historical log query used by the artist console. Product mode must use the backend catalog/read-model indexer, or a future Product event/indexer API, for that history. -The remaining Product contract work is integration and evidence work, not UI -rewiring. Operators still need CDM-deployed Dotify runtime packages, -`cdm.json`/generated contract types, `pallet-revive` account mapping, and real -host-signed transaction smoke evidence before Product writes can replace the -EVM wallet path. +### The CDM Manifest Is Generated, Not Installed + +Dotify has no CDM-registered packages, and `cdm install` is not available. It +also does not need them. Dotify's Solidity contracts are deployed through Asset +Hub's `eth-rpc`, which is a compatibility layer over `pallet-revive` - the same +pallet the Product SDK contract helpers target. The deployed H160 addresses are +therefore already reachable through `@parity/product-sdk-contracts` with no +PolkaVM recompilation and no registry entry. + +`CdmJsonContract` needs only `version`, `address`, and `abi` for +`getContract()`, and `new ContractManager(...)` is documented as snapshot-only. +`web/scripts/generate-cdm-manifest.mjs` emits exactly that snapshot from the +same Hardhat artifacts the viem bindings come from, so the two adapters cannot +disagree about an ABI: + +| Output | Contents | +| --- | --- | +| `cdm.json` | `@dotify/artist-directory` and `@dotify/artist-runtime-factory` with their `deployments.json` addresses | +| `smartRuntime.ts` | merged artist-runtime diamond facet ABI, bound to a per-artist address at call time | +| `cdm.d.ts` | `Contracts` module augmentation for typed `getContract()` handles | + +Artist runtimes are deliberately absent from the manifest: a diamond is +deployed per artist, so its address is known at call time, not build time. +Inventing a placeholder address would misrepresent the deployment. +`productCdmContracts.ts` resolves those through `createContract`, which needs no +manifest entry. + +### Two Constraints On Product Contract Mode + +**It only runs inside a Product host.** `createChainClient`/`getChainAPI` route +exclusively through the host provider and throw when none is present - there is +no direct-WebSocket fallback. Product CDM mode is therefore impossible in the +standalone build, and `validateProductionEnvironment` rejects +`VITE_DOTIFY_RUNTIME_ADAPTER=product-cdm` unless `VITE_DOTIFY_HOST_MODE` is +enabled. + +**The host decides which chain an environment resolves to.** Dotify's runtimes +are deployed on Polkadot Hub TestNet (EVM chain `420420417`), which the Product +chain client reaches through its `paseo` preset - *not* `devnet`. If the host +connects an environment that does not hold them, every manifest address +resolves to an account with no code, which would look like a catalog of artists +with no releases. `verifyDeployment()` queries `artistCount` on the directory +and fails closed with a named error instead. + +**Selection is build-time, and reads only.** `VITE_DOTIFY_RUNTIME_ADAPTER` is +inlined by Vite, so a `viem` build tree-shakes the entire Product contract graph +away - 4.4 MB output versus 10 MB when opted in. The difference is +`@parity/product-sdk-descriptors`, whose shared descriptors module references +every chain's metadata; only one chunk is ever fetched, but all are published, +and Bulletin storage is a finite quota. Contract *writes* stay on the viem +signer path in every mode, since routing a payment or a publication through an +unproven signer is not a reasonable default. + +This is the real remaining gate for Product contract writes: not UI rewiring, +and no longer missing manifest or types, but confirming the Product host serves +a chain that holds Dotify's runtimes, plus `pallet-revive` account mapping and +real host-signed transaction smoke evidence. Until that evidence exists, +`VITE_DOTIFY_RUNTIME_ADAPTER` defaults to `viem`. The backend authentication protocol now has an explicit signature scheme field. Standalone clients use the default `eip191` scheme. Product-host clients can diff --git a/docs/reference/environment-variables.md b/docs/reference/environment-variables.md index beef044..7670638 100644 --- a/docs/reference/environment-variables.md +++ b/docs/reference/environment-variables.md @@ -55,6 +55,68 @@ the listener selects **Use Polkadot app**. --- +### `VITE_DOTIFY_RUNTIME_ADAPTER` + +| Property | Value | +| ------------ | ------------------------ | +| **Type** | `viem` or `product-cdm` | +| **Required** | No | +| **Default** | `viem` | +| **Example** | `viem` | + +Selects which adapter backs the runtime contract ports. `viem` is the only path +with production evidence. `product-cdm` routes reads and writes through the +Product SDK contract handles over the generated `cdm.json` snapshot. + +This selects **reads only**. Contract writes stay on the viem signer path in +every mode, because the Product write path has no host-signed transaction +evidence yet. + +Any unrecognised value falls back to `viem`, so a typo cannot silently disable +contract reads. `product-cdm` additionally requires `VITE_DOTIFY_HOST_MODE` to +be `auto` or `required`: the Product chain client connects only through a host +container and has no direct-WebSocket fallback. The production guard rejects +that combination rather than shipping a frontend that cannot read the catalog. + +**Build size.** This flag is read at build time, not runtime. A `viem` build +tree-shakes the entire Product contract graph away; opting in pulls it back in +along with `@parity/product-sdk-descriptors`, whose shared descriptors module +references every chain's metadata. Measured on this branch: + +| Build | Output size | +| --- | --- | +| `VITE_DOTIFY_RUNTIME_ADAPTER` unset or `viem` | 4.4 MB | +| `VITE_DOTIFY_RUNTIME_ADAPTER=product-cdm` | 10 MB | + +Only one metadata chunk is ever fetched at runtime, but all of them are +published. Weigh that against the Bulletin storage quota before enabling this +for a `.dot` deployment. + +--- + +### `VITE_DOTIFY_PRODUCT_CHAIN` + +| Property | Value | +| ------------ | ---------------------------------------------- | +| **Type** | `paseo`, `devnet`, `polkadot`, or `kusama` | +| **Required** | No | +| **Default** | `paseo` | +| **Example** | `paseo` | + +Product chain preset used only when `VITE_DOTIFY_RUNTIME_ADAPTER=product-cdm`. + +The default is `paseo`, not `devnet`: Dotify's runtimes are deployed on +Polkadot Hub TestNet (EVM chain `420420417`), which the Product chain client +reaches through its `paseo` preset. Pointing this at a chain that does not hold +those contracts resolves every manifest address to an account with no code - +indistinguishable from artists with no releases. `verifyDeployment()` turns +that into an explicit error at startup. + +Regenerate the manifest with `npm run generate:cdm` after any contract +redeploy, or the addresses in `cdm.json` go stale. + +--- + ### `VITE_DOTIFY_PRODUCT_ID` | Property | Value | From 924653740d46e5984d08a1fe831493bea4999742 Mon Sep 17 00:00:00 2001 From: Kevin Nzeng Essimengane Date: Wed, 29 Jul 2026 04:40:30 +0200 Subject: [PATCH 13/22] fix: target the devnet preset, the only chain holding Dotify runtimes The previous default was wrong in a way that would have produced an empty catalog. Product DevNet is not a separate network: it is a preset over the Paseo system parachains - Asset Hub (1000), People (1004), Bulletin (1010) - at EVM chain 420420417. That is exactly where Dotify is already deployed. Verified read-only against both endpoints for the ArtistDirectory at 0xcf1534c6e2b0e43b9436c1e86a076466dc0f2108: eth-rpc-testnet.polkadot.io and paseo-assethub-rpc.laissez-faire.trade both report chain id 0x190f1b41, blocks one apart, and byte-identical contract code. They are two providers for one chain, so no contract redeploy is needed to port Dotify to DevNet. The SDK's `paseo` preset is the trap: it targets Paseo Next (Asset Hub Next 1500 / People Next 1502), which the Product docs call a different network where "funds sent there will not appear on this Devnet". Dotify has no deployment there, so ProductChainEnvironment now admits only `devnet` - selecting a chain that cannot hold the catalog is a bug, not a configuration option. Also realigns the environment reference tables, clearing the markdownlint MD060 warnings. Co-Authored-By: Claude Opus 5 (1M context) --- ...oduct-readiness-and-killer-dapp-roadmap.md | 15 ++- .../product-devnet-architecture.md | 48 ++++++-- docs/reference/environment-variables.md | 105 +++++++++--------- .../runtime/productCdmContracts.test.ts | 12 +- .../features/runtime/productCdmContracts.ts | 29 +++-- .../runtime/runtimeAdapterConfig.test.ts | 16 +-- .../features/runtime/runtimeAdapterConfig.ts | 12 +- .../runtime/runtimeReaderProvider.test.ts | 6 +- .../shared/config/deploymentSafety.test.ts | 6 +- web/src/shared/config/deploymentSafety.ts | 6 +- 10 files changed, 153 insertions(+), 102 deletions(-) diff --git a/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md b/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md index 5d0f02b..d6ae7ed 100644 --- a/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md +++ b/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md @@ -178,13 +178,16 @@ Goal: deepen the delivered Product mode one adapter at a time. contract augmentation from the same Hardhat artifacts as the viem bindings, and implement the real Product contract resolver behind the runtime ports. Selection stays opt-in behind `VITE_DOTIFY_RUNTIME_ADAPTER=product-cdm`. -- Next: confirm the Product host serves a chain that holds Dotify's runtimes. - `createChainClient`/`getChainAPI` connect only through the host container, and - the host decides which chain each environment resolves to. Dotify's contracts - are on Polkadot Hub TestNet (EVM chain 420420417), reached through the `paseo` - preset - not `devnet`. This, not the manifest, is now the blocking unknown. +- Settled: the chain question. Product DevNet is a preset over the Paseo system + parachains (Asset Hub 1000, People 1004, Bulletin 1010) at EVM chain + 420420417, not a separate network. Dotify's contracts are already there, + verified by byte-identical ArtistDirectory code served from both the DevNet + and Hub TestNet endpoints. No contract redeploy is needed to port to DevNet. + The SDK's `paseo` preset is Paseo Next (1500/1502), a different network, so + `devnet` is the only environment Dotify can serve a catalog from. - Next: `pallet-revive` account mapping plus real host-signed transaction smoke - tests before Product writes can replace the EVM wallet path. + tests before Product writes can replace the EVM wallet path. This is now the + only gate left for Product contract mode. - Next: run real Product host smoke tests for protected playback and capture the Product sr25519 request evidence. - Keep backend key delivery authoritative unless a Product-host design proves a diff --git a/docs/explanation/product-devnet-architecture.md b/docs/explanation/product-devnet-architecture.md index b9c30d5..516c40a 100644 --- a/docs/explanation/product-devnet-architecture.md +++ b/docs/explanation/product-devnet-architecture.md @@ -199,13 +199,39 @@ standalone build, and `validateProductionEnvironment` rejects `VITE_DOTIFY_RUNTIME_ADAPTER=product-cdm` unless `VITE_DOTIFY_HOST_MODE` is enabled. -**The host decides which chain an environment resolves to.** Dotify's runtimes -are deployed on Polkadot Hub TestNet (EVM chain `420420417`), which the Product -chain client reaches through its `paseo` preset - *not* `devnet`. If the host -connects an environment that does not hold them, every manifest address -resolves to an account with no code, which would look like a catalog of artists -with no releases. `verifyDeployment()` queries `artistCount` on the directory -and fails closed with a named error instead. +**The host decides which chain an environment resolves to**, and only one +environment is correct. See "DevNet Is Not A Separate Chain" below. +`verifyDeployment()` queries `artistCount` on the directory before any catalog +read, so a wrong-chain connection fails closed with a named error instead of +looking like a catalog of artists with no releases. + +### DevNet Is Not A Separate Chain + +Product DevNet is a *preset*, not a network. It targets the Paseo system +parachains - Asset Hub (1000), People (1004), Bulletin (1010) - with EVM chain +id `420420417` and the `dev-dot.li` web gateway. + +That is the chain Dotify is already deployed on. Verified read-only on +2026-07-29 by querying both endpoints for the ArtistDirectory at +`0xcf1534c6e2b0e43b9436c1e86a076466dc0f2108`: + +| Endpoint | `eth_chainId` | Block | Directory bytecode | +| --- | --- | --- | --- | +| `https://eth-rpc-testnet.polkadot.io/` | `0x190f1b41` | 11546347 | 3660 chars, sha256 `36707b24…` | +| `https://paseo-assethub-rpc.laissez-faire.trade` | `0x190f1b41` | 11546348 | 3660 chars, sha256 `36707b24…` | + +Same chain id, blocks one apart, byte-identical contract code. The two URLs are +different providers for one chain. + +**No contract redeploy is required to port Dotify to Product DevNet.** The +addresses in `deployments.json` are already DevNet addresses. + +The trap is the SDK's `paseo` preset, which points at the Paseo **Next** v2 +deployment (Asset Hub Next 1500 / People Next 1502). The Product documentation +is explicit that those "belong to a different network" and that "funds sent +there will not appear on this Devnet". Dotify has no deployment there, so +`ProductChainEnvironment` admits only `devnet` - a wrong preset is not a +configuration option, it is a bug. **Selection is build-time, and reads only.** `VITE_DOTIFY_RUNTIME_ADAPTER` is inlined by Vite, so a `viem` build tree-shakes the entire Product contract graph @@ -216,10 +242,10 @@ and Bulletin storage is a finite quota. Contract *writes* stay on the viem signer path in every mode, since routing a payment or a publication through an unproven signer is not a reasonable default. -This is the real remaining gate for Product contract writes: not UI rewiring, -and no longer missing manifest or types, but confirming the Product host serves -a chain that holds Dotify's runtimes, plus `pallet-revive` account mapping and -real host-signed transaction smoke evidence. Until that evidence exists, +The remaining gate for Product contract *writes* is now narrow: `pallet-revive` +account mapping for the signing account, and real host-signed transaction smoke +evidence from inside the container. The chain question is settled, the manifest +and types exist, and reads are wired. Until that write evidence exists, `VITE_DOTIFY_RUNTIME_ADAPTER` defaults to `viem`. The backend authentication protocol now has an explicit signature scheme field. diff --git a/docs/reference/environment-variables.md b/docs/reference/environment-variables.md index 7670638..2f38cec 100644 --- a/docs/reference/environment-variables.md +++ b/docs/reference/environment-variables.md @@ -38,12 +38,12 @@ production build contract without printing real secret values. ### `VITE_DOTIFY_HOST_MODE` -| Property | Value | -| ------------ | ------------------------------ | -| **Type** | `off`, `auto`, or `required` | -| **Required** | Product builds | -| **Default** | `off` | -| **Example** | `required` | +| Property | Value | +| ------------ | ---------------------------- | +| **Type** | `off`, `auto`, or `required` | +| **Required** | Product builds | +| **Default** | `off` | +| **Example** | `required` | Controls Product host discovery. `off` keeps the standalone app independent from the Product SDK. `auto` enables progressive host detection. `required` @@ -57,12 +57,12 @@ the listener selects **Use Polkadot app**. ### `VITE_DOTIFY_RUNTIME_ADAPTER` -| Property | Value | -| ------------ | ------------------------ | -| **Type** | `viem` or `product-cdm` | -| **Required** | No | -| **Default** | `viem` | -| **Example** | `viem` | +| Property | Value | +| ------------ | ----------------------- | +| **Type** | `viem` or `product-cdm` | +| **Required** | No | +| **Default** | `viem` | +| **Example** | `viem` | Selects which adapter backs the runtime contract ports. `viem` is the only path with production evidence. `product-cdm` routes reads and writes through the @@ -83,10 +83,10 @@ tree-shakes the entire Product contract graph away; opting in pulls it back in along with `@parity/product-sdk-descriptors`, whose shared descriptors module references every chain's metadata. Measured on this branch: -| Build | Output size | -| --- | --- | -| `VITE_DOTIFY_RUNTIME_ADAPTER` unset or `viem` | 4.4 MB | -| `VITE_DOTIFY_RUNTIME_ADAPTER=product-cdm` | 10 MB | +| Build | Output size | +| --------------------------------------------- | ----------- | +| `VITE_DOTIFY_RUNTIME_ADAPTER` unset or `viem` | 4.4 MB | +| `VITE_DOTIFY_RUNTIME_ADAPTER=product-cdm` | 10 MB | Only one metadata chunk is ever fetched at runtime, but all of them are published. Weigh that against the Bulletin storage quota before enabling this @@ -96,21 +96,26 @@ for a `.dot` deployment. ### `VITE_DOTIFY_PRODUCT_CHAIN` -| Property | Value | -| ------------ | ---------------------------------------------- | -| **Type** | `paseo`, `devnet`, `polkadot`, or `kusama` | -| **Required** | No | -| **Default** | `paseo` | -| **Example** | `paseo` | +| Property | Value | +| ------------ | -------- | +| **Type** | `devnet` | +| **Required** | No | +| **Default** | `devnet` | +| **Example** | `devnet` | Product chain preset used only when `VITE_DOTIFY_RUNTIME_ADAPTER=product-cdm`. -The default is `paseo`, not `devnet`: Dotify's runtimes are deployed on -Polkadot Hub TestNet (EVM chain `420420417`), which the Product chain client -reaches through its `paseo` preset. Pointing this at a chain that does not hold -those contracts resolves every manifest address to an account with no code - -indistinguishable from artists with no releases. `verifyDeployment()` turns -that into an explicit error at startup. +`devnet` is the only accepted value, and that is a correctness constraint. +Product DevNet is a preset over the Paseo system parachains - Asset Hub (1000), +People (1004), Bulletin (1010) - at EVM chain `420420417`, which is exactly +where Dotify's contracts are deployed. + +The SDK's `paseo` preset is *not* an alternative: it targets Paseo Next +(Asset Hub Next 1500 / People Next 1502), which the Product documentation calls +a different network. Selecting it would resolve every manifest address to an +account with no code - indistinguishable from artists with no releases. +`verifyDeployment()` turns that into an explicit error at startup, and the +config layer refuses the value outright. Regenerate the manifest with `npm run generate:cdm` after any contract redeploy, or the addresses in `cdm.json` go stale. @@ -134,11 +139,11 @@ identity/access migration review. ### `VITE_PUBLIC_APP_URL` -| Property | Value | -| ------------ | -------------------------------- | -| **Type** | HTTPS URL | -| **Required** | Product production builds | -| **Default** | Current browser URL | +| Property | Value | +| ------------ | ---------------------------------- | +| **Type** | HTTPS URL | +| **Required** | Product production builds | +| **Default** | Current browser URL | | **Example** | `https://dotify-test01.dev-dot.li` | Canonical public origin used when copying room links. Product builds must set @@ -148,12 +153,12 @@ this so invitations never expose an internal host/container or raw gateway URL. ### `VITE_DOTIFY_DEBUG_PANEL` -| Property | Value | -| ------------ | ----------------- | -| **Type** | Boolean string | -| **Required** | No | -| **Default** | `false` | -| **Example** | `true` | +| Property | Value | +| ------------ | -------------- | +| **Type** | Boolean string | +| **Required** | No | +| **Default** | `false` | +| **Example** | `true` | Enables the optional Production readiness panel under the `You` tab. The panel performs read-only checks for the backend readiness endpoint, signaling health, @@ -353,11 +358,11 @@ Network interface to bind. ### `SIGNAL_ORIGINS` -| Property | Value | -| ------------ | --------------------------------------------------- | -| **Type** | Comma-separated URL list or `*` | -| **Required** | No | -| **Default** | `*` | +| Property | Value | +| ------------ | -------------------------------------------------------------- | +| **Type** | Comma-separated URL list or `*` | +| **Required** | No | +| **Default** | `*` | | **Example** | `https://muzinga.netlify.app,https://dotify-test01.dev-dot.li` | CORS allowed origins for Socket.IO and status endpoints. Set explicit frontend @@ -446,12 +451,12 @@ backwards-compatible fallback when `API_ORIGINS` is not set. ### `API_ORIGINS` -| Property | Value | -| ------------ | -------------------------------------------------------- | -| **Type** | Comma-separated HTTPS origin list | -| **Required** | Multiple hosted frontends | -| **Default** | The single `API_ORIGIN` value | -| **Example** | `https://muzinga.netlify.app,https://dotify-test01.dev-dot.li` | +| Property | Value | +| ------------ | -------------------------------------------------------------- | +| **Type** | Comma-separated HTTPS origin list | +| **Required** | Multiple hosted frontends | +| **Default** | The single `API_ORIGIN` value | +| **Example** | `https://muzinga.netlify.app,https://dotify-test01.dev-dot.li` | Exact frontend origins accepted by backend CORS. When set, it takes precedence over `API_ORIGIN`. Do not use `*`: the API carries authenticated upload and diff --git a/web/src/features/runtime/productCdmContracts.test.ts b/web/src/features/runtime/productCdmContracts.test.ts index 10f915a..3bb1a41 100644 --- a/web/src/features/runtime/productCdmContracts.test.ts +++ b/web/src/features/runtime/productCdmContracts.test.ts @@ -40,7 +40,7 @@ function buildDeps(overrides: Handles = {}, spies: Record { it('resolves the manifest contracts by their deployed addresses', async () => { - const { resolver } = await createProductCdmContracts({ environment: 'paseo' }, buildDeps()); + const { resolver } = await createProductCdmContracts({ environment: 'devnet' }, buildDeps()); expect(() => resolver.getDirectoryContract(DIRECTORY)).not.toThrow(); expect(() => resolver.getFactoryContract(FACTORY)).not.toThrow(); @@ -49,14 +49,14 @@ describe('createProductCdmContracts', () => { }); it('refuses a directory address that does not match the manifest', async () => { - const { resolver } = await createProductCdmContracts({ environment: 'paseo' }, buildDeps()); + const { resolver } = await createProductCdmContracts({ environment: 'devnet' }, buildDeps()); expect(() => resolver.getDirectoryContract(RUNTIME)).toThrow(/does not match CDM package/); }); it('binds the merged facet ABI to a per-artist runtime address', async () => { const createContract = vi.fn((..._args: unknown[]) => ({ musicAccCanAccess: { query: vi.fn() } })); - const { resolver } = await createProductCdmContracts({ environment: 'paseo' }, buildDeps({}, { createContract })); + const { resolver } = await createProductCdmContracts({ environment: 'devnet' }, buildDeps({}, { createContract })); resolver.getRuntimeContract(RUNTIME); @@ -71,7 +71,9 @@ describe('createProductCdmContracts', () => { throw new Error('no host provider'); }); - await expect(createProductCdmContracts({ environment: 'paseo' }, buildDeps({}, { createChainClient }))).rejects.toThrow(/Polkadot Product host connection/); + await expect(createProductCdmContracts({ environment: 'devnet' }, buildDeps({}, { createChainClient }))).rejects.toThrow( + /Polkadot Product host connection/ + ); }); it('verifyDeployment rejects a chain that does not answer for the directory', async () => { @@ -82,7 +84,7 @@ describe('createProductCdmContracts', () => { }); it('verifyDeployment passes when the directory answers', async () => { - const { verifyDeployment } = await createProductCdmContracts({ environment: 'paseo' }, buildDeps()); + const { verifyDeployment } = await createProductCdmContracts({ environment: 'devnet' }, buildDeps()); await expect(verifyDeployment()).resolves.toBeUndefined(); }); diff --git a/web/src/features/runtime/productCdmContracts.ts b/web/src/features/runtime/productCdmContracts.ts index 39e0c15..fc513da 100644 --- a/web/src/features/runtime/productCdmContracts.ts +++ b/web/src/features/runtime/productCdmContracts.ts @@ -47,15 +47,27 @@ export const DOTIFY_CDM_PACKAGES: ProductCdmRuntimePackages = { }; /** - * Product chain environments Dotify can target. + * The Product chain environment Dotify targets. * - * Deliberately not the SDK's full preset list. Each descriptor is a ~850 kB - * metadata chunk that ships with the Bulletin publication whether or not it is - * fetched, and Bulletin storage is a finite quota. Dotify has no deployment on - * Polkadot or Kusama Asset Hub, so carrying their metadata would be dead - * weight. Add one here only when Dotify actually deploys there. + * Only `devnet`, and that is a correctness constraint rather than a + * simplification. Product DevNet is not a separate chain: it is a preset over + * the Paseo system parachains - Asset Hub (1000), People (1004), Bulletin + * (1010) - with EVM chain id 420420417. That is exactly where Dotify's + * contracts are already deployed, confirmed by identical ArtistDirectory + * bytecode served from both the DevNet and Hub TestNet endpoints. + * + * The SDK's `paseo` preset is a trap here: it points at the Paseo **Next** v2 + * deployment (Asset Hub Next 1500 / People Next 1502), which the Product docs + * describe as "a different network" where "funds sent there will not appear on + * this Devnet". Dotify has no deployment there, and none on Polkadot or Kusama + * Asset Hub. Offering those presets would only let an operator select a chain + * that cannot hold the catalog. + * + * Each descriptor is also a ~850 kB metadata chunk that ships with the Bulletin + * publication whether or not it is fetched, and Bulletin storage is a finite + * quota. Add an environment here only when Dotify actually deploys there. */ -export type ProductChainEnvironment = 'paseo' | 'devnet'; +export type ProductChainEnvironment = 'devnet'; export type ProductCdmContractsOptions = { environment: ProductChainEnvironment; @@ -73,7 +85,6 @@ export type ProductCdmContractsDeps = { }; const DESCRIPTOR_LOADERS: Record Promise> = { - paseo: async () => (await import('@parity/product-sdk-descriptors/paseo-asset-hub')).paseo_asset_hub, devnet: async () => (await import('@parity/product-sdk-descriptors/devnet-asset-hub')).devnet_asset_hub }; @@ -186,7 +197,7 @@ export async function createProductCdmContracts( const result = await artistCount.query(); if (!result.success) { throw new ProductCdmRuntimeError( - `ArtistDirectory at ${manifestAddress(DOTIFY_CDM_PACKAGES.directory)} did not answer on the "${options.environment}" chain. Dotify's runtimes are deployed on Polkadot Hub TestNet; confirm the Product host connects that chain before enabling Product CDM mode.` + `ArtistDirectory at ${manifestAddress(DOTIFY_CDM_PACKAGES.directory)} did not answer on the "${options.environment}" chain. Dotify's runtimes live on Paseo Asset Hub (parachain 1000, EVM chain 420420417), which is what the Product DevNet preset targets; confirm the host connected that chain and not Asset Hub Next (1500), which is a different network.` ); } } diff --git a/web/src/features/runtime/runtimeAdapterConfig.test.ts b/web/src/features/runtime/runtimeAdapterConfig.test.ts index e11ef0d..152a593 100644 --- a/web/src/features/runtime/runtimeAdapterConfig.test.ts +++ b/web/src/features/runtime/runtimeAdapterConfig.test.ts @@ -3,7 +3,7 @@ import { resolveRuntimeAdapterConfig } from './runtimeAdapterConfig'; describe('resolveRuntimeAdapterConfig', () => { it('defaults to the viem adapter when nothing is configured', () => { - expect(resolveRuntimeAdapterConfig({})).toEqual({ kind: 'viem', productEnvironment: 'paseo' }); + expect(resolveRuntimeAdapterConfig({})).toEqual({ kind: 'viem', productEnvironment: 'devnet' }); }); it('selects the Product CDM adapter only on an exact opt-in', () => { @@ -17,13 +17,15 @@ describe('resolveRuntimeAdapterConfig', () => { }); it('defaults the Product chain to the preset holding Dotify runtimes', () => { - // Dotify's contracts are on Polkadot Hub TestNet, reached via the paseo - // preset - defaulting to devnet would resolve addresses holding no code. - expect(resolveRuntimeAdapterConfig({ VITE_DOTIFY_RUNTIME_ADAPTER: 'product-cdm' }).productEnvironment).toBe('paseo'); - expect(resolveRuntimeAdapterConfig({ VITE_DOTIFY_PRODUCT_CHAIN: 'nowhere' }).productEnvironment).toBe('paseo'); + // Product DevNet is a preset over Paseo Asset Hub 1000 (EVM chain + // 420420417), which is where Dotify's contracts already live. + expect(resolveRuntimeAdapterConfig({ VITE_DOTIFY_RUNTIME_ADAPTER: 'product-cdm' }).productEnvironment).toBe('devnet'); }); - it('accepts an explicit supported Product chain', () => { - expect(resolveRuntimeAdapterConfig({ VITE_DOTIFY_PRODUCT_CHAIN: 'devnet' }).productEnvironment).toBe('devnet'); + it('refuses the paseo preset, which is a different network from Product DevNet', () => { + // The SDK's `paseo` preset is Paseo Next (Asset Hub Next 1500). Dotify has + // no deployment there, so it must never be selectable by configuration. + expect(resolveRuntimeAdapterConfig({ VITE_DOTIFY_PRODUCT_CHAIN: 'paseo' }).productEnvironment).toBe('devnet'); + expect(resolveRuntimeAdapterConfig({ VITE_DOTIFY_PRODUCT_CHAIN: 'nowhere' }).productEnvironment).toBe('devnet'); }); }); diff --git a/web/src/features/runtime/runtimeAdapterConfig.ts b/web/src/features/runtime/runtimeAdapterConfig.ts index 9120a5c..7409064 100644 --- a/web/src/features/runtime/runtimeAdapterConfig.ts +++ b/web/src/features/runtime/runtimeAdapterConfig.ts @@ -14,12 +14,12 @@ export type RuntimeAdapterKind = 'viem' | 'product-cdm'; export type RuntimeAdapterConfig = { kind: RuntimeAdapterKind; /** Product chain environment used only when kind is 'product-cdm'. */ - productEnvironment: 'paseo' | 'devnet'; + productEnvironment: 'devnet'; }; type EnvironmentLike = Record; -const PRODUCT_ENVIRONMENTS = ['paseo', 'devnet'] as const; +const PRODUCT_ENVIRONMENTS = ['devnet'] as const; function envValue(env: EnvironmentLike, key: string): string { const value = env[key]; @@ -35,11 +35,13 @@ export function resolveRuntimeAdapterConfig(env: EnvironmentLike): RuntimeAdapte const kind: RuntimeAdapterKind = requested === 'product-cdm' ? 'product-cdm' : 'viem'; const requestedEnvironment = envValue(env, 'VITE_DOTIFY_PRODUCT_CHAIN'); - // Dotify's runtimes are deployed on Polkadot Hub TestNet, which the Product - // chain client reaches through its `paseo` preset - not `devnet`. + // Product DevNet is a preset over the Paseo system parachains (Asset Hub + // 1000, EVM chain 420420417) - exactly where Dotify's contracts already live. + // The SDK's `paseo` preset points at Paseo Next instead, a different network, + // so `devnet` is the only environment Dotify can serve a catalog from. const productEnvironment = (PRODUCT_ENVIRONMENTS as readonly string[]).includes(requestedEnvironment) ? (requestedEnvironment as RuntimeAdapterConfig['productEnvironment']) - : 'paseo'; + : 'devnet'; return { kind, productEnvironment }; } diff --git a/web/src/features/runtime/runtimeReaderProvider.test.ts b/web/src/features/runtime/runtimeReaderProvider.test.ts index a505fe2..8d4612f 100644 --- a/web/src/features/runtime/runtimeReaderProvider.test.ts +++ b/web/src/features/runtime/runtimeReaderProvider.test.ts @@ -18,7 +18,7 @@ async function loadProvider() { describe('createRuntimeReader', () => { it('uses the viem reader by default', async () => { const createRuntimeReader = await loadProvider(); - const reader = createRuntimeReader({ ethRpcUrl: 'https://rpc.example', config: { kind: 'viem', productEnvironment: 'paseo' } }); + const reader = createRuntimeReader({ ethRpcUrl: 'https://rpc.example', config: { kind: 'viem', productEnvironment: 'devnet' } }); await expect(reader.getArtistCount('0x1' as never)).resolves.toBe(7n); }); @@ -33,7 +33,7 @@ describe('createRuntimeReader', () => { const reader = createRuntimeReader({ ethRpcUrl: 'https://rpc.example', - config: { kind: 'product-cdm', productEnvironment: 'paseo' } + config: { kind: 'product-cdm', productEnvironment: 'devnet' } }); await expect(reader.getArtistCount('0x1' as never)).rejects.toThrow(/not bundled/); @@ -53,7 +53,7 @@ describe('createRuntimeReader', () => { const reader = createRuntimeReader({ ethRpcUrl: 'https://rpc.example', - config: { kind: 'product-cdm', productEnvironment: 'paseo' } + config: { kind: 'product-cdm', productEnvironment: 'devnet' } }); await expect(reader.getArtistCount('0x1' as never)).rejects.toThrow(/no host provider/); diff --git a/web/src/shared/config/deploymentSafety.test.ts b/web/src/shared/config/deploymentSafety.test.ts index 96c8e1b..2ce475d 100644 --- a/web/src/shared/config/deploymentSafety.test.ts +++ b/web/src/shared/config/deploymentSafety.test.ts @@ -245,9 +245,9 @@ describe('validateProductionEnvironment', () => { VITE_DOTIFY_HOST_MODE: 'required', VITE_DOTIFY_PRODUCT_ID: 'dotify-test01.dot', VITE_PUBLIC_APP_URL: 'https://dotify-test01.dev-dot.li', - VITE_DOTIFY_PRODUCT_CHAIN: 'nowhere' + VITE_DOTIFY_PRODUCT_CHAIN: 'paseo' }).errors - ).toEqual(['VITE_DOTIFY_PRODUCT_CHAIN must be one of paseo or devnet.']); + ).toEqual(['VITE_DOTIFY_PRODUCT_CHAIN must be devnet: Product DevNet targets Paseo Asset Hub 1000, the only chain holding Dotify runtimes.']); }); it('accepts the Product CDM adapter alongside an enabled Product host', () => { @@ -255,7 +255,7 @@ describe('validateProductionEnvironment', () => { validateProductionEnvironment({ ...validProductionEnv, VITE_DOTIFY_RUNTIME_ADAPTER: 'product-cdm', - VITE_DOTIFY_PRODUCT_CHAIN: 'paseo', + VITE_DOTIFY_PRODUCT_CHAIN: 'devnet', VITE_DOTIFY_HOST_MODE: 'required', VITE_DOTIFY_PRODUCT_ID: 'dotify-test01.dot', VITE_PUBLIC_APP_URL: 'https://dotify-test01.dev-dot.li' diff --git a/web/src/shared/config/deploymentSafety.ts b/web/src/shared/config/deploymentSafety.ts index 002703f..d311d84 100644 --- a/web/src/shared/config/deploymentSafety.ts +++ b/web/src/shared/config/deploymentSafety.ts @@ -264,9 +264,9 @@ export function validateProductionEnvironment(env: EnvironmentLike): ProductionE errors.push('VITE_DOTIFY_RUNTIME_ADAPTER must be one of viem or product-cdm.'); } if (runtimeAdapter === 'product-cdm') { - const productChain = readEnvironmentValue(env, 'VITE_DOTIFY_PRODUCT_CHAIN').toLowerCase() || 'paseo'; - if (!['paseo', 'devnet'].includes(productChain)) { - errors.push('VITE_DOTIFY_PRODUCT_CHAIN must be one of paseo or devnet.'); + const productChain = readEnvironmentValue(env, 'VITE_DOTIFY_PRODUCT_CHAIN').toLowerCase() || 'devnet'; + if (!['devnet'].includes(productChain)) { + errors.push('VITE_DOTIFY_PRODUCT_CHAIN must be devnet: Product DevNet targets Paseo Asset Hub 1000, the only chain holding Dotify runtimes.'); } // The Product chain client only connects through a host container, so a // production build selecting this adapter without the host would ship a From b587bd7ae4f5e0bed97d6eb19176bfffcca040b4 Mon Sep 17 00:00:00 2001 From: Kevin Nzeng Essimengane Date: Wed, 29 Jul 2026 04:46:46 +0200 Subject: [PATCH 14/22] feat: prove the DevNet endpoint set with a read-only smoke check Porting Dotify to Product DevNet turned out to be a configuration question rather than a migration: DevNet is a preset over the Paseo system parachains (Asset Hub 1000, People 1004, Bulletin 1010) at EVM chain 420420417, which is where Dotify's contracts already are. This makes that claim checkable instead of asserted. `npm run smoke:devnet` reads web/.env.product-devnet and deployments.json and verifies, read-only, that the configured Asset Hub reports chain 420420417, is producing blocks past the 2026-07 halt, still serves bytecode for the ArtistDirectory and ArtistRuntimeFactory, and that the Bulletin RPC and IPFS gateway respond. It sends no transaction, reads no secret, and prints no credential. Network-dependent, so it stays out of the unit test path. The build profile needed no endpoint changes - the configured Bulletin and IPFS gateway were already the DevNet ones. What it needed was honest comments: the previous note framed the Asset Hub endpoint as a DevNet-compatible stopgap when it is in fact the DevNet chain. Adds the second DevNet IPFS gateway as a read fallback, and a warning against Asset Hub Next (1500) and People Next (1502), which are a different network holding none of Dotify's contracts. Verified: 6/6 checks pass against the live chain at head 11546553. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 8 + docs/operations/product-devnet-deployment.md | 36 +++++ web/.env.product-devnet | 16 +- web/package.json | 1 + web/scripts/devnet-endpoint-smoke.mjs | 145 +++++++++++++++++++ 5 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 web/scripts/devnet-endpoint-smoke.mjs diff --git a/CLAUDE.md b/CLAUDE.md index b1a96b0..7d211e6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -250,6 +250,14 @@ cd web npm run smoke:production-env ``` +When Product DevNet endpoints, `deployments.json`, or the DevNet build profile +change, also run the read-only endpoint check: + +```bash +cd web +npm run smoke:devnet +``` + For deployment-sensitive work, add the relevant read-only health, chain, contract, IPFS gateway, and wrong-network checks. Record the environment and evidence without exposing credentials. diff --git a/docs/operations/product-devnet-deployment.md b/docs/operations/product-devnet-deployment.md index eaf3fad..246b58e 100644 --- a/docs/operations/product-devnet-deployment.md +++ b/docs/operations/product-devnet-deployment.md @@ -4,6 +4,31 @@ This runbook publishes the Product build to Bulletin/DotNS and connects it to the existing Fly API and signaling services. It does not deploy contracts or change production secrets. +## Contracts Need No Redeploy + +Product DevNet is a preset over the Paseo system parachains - Asset Hub (1000), +People (1004), Bulletin (1010) - at EVM chain `420420417`. Dotify's contracts +are already deployed on that chain, so porting to DevNet is a configuration +change, not a migration. The addresses in `deployments.json` are DevNet +addresses. + +Confirm before every publish: + +```bash +cd web +npm run smoke:devnet +``` + +It reads `web/.env.product-devnet` and `deployments.json` and checks, read-only, +that the configured Asset Hub reports chain `420420417`, is producing blocks +past the 2026-07 halt, still serves bytecode for the ArtistDirectory and +ArtistRuntimeFactory, and that the Bulletin RPC and IPFS gateway respond. It +sends no transaction and prints no credential. + +Do not point the build at **Asset Hub Next (1500)** or **People Next (1502)**. +The Product documentation is explicit that those belong to a different network; +Dotify has no contracts there, and the catalog would load empty. + ## Prerequisites - Node.js 22 and npm 10+ @@ -78,12 +103,19 @@ with an internal host URL or a raw CID gateway. cd web npm ci npm run test:unit +npm run smoke:devnet npm run build:product-devnet ``` Expected output is `web/dist-product`. The production guard must fail if a browser upload token or content secret is present. +The default build keeps the viem runtime adapter, which tree-shakes the Product +contract graph away and publishes at roughly 4.4 MB. Building with +`VITE_DOTIFY_RUNTIME_ADAPTER=product-cdm` pulls in the Product SDK descriptors +and roughly doubles that. Bulletin storage is a finite quota, so only opt in +when the Product contract path is actually being exercised. + ## 4. Authenticate The Deploy Tool The repository pins the CLI version in the npm deploy command but does not add @@ -201,3 +233,7 @@ active. - Product personhood is not yet an access decision source. - A durable `CATALOG_SNAPSHOT_PATH` remains recommended for production-grade catalog recovery but is not required for API startup. +- Product contract mode (`VITE_DOTIFY_RUNTIME_ADAPTER=product-cdm`) covers + catalog reads only, and only inside the Product host. Contract writes stay on + the passkey/EVM signer in every mode until `pallet-revive` account mapping and + host-signed transaction evidence exist. diff --git a/web/.env.product-devnet b/web/.env.product-devnet index e472695..ac9e3f0 100644 --- a/web/.env.product-devnet +++ b/web/.env.product-devnet @@ -7,13 +7,23 @@ VITE_PUBLIC_APP_URL=https://dotify-test01.dev-dot.li VITE_SIGNAL_URL=https://dotify-signal.fly.dev VITE_DOTIFY_API_URL=https://dotify-api.fly.dev -# Existing Dotify contracts currently remain on the Product DevNet-compatible -# Asset Hub EVM endpoint while the write adapter is ported to CDM/PAPI. +# Product DevNet is a preset over the Paseo system parachains - Asset Hub +# (1000), People (1004), Bulletin (1010) - at EVM chain 420420417. Dotify's +# contracts in deployments.json are already on that chain, so these are DevNet +# endpoints, not a compatibility stopgap. Verify with `npm run smoke:devnet`. +# +# The DevNet reference also lists https://paseo-assethub-rpc.laissez-faire.trade +# for Asset Hub; it serves the identical chain (same chain id, same contract +# bytecode). The Parity-operated endpoint below is kept as primary because +# Paseo's community endpoints can be re-homed. +# +# Do NOT point this at Asset Hub Next (1500) or People Next (1502). Those are a +# different network and hold none of Dotify's contracts. VITE_ETH_RPC_URL=https://eth-rpc-testnet.polkadot.io/ VITE_BULLETIN_WS_URL=wss://bulletin-paseo.tservices.es:8443 VITE_PINATA_GATEWAY=https://devnet-ipfs.api.polkadotcommunity.foundation -VITE_IPFS_READ_GATEWAYS=https://devnet-ipfs.api.polkadotcommunity.foundation,https://ipfs.io,https://dweb.link +VITE_IPFS_READ_GATEWAYS=https://devnet-ipfs.api.polkadotcommunity.foundation,https://bulletin-kubo.tservices.es:9443,https://ipfs.io,https://dweb.link VITE_BLOCKSCOUT_BASE_URL=https://blockscout-testnet.polkadot.io # Explicitly shadow any local demo values from `.env`; Product builds use the diff --git a/web/package.json b/web/package.json index 75edb7f..22ea1f5 100644 --- a/web/package.json +++ b/web/package.json @@ -20,6 +20,7 @@ "deploy:bulletin": "node scripts/deploy-bulletin.cjs", "deploy:product-devnet": "npm run build:product-devnet && npx --yes --package @polkadot-community-foundation/polkadot-app-deploy@0.13.1 pad ./dist-product dotify-test01.dot --env devnet --js-merkle --config ./polkadot-app-deploy.config.ts", "smoke:production-env": "node scripts/production-env-smoke.mjs", + "smoke:devnet": "node scripts/devnet-endpoint-smoke.mjs", "smoke:signal": "node scripts/signaling-smoke.mjs", "lint": "eslint .", "fmt": "prettier --write 'src/**/*.{ts,tsx}' 'server/**/*.mjs' 'scripts/**/*.mjs' README.md", diff --git a/web/scripts/devnet-endpoint-smoke.mjs b/web/scripts/devnet-endpoint-smoke.mjs new file mode 100644 index 0000000..b736c14 --- /dev/null +++ b/web/scripts/devnet-endpoint-smoke.mjs @@ -0,0 +1,145 @@ +// Read-only DevNet endpoint smoke check. +// +// Answers one question with evidence rather than assertion: does the Product +// DevNet build profile point at a chain that actually holds Dotify's contracts? +// +// Product DevNet is a preset over the Paseo system parachains - Asset Hub +// (1000), People (1004), Bulletin (1010) - at EVM chain 420420417. Dotify is +// already deployed there, so porting to DevNet is a configuration question, not +// a redeploy. This check proves the configuration. +// +// Run: npm run smoke:devnet +// +// Network-dependent and therefore not part of `npm run test:unit`. It performs +// only eth_chainId / eth_getCode reads and unauthenticated GETs; it sends no +// transaction, reads no secret, and prints no credential. + +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(scriptDir, '../..'); + +const EXPECTED_CHAIN_ID = 420420417; +const REQUEST_TIMEOUT_MS = 20_000; + +function parseEnvFile(path) { + const env = {}; + for (const line of readFileSync(path, 'utf8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eq = trimmed.indexOf('='); + if (eq === -1) continue; + env[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim(); + } + return env; +} + +async function withTimeout(run) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); + try { + return await run(controller.signal); + } finally { + clearTimeout(timer); + } +} + +async function ethCall(rpcUrl, method, params) { + return withTimeout(async signal => { + const response = await fetch(rpcUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }), + signal + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const body = await response.json(); + if (body.error) throw new Error(body.error.message ?? 'RPC error'); + return body.result; + }); +} + +async function reachable(url) { + return withTimeout(async signal => { + const response = await fetch(url, { method: 'GET', signal }); + // A Substrate WS RPC answers a plain GET with 405, and an IPFS gateway + // redirects. Both prove the endpoint is serving. + return response.status; + }); +} + +const results = []; +function record(ok, label, detail) { + results.push({ ok, label, detail }); + console.log(`${ok ? 'ok ' : 'FAIL'} - ${label}${detail ? ` (${detail})` : ''}`); +} + +const env = parseEnvFile(resolve(repoRoot, 'web/.env.product-devnet')); +const deployments = JSON.parse(readFileSync(resolve(repoRoot, 'deployments.json'), 'utf8')); +const rpcUrl = env.VITE_ETH_RPC_URL; + +if (!rpcUrl) { + console.error('VITE_ETH_RPC_URL is not set in web/.env.product-devnet'); + process.exit(1); +} + +console.log(`Dotify Product DevNet endpoint smoke\nAsset Hub RPC: ${rpcUrl}\n`); + +try { + const chainIdHex = await ethCall(rpcUrl, 'eth_chainId', []); + const chainId = Number.parseInt(chainIdHex, 16); + record(chainId === EXPECTED_CHAIN_ID, `Asset Hub reports EVM chain ${EXPECTED_CHAIN_ID}`, `got ${chainId}`); +} catch (error) { + record(false, 'Asset Hub reports the expected EVM chain', error.message); +} + +try { + const blockHex = await ethCall(rpcUrl, 'eth_blockNumber', []); + const block = Number.parseInt(blockHex, 16); + // The chain stalled at 10612201 on 2026-07-01 and later resumed. A head at or + // below that is the signature of a frozen chain, not a healthy one. + record(block > 10_612_201, 'Asset Hub is producing blocks past the 2026-07 halt', `head ${block}`); +} catch (error) { + record(false, 'Asset Hub is producing blocks', error.message); +} + +// The contracts Dotify reads on every catalog load. Code present here is the +// evidence that no redeploy is needed to serve the catalog on DevNet. +for (const [label, address] of [ + ['ArtistDirectory', deployments.directory], + ['ArtistRuntimeFactory', deployments.factory] +]) { + try { + const code = await ethCall(rpcUrl, 'eth_getCode', [address, 'latest']); + const deployed = typeof code === 'string' && code !== '0x' && code.length > 2; + record(deployed, `${label} is deployed at ${address}`, deployed ? `${code.length} chars of bytecode` : 'no code'); + } catch (error) { + record(false, `${label} is deployed at ${address}`, error.message); + } +} + +for (const [label, url] of [ + ['Bulletin RPC', env.VITE_BULLETIN_WS_URL?.replace(/^wss:/, 'https:')], + ['IPFS gateway', env.VITE_PINATA_GATEWAY] +]) { + if (!url) { + record(false, `${label} is configured`, 'missing'); + continue; + } + try { + const status = await reachable(url); + record(status > 0 && status < 500, `${label} responds`, `HTTP ${status}`); + } catch (error) { + record(false, `${label} responds`, error.message); + } +} + +const failed = results.filter(result => !result.ok); +if (failed.length > 0) { + console.error(`\nDotify DevNet endpoint smoke failed: ${failed.length} of ${results.length} checks.`); + process.exit(1); +} + +console.log(`\nDotify DevNet endpoint smoke passed (${results.length} checks).`); From 14762089d394aa7b5ba1951de73f9ebfa26c1e61 Mon Sep 17 00:00:00 2001 From: Kevin Nzeng Essimengane Date: Wed, 29 Jul 2026 11:13:33 +0200 Subject: [PATCH 15/22] docs: correct the stale CDM prerequisite in the deploy runbook The known-limits bullet still said Product contract mode was blocked on CDM-installed packages and generated contract types. Both now exist, so the list overstated what is missing. Only pallet-revive account mapping and host-signed transaction evidence remain. Co-Authored-By: Claude Opus 5 (1M context) --- docs/operations/product-devnet-deployment.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/operations/product-devnet-deployment.md b/docs/operations/product-devnet-deployment.md index 246b58e..f8809fa 100644 --- a/docs/operations/product-devnet-deployment.md +++ b/docs/operations/product-devnet-deployment.md @@ -225,9 +225,10 @@ active. records which shape the live host actually produced - that observation is the evidence, and until it is captured the accepted set stays deliberately wide. - Contract writes still require passkey/EVM signing in the shipped UI. The - experimental Product CDM/PAPI runtime adapter is present in code, but it is - not selected until Dotify has CDM-installed runtime packages, generated - contract types, and real host-signed transaction evidence. + Product CDM/PAPI runtime adapter now has its generated manifest, contract + types, and a live resolver, so the only thing still missing before it can be + selected is `pallet-revive` account mapping plus real host-signed transaction + evidence. - Rooms still depend on one in-memory Fly signaling machine. - Product-host cloud storage does not hold Dotify audio or content keys. - Product personhood is not yet an access decision source. From 10a4a594e1790d7444e44a17b653d48f20cd3d3b Mon Sep 17 00:00:00 2001 From: Kevin Nzeng Essimengane Date: Wed, 29 Jul 2026 12:06:15 +0200 Subject: [PATCH 16/22] perf: bound and hedge the legacy IPFS gateway reads fetchIpfsCid and fetchAssetRef walked 4-6 gateways serially with no per-gateway timeout, so one unresponsive gateway cost the listener the browser's full connection timeout before the next candidate was tried. Covers, manifests, and v1 audio all read through that path. Race the candidates instead, the way audioV2Gateway already does for byte ranges: bound time-to-headers, hedge onto the next gateway after a delay, cap concurrency at 3, and abort the losers once a winner answers. Every URL addresses the same immutable CID, so racing them cannot diverge. The winner's own controller is never aborted, so its body stays readable, and a caller abort stops the queue where it is rather than walking the rest of the list. Co-Authored-By: Claude Opus 5 (1M context) --- web/src/services/gatewayRace.test.ts | 154 +++++++++++++++++++++++ web/src/services/gatewayRace.ts | 176 +++++++++++++++++++++++++++ web/src/services/pinata.ts | 35 +----- 3 files changed, 335 insertions(+), 30 deletions(-) create mode 100644 web/src/services/gatewayRace.test.ts create mode 100644 web/src/services/gatewayRace.ts diff --git a/web/src/services/gatewayRace.test.ts b/web/src/services/gatewayRace.test.ts new file mode 100644 index 0000000..6ceee5b --- /dev/null +++ b/web/src/services/gatewayRace.test.ts @@ -0,0 +1,154 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { fetchThroughGateways } from './gatewayRace'; + +const FIRST = 'https://first.example/ipfs/QmTest'; +const SECOND = 'https://second.example/ipfs/QmTest'; +const THIRD = 'https://third.example/ipfs/QmTest'; + +/** A fetch that never settles until the attempt's own signal aborts. */ +function stalledFetch(): Promise { + return new Promise(() => {}); +} + +function abortableStall(signal: AbortSignal | undefined): Promise { + return new Promise((_resolve, reject) => { + signal?.addEventListener('abort', () => reject(new Error('aborted')), { once: true }); + }); +} + +describe('fetchThroughGateways', () => { + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it('returns the first gateway response without touching the others', async () => { + const fetchMock = vi.fn().mockResolvedValue(new Response('ok')); + + const response = await fetchThroughGateways([FIRST, SECOND, THIRD], { fetchImpl: fetchMock }); + + expect(await response.text()).toBe('ok'); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock.mock.calls[0][0]).toBe(FIRST); + }); + + it('advances to the next gateway when one fails outright', async () => { + const fetchMock = vi.fn().mockRejectedValueOnce(new Error('dns failure')).mockResolvedValueOnce(new Response('second wins')); + + const response = await fetchThroughGateways([FIRST, SECOND], { fetchImpl: fetchMock }); + + expect(await response.text()).toBe('second wins'); + expect(fetchMock).toHaveBeenCalledTimes(2); + }); + + it('treats a non-ok status as a failed gateway', async () => { + const fetchMock = vi + .fn() + .mockResolvedValueOnce(new Response('nope', { status: 504 })) + .mockResolvedValueOnce(new Response('recovered')); + + const response = await fetchThroughGateways([FIRST, SECOND], { fetchImpl: fetchMock }); + + expect(await response.text()).toBe('recovered'); + }); + + it('hedges onto the next gateway when the first one stalls, and does not wait for it', async () => { + vi.useFakeTimers(); + const fetchMock = vi + .fn() + .mockImplementationOnce(() => stalledFetch()) + .mockResolvedValueOnce(new Response('hedge wins')); + + const pending = fetchThroughGateways([FIRST, SECOND], { fetchImpl: fetchMock, hedgeDelayMs: 1_000 }); + + await vi.advanceTimersByTimeAsync(1_000); + const response = await pending; + + expect(await response.text()).toBe('hedge wins'); + expect(fetchMock).toHaveBeenCalledTimes(2); + }); + + it('aborts a stalled gateway once the timeout budget elapses', async () => { + vi.useFakeTimers(); + const fetchMock = vi + .fn() + .mockImplementationOnce((_url, init) => abortableStall(init?.signal ?? undefined)) + .mockResolvedValueOnce(new Response('after timeout')); + + const pending = fetchThroughGateways([FIRST, SECOND], { + fetchImpl: fetchMock, + timeoutMs: 500, + // Hedging disabled by pushing it past the timeout, so this proves the + // timeout alone releases the queue. + hedgeDelayMs: 10_000 + }); + + await vi.advanceTimersByTimeAsync(500); + const response = await pending; + + expect(await response.text()).toBe('after timeout'); + }); + + it('aborts the losing attempts but leaves the winner readable', async () => { + vi.useFakeTimers(); + const signals: Array = []; + const fetchMock = vi + .fn() + .mockImplementationOnce((_url, init) => { + signals.push(init?.signal ?? undefined); + return abortableStall(init?.signal ?? undefined); + }) + .mockImplementationOnce((_url, init) => { + signals.push(init?.signal ?? undefined); + return Promise.resolve(new Response('winner body')); + }); + + const pending = fetchThroughGateways([FIRST, SECOND], { fetchImpl: fetchMock, hedgeDelayMs: 100 }); + await vi.advanceTimersByTimeAsync(100); + const response = await pending; + + expect(signals[0]?.aborted).toBe(true); + expect(signals[1]?.aborted).toBe(false); + expect(await response.text()).toBe('winner body'); + }); + + it('surfaces the last error when every gateway fails', async () => { + const fetchMock = vi.fn().mockRejectedValueOnce(new Error('first down')).mockRejectedValueOnce(new Error('second down')); + + await expect(fetchThroughGateways([FIRST, SECOND], { fetchImpl: fetchMock })).rejects.toThrow('second down'); + }); + + it('rejects an empty gateway list rather than hanging', async () => { + await expect(fetchThroughGateways([], { label: 'cover image' })).rejects.toThrow('No gateways configured for cover image'); + }); + + it('honours a caller abort signal that fires before the read starts', async () => { + const controller = new AbortController(); + controller.abort(); + const fetchMock = vi.fn(); + + await expect(fetchThroughGateways([FIRST], { fetchImpl: fetchMock, signal: controller.signal })).rejects.toThrow(/cancelled/); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('propagates a caller abort that fires mid-flight', async () => { + const controller = new AbortController(); + const fetchMock = vi.fn().mockImplementation((_url, init) => abortableStall(init?.signal ?? undefined)); + + const pending = fetchThroughGateways([FIRST], { fetchImpl: fetchMock, signal: controller.signal }); + controller.abort(); + + await expect(pending).rejects.toThrow(); + }); + + it('does not walk the remaining gateways after a caller abort', async () => { + const controller = new AbortController(); + const fetchMock = vi.fn().mockImplementation((_url, init) => abortableStall(init?.signal ?? undefined)); + + const pending = fetchThroughGateways([FIRST, SECOND, THIRD], { fetchImpl: fetchMock, signal: controller.signal }); + controller.abort(); + + await expect(pending).rejects.toMatchObject({ name: 'AbortError' }); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/web/src/services/gatewayRace.ts b/web/src/services/gatewayRace.ts new file mode 100644 index 0000000..f288c3f --- /dev/null +++ b/web/src/services/gatewayRace.ts @@ -0,0 +1,176 @@ +// Bounded, hedged reads across a list of interchangeable gateway URLs. +// +// The serial alternative - await each gateway in turn with no timeout - makes +// one unresponsive gateway cost the user the browser's full connection +// timeout before the next candidate is even attempted, and the browser will +// happily stall a request for far longer than a listener will wait for sound. +// Every URL here addresses the same immutable CID, so racing them is free of +// consistency concerns: whichever answers first is the same object. +// +// This is the whole-object sibling of `audioV2Gateway`, which does the same +// for byte ranges. It lives under services/ with no project imports so +// `pinata` can use it without depending on a feature module. + +export type GatewayRaceOptions = { + /** Time budget for one gateway to return response headers. */ + timeoutMs?: number; + /** Wait before racing the next gateway alongside the current one. */ + hedgeDelayMs?: number; + fetchImpl?: typeof fetch; + signal?: AbortSignal; + /** Request init applied to every attempt. `signal` is managed internally. */ + init?: Omit; + /** Label used in the aggregate error when every gateway fails. */ + label?: string; +}; + +type AttemptOutcome = + | { ok: true; id: number; response: Response; url: string; controller: AbortController } + | { ok: false; id: number; error: unknown; url: string }; + +type Attempt = { + id: number; + controller: AbortController; + promise: Promise; +}; + +const DEFAULT_TIMEOUT_MS = 8_000; +const DEFAULT_HEDGE_DELAY_MS = 1_200; +const MAX_PARALLEL_ATTEMPTS = 3; + +function createAbortError(message: string): Error { + if (typeof DOMException !== 'undefined') return new DOMException(message, 'AbortError'); + const error = new Error(message); + error.name = 'AbortError'; + return error; +} + +function throwIfAborted(signal: AbortSignal | undefined, message: string): void { + if (signal?.aborted) throw createAbortError(message); +} + +function formatError(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +function makeAttempt(id: number, url: string, timeoutMs: number, fetchImpl: typeof fetch, init: Omit, signal?: AbortSignal): Attempt { + const controller = new AbortController(); + const abortFromParent = () => controller.abort(); + + if (signal?.aborted) { + controller.abort(); + } else { + signal?.addEventListener('abort', abortFromParent, { once: true }); + } + + // Bounds time-to-headers only. Once a gateway starts answering, the body is + // allowed to stream at its own pace - cutting a healthy download short would + // trade a slow track for a broken one. + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + + const promise = fetchImpl(url, { ...init, signal: controller.signal }) + .then(response => { + if (!response.ok) { + throw new Error(`Gateway ${url} returned ${response.status}`); + } + return { ok: true as const, id, response, url, controller }; + }) + .catch(error => ({ ok: false as const, id, error, url })) + .finally(() => { + clearTimeout(timeoutId); + signal?.removeEventListener('abort', abortFromParent); + }); + + return { id, controller, promise }; +} + +/** + * Fetch the first successful response among interchangeable gateway URLs. + * + * Attempts start staggered rather than all at once, so a healthy primary + * gateway still serves nearly every read alone and slow ones stop blocking the + * queue behind them. Losing attempts are aborted as soon as a winner is known; + * the winner's own abort controller is never triggered, so its body stays + * readable by the caller. + */ +export async function fetchThroughGateways(urls: string[], options: GatewayRaceOptions = {}): Promise { + const message = `${options.label ?? 'Gateway'} read cancelled`; + throwIfAborted(options.signal, message); + + if (urls.length === 0) { + throw new Error(`No gateways configured for ${options.label ?? 'this read'}`); + } + + const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; + const hedgeDelayMs = options.hedgeDelayMs ?? DEFAULT_HEDGE_DELAY_MS; + const fetchImpl = options.fetchImpl ?? fetch; + const init = options.init ?? {}; + + const active = new Map(); + let nextUrlIndex = 0; + let nextAttemptId = 0; + let lastError: unknown; + + // A caller abort must stop the queue where it is. Without this guard the + // failure branch below would answer each in-flight abort by starting the + // next gateway, turning one cancellation into a walk down the whole list. + const launch = () => { + if (options.signal?.aborted || nextUrlIndex >= urls.length) return; + const url = urls[nextUrlIndex]; + nextUrlIndex += 1; + const attempt = makeAttempt(nextAttemptId, url, timeoutMs, fetchImpl, init, options.signal); + nextAttemptId += 1; + active.set(attempt.id, attempt); + }; + + launch(); + + try { + while (active.size > 0) { + throwIfAborted(options.signal, message); + + const canHedge = active.size < MAX_PARALLEL_ATTEMPTS && nextUrlIndex < urls.length; + const raceItems: Array> = Array.from(active.values()).map(attempt => attempt.promise); + let hedgeTimerId: ReturnType | undefined; + if (canHedge) { + raceItems.push( + new Promise<{ hedge: true }>(resolve => { + hedgeTimerId = setTimeout(() => resolve({ hedge: true }), hedgeDelayMs); + }) + ); + } + + const outcome = await Promise.race(raceItems); + if (hedgeTimerId) clearTimeout(hedgeTimerId); + + if ('hedge' in outcome) { + launch(); + continue; + } + + active.delete(outcome.id); + + if (outcome.ok) { + throwIfAborted(options.signal, message); + for (const attempt of active.values()) { + attempt.controller.abort(); + } + active.clear(); + return outcome.response; + } + + lastError = outcome.error; + if (active.size === 0 && nextUrlIndex < urls.length) { + launch(); + } + } + } finally { + // Covers the abort/throw paths; the success path already cleared `active`. + for (const attempt of active.values()) { + attempt.controller.abort(); + } + } + + throwIfAborted(options.signal, message); + throw lastError instanceof Error ? lastError : new Error(`Unable to fetch ${options.label ?? 'resource'}: ${formatError(lastError)}`); +} diff --git a/web/src/services/pinata.ts b/web/src/services/pinata.ts index 0e1f585..14e8f81 100644 --- a/web/src/services/pinata.ts +++ b/web/src/services/pinata.ts @@ -14,6 +14,7 @@ import { getArtistPublishE2eCid, getArtistPublishE2eScenario, isArtistPublishE2e, recordArtistPublishUploadFailure } from '../e2e/artistPublishMock'; import { encryptedRefToCID, normalizeEncryptedAudioRef } from '../shared/utils/protectedAudio'; +import { fetchThroughGateways } from './gatewayRace'; // Backend API base URL. When set, uploads are routed server-side. const API_URL = (import.meta.env.VITE_DOTIFY_API_URL as string | undefined)?.replace(/\/$/, ''); @@ -120,39 +121,13 @@ function throwIfGatewayReadAborted(signal?: AbortSignal): void { } export async function fetchIpfsCid(cid: string, options: GatewayReadOptions = {}): Promise { - let lastError: unknown; - - for (const url of getGatewayUrls(cid)) { - throwIfGatewayReadAborted(options.signal); - try { - const response = await fetch(url, { signal: options.signal }); - if (response.ok) return response; - lastError = new Error(`Gateway ${url} returned ${response.status}`); - } catch (error) { - if (options.signal?.aborted) throw error; - lastError = error; - } - } - - throw lastError instanceof Error ? lastError : new Error(`Unable to fetch IPFS CID ${cid}`); + throwIfGatewayReadAborted(options.signal); + return fetchThroughGateways(getGatewayUrls(cid), { signal: options.signal, label: `IPFS CID ${cid}` }); } export async function fetchAssetRef(assetRef: string, options: GatewayReadOptions = {}): Promise { - let lastError: unknown; - - for (const url of getGatewayUrlsForAssetRef(assetRef)) { - throwIfGatewayReadAborted(options.signal); - try { - const response = await fetch(url, { signal: options.signal }); - if (response.ok) return response; - lastError = new Error(`Gateway ${url} returned ${response.status}`); - } catch (error) { - if (options.signal?.aborted) throw error; - lastError = error; - } - } - - throw lastError instanceof Error ? lastError : new Error(`Unable to fetch asset ${assetRef}`); + throwIfGatewayReadAborted(options.signal); + return fetchThroughGateways(getGatewayUrlsForAssetRef(assetRef), { signal: options.signal, label: `asset ${assetRef}` }); } // --------------------------------------------------------------------------- From 0f7438eb6e7329f47f732e9d32ab4307799ea6a0 Mon Sep 17 00:00:00 2001 From: Kevin Nzeng Essimengane Date: Wed, 29 Jul 2026 12:06:30 +0200 Subject: [PATCH 17/22] fix: keep one API machine warm for first sound min_machines_running = 0 let the API scale to zero, and the first content key request after an idle period paid the cold start. Measured against the running deployment: 8.17s to /health cold (uptime 0), 0.11s warm. That request sits directly in front of first sound, so the saving was being taken out of the listening experience. Co-Authored-By: Claude Opus 5 (1M context) --- services/api/fly.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/api/fly.toml b/services/api/fly.toml index 7e78384..d5222f7 100644 --- a/services/api/fly.toml +++ b/services/api/fly.toml @@ -21,7 +21,11 @@ primary_region = "ams" force_https = true auto_stop_machines = "stop" auto_start_machines = true - min_machines_running = 0 + # Keep one machine warm. A stopped machine cold-starts on the first content + # key request, and that request sits directly in front of first sound: a + # measured 8.2s to /health cold against 0.11s warm. Scaling to zero saves + # nothing a listener would trade eight silent seconds for. + min_machines_running = 1 processes = ["app"] [[vm]] From 072e9f951626b4b0e87af140ced94913bcd8f3d1 Mon Sep 17 00:00:00 2001 From: Kevin Nzeng Essimengane Date: Wed, 29 Jul 2026 12:06:30 +0200 Subject: [PATCH 18/22] feat: explain why a room connection was refused Socket.IO reports a CORS rejection, a stopped server, and a wrong URL identically, so every one of them surfaced as "Room service unavailable." That is the vague failure the product invariants rule out, and it hid a real deployment gap: the running signaling server allows only muzinga.netlify.app, so opening a room from the Polkadot Product host origin is refused with a 403 the browser will not explain. On connect_error, read the server's unauthenticated /health, compare the page origin against the allowlist it reports, and upgrade the message in place. The generic reason stands if health cannot be read or if the origin is allowed, so this only ever widens an error - room access stays decided by the server. Co-Authored-By: Claude Opus 5 (1M context) --- .../features/rooms/signalDiagnostics.test.ts | 76 ++++++++++++++++++ web/src/features/rooms/signalDiagnostics.ts | 80 +++++++++++++++++++ web/src/hooks/useSession.ts | 8 ++ 3 files changed, 164 insertions(+) create mode 100644 web/src/features/rooms/signalDiagnostics.test.ts create mode 100644 web/src/features/rooms/signalDiagnostics.ts diff --git a/web/src/features/rooms/signalDiagnostics.test.ts b/web/src/features/rooms/signalDiagnostics.test.ts new file mode 100644 index 0000000..0636144 --- /dev/null +++ b/web/src/features/rooms/signalDiagnostics.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it, vi } from 'vitest'; +import { diagnoseSignalFailure, explainSignalFailure } from './signalDiagnostics'; + +const SIGNAL_URL = 'https://dotify-signal.example'; +const HOST_ORIGIN = 'https://dotify-test01.dev-dot.li'; + +describe('explainSignalFailure', () => { + it('names the rejected origin when the server allowlist excludes it', () => { + const reason = explainSignalFailure({ ok: true, allowedOrigins: ['https://muzinga.netlify.app'] }, HOST_ORIGIN); + + expect(reason).toContain(HOST_ORIGIN); + expect(reason).toContain('SIGNAL_ORIGINS'); + }); + + it('ignores a trailing slash when comparing origins', () => { + const reason = explainSignalFailure({ ok: true, allowedOrigins: ['https://muzinga.netlify.app/'] }, 'https://muzinga.netlify.app'); + + expect(reason).toBe('Room service unavailable.'); + }); + + it('does not blame configuration when the origin is allowed', () => { + const reason = explainSignalFailure({ ok: true, allowedOrigins: [HOST_ORIGIN] }, HOST_ORIGIN); + + expect(reason).toBe('Room service unavailable.'); + }); + + it('does not blame configuration on a wildcard allowlist', () => { + const reason = explainSignalFailure({ ok: true, allowedOrigins: '*' }, HOST_ORIGIN); + + expect(reason).toBe('Room service unavailable.'); + }); + + it('reports an unreachable server rather than an origin problem', () => { + const reason = explainSignalFailure(null, HOST_ORIGIN); + + expect(reason).toContain('did not answer'); + expect(reason).not.toContain('SIGNAL_ORIGINS'); + }); +}); + +describe('diagnoseSignalFailure', () => { + it('reads the health endpoint and explains an origin rejection', async () => { + const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ ok: true, allowedOrigins: ['https://muzinga.netlify.app'] }))); + + const reason = await diagnoseSignalFailure(SIGNAL_URL, HOST_ORIGIN, { fetchImpl: fetchMock }); + + expect(fetchMock.mock.calls[0][0]).toBe('https://dotify-signal.example/health'); + expect(reason).toContain(HOST_ORIGIN); + }); + + it('falls back to the generic reason when health cannot be read', async () => { + const fetchMock = vi.fn().mockRejectedValue(new TypeError('Failed to fetch')); + + const reason = await diagnoseSignalFailure(SIGNAL_URL, HOST_ORIGIN, { fetchImpl: fetchMock }); + + expect(reason).toContain('Room service unavailable.'); + expect(reason).toContain('did not answer'); + }); + + it('falls back when health returns a non-ok status', async () => { + const fetchMock = vi.fn().mockResolvedValue(new Response('nope', { status: 502 })); + + const reason = await diagnoseSignalFailure(SIGNAL_URL, HOST_ORIGIN, { fetchImpl: fetchMock }); + + expect(reason).toContain('did not answer'); + }); + + it('rejects nothing when the signal URL is unusable', async () => { + const fetchMock = vi.fn(); + + const reason = await diagnoseSignalFailure('not a url', HOST_ORIGIN, { fetchImpl: fetchMock }); + + expect(reason).toContain('not valid'); + expect(fetchMock).not.toHaveBeenCalled(); + }); +}); diff --git a/web/src/features/rooms/signalDiagnostics.ts b/web/src/features/rooms/signalDiagnostics.ts new file mode 100644 index 0000000..92906d1 --- /dev/null +++ b/web/src/features/rooms/signalDiagnostics.ts @@ -0,0 +1,80 @@ +// Turn a bare Socket.IO `connect_error` into something the person in front of +// the app can act on. +// +// The transport gives the browser no reason for the failure - a CORS rejection, +// a stopped server, and a wrong URL all arrive identically. The signaling +// server's /health endpoint is unauthenticated and reports the origin allowlist +// it is actually running, so one read distinguishes the common cases. That +// matters most inside the Polkadot Product host, where the app is served from a +// DotNS origin an operator has to add to SIGNAL_ORIGINS deliberately. +// +// This only ever widens an error message. Room access itself stays decided by +// the server. + +const GENERIC_REASON = 'Room service unavailable.'; + +export type SignalHealth = { + ok?: boolean; + allowedOrigins?: string[] | '*'; +}; + +export type SignalDiagnosisDeps = { + fetchImpl?: typeof fetch; + timeoutMs?: number; +}; + +function normalizeOrigin(origin: string): string { + return origin.trim().replace(/\/$/, ''); +} + +/** + * Build the user-facing reason from a health payload, or null when the payload + * gives no better explanation than the generic one. + */ +export function explainSignalFailure(health: SignalHealth | null, pageOrigin: string): string { + if (!health) { + // /health itself is unreachable, so this is not an origin problem. + return `${GENERIC_REASON} The signaling server did not answer. It may be starting up or offline.`; + } + + const allowed = health.allowedOrigins; + if (allowed === '*' || !Array.isArray(allowed)) return GENERIC_REASON; + + const origin = normalizeOrigin(pageOrigin); + if (allowed.map(normalizeOrigin).includes(origin)) { + // Reachable, origin is allowed - the fault is elsewhere (transport, + // proxy, or the socket path), so do not blame configuration. + return GENERIC_REASON; + } + + return `${GENERIC_REASON} The signaling server is running but does not accept connections from ${origin || 'this page'}. Add that origin to SIGNAL_ORIGINS and redeploy the signaling service.`; +} + +/** + * Read the signaling server's health and describe why a connection failed. + * Never rejects: a failed diagnosis falls back to the generic reason. + */ +export async function diagnoseSignalFailure(signalUrl: string, pageOrigin: string, deps: SignalDiagnosisDeps = {}): Promise { + const fetchImpl = deps.fetchImpl ?? fetch; + const timeoutMs = deps.timeoutMs ?? 5_000; + + let healthUrl: string; + try { + healthUrl = new URL('/health', signalUrl).toString(); + } catch { + return `${GENERIC_REASON} The configured signaling URL is not valid.`; + } + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + + try { + const response = await fetchImpl(healthUrl, { signal: controller.signal }); + if (!response.ok) return explainSignalFailure(null, pageOrigin); + return explainSignalFailure((await response.json()) as SignalHealth, pageOrigin); + } catch { + return explainSignalFailure(null, pageOrigin); + } finally { + clearTimeout(timeoutId); + } +} diff --git a/web/src/hooks/useSession.ts b/web/src/hooks/useSession.ts index 636db52..c59e86c 100644 --- a/web/src/hooks/useSession.ts +++ b/web/src/hooks/useSession.ts @@ -13,6 +13,7 @@ import { roomJoinE2eIceServers } from '../e2e/roomJoinMock'; import { buildSessionLink, getInitialRoomCode } from '../features/rooms/roomState'; +import { diagnoseSignalFailure } from '../features/rooms/signalDiagnostics'; import { isChosenDisplayName, sanitizeDisplayName, storeDisplayName } from '../features/identity/walletIdentity'; import { nextCaptureAttempt, shouldReuseCapture, type CaptureAttempt } from '../features/rooms/streamCapture'; import { CHAT_CLIENT_LIMIT, CHAT_TEXT_MAX_LENGTH, REQUEST_QUEUE_CLIENT_LIMIT, REQUEST_TEXT_MAX_LENGTH } from '../shared/social'; @@ -306,6 +307,13 @@ export function useSession(deps: UseSessionDeps) { setSessionAction('idle'); setIsRefreshingRooms(false); setError('Room service unavailable.'); + // Socket.IO cannot tell us why. Ask the server's public /health and + // upgrade the message in place once it answers; the generic reason above + // already stands if it does not. + void diagnoseSignalFailure(signalUrl, window.location.origin).then(reason => { + if (socketRef.current !== socket || socket.connected) return; + setError(reason); + }); }); socket.on('disconnect', () => { setSocketStatus('offline'); From b06e765f604b864b25af4f7a1327c4a4bccf017c Mon Sep 17 00:00:00 2001 From: Kevin Nzeng Essimengane Date: Wed, 29 Jul 2026 12:26:07 +0200 Subject: [PATCH 19/22] docs: warn that flyctl -c does not set the Docker build context Deploying with `flyctl deploy -c services/api/fly.toml` from the repository root fails at `COPY src ./src`: -c selects the config file, but the build context stays the shell's working directory, and the Dockerfile is written against services/api. It also uploads a ~1.3 GB context, because Docker reads .dockerignore from the context root and only the service directories have one. A cached `npm ci` layer from an earlier correct build hides the cause, so the error surfaces at the first uncached step rather than the first wrong one. Co-Authored-By: Claude Opus 5 (1M context) --- docs/operations/product-devnet-deployment.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/operations/product-devnet-deployment.md b/docs/operations/product-devnet-deployment.md index f8809fa..74c78fb 100644 --- a/docs/operations/product-devnet-deployment.md +++ b/docs/operations/product-devnet-deployment.md @@ -66,16 +66,30 @@ API_ORIGINS=https://muzinga.netlify.app,https://dotify-test01.dev-dot.li SIGNAL_ORIGINS=https://muzinga.netlify.app,https://dotify-test01.dev-dot.li ``` -Deploy both services before publishing the frontend: +Deploy both services before publishing the frontend. `cd` into each service +first - this is not cosmetic: ```bash cd services/api -flyctl deploy -c fly.toml +flyctl deploy cd ../../web flyctl deploy -c fly.signal.toml ``` +`-c` selects the config file only; it does not set the Docker build context, +which is always the shell's working directory. Running +`flyctl deploy -c services/api/fly.toml` from the repository root fails at +`COPY src ./src`, because the Dockerfile is written against `services/api` as +its context and there is no `src/` at the root. It also uploads a ~1.3 GB +context, since Docker reads `.dockerignore` from the context root and only the +service directories have one. Passing the directory positionally +(`flyctl deploy services/api`) works too, because that sets the context. + +An earlier cached layer can hide the mistake: `COPY package*.json ./` and +`npm ci` may report `CACHED` from a previous correct build, so the failure +surfaces at the first genuinely uncached step rather than the first wrong one. + Keep backend secrets unchanged. `API_ORIGINS` supersedes singular `API_ORIGIN`; the latter remains only as a compatibility fallback. From 7e64f10a23b0dfc58cb846b2fc902af50c3828ad Mon Sep 17 00:00:00 2001 From: Kevin Nzeng Essimengane Date: Wed, 29 Jul 2026 12:34:42 +0200 Subject: [PATCH 20/22] fix: keep a raced gateway read cancellable after the winner returns `makeAttempt` detaches its parent-abort listener as soon as the fetch settles, which is when headers arrive - before the body has streamed. The winner was therefore returned already disconnected from the caller's signal, so aborting afterwards no longer stopped the download. That is a regression against the serial reader this replaced, which passed the caller's signal straight to `fetch`. It bites where it matters: `useCatalog` passes a signal to every audio and asset read, so a listener skipping tracks left the previous audio downloading to completion, unread. Re-link the winner to the caller's signal before returning it, and abort the winner on the late-abort path too - it has already been removed from `active`, so the cleanup block would not otherwise reach it. Covered by a regression test that failed before this change. Co-Authored-By: Claude Opus 5 (1M context) --- web/src/services/gatewayRace.test.ts | 17 +++++++++++++++++ web/src/services/gatewayRace.ts | 12 +++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/web/src/services/gatewayRace.test.ts b/web/src/services/gatewayRace.test.ts index 6ceee5b..22e9960 100644 --- a/web/src/services/gatewayRace.test.ts +++ b/web/src/services/gatewayRace.test.ts @@ -112,6 +112,23 @@ describe('fetchThroughGateways', () => { expect(await response.text()).toBe('winner body'); }); + it('keeps the winner cancellable after it is returned', async () => { + // The body streams after headers arrive, so a caller that cancels then - + // a listener skipping to another track - must still stop the download. + let winnerSignal: AbortSignal | undefined; + const fetchMock = vi.fn().mockImplementation((_url, init) => { + winnerSignal = init?.signal ?? undefined; + return Promise.resolve(new Response('winner body')); + }); + const controller = new AbortController(); + + await fetchThroughGateways([FIRST], { fetchImpl: fetchMock, signal: controller.signal }); + + expect(winnerSignal?.aborted).toBe(false); + controller.abort(); + expect(winnerSignal?.aborted).toBe(true); + }); + it('surfaces the last error when every gateway fails', async () => { const fetchMock = vi.fn().mockRejectedValueOnce(new Error('first down')).mockRejectedValueOnce(new Error('second down')); diff --git a/web/src/services/gatewayRace.ts b/web/src/services/gatewayRace.ts index f288c3f..cf1e050 100644 --- a/web/src/services/gatewayRace.ts +++ b/web/src/services/gatewayRace.ts @@ -151,11 +151,21 @@ export async function fetchThroughGateways(urls: string[], options: GatewayRaceO active.delete(outcome.id); if (outcome.ok) { - throwIfAborted(options.signal, message); + if (options.signal?.aborted) { + // A late abort still has to stop the winner: it is no longer in + // `active`, so the finally block below would not reach it. + outcome.controller.abort(); + throwIfAborted(options.signal, message); + } for (const attempt of active.values()) { attempt.controller.abort(); } active.clear(); + // `makeAttempt` detaches its parent-abort link once headers arrive, but + // the body has only just started streaming. Re-link the winner so a + // caller that cancels - a listener skipping to another track - actually + // stops the download instead of leaving it to run to completion unread. + options.signal?.addEventListener('abort', () => outcome.controller.abort(), { once: true }); return outcome.response; } From 3254cecdb8ea135448bd324894f95ef9ebf5408a Mon Sep 17 00:00:00 2001 From: Kevin Nzeng Essimengane Date: Wed, 29 Jul 2026 13:30:55 +0200 Subject: [PATCH 21/22] fix: admit the Product host container origin to the API allowlist Inside the Product host the app is served from polkadot://app.dotify-test01.dot, not the DotNS web gateway. SIGNAL_ORIGINS already carried it; API_ORIGINS did not, so a container would have had working rooms and no content keys - catalog and key delivery both go to the API, so free and protected playback would fail CORS while the room layer looked healthy. Deliberately does not add a bare `null`. `polkadot:` is a non-special scheme, so its origin is opaque and a browser may send `Origin: null` instead of the literal value. Allowing that would admit every sandboxed iframe and file:// page on the web to the authenticated upload and content-key routes. If a host request is still refused, the actual Origin header from the Fly log is the evidence to act on. Two regression tests pin both halves: the custom-scheme origin is answered, a null origin is not. Co-Authored-By: Claude Opus 5 (1M context) --- docs/operations/product-devnet-deployment.md | 17 +++++++++-- docs/reference/environment-variables.md | 24 +++++++-------- services/api/fly.toml | 12 +++++++- services/api/src/cors.test.ts | 31 ++++++++++++++++++++ web/fly.signal.toml | 2 +- 5 files changed, 70 insertions(+), 16 deletions(-) diff --git a/docs/operations/product-devnet-deployment.md b/docs/operations/product-devnet-deployment.md index 74c78fb..8e52341 100644 --- a/docs/operations/product-devnet-deployment.md +++ b/docs/operations/product-devnet-deployment.md @@ -62,10 +62,23 @@ for the current faucet, storage console, mapping, and DotNS registration steps. The tracked Fly configuration must contain: ```txt -API_ORIGINS=https://muzinga.netlify.app,https://dotify-test01.dev-dot.li -SIGNAL_ORIGINS=https://muzinga.netlify.app,https://dotify-test01.dev-dot.li +API_ORIGINS=https://muzinga.netlify.app,https://dotify-test01.dev-dot.li,polkadot://app.dotify-test01.dot +SIGNAL_ORIGINS=https://muzinga.netlify.app,https://dotify-test01.dev-dot.li,polkadot://app.dotify-test01.dot ``` +Three frontends reach these services: Netlify, the DotNS web gateway, and the +app as served inside the Product host container, which uses a custom scheme. +Both lists must carry all three - a container with only the signaling origin +gets rooms but no content keys, because catalog and key delivery go to the API. + +`polkadot:` is a non-special scheme, so its origin is opaque and a browser may +send `Origin: null` rather than the literal value. If a host request is still +refused after this change, read the actual `Origin` header from the Fly log +before widening either list. Never add a bare `null`: that admits every +sandboxed iframe and `file://` page on the web to the authenticated upload and +content-key routes. A regression test in `services/api/src/cors.test.ts` pins +that refusal. + Deploy both services before publishing the frontend. `cd` into each service first - this is not cosmetic: diff --git a/docs/reference/environment-variables.md b/docs/reference/environment-variables.md index 2f38cec..61123c7 100644 --- a/docs/reference/environment-variables.md +++ b/docs/reference/environment-variables.md @@ -358,12 +358,12 @@ Network interface to bind. ### `SIGNAL_ORIGINS` -| Property | Value | -| ------------ | -------------------------------------------------------------- | -| **Type** | Comma-separated URL list or `*` | -| **Required** | No | -| **Default** | `*` | -| **Example** | `https://muzinga.netlify.app,https://dotify-test01.dev-dot.li` | +| Property | Value | +| ------------ | ----------------------------------------------------------------------------------------------- | +| **Type** | Comma-separated URL list or `*` | +| **Required** | No | +| **Default** | `*` | +| **Example** | `https://muzinga.netlify.app,https://dotify-test01.dev-dot.li,polkadot://app.dotify-test01.dot` | CORS allowed origins for Socket.IO and status endpoints. Set explicit frontend origins in production. `SIGNAL_ORIGIN` is still accepted as a backwards-compatible @@ -451,12 +451,12 @@ backwards-compatible fallback when `API_ORIGINS` is not set. ### `API_ORIGINS` -| Property | Value | -| ------------ | -------------------------------------------------------------- | -| **Type** | Comma-separated HTTPS origin list | -| **Required** | Multiple hosted frontends | -| **Default** | The single `API_ORIGIN` value | -| **Example** | `https://muzinga.netlify.app,https://dotify-test01.dev-dot.li` | +| Property | Value | +| ------------ | ----------------------------------------------------------------------------------------------- | +| **Type** | Comma-separated HTTPS origin list | +| **Required** | Multiple hosted frontends | +| **Default** | The single `API_ORIGIN` value | +| **Example** | `https://muzinga.netlify.app,https://dotify-test01.dev-dot.li,polkadot://app.dotify-test01.dot` | Exact frontend origins accepted by backend CORS. When set, it takes precedence over `API_ORIGIN`. Do not use `*`: the API carries authenticated upload and diff --git a/services/api/fly.toml b/services/api/fly.toml index d5222f7..baa8017 100644 --- a/services/api/fly.toml +++ b/services/api/fly.toml @@ -10,7 +10,17 @@ primary_region = "ams" # checks target the owner-guarded factory/directory pair. API_PORT = "8790" NODE_ENV = "production" - API_ORIGINS = "https://muzinga.netlify.app,https://dotify-test01.dev-dot.li" + # Three frontends reach this API: Netlify, the DotNS web gateway, and the app + # as served inside the Polkadot Product host container, which uses a custom + # scheme. Keep this in step with SIGNAL_ORIGINS in web/fly.signal.toml. + # + # `polkadot:` is a non-special scheme, so `new URL(...).origin` is opaque and + # a browser may send `Origin: null` instead of the literal value below. If a + # host request is still refused, read the actual Origin header off the Fly log + # before widening this list - never add a bare `null`, which would admit every + # sandboxed iframe and file:// page on the web to authenticated upload and + # content-key routes. + API_ORIGINS = "https://muzinga.netlify.app,https://dotify-test01.dev-dot.li,polkadot://app.dotify-test01.dot" PASEO_ASSET_HUB_RPC = "https://eth-rpc-testnet.polkadot.io/" DOTIFY_FACTORY_ADDRESS = "0xbd1a11cfce8b5ef7a37e507bc5109895f8f42a72" DOTIFY_DIRECTORY_ADDRESS = "0xcf1534c6e2b0e43b9436c1e86a076466dc0f2108" diff --git a/services/api/src/cors.test.ts b/services/api/src/cors.test.ts index 22e3894..f61ac66 100644 --- a/services/api/src/cors.test.ts +++ b/services/api/src/cors.test.ts @@ -34,4 +34,35 @@ describe('frontend origin boundary', () => { }); assert.equal(unrelated.headers['access-control-allow-origin'], undefined); }); + + it('allows the Product host container origin, which uses a custom scheme', async () => { + // Inside the Product host the app is served from polkadot://, not the DotNS + // web gateway. Without this the container gets rooms but no content keys. + const hostOrigin = 'polkadot://app.dotify-test01.dot'; + app = await buildApp({ logging: false, apiOrigins: [hostOrigin] }); + + const response = await app.inject({ + method: 'GET', + url: '/health', + headers: { origin: hostOrigin }, + }); + + assert.equal(response.headers['access-control-allow-origin'], hostOrigin); + }); + + it('refuses a null origin even when a custom-scheme origin is allowed', async () => { + // `polkadot:` is a non-special scheme, so browsers may send `Origin: null`. + // Answering that would admit every sandboxed iframe and file:// page to the + // authenticated upload and content-key routes, so it must stay refused + // until the real header is observed and allowlisted deliberately. + app = await buildApp({ logging: false, apiOrigins: ['polkadot://app.dotify-test01.dot'] }); + + const response = await app.inject({ + method: 'GET', + url: '/health', + headers: { origin: 'null' }, + }); + + assert.equal(response.headers['access-control-allow-origin'], undefined); + }); }); diff --git a/web/fly.signal.toml b/web/fly.signal.toml index 5acd881..fb2b325 100644 --- a/web/fly.signal.toml +++ b/web/fly.signal.toml @@ -10,7 +10,7 @@ primary_region = "ams" SIGNAL_ROOM_TTL_MS = "21600000" SIGNAL_HOST_TIMEOUT_MS = "120000" SIGNAL_MAX_LISTENERS = "24" - SIGNAL_ORIGINS = "https://muzinga.netlify.app,https://dotify-test01.dev-dot.li" + SIGNAL_ORIGINS = "https://muzinga.netlify.app,https://dotify-test01.dev-dot.li,polkadot://app.dotify-test01.dot" [http_service] internal_port = 8788 From cb41c269a54539e725c83fe6195edd68dd4ed6f0 Mon Sep 17 00:00:00 2001 From: Kevin Nzeng Essimengane Date: Wed, 29 Jul 2026 13:53:08 +0200 Subject: [PATCH 22/22] docs: assess Dotify against the official Product stack Grounded in the Product docs, SDK reference, and Community Foundation repos rather than assumption. Three findings drive the proposal. Dotify is closer to the stack than the roadmap assumed. Its Solidity contracts already run on pallet-revive via Asset Hub's eth-rpc - the exact pallet the stack specifies - and app delivery, content addressing, and app-scoped identity are already on-stack. Its DAV2 encryption is not redundant with a Bulletin move but the precondition for one, since Bulletin gates storing, not reading. The genuine gaps are worth closing and mostly make the product better: the personhood precompile returns a per-app unlinkable alias and would finally make `human-free` real while retiring the dev registrar; CDM registration makes the catalog composable by other products; CASH is the asset users actually hold. One gap will not close. Statement Store writes require an Individuality allowance, official calls are 1:1 and mobile-only, and an SDP exceeds both the 512-byte statement and 1 KiB per-account ceilings. Moving rooms onto the official messaging layer would convert every listener into an attested person, which does not degrade the product - it deletes the gesture it exists to protect. The anonymous guest is therefore treated as a design constraint, not a legacy compromise. Proposes a three-ring architecture that shrinks the trusted core instead of denying it, and names content-key custody as a stated exception with per-artist custody as the strongest remedy - turning the most centralized component into an expression of artist sovereignty. Co-Authored-By: Claude Opus 5 (1M context) --- docs/design/dotify-product-stack-alignment.md | 278 ++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 docs/design/dotify-product-stack-alignment.md diff --git a/docs/design/dotify-product-stack-alignment.md b/docs/design/dotify-product-stack-alignment.md new file mode 100644 index 0000000..22b88d8 --- /dev/null +++ b/docs/design/dotify-product-stack-alignment.md @@ -0,0 +1,278 @@ +# Dotify On The Product Stack: Assessment And Proposed Architecture + +Status: proposal. No code changes implied by this document alone. + +Sources: [Product docs](https://docs.polkadotcommunity.foundation/), +[Product SDK](https://paritytech.github.io/product-sdk/), +[resources](https://docs.polkadotcommunity.foundation/reference/resources/), +[Polkadot Community Foundation](https://github.com/Polkadot-Community-Foundation). +Claims below are quoted or cited; where the documentation is silent, this +document says so rather than guessing. + +## 1. What The Official Stack Actually Is + +Ten architecture layers, each with a defined owner: + +| Layer | What it provides | Where it lives | +| --- | --- | --- | +| Client tier | The Polkadot app; apps run *inside* a host container | Desktop / Mobile / `dev-dot.li` | +| Identity | Device attestation -> JWT, Lite usernames, Full personhood | `identity-backend` (centralized HTTP), `people-lite`, `proof-of-ink` | +| Naming | `.dot` names; usernames mirror into DotNS | DotNS | +| App delivery | build -> Bulletin -> DotNS bind -> Browse listing | Bulletin + DotNS | +| Storage | Content-addressed CIDs; authorization is a byte/tx quota with expiry | Bulletin (para 1010) | +| Contracts | PolkaVM via `pallet-revive`; CDM builds, deploys, registers, resolves | Asset Hub (1000) | +| Identity in contracts | **Personhood precompile** returning a per-app privacy-preserving alias | Asset Hub | +| Money | CASH (pUSD asset 1) spent through Coinage; PAS pays fees | People chain (1004) | +| Messaging & calls | Encrypted chat, 1:1 voice/video; **signaling travels on-chain** | People statement store + platform TURN | +| Discovery | Browse | `browse.dev-dot.li` | + +Three properties matter more than the inventory. + +**The host is the runtime.** `createApp` "requires a host and will throw on boot +without one". The chain client has no direct-WebSocket fallback. An app on this +stack is not a website that talks to chains; it is a guest process inside the +Polkadot app. + +**Writing is gated by personhood.** Statement Store is a custom RPC on People +chain nodes, 512 bytes per statement, 1 KiB per account, ~48h retention, and an +account "MUST have a Statement Store allowance to write - granted via +Individuality runtime registration". Publishing is a privilege attached to an +attested person. + +**The stack keeps its own centralized pieces.** `identity-backend` is "a +centralized HTTP service handling device attestation, username allocation, and +JWT sessions". Calls get "temporary TURN credentials" from platform +infrastructure. This is not hypocrisy; it is an honest admission that some roles +have no decentralized implementation yet. Dotify is entitled to the same honesty. + +## 2. Where Dotify Already Aligns + +More than the roadmap assumed. + +**Contracts are already in the right execution environment.** Dotify's Solidity +contracts are deployed through Asset Hub's `eth-rpc`, which is a compatibility +layer over `pallet-revive` - the exact pallet the stack specifies. Dotify is not +on a neighbouring chain; it is on the same VM, reached through a different +toolchain. Verified: identical ArtistDirectory bytecode from both the DevNet and +Hub TestNet endpoints. + +**App delivery is on-stack.** Bulletin chunked upload, DotNS binding to +`dotify-test01.dot`, `dev-dot.li` gateway. Delivered. + +**Identity is on-stack.** App-scoped Product account, SS58 plus derived H160, +connected only on explicit user action. + +**Content addressing matches.** Dotify already treats audio as immutable CIDs. +Bulletin is the same idea with a different authorizer. + +**Encryption already assumes ungated reads.** Bulletin "reading never needs" +authorization - it gates storing, not retrieval. Dotify's DAV2 encryption is +therefore not redundant with a move to Bulletin; it is the *precondition* for +one. Protected audio on a public content-addressed store must be encrypted, and +Dotify already does that. + +## 3. Where Dotify Diverges + +| Concern | Dotify today | Stack model | Real gap? | +| --- | --- | --- | --- | +| Contract toolchain | Solidity, Hardhat, viem, hand-built manifest | PolkaVM, CDM, `@org/name` resolution | Yes - composability and discoverability | +| Personhood | Dev-operated registrar, unused | Personhood precompile, contextual alias | Yes - and the stack's answer is better | +| Payments | `payForAccess` in native token | CASH via Coinage, host payment APIs | Yes - wrong asset, wrong surface | +| Catalog metadata | Fly read model over EVM logs | Bulletin CIDs + CDM resolution | Partly - a cache is legitimate | +| Audio storage | Pinata / IPFS pinning | Bulletin | Contested - see §6 | +| Room signaling | Socket.IO on Fly | People statement store | **Blocked** - see §4 | +| Content-key custody | Fly, `CONTENT_KEY_MASTER_SECRET` | No equivalent | **No stack answer exists** | + +## 4. The Constraint That Shapes Everything + +Dotify's first product invariant: + +> A room guest can join from a link without a wallet, signature, or payment. + +The stack's messaging model is the opposite by construction: + +- statements require an allowance, granted by Individuality registration; +- official calls are **1:1**, and "voice and video calls are a mobile-only + feature today"; +- signaling rides the People statement store, 512 B per statement, 1 KiB per + account. + +A WebRTC offer is roughly 1.5-4 KB. That is 3-8x the per-statement ceiling, and +the per-account ceiling is 1 KiB - so a peer cannot hold even one SDP in the +store. Chunking does not rescue it; the budget is the wall, not the chunk size. + +And the arithmetic is the *lesser* problem. The greater one is that a guest must +publish an answer to complete a handshake, which requires an attested identity. + +**Moving Dotify's rooms onto the official messaging layer would convert every +listener into a registered, attested person.** That does not degrade the +product; it deletes it. The gesture Dotify exists to protect - "someone lets +another person listen with them" - becomes an onboarding funnel. + +This is where the word *convivial* earns its keep. A convivial tool, in Illich's +sense, is one people can use without first submitting to an institution. A +listening room that demands attestation at the door is a well-engineered +enclosure. The north star is explicit that Web3 here is "invisible trust", not +decoration - and an identity checkpoint is the most visible decoration there is. + +So: **the anonymous guest is not a legacy compromise to be migrated away. It is +the design constraint the architecture must be built around.** + +## 5. Proposed Architecture: Three Rings + +Organise every component by how much trust it requires, and shrink the inner +rings rather than pretending they are empty. + +```text +Ring 1 On-stack, no compromise + contracts (CDM) · personhood (precompile) · payments (CASH) + app delivery (Bulletin/DotNS/Browse) · catalog metadata (Bulletin) + room discovery + presence (statement store) + +Ring 2 Minimal necessary infrastructure + stateless SDP rendezvous · TURN relay + +Ring 3 The stated exception + content-key custody +``` + +The rule: a component may only sit in an outer ring if no inner-ring mechanism +can hold it, and the reason is written down. + +### Ring 1 - move these, they are strictly better on-stack + +**Contracts into CDM.** Register the runtime family as `@dotify/*`. The +generated manifest already exists; CDM registration adds name-based resolution, +Bulletin-hosted ABIs, and composability - another product can resolve +`@dotify/artist-runtime` and read a catalog without asking Dotify. First-writer +-owns makes the name a durable asset. Solidity stays; the target is already +PolkaVM. + +**Personhood onto the precompile.** Replace the dev registrar. The runtime's +`requiredPersonhood` reads the precompile directly, receiving "a +per-application, privacy-preserving pseudonym: the same person yields a +different alias in a different context". This is the single strongest alignment +available: Dotify's `human-free` access mode becomes real, private, and +unlinkable across apps, and the registrar disappears. It also retires the +project's weakest claim. + +**Payments to CASH.** Users see CASH as their balance; PAS is a fee token they +should not think about. Charging in PAS is a category error on this stack. The +runtime stays the authority on entitlement; settlement moves to host payment +APIs. + +*Open problem, stated plainly:* CASH lives on People chain, the runtime lives on +Asset Hub. Cross-chain settlement is unsolved here. Two candidate shapes - a +host-signed payment receipt the runtime verifies, or an Asset-Hub-side +entitlement credited from an attested People-chain transfer. Both need design +work. Do not ship a payment path until this is settled. + +**Catalog metadata to Bulletin.** Release metadata, artwork, and manifests are +small, immutable, and public. Exactly Bulletin's shape. The Fly read model +becomes a cache with a provable source, not the source. + +**Room discovery and presence to the statement store.** A `{room, host, +listeners, ts}` record is ~100 B, well inside 512 B, and `ChannelStore`'s +last-write-wins is the right primitive. The *host* is identified and can hold an +allowance, so this works without touching the guest. Rooms become discoverable +without Dotify's servers - a genuine decentralization win that costs the product +nothing. + +### Ring 2 - shrink, do not eliminate + +The current signaling service does rooms state, presence, chat, reactions, +requests, and SDP relay. Most of that moves to Ring 1. What is left: + +**A stateless SDP rendezvous.** No room registry, no chat, no persistence - +short-TTL mailboxes keyed by room code, so an anonymous guest can hand its +answer to a host. This is the irreducible remainder of "let a stranger connect +without an account". + +**TURN**, for peers behind symmetric NAT. + +The stack does the same thing for its own calls: platform-issued TURN +credentials, because NAT traversal has no on-chain answer. Ring 2 is not +Dotify's deviation from the stack; it is the same concession the stack makes, +kept as small as the product allows. + +*Open question worth asking the Foundation:* can third-party products obtain +TURN credentials from platform infrastructure? If yes, Ring 2 halves. + +### Ring 3 - name the exception + +Content-key custody cannot move. Protected audio must be encrypted at rest on a +publicly readable store, the key must be released only after a server-side +access check, and the stack offers no confidential compute to run that check. +Putting the key in the client defeats the encryption; putting it on-chain +publishes it. + +The honest position is to say so, and to reduce the blast radius rather than +claim it away: + +- **Per-artist custody** - an artist's runtime designates its keyholder, so + Dotify is not one master secret for the whole commons. This follows directly + from artist sovereignty: an artist who controls catalog, access, and rights + should control the key too. +- **Threshold shares**, so no single operator can unilaterally release. +- **Narrow the window** - keys scoped per track, per session, short-lived. + +Ranked by fit with the north star, per-artist custody is the strongest: it turns +the platform's most centralized component into an expression of the project's +central political claim. + +## 6. What I Would Not Do + +**Do not move audio to Bulletin yet.** Bulletin authorization is "a bounded +quota with an expiry, not a permanent grant", and the docs give no size limits +or retention guarantee for MB-scale media. A growing catalog would need +perpetual re-authorization, and an expired quota on a music library is a dead +catalog. Move metadata now; move audio when quota economics for large media and +indefinite retention are demonstrated. Revisit, do not assume. + +**Do not adopt the official calls layer.** 1:1 and mobile-only cannot serve one +host with many listeners. + +**Do not rewrite the contracts to ink!.** They already run on the target VM. +Rewriting spends the project's scarcest resource on zero user-visible gain. + +**Do not delete the Fly API to look decentralized.** It would move key custody +into the browser - strictly worse for artists and listeners, and dishonest about +where trust sits. The stack runs a centralized identity backend for the same +class of reason. + +## 7. Sequence + +Ordered by value per unit of risk: + +1. **Personhood precompile** - retires the weakest claim, unlocks `human-free`, + no user-facing regression. Highest value, self-contained. +2. **CDM registration of `@dotify/*`** - claims the names, makes the catalog + composable. Manifest work already done. +3. **Presence and discovery to the statement store** - real decentralization, + guest path untouched. +4. **Catalog metadata to Bulletin** - Fly read model demoted to cache. +5. **Shrink signaling to a rendezvous** - only after 3 lands. +6. **Per-artist key custody** - the deepest change; do it when the runtime work + above has settled. +7. **CASH settlement** - last, and only after the cross-chain design is proven. + +Steps 1-4 are additive and independently shippable. Nothing before step 5 +touches the walletless guest path. + +## 8. Honest Summary + +Dotify is closer to the official stack than the roadmap assumed - same VM, same +delivery path, same content addressing, and an encryption model that Bulletin +would require anyway. The genuine gaps are personhood, contract registration, +payments, and metadata storage, and all four are improvements Dotify should +want. + +One gap will not close: the stack's messaging assumes attested participants, and +Dotify's rooms assume strangers. That is not a defect on either side. It is two +products with different social contracts. Dotify should adopt the stack +everywhere it fits, and keep the smallest possible amount of infrastructure to +protect the one promise the stack cannot make - that you can send someone a +link, and they can just listen. + +Build infrastructure for relation, not a casino wearing headphones, and not a +turnstile either.