diff --git a/.gitignore b/.gitignore
index 3214ad8..60c65b8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,11 +8,14 @@ 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/*.car
web/.playwright*
web/playwright-report/
web/test-results/
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/README.md b/README.md
index eed1f79..0875db9 100644
--- a/README.md
+++ b/README.md
@@ -34,20 +34,28 @@ 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-test01.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-test01.dot`. It keeps standalone link-first rooms and Free listening intact,
+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.
+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 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).
## Deployed
@@ -69,7 +77,7 @@ becomes a production dependency.
**Gateway URL** —
-**DotNS name** — `dotify.dot.li`
+**DotNS name** — `dotify-test01.dev-dot.li`
## How to run end-to-end (locally)
@@ -118,7 +126,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 |
@@ -298,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
@@ -430,9 +443,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 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/contracts/evm/.cdm/cdm.d.ts b/contracts/evm/.cdm/cdm.d.ts
new file mode 100644
index 0000000..d44f402
--- /dev/null
+++ b/contracts/evm/.cdm/cdm.d.ts
@@ -0,0 +1,37 @@
+// Auto-generated by cdm install -- do not edit
+import type { HexString, SizedHex } from "polkadot-api";
+
+declare module "@parity/cdm-codegen" {
+ interface CdmContracts {
+ "@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 };
+ };
+ };
+ }
+}
diff --git a/contracts/evm/.cdm/contracts.d.ts b/contracts/evm/.cdm/contracts.d.ts
new file mode 100644
index 0000000..23f66df
--- /dev/null
+++ b/contracts/evm/.cdm/contracts.d.ts
@@ -0,0 +1,37 @@
+// 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 };
+ };
+ };
+ }
+}
diff --git a/contracts/evm/.cdm/contracts/@dotify/artist-directory/0/abi.json b/contracts/evm/.cdm/contracts/@dotify/artist-directory/0/abi.json
new file mode 100644
index 0000000..0448cfd
--- /dev/null
+++ b/contracts/evm/.cdm/contracts/@dotify/artist-directory/0/abi.json
@@ -0,0 +1,176 @@
+[
+ {
+ "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"
+ }
+]
\ No newline at end of file
diff --git a/contracts/evm/.cdm/contracts/@dotify/artist-directory/0/info.json b/contracts/evm/.cdm/contracts/@dotify/artist-directory/0/info.json
new file mode 100644
index 0000000..d01ceb3
--- /dev/null
+++ b/contracts/evm/.cdm/contracts/@dotify/artist-directory/0/info.json
@@ -0,0 +1,6 @@
+{
+ "name": "@dotify/artist-directory",
+ "version": 0,
+ "address": "0xcf1534C6e2B0E43B9436c1e86A076466dC0F2108",
+ "metadataCid": "bafk2bzacebiynqo7tjvxa3xlvf6pphgq3nzatqjbn3yo2fszrlvxibtsr4ce6"
+}
\ No newline at end of file
diff --git a/contracts/evm/.cdm/contracts/@dotify/artist-directory/0/metadata.json b/contracts/evm/.cdm/contracts/@dotify/artist-directory/0/metadata.json
new file mode 100644
index 0000000..be8f5df
--- /dev/null
+++ b/contracts/evm/.cdm/contracts/@dotify/artist-directory/0/metadata.json
@@ -0,0 +1,193 @@
+{
+ "name": "@dotify/artist-directory",
+ "description": "Registry mapping each artist address to their owned SmartRuntime. Entry point for enumerating the Dotify catalog.",
+ "readme": "# @dotify/artist-directory\n\nMaps an artist address to the address of the SmartRuntime they own, and enumerates\nevery registered artist.\n\nStart here to read the Dotify catalog: `artistCount()` and `artistsPage(offset, limit)`\nenumerate artists with their runtimes, and `runtimeOf(artist)` resolves one directly.\nEach runtime then exposes its own tracks and access policy.\n\nRegistration is performed by the artist runtime factory, not by callers.",
+ "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"
+ }
+ ],
+ "homepage": "https://muzinga.netlify.app",
+ "repository": "https://github.com/knzeng-e/dotify",
+ "license": "MIT",
+ "authors": [
+ "Dotify"
+ ],
+ "keywords": [
+ "music",
+ "dotify",
+ "artist-runtime",
+ "access-control"
+ ]
+}
\ No newline at end of file
diff --git a/contracts/evm/.cdm/contracts/@dotify/artist-directory/latest b/contracts/evm/.cdm/contracts/@dotify/artist-directory/latest
new file mode 120000
index 0000000..c227083
--- /dev/null
+++ b/contracts/evm/.cdm/contracts/@dotify/artist-directory/latest
@@ -0,0 +1 @@
+0
\ No newline at end of file
diff --git a/contracts/evm/.cdm/contracts/@dotify/artist-runtime-factory/0/abi.json b/contracts/evm/.cdm/contracts/@dotify/artist-runtime-factory/0/abi.json
new file mode 100644
index 0000000..1f380ce
--- /dev/null
+++ b/contracts/evm/.cdm/contracts/@dotify/artist-runtime-factory/0/abi.json
@@ -0,0 +1,316 @@
+[
+ {
+ "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"
+ }
+]
\ No newline at end of file
diff --git a/contracts/evm/.cdm/contracts/@dotify/artist-runtime-factory/0/info.json b/contracts/evm/.cdm/contracts/@dotify/artist-runtime-factory/0/info.json
new file mode 100644
index 0000000..7cd09ff
--- /dev/null
+++ b/contracts/evm/.cdm/contracts/@dotify/artist-runtime-factory/0/info.json
@@ -0,0 +1,6 @@
+{
+ "name": "@dotify/artist-runtime-factory",
+ "version": 0,
+ "address": "0xBd1a11cFcE8B5Ef7a37E507bC5109895F8F42a72",
+ "metadataCid": "bafk2bzaceckm27ft3kvt4mjs67hzklylws5fp5d4z4nrvpa34gmzndtqz7lwg"
+}
\ No newline at end of file
diff --git a/contracts/evm/.cdm/contracts/@dotify/artist-runtime-factory/0/metadata.json b/contracts/evm/.cdm/contracts/@dotify/artist-runtime-factory/0/metadata.json
new file mode 100644
index 0000000..e2af81e
--- /dev/null
+++ b/contracts/evm/.cdm/contracts/@dotify/artist-runtime-factory/0/metadata.json
@@ -0,0 +1,333 @@
+{
+ "name": "@dotify/artist-runtime-factory",
+ "description": "Deploys one artist-owned SmartRuntime per artist and registers it in the artist directory.",
+ "readme": "# @dotify/artist-runtime-factory\n\nDeploys a SmartRuntime for an artist and registers it in `@dotify/artist-directory`.\n\nA runtime is a diamond: music registry, royalties, access, and NFT pallets are\ninstalled as facets, and the artist is set as its owner. Because each artist owns\ntheir own runtime, catalog, access policy, and royalty splits stay under the\nartist's control rather than the platform's.\n\nCreation is staged - `createRuntime()` then `installRuntimeStep()` until\n`pendingRuntimeStageOf(artist)` reports completion - so that installation fits\nwithin block limits.",
+ "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"
+ }
+ ],
+ "homepage": "https://muzinga.netlify.app",
+ "repository": "https://github.com/knzeng-e/dotify",
+ "license": "MIT",
+ "authors": [
+ "Dotify"
+ ],
+ "keywords": [
+ "music",
+ "dotify",
+ "artist-runtime",
+ "access-control"
+ ]
+}
\ No newline at end of file
diff --git a/contracts/evm/.cdm/contracts/@dotify/artist-runtime-factory/latest b/contracts/evm/.cdm/contracts/@dotify/artist-runtime-factory/latest
new file mode 120000
index 0000000..c227083
--- /dev/null
+++ b/contracts/evm/.cdm/contracts/@dotify/artist-runtime-factory/latest
@@ -0,0 +1 @@
+0
\ No newline at end of file
diff --git a/contracts/evm/.cdm/solidity/dotify/artist-directory.sol b/contracts/evm/.cdm/solidity/dotify/artist-directory.sol
new file mode 100644
index 0000000..4f7be3c
--- /dev/null
+++ b/contracts/evm/.cdm/solidity/dotify/artist-directory.sol
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: Apache-2.0
+pragma solidity ^0.8.28;
+
+// Auto-generated by cdm install. Do not edit by hand.
+// CDM package: @dotify/artist-directory
+// CDM version: 0
+
+interface IDotifyArtistDirectory {
+ event ArtistRegistered(address indexed artist, address indexed runtime);
+ event FactorySet(address indexed factory);
+ function artistAtIndex(uint256 index) external view returns (address);
+ function artistCount() external view returns (uint256);
+ function artistsPage(uint256 offset, uint256 limit) external view returns (address[] memory, address[] memory);
+ function deployer() external view returns (address);
+ function factory() external view returns (address);
+ function register(address artist, address runtime) external;
+ function runtimeOf(address arg0) external view returns (address);
+ function setFactory(address _factory) external;
+}
+
+library DotifyArtistDirectory {
+ address internal constant ADDRESS = 0xcf1534C6e2B0E43B9436c1e86A076466dC0F2108;
+
+ function ref() internal pure returns (IDotifyArtistDirectory) {
+ return IDotifyArtistDirectory(ADDRESS);
+ }
+
+ function cdm() internal pure returns (IDotifyArtistDirectory) {
+ return ref();
+ }
+}
diff --git a/contracts/evm/.cdm/solidity/dotify/artist-runtime-factory.sol b/contracts/evm/.cdm/solidity/dotify/artist-runtime-factory.sol
new file mode 100644
index 0000000..91c8ed6
--- /dev/null
+++ b/contracts/evm/.cdm/solidity/dotify/artist-runtime-factory.sol
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: Apache-2.0
+pragma solidity ^0.8.28;
+
+// Auto-generated by cdm install. Do not edit by hand.
+// CDM package: @dotify/artist-runtime-factory
+// CDM version: 0
+
+interface IDotifyArtistRuntimeFactory {
+ event ArtistRuntimeBootstrapStarted(address indexed artist, address indexed runtime);
+ event ArtistRuntimeBootstrapStep(address indexed artist, address indexed runtime, uint8 indexed completedStage);
+ event ArtistRuntimeCreated(address indexed artist, address indexed runtime);
+ function accessPallet() external view returns (address);
+ function createRuntime() external returns (address);
+ function cutPallet() external view returns (address);
+ function directory() external view returns (address);
+ function initContract() external view returns (address);
+ function installRuntimeStep() external returns (uint8);
+ function loupePallet() external view returns (address);
+ function nftPallet() external view returns (address);
+ function ownershipPallet() external view returns (address);
+ function pendingRuntimeOf(address arg0) external view returns (address);
+ function pendingRuntimeStageOf(address arg0) external view returns (uint8);
+ function registryPallet() external view returns (address);
+ function royaltiesPallet() external view returns (address);
+ function runtimeOf(address artist) external view returns (address);
+}
+
+library DotifyArtistRuntimeFactory {
+ address internal constant ADDRESS = 0xBd1a11cFcE8B5Ef7a37E507bC5109895F8F42a72;
+
+ function ref() internal pure returns (IDotifyArtistRuntimeFactory) {
+ return IDotifyArtistRuntimeFactory(ADDRESS);
+ }
+
+ function cdm() internal pure returns (IDotifyArtistRuntimeFactory) {
+ return ref();
+ }
+}
diff --git a/contracts/evm/cdm.json b/contracts/evm/cdm.json
new file mode 100644
index 0000000..b7560fa
--- /dev/null
+++ b/contracts/evm/cdm.json
@@ -0,0 +1,511 @@
+{
+ "dependencies": {
+ "@dotify/artist-directory": "latest",
+ "@dotify/artist-runtime-factory": "latest"
+ },
+ "contracts": {
+ "@dotify/artist-directory": {
+ "version": 0,
+ "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"
+ }
+ ],
+ "metadataCid": "bafk2bzacebiynqo7tjvxa3xlvf6pphgq3nzatqjbn3yo2fszrlvxibtsr4ce6"
+ },
+ "@dotify/artist-runtime-factory": {
+ "version": 0,
+ "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"
+ }
+ ],
+ "metadataCid": "bafk2bzaceckm27ft3kvt4mjs67hzklylws5fp5d4z4nrvpa34gmzndtqz7lwg"
+ }
+ },
+ "registry": "0x59b0245778917af55224e5f8fb55f7f8d452619f"
+}
diff --git a/contracts/evm/contracts/ArtistRuntimeFactory.sol b/contracts/evm/contracts/ArtistRuntimeFactory.sol
index ab648d4..1b43c75 100644
--- a/contracts/evm/contracts/ArtistRuntimeFactory.sol
+++ b/contracts/evm/contracts/ArtistRuntimeFactory.sol
@@ -253,8 +253,12 @@ contract ArtistRuntimeFactory {
selectors[4] = MusicRoyaltiesPallet.musicRoyTotalBps.selector;
}
+ /// @dev The two registrar selectors are retained so already-deployed runtimes keep a
+ /// stable ABI; the setter now reverts. `musicAccPersonhoodInfo` is new, so a
+ /// runtime created before this change needs a diamond Add cut to gain it — its
+ /// access decisions already follow the precompile without it.
function _musicAccessSelectors() private pure returns (bytes4[] memory selectors) {
- selectors = new bytes4[](7);
+ selectors = new bytes4[](8);
selectors[0] = MusicAccessPallet.setPersonhoodRegistrar.selector;
selectors[1] = MusicAccessPallet.musicAccSetPersonhoodLevel.selector;
selectors[2] = MusicAccessPallet.musicAccCanAccess.selector;
@@ -262,5 +266,6 @@ contract ArtistRuntimeFactory {
selectors[4] = MusicAccessPallet.musicAccPersonhoodLevel.selector;
selectors[5] = MusicAccessPallet.musicAccHasPersonhood.selector;
selectors[6] = MusicAccessPallet.musicAccGetRegistrar.selector;
+ selectors[7] = MusicAccessPallet.musicAccPersonhoodInfo.selector;
}
}
diff --git a/contracts/evm/contracts/interfaces/IPersonhood.sol b/contracts/evm/contracts/interfaces/IPersonhood.sol
new file mode 100644
index 0000000..09519c8
--- /dev/null
+++ b/contracts/evm/contracts/interfaces/IPersonhood.sol
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.28;
+
+/// @title IPersonhood — Proof of Personhood precompile (Individuality)
+/// @notice Minimal interface for the `pallet-revive` personhood precompile, live on
+/// Asset Hub at `0x000000000000000000000000000000000A010000`.
+///
+/// Mirrors the canonical declaration in
+/// `paseo-network/runtimes/precompiles/personhood/sol/IPersonhood.sol`.
+/// Only `personhoodStatus` is declared here: Dotify reads status, it does not
+/// verify raw ring proofs, so `personhoodInfoByProof` is deliberately omitted
+/// rather than carried as unused surface.
+///
+/// The precompile reads the alias-accounts pallet, which stores per-context
+/// alias mappings backed by ring membership proofs. Ring roots arrive from the
+/// People chain by XCM.
+interface IPersonhood {
+ /// @param status Personhood tier: 0 = None, 1 = Lite, 2 = Full. Tiers are
+ /// incremental, so a future tier leaves these values unchanged.
+ /// @param contextAlias Per-context 32-byte pseudonym derived from the ring membership
+ /// proof. Unique per person per context, which is what prevents
+ /// cross-application linkability. Zero when status is None.
+ struct PersonhoodInfo {
+ uint8 status;
+ bytes32 contextAlias;
+ }
+
+ /// @notice Personhood info for `account` within a specific application `context`.
+ /// @param context A fixed 32-byte application identifier. The same person yields a
+ /// different `contextAlias` under a different context.
+ function personhoodStatus(address account, bytes32 context) external view returns (PersonhoodInfo memory info);
+}
diff --git a/contracts/evm/contracts/libraries/LibMusicAccess.sol b/contracts/evm/contracts/libraries/LibMusicAccess.sol
index 59adc1c..583b01c 100644
--- a/contracts/evm/contracts/libraries/LibMusicAccess.sol
+++ b/contracts/evm/contracts/libraries/LibMusicAccess.sol
@@ -2,11 +2,21 @@
pragma solidity ^0.8.28;
import { LibMusicRegistry } from './LibMusicRegistry.sol';
+import { LibPersonhood } from './LibPersonhood.sol';
/// @title LibMusicAccess
-/// @notice Namespaced storage for listener access records and proof-of-personhood levels.
-/// Personhood levels mirror the Individuality Chain DIM tiers; in the current
-/// prototype they are set via an admin registrar account.
+/// @notice Namespaced storage for listener access records, and personhood gating read
+/// from the Individuality precompile.
+///
+/// Personhood is no longer stored here. `personhoodLevelOf` and
+/// `personhoodRegistrar` remain declared so existing runtimes keep their storage
+/// layout intact — a diamond cannot safely reorder occupied slots — but neither
+/// participates in an access decision any more. The registrar defaulted to the
+/// artist, which meant an artist could grant personhood to their own listeners;
+/// the precompile removes that path.
+///
+/// PersonhoodLevel maps onto the precompile tiers by ordinal:
+/// None(0) -> None(0), DIM1(1) -> Lite(1), DIM2(2) -> Full(2).
///
/// Storage slot: keccak256("smart.runtime.pallet.music-access.storage")
library LibMusicAccess {
@@ -15,9 +25,10 @@ library LibMusicAccess {
struct Storage {
// contentHash → listener → paid
mapping(bytes32 => mapping(address => bool)) paidAccess;
- // account → verified personhood tier
+ // DEPRECATED — no longer read for access. Kept to preserve the storage layout of
+ // already-deployed runtimes. Personhood now comes from the precompile.
mapping(address => LibMusicRegistry.PersonhoodLevel) personhoodLevelOf;
- // address authorised to set personhood levels
+ // DEPRECATED — see above. Retained for layout compatibility only.
address personhoodRegistrar;
}
@@ -32,9 +43,19 @@ library LibMusicAccess {
// Internal helpers
// -------------------------------------------------------------------------
- function hasRequiredPersonhood(Storage storage s, address account, LibMusicRegistry.PersonhoodLevel required) internal view returns (bool) {
- if (required == LibMusicRegistry.PersonhoodLevel.None) return true;
- return uint8(s.personhoodLevelOf[account]) >= uint8(required);
+ /// @notice True when `account` meets `required` personhood, per the Individuality
+ /// precompile. The storage argument is unused and kept only so existing
+ /// call sites and the pallet ABI stay unchanged.
+ /// @dev Fails closed when the precompile cannot answer. See LibPersonhood.hasStatus.
+ function hasRequiredPersonhood(Storage storage, address account, LibMusicRegistry.PersonhoodLevel required) internal view returns (bool) {
+ return LibPersonhood.hasStatus(account, uint8(required));
+ }
+
+ /// @notice Dotify-context personhood tier and pseudonym for `account`.
+ /// @dev Exposes the alias so a runtime can later count distinct people rather than
+ /// distinct addresses. Not used for access decisions today.
+ function personhoodOf(address account) internal view returns (uint8 status, bytes32 contextAlias, bool live) {
+ return LibPersonhood.readStatus(account);
}
function setPersonhoodRegistrar(Storage storage s, address registrar) internal returns (address previousRegistrar) {
diff --git a/contracts/evm/contracts/libraries/LibPersonhood.sol b/contracts/evm/contracts/libraries/LibPersonhood.sol
new file mode 100644
index 0000000..aaab23c
--- /dev/null
+++ b/contracts/evm/contracts/libraries/LibPersonhood.sol
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.28;
+
+import { IPersonhood } from '../interfaces/IPersonhood.sol';
+
+/// @title LibPersonhood
+/// @notice Reads proof of personhood from the Individuality precompile.
+///
+/// This replaces Dotify's admin-registrar personhood, which could only ever be
+/// as trustworthy as the account operating it — and that account defaults to
+/// the artist, who therefore had the technical ability to grant personhood to
+/// their own listeners. Reading the precompile removes that forgery path
+/// entirely: personhood becomes a fact about a person on the People chain, not
+/// a row an operator can write.
+///
+/// It also earns Dotify a property the registrar could not offer. The precompile
+/// returns a per-context alias, so the same listener appears under a different
+/// pseudonym in every application. Dotify learns "this is a distinct person"
+/// without learning who they are anywhere else.
+library LibPersonhood {
+ /// @dev Fixed precompile address. `pallet-revive` left-shifts the user-defined
+ /// `AddressMatcher::Fixed(0x0A01)` index by 16 bits to form this suffix.
+ /// Verified live on EVM chain 420420417 (Paseo Asset Hub, para 1000): a call
+ /// returns a 64-byte PersonhoodInfo, where absent addresses return empty.
+ address internal constant PERSONHOOD_PRECOMPILE = 0x000000000000000000000000000000000a010000;
+
+ /// @dev Dotify's application context. Fixed forever: changing it re-pseudonymises
+ /// every listener, so any change is an identity migration, not a config edit.
+ bytes32 internal constant DOTIFY_CONTEXT = bytes32('dotify');
+
+ uint8 internal constant STATUS_NONE = 0;
+ uint8 internal constant STATUS_LITE = 1;
+ uint8 internal constant STATUS_FULL = 2;
+
+ /// @notice Read personhood for `account` in Dotify's context.
+ /// @return status Personhood tier, or 0 when the precompile is unavailable.
+ /// @return alias_ Per-context pseudonym, zero when status is 0.
+ /// @return live True when the precompile answered with a decodable struct.
+ ///
+ /// @dev Deliberately a low-level staticcall rather than a typed call. The precompile
+ /// declares `HAS_CONTRACT_INFO = false`, so its `extcodesize` can be zero, and
+ /// Solidity's high-level call inserts an `extcodesize` check that would revert
+ /// against it. The staticcall also lets a chain without the precompile — a local
+ /// Hardhat node — resolve to "not live" instead of reverting every access query.
+ ///
+ /// `live` is returned rather than swallowed so callers can distinguish "this
+ /// person has no personhood" from "this chain cannot answer". Those are the same
+ /// decision (deny) but not the same diagnosis, and conflating them is how a
+ /// misconfigured deployment gets mistaken for an empty user base.
+ function readStatus(address account) internal view returns (uint8 status, bytes32 alias_, bool live) {
+ bytes memory callData = abi.encodeWithSelector(IPersonhood.personhoodStatus.selector, account, DOTIFY_CONTEXT);
+
+ (bool ok, bytes memory returnData) = PERSONHOOD_PRECOMPILE.staticcall(callData);
+
+ // A call to an address with no code succeeds with empty returndata, so success
+ // alone proves nothing. Only a full struct counts as an answer.
+ if (!ok || returnData.length < 64) {
+ return (STATUS_NONE, bytes32(0), false);
+ }
+
+ IPersonhood.PersonhoodInfo memory info = abi.decode(returnData, (IPersonhood.PersonhoodInfo));
+ return (info.status, info.contextAlias, true);
+ }
+
+ /// @notice True when `account` holds at least `requiredStatus` in Dotify's context.
+ /// @dev Fails closed: an unavailable precompile denies every gated track rather than
+ /// admitting everyone. Product invariant — ambiguous access decisions fail closed.
+ function hasStatus(address account, uint8 requiredStatus) internal view returns (bool) {
+ if (requiredStatus == STATUS_NONE) return true;
+ (uint8 status, , bool live) = readStatus(account);
+ if (!live) return false;
+ return status >= requiredStatus;
+ }
+}
diff --git a/contracts/evm/contracts/pallets/MusicAccessPallet.sol b/contracts/evm/contracts/pallets/MusicAccessPallet.sol
index 27e72f8..a2afafe 100644
--- a/contracts/evm/contracts/pallets/MusicAccessPallet.sol
+++ b/contracts/evm/contracts/pallets/MusicAccessPallet.sol
@@ -15,10 +15,17 @@ import { LibMusicNFT } from '../libraries/LibMusicNFT.sol';
/// 3. HumanFree track → granted if caller meets personhood level
/// 4. Classic track → granted if caller has paid
///
-/// Personhood levels (DIM1, DIM2) are set by the personhood registrar —
-/// an admin account that in production will mirror the Individuality Chain.
-/// The SmartRuntime owner remains the only account allowed to update
-/// the registrar assignment.
+/// Personhood (DIM1, DIM2) is read from the Individuality precompile at
+/// 0x000000000000000000000000000000000A010000, in Dotify's own application
+/// context. DIM1 maps to Lite, DIM2 to Full.
+///
+/// The former admin registrar is retired. It defaulted to the artist, so an
+/// artist could grant personhood to their own listeners — which made
+/// `human-free` a claim the contract could not actually support. Reading the
+/// precompile removes that path, and adds a per-context alias so Dotify can
+/// recognise a distinct person without learning who they are in any other
+/// application. Registrar entry points remain declared for ABI stability;
+/// the setter reverts.
///
/// Storage: LibMusicAccess (owns), LibMusicRegistry (reads), LibMusicNFT (reads)
/// Prefix: musicAcc — avoids selector collisions with other pallets
@@ -47,14 +54,14 @@ contract MusicAccessPallet {
// Personhood level management (registrar-only)
// -------------------------------------------------------------------------
- /// @notice Set the proof-of-personhood level for an account.
- /// In production this would be called by an oracle reading the Individuality Chain.
- function musicAccSetPersonhoodLevel(address account, LibMusicRegistry.PersonhoodLevel level) external {
- LibMusicAccess.Storage storage as_ = LibMusicAccess.store();
- LibMusicAccess.requireRegistrar(as_);
- require(account != address(0), 'MusicAccess: zero address');
- as_.personhoodLevelOf[account] = level;
- emit MusicAccPersonhoodLevelSet(account, level);
+ /// @notice DEPRECATED — personhood is read from the Individuality precompile and can
+ /// no longer be assigned by an operator.
+ /// @dev Reverts rather than writing to storage no access decision reads. Accepting a
+ /// write that silently changes nothing would leave an operator believing a
+ /// listener was granted access they do not have. The parameters are retained so
+ /// the selector and ABI stay stable for already-deployed runtimes.
+ function musicAccSetPersonhoodLevel(address, LibMusicRegistry.PersonhoodLevel) external pure {
+ revert('MusicAccess: personhood is read from the Individuality precompile');
}
// -------------------------------------------------------------------------
@@ -89,9 +96,24 @@ contract MusicAccessPallet {
return LibMusicAccess.store().paidAccess[contentHash][listener];
}
- /// @notice Returns the verified personhood level for `account`.
+ /// @notice Returns the verified personhood level for `account`, read from the
+ /// Individuality precompile in Dotify's application context.
+ /// @dev Returns None when the precompile is unavailable, matching the access
+ /// decision. Use `musicAccPersonhoodInfo` to tell those two cases apart.
function musicAccPersonhoodLevel(address account) external view returns (LibMusicRegistry.PersonhoodLevel) {
- return LibMusicAccess.store().personhoodLevelOf[account];
+ (uint8 status, , bool live) = LibMusicAccess.personhoodOf(account);
+ if (!live) return LibMusicRegistry.PersonhoodLevel.None;
+ return LibMusicRegistry.PersonhoodLevel(status);
+ }
+
+ /// @notice Full personhood reading for `account`: tier, Dotify-context pseudonym, and
+ /// whether the precompile answered at all.
+ /// @dev `live == false` means this chain cannot answer, which is a deployment
+ /// diagnosis, not a statement about the listener. `contextAlias` is the same
+ /// person under a different pseudonym in every other application, so it can
+ /// identify a returning listener without revealing who they are elsewhere.
+ function musicAccPersonhoodInfo(address account) external view returns (uint8 status, bytes32 contextAlias, bool live) {
+ return LibMusicAccess.personhoodOf(account);
}
/// @notice Returns true if `account` meets `required` personhood level.
diff --git a/contracts/evm/contracts/test/MockPersonhoodPrecompile.sol b/contracts/evm/contracts/test/MockPersonhoodPrecompile.sol
new file mode 100644
index 0000000..97be2e7
--- /dev/null
+++ b/contracts/evm/contracts/test/MockPersonhoodPrecompile.sol
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.28;
+
+/// @dev TEST-ONLY stand-in for the Individuality personhood precompile.
+///
+/// The real precompile lives at a fixed address inside `pallet-revive` and has no
+/// deployable bytecode, so a Hardhat node cannot host it. Tests install this
+/// contract's runtime code at that same address with `hardhat_setCode`, then write
+/// its storage through the ordinary setter below — the storage lands under the
+/// precompile address, so the runtime's staticcall reads it exactly as it would
+/// read the real thing.
+///
+/// This is the only way to exercise the fail-closed path and the granted path on a
+/// chain that has no Individuality pallet.
+contract MockPersonhoodPrecompile {
+ struct PersonhoodInfo {
+ uint8 status;
+ bytes32 contextAlias;
+ }
+
+ // account => context => info
+ mapping(address => mapping(bytes32 => PersonhoodInfo)) private _info;
+
+ /// @notice Set the personhood reading returned for `account` in `context`.
+ function setPersonhood(address account, bytes32 context, uint8 status, bytes32 contextAlias) external {
+ _info[account][context] = PersonhoodInfo({ status: status, contextAlias: contextAlias });
+ }
+
+ /// @notice Matches the real precompile's signature and return shape.
+ function personhoodStatus(address account, bytes32 context) external view returns (PersonhoodInfo memory info) {
+ return _info[account][context];
+ }
+}
diff --git a/contracts/evm/hardhat.config.ts b/contracts/evm/hardhat.config.ts
index cfa5c24..1b4af55 100644
--- a/contracts/evm/hardhat.config.ts
+++ b/contracts/evm/hardhat.config.ts
@@ -2,6 +2,7 @@ import type { HardhatUserConfig } from 'hardhat/config';
import '@nomicfoundation/hardhat-viem';
import '@nomicfoundation/hardhat-verify';
import './tasks/registryUpgrade';
+import './tasks/cdmPublish';
const config: HardhatUserConfig = {
solidity: {
diff --git a/contracts/evm/package.json b/contracts/evm/package.json
index 40c6f18..66be05a 100644
--- a/contracts/evm/package.json
+++ b/contracts/evm/package.json
@@ -18,7 +18,8 @@
"registry:deploy-facet:testnet": "npx hardhat registry:deploy-facet --network polkadotTestnet",
"registry:upgrade:testnet": "npx hardhat registry:upgrade --network polkadotTestnet",
"fmt": "prettier --plugin=prettier-plugin-solidity --write 'contracts/**/*.sol' 'scripts/**/*.ts' 'tasks/**/*.ts' 'test/**/*.ts' hardhat.config.ts",
- "fmt:check": "prettier --plugin=prettier-plugin-solidity --check 'contracts/**/*.sol' 'scripts/**/*.ts' 'tasks/**/*.ts' 'test/**/*.ts' hardhat.config.ts"
+ "fmt:check": "prettier --plugin=prettier-plugin-solidity --check 'contracts/**/*.sol' 'scripts/**/*.ts' 'tasks/**/*.ts' 'test/**/*.ts' hardhat.config.ts",
+ "cdm:publish:testnet": "npx hardhat cdm:publish --network polkadotTestnet"
},
"devDependencies": {
"@nomicfoundation/hardhat-network-helpers": "^1.0.0",
diff --git a/contracts/evm/tasks/cdmPublish.ts b/contracts/evm/tasks/cdmPublish.ts
new file mode 100644
index 0000000..fa25492
--- /dev/null
+++ b/contracts/evm/tasks/cdmPublish.ts
@@ -0,0 +1,277 @@
+// Register Dotify's already-deployed contracts in the Product CDM registry.
+//
+// Why a task instead of `cdm deploy`:
+//
+// `cdm deploy` builds, deploys, publishes metadata, and registers in one pass. Dotify's
+// contracts are already deployed and already hold the live catalog, so deploying again
+// would mint new addresses and orphan every existing artist runtime. The registry's
+// `publishLatest(name, address, metadataUri)` registers a name against an arbitrary
+// address, which is exactly the operation Dotify needs and the one the CLI does not
+// expose on its own.
+//
+// Registration is first-writer-owns and there is no release or transfer entry point in
+// the registry contract, so claiming a name is effectively permanent. This task is
+// therefore read-only by default: it reports what it would do and stops. Execution
+// requires --confirm plus an explicit private key.
+
+import { task, types } from 'hardhat/config';
+import type { HardhatRuntimeEnvironment } from 'hardhat/types';
+import * as fs from 'node:fs';
+import * as path from 'node:path';
+import { createPublicClient, createWalletClient, encodeFunctionData, getAddress, http, type Address, type Hex } from 'viem';
+import { privateKeyToAccount } from 'viem/accounts';
+import { POLKADOT_TESTNET_CHAIN, readDeployments } from '../scripts/smartRuntime';
+
+/// Written by `npm run generate:cdm-metadata` in web/. Holds the deterministic CID of
+/// each package's metadata blob, so the CID published on-chain is derived from the
+/// generated bytes rather than pasted by hand.
+const CID_INDEX_PATH = path.resolve(__dirname, '../../../web/src/generated/contracts/cdm-metadata/cids.json');
+
+type CidIndex = Record;
+
+function readCidIndex(): CidIndex | null {
+ try {
+ return JSON.parse(fs.readFileSync(CID_INDEX_PATH, 'utf8')) as CidIndex;
+ } catch {
+ return null;
+ }
+}
+
+/// Community-operated ContractRegistry for the Product `devnet` preset, i.e. Paseo
+/// testnet Asset Hub (para 1000, EVM chain 420420417). Sourced from
+/// paritytech/contract-dependency-manager `src/lib/env/src/registry.ts`.
+///
+/// Deliberately not the `paseo` preset registry: CDM's own docs note that `paseo`
+/// targets paseo-next (para 1500), a different network that holds no Dotify contracts.
+const DEVNET_REGISTRY_ADDRESS = '0x59b0245778917af55224e5f8fb55f7f8d452619f' as const;
+
+const REGISTRY_ABI = [
+ {
+ type: 'function',
+ name: 'publishLatest',
+ stateMutability: 'nonpayable',
+ inputs: [
+ { name: 'contract_name', type: 'string' },
+ { name: 'contract_address', type: 'address' },
+ { name: 'metadata_uri', type: 'string' }
+ ],
+ outputs: []
+ },
+ {
+ type: 'function',
+ name: 'getAddress',
+ stateMutability: 'view',
+ inputs: [{ name: 'contract_name', type: 'string' }],
+ outputs: [
+ {
+ name: '',
+ type: 'tuple',
+ components: [
+ { name: 'isSome', type: 'bool' },
+ { name: 'value', type: 'address' }
+ ]
+ }
+ ]
+ },
+ {
+ type: 'function',
+ name: 'getOwner',
+ stateMutability: 'view',
+ inputs: [{ name: 'contract_name', type: 'string' }],
+ outputs: [
+ {
+ name: '',
+ type: 'tuple',
+ components: [
+ { name: 'isSome', type: 'bool' },
+ { name: 'value', type: 'address' }
+ ]
+ }
+ ]
+ }
+] as const;
+
+/// Only fixed-address contracts can be registered. Artist runtimes are per-artist
+/// diamonds with no single address, so `@dotify/smart-runtime` is intentionally absent:
+/// registering one artist's runtime under a shared name would misrepresent the catalog.
+const PACKAGES = [
+ { name: '@dotify/artist-directory', deploymentKey: 'directory' as const },
+ { name: '@dotify/artist-runtime-factory', deploymentKey: 'factory' as const }
+];
+
+type PackagePlan = {
+ name: string;
+ address: Address;
+ hasCode: boolean;
+ registeredAddress: Address | null;
+ owner: Address | null;
+ action: 'register' | 'already-current' | 'blocked-owned-by-other' | 'update-version' | 'blocked-no-code';
+ detail: string;
+};
+
+/// The registry returns Rust `Option` as a `(bool, address)` tuple. viem may
+/// surface it as either an array or a named object depending on ABI shape, so accept
+/// both rather than depending on that detail.
+function optional(result: unknown): Address | null {
+ if (Array.isArray(result)) {
+ return result[0] ? getAddress(result[1] as string) : null;
+ }
+ if (result && typeof result === 'object' && 'isSome' in result) {
+ const { isSome, value } = result as { isSome: boolean; value: string };
+ return isSome ? getAddress(value) : null;
+ }
+ return null;
+}
+
+async function buildPlan(hre: HardhatRuntimeEnvironment, registry: Address, signer: Address | null): Promise {
+ const rpcUrl = (hre.network.config as { url?: string }).url;
+ const publicClient = createPublicClient({ chain: POLKADOT_TESTNET_CHAIN, transport: http(rpcUrl) });
+ const deployments = readDeployments();
+
+ const plans: PackagePlan[] = [];
+
+ for (const pkg of PACKAGES) {
+ const configured = deployments[pkg.deploymentKey];
+ if (!configured) {
+ throw new Error(`deployments.json has no "${pkg.deploymentKey}" address. Deploy before registering.`);
+ }
+ const address = getAddress(configured);
+
+ const code = await publicClient.getCode({ address });
+ const hasCode = Boolean(code && code !== '0x');
+
+ const [registeredRaw, ownerRaw] = await Promise.all([
+ publicClient.readContract({ address: registry, abi: REGISTRY_ABI, functionName: 'getAddress', args: [pkg.name] }),
+ publicClient.readContract({ address: registry, abi: REGISTRY_ABI, functionName: 'getOwner', args: [pkg.name] }).catch(() => null)
+ ]);
+
+ const registeredAddress = optional(registeredRaw);
+ const owner = ownerRaw ? optional(ownerRaw) : null;
+
+ let action: PackagePlan['action'];
+ let detail: string;
+
+ if (!hasCode) {
+ action = 'blocked-no-code';
+ detail = `${address} has no bytecode on this chain. Registering it would publish a dead pointer.`;
+ } else if (registeredAddress === null) {
+ action = 'register';
+ detail = `name is free; first publisher becomes its permanent owner`;
+ } else if (owner && signer && owner.toLowerCase() !== signer.toLowerCase()) {
+ action = 'blocked-owned-by-other';
+ detail = `already owned by ${owner}; the registry rejects a publish from any other account`;
+ } else if (registeredAddress.toLowerCase() === address.toLowerCase()) {
+ action = 'already-current';
+ detail = `already points at ${address}; nothing to do`;
+ } else {
+ action = 'update-version';
+ detail = `currently ${registeredAddress}; publishing appends a new version pointing at ${address}`;
+ }
+
+ plans.push({ name: pkg.name, address, hasCode, registeredAddress, owner, action, detail });
+ }
+
+ return plans;
+}
+
+task('cdm:publish', 'Register Dotify contracts in the Product CDM registry. Read-only unless --confirm is passed.')
+ .addOptionalParam('registry', 'ContractRegistry address', DEVNET_REGISTRY_ADDRESS, types.string)
+ .addOptionalParam('metadataUri', 'Override the metadata pointer for every package. Defaults to each package generated CID.', '', types.string)
+ .addOptionalParam('privateKey', 'Override the publisher key. Normally unnecessary: the configured PRIVATE_KEY hardhat var is used.', '', types.string)
+ .addFlag('confirm', 'Actually submit the registration transactions')
+ .setAction(async (args, hre: HardhatRuntimeEnvironment) => {
+ const registry = getAddress(args.registry as string);
+ const rpcUrl = (hre.network.config as { url?: string }).url;
+ const publicClient = createPublicClient({ chain: POLKADOT_TESTNET_CHAIN, transport: http(rpcUrl) });
+
+ const registryCode = await publicClient.getCode({ address: registry });
+ if (!registryCode || registryCode === '0x') {
+ throw new Error(`No ContractRegistry at ${registry} on this network. Check --registry and the RPC endpoint.`);
+ }
+
+ // The publisher is not just paying fees: publish_latest records `caller` as the
+ // permanent owner of every name it creates. So this key decides who owns @dotify/*
+ // for good, which is why it is resolved explicitly and reported before any write.
+ //
+ // Default to the account hardhat already has for this network - sourced from the
+ // encrypted `PRIVATE_KEY` var - rather than asking for a key on the command line,
+ // where it would land in shell history and process listings.
+ const overrideAccount = args.privateKey ? privateKeyToAccount(args.privateKey as Hex) : null;
+ const [configuredWallet] = overrideAccount ? [] : await hre.viem.getWalletClients({ chain: POLKADOT_TESTNET_CHAIN });
+ const account = overrideAccount ?? configuredWallet?.account ?? null;
+
+ const plans = await buildPlan(hre, registry, account?.address ?? null);
+
+ const cidIndex = readCidIndex();
+ const override = (args.metadataUri as string) || null;
+ const metadataFor = (name: string): string => override ?? cidIndex?.[name]?.cid ?? '';
+
+ if (!override && !cidIndex) {
+ console.log('No generated CID index found. Run `npm run generate:cdm-metadata` in web/ so consumers can fetch an ABI.\n');
+ }
+
+ console.log(`\nCDM registry: ${registry}`);
+ console.log(`Chain: ${await publicClient.getChainId()}`);
+ console.log(`Publisher: ${account?.address ?? '(none configured — read-only plan)'}${overrideAccount ? ' (--private-key override)' : ''}`);
+ console.log(` this account becomes the permanent owner of any name it registers\n`);
+
+ for (const plan of plans) {
+ console.log(`${plan.name}`);
+ console.log(` address: ${plan.address}`);
+ console.log(` action: ${plan.action}`);
+ console.log(` detail: ${plan.detail}`);
+ console.log(` metadata: ${metadataFor(plan.name) || '(none — cdm install will have no ABI to fetch)'}`);
+ if (!args.confirm) {
+ console.log(
+ ` calldata: ${encodeFunctionData({
+ abi: REGISTRY_ABI,
+ functionName: 'publishLatest',
+ args: [plan.name, plan.address, metadataFor(plan.name)]
+ })}`
+ );
+ }
+ console.log('');
+ }
+
+ const blocked = plans.filter(plan => plan.action.startsWith('blocked'));
+ if (blocked.length > 0) {
+ throw new Error(`Refusing to proceed: ${blocked.map(plan => `${plan.name} (${plan.action})`).join(', ')}`);
+ }
+
+ const actionable = plans.filter(plan => plan.action === 'register' || plan.action === 'update-version');
+
+ if (!args.confirm) {
+ console.log(
+ actionable.length === 0
+ ? 'Nothing to publish — every name already resolves to the configured address.'
+ : `Dry run. ${actionable.length} name(s) would be published. Re-run with --confirm to submit.\n` +
+ 'Registration is first-writer-owns and the registry has no release or transfer entry point, so a claimed name is permanent.'
+ );
+ return;
+ }
+
+ if (!account) {
+ throw new Error(
+ 'No publisher account for this network. Set one with `npx hardhat vars set PRIVATE_KEY`, ' + 'or pass --private-key to override it for this run.'
+ );
+ }
+ if (actionable.length === 0) {
+ console.log('Nothing to publish.');
+ return;
+ }
+
+ const walletClient = configuredWallet ?? createWalletClient({ account, chain: POLKADOT_TESTNET_CHAIN, transport: http(rpcUrl) });
+
+ for (const plan of actionable) {
+ const hash = await walletClient.writeContract({
+ address: registry,
+ abi: REGISTRY_ABI,
+ functionName: 'publishLatest',
+ args: [plan.name, plan.address, metadataFor(plan.name)]
+ });
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
+ console.log(`${plan.name} -> ${plan.address} tx ${hash} ${receipt.status}`);
+ }
+
+ console.log('\nPublished. Verify with: cdm i -n devnet ' + actionable.map(plan => plan.name).join(' '));
+ });
diff --git a/contracts/evm/test/ArtistRuntime.test.ts b/contracts/evm/test/ArtistRuntime.test.ts
index 05961fa..0b804c4 100644
--- a/contracts/evm/test/ArtistRuntime.test.ts
+++ b/contracts/evm/test/ArtistRuntime.test.ts
@@ -29,6 +29,20 @@ import { MUSIC_REGISTRY_REGISTER_SELECTOR, buildRegistryHotfixCalldata, registry
const FacetCutAction = { Add: 0, Replace: 1, Remove: 2 } as const;
const AccessMode = { HumanFree: 0, Classic: 1, Free: 2 } as const;
const PersonhoodLevel = { None: 0, DIM1: 1, DIM2: 2 } as const;
+
+// The Individuality personhood precompile. Fixed inside pallet-revive, so a Hardhat
+// node has nothing there until a test installs mock code at the same address.
+const PERSONHOOD_PRECOMPILE = '0x000000000000000000000000000000000a010000' as const;
+const DOTIFY_CONTEXT = `0x${Buffer.from('dotify').toString('hex').padEnd(64, '0')}` as `0x${string}`;
+
+/** Install the mock precompile at the real precompile address and return a handle. */
+async function installPersonhoodPrecompile() {
+ const mock = await hre.viem.deployContract('MockPersonhoodPrecompile');
+ const publicClient = await hre.viem.getPublicClient();
+ const runtimeCode = await publicClient.getCode({ address: mock.address });
+ await hre.network.provider.request({ method: 'hardhat_setCode', params: [PERSONHOOD_PRECOMPILE, runtimeCode] });
+ return hre.viem.getContractAt('MockPersonhoodPrecompile', PERSONHOOD_PRECOMPILE);
+}
const ZERO_ADDR = '0x0000000000000000000000000000000000000000' as const;
function selectorsFromAbi(abi: Abi): `0x${string}`[] {
@@ -251,7 +265,10 @@ describe('DotifyRuntimeInitializer — bootstrap', () => {
expect(registrar.toLowerCase()).to.equal(artistA.account.address.toLowerCase());
});
- it('owner can reassign the personhood registrar and the new registrar can grant levels', async () => {
+ it('no registrar can grant personhood any more, not even the artist', async () => {
+ // The registrar defaulted to the artist, so this path let an artist manufacture
+ // personhood for their own listeners. It must now fail loudly rather than write
+ // to storage that no access decision reads.
const { factory, directory, artistA, other, listener } = await loadFixture(deployDotifySystemFixture);
await createArtistRuntime(factory, artistA);
@@ -260,13 +277,14 @@ describe('DotifyRuntimeInitializer — bootstrap', () => {
const artistAccess = await hre.viem.getContractAt('MusicAccessPallet', runtimeAddr, { client: { wallet: artistA } });
await artistAccess.write.setPersonhoodRegistrar([other.account.address]);
- const access = await hre.viem.getContractAt('MusicAccessPallet', runtimeAddr);
- expect((await access.read.musicAccGetRegistrar()).toLowerCase()).to.equal(other.account.address.toLowerCase());
-
const delegatedRegistrar = await hre.viem.getContractAt('MusicAccessPallet', runtimeAddr, { client: { wallet: other } });
- await delegatedRegistrar.write.musicAccSetPersonhoodLevel([listener.account.address, PersonhoodLevel.DIM1]);
- expect(await access.read.musicAccPersonhoodLevel([listener.account.address])).to.equal(PersonhoodLevel.DIM1);
+ try {
+ await delegatedRegistrar.write.musicAccSetPersonhoodLevel([listener.account.address, PersonhoodLevel.DIM1]);
+ expect.fail('Should have reverted');
+ } catch (e: unknown) {
+ expect((e as Error).message).to.include('Individuality precompile');
+ }
});
it('delegated registrar cannot rotate itself; only the owner can update the registrar', async () => {
@@ -365,8 +383,8 @@ describe('Artist SmartRuntime — music pallets', () => {
expect((await publicClient.getBalance({ address: royaltyRecip.account.address })) > recipBefore).to.equal(true);
});
- it('HumanFree track: access granted after artist sets DIM1 personhood', async () => {
- const { registry, royalties, access, artistA, listener, royaltyRecip } = await withArtistRuntime();
+ it('HumanFree track: access follows the Individuality precompile, not the artist', async () => {
+ const { registry, access, artistA, listener, royaltyRecip } = await withArtistRuntime();
const artistRegistry = await hre.viem.getContractAt('MusicRegistryPallet', registry.address, { client: { wallet: artistA } });
await artistRegistry.write.musicRegRegister([
@@ -380,15 +398,74 @@ describe('Artist SmartRuntime — music pallets', () => {
[10_000]
]);
+ // No precompile installed yet: the chain cannot answer, so a gated track denies.
+ expect(await access.read.musicAccCanAccess([TRACK_HASH2, listener.account.address])).to.equal(false);
+
+ const precompile = await installPersonhoodPrecompile();
+
+ // Present but with no personhood recorded — still denied.
expect(await access.read.musicAccCanAccess([TRACK_HASH2, listener.account.address])).to.equal(false);
- // Artist is the registrar (bootstrapped by initializer) — grant DIM1
- const artistAccess = await hre.viem.getContractAt('MusicAccessPallet', access.address, { client: { wallet: artistA } });
- await artistAccess.write.musicAccSetPersonhoodLevel([listener.account.address, PersonhoodLevel.DIM1]);
+ const alias_ = `0x${'ab'.repeat(32)}` as `0x${string}`;
+ await precompile.write.setPersonhood([listener.account.address, DOTIFY_CONTEXT, PersonhoodLevel.DIM1, alias_]);
+
+ expect(await access.read.musicAccCanAccess([TRACK_HASH2, listener.account.address])).to.equal(true);
+ expect(await access.read.musicAccPersonhoodLevel([listener.account.address])).to.equal(PersonhoodLevel.DIM1);
+
+ const [status, contextAlias, live] = await access.read.musicAccPersonhoodInfo([listener.account.address]);
+ expect(status).to.equal(PersonhoodLevel.DIM1);
+ expect(contextAlias).to.equal(alias_);
+ expect(live).to.equal(true);
+ void artistA;
+ });
+
+ it('HumanFree track: a lower tier than required is still denied', async () => {
+ const { registry, access, artistA, listener, royaltyRecip } = await withArtistRuntime();
+
+ const artistRegistry = await hre.viem.getContractAt('MusicRegistryPallet', registry.address, { client: { wallet: artistA } });
+ await artistRegistry.write.musicRegRegister([
+ sampleRegistration({
+ contentHash: TRACK_HASH2,
+ accessMode: AccessMode.HumanFree,
+ pricePlanck: 0n,
+ requiredPersonhood: PersonhoodLevel.DIM2
+ }),
+ [royaltyRecip.account.address],
+ [10_000]
+ ]);
+
+ const precompile = await installPersonhoodPrecompile();
+ await precompile.write.setPersonhood([listener.account.address, DOTIFY_CONTEXT, PersonhoodLevel.DIM1, `0x${'cd'.repeat(32)}`]);
+ expect(await access.read.musicAccCanAccess([TRACK_HASH2, listener.account.address])).to.equal(false);
+ await precompile.write.setPersonhood([listener.account.address, DOTIFY_CONTEXT, PersonhoodLevel.DIM2, `0x${'cd'.repeat(32)}`]);
expect(await access.read.musicAccCanAccess([TRACK_HASH2, listener.account.address])).to.equal(true);
});
+ it('personhood granted in another application context does not unlock Dotify', async () => {
+ // This is the property the registrar could never provide: the same person carries a
+ // different alias per context, and a proof issued to another app is not Dotify's.
+ const { registry, access, artistA, listener, royaltyRecip } = await withArtistRuntime();
+
+ const artistRegistry = await hre.viem.getContractAt('MusicRegistryPallet', registry.address, { client: { wallet: artistA } });
+ await artistRegistry.write.musicRegRegister([
+ sampleRegistration({
+ contentHash: TRACK_HASH2,
+ accessMode: AccessMode.HumanFree,
+ pricePlanck: 0n,
+ requiredPersonhood: PersonhoodLevel.DIM1
+ }),
+ [royaltyRecip.account.address],
+ [10_000]
+ ]);
+
+ const precompile = await installPersonhoodPrecompile();
+ const otherContext = `0x${Buffer.from('dotns').toString('hex').padEnd(64, '0')}` as `0x${string}`;
+ await precompile.write.setPersonhood([listener.account.address, otherContext, PersonhoodLevel.DIM2, `0x${'ef'.repeat(32)}`]);
+
+ expect(await access.read.musicAccCanAccess([TRACK_HASH2, listener.account.address])).to.equal(false);
+ });
+
it('NFT owner is the artist; NFT transfer moves ownership', async () => {
const { registry, nft, artistA, other, royaltyRecip } = await withArtistRuntime();
@@ -599,7 +676,13 @@ describe('Artist isolation', () => {
expect(await registryB.read.musicRegTrackCount()).to.equal(0n);
});
- it('personhood granted on artist A has no effect on artist B', async () => {
+ it('personhood is a property of the person, so it reads the same on every runtime', async () => {
+ // This deliberately inverts the previous expectation. Personhood used to be
+ // per-runtime state an artist wrote, so it could differ between two artists for the
+ // same listener - which is exactly what made it forgeable. It is now one fact about
+ // a person in Dotify's context, so every runtime reads the same answer and no
+ // artist can change it. Catalog and payment state stay per-runtime; only the
+ // question "is this a distinct human" became global.
const ctx = await loadFixture(deployDotifySystemFixture);
await createArtistRuntime(ctx.factory, ctx.artistA);
@@ -608,13 +691,27 @@ describe('Artist isolation', () => {
const runtimeAddrA = (await ctx.directory.read.runtimeOf([ctx.artistA.account.address])) as `0x${string}`;
const runtimeAddrB = (await ctx.directory.read.runtimeOf([ctx.artistB.account.address])) as `0x${string}`;
- // Artist A grants listener DIM1
- const accessA = await hre.viem.getContractAt('MusicAccessPallet', runtimeAddrA, { client: { wallet: ctx.artistA } });
- await accessA.write.musicAccSetPersonhoodLevel([ctx.listener.account.address, PersonhoodLevel.DIM1]);
-
- // Listener's level on B's runtime is still None
+ const accessA = await hre.viem.getContractAt('MusicAccessPallet', runtimeAddrA);
const accessB = await hre.viem.getContractAt('MusicAccessPallet', runtimeAddrB);
+
+ // No precompile on this chain yet: both runtimes agree the answer is None.
+ expect(await accessA.read.musicAccPersonhoodLevel([ctx.listener.account.address])).to.equal(PersonhoodLevel.None);
expect(await accessB.read.musicAccPersonhoodLevel([ctx.listener.account.address])).to.equal(PersonhoodLevel.None);
+
+ const precompile = await installPersonhoodPrecompile();
+ await precompile.write.setPersonhood([ctx.listener.account.address, DOTIFY_CONTEXT, PersonhoodLevel.DIM2, `0x${'11'.repeat(32)}`]);
+
+ expect(await accessA.read.musicAccPersonhoodLevel([ctx.listener.account.address])).to.equal(PersonhoodLevel.DIM2);
+ expect(await accessB.read.musicAccPersonhoodLevel([ctx.listener.account.address])).to.equal(PersonhoodLevel.DIM2);
+
+ // And neither artist can alter it.
+ const artistAccessA = await hre.viem.getContractAt('MusicAccessPallet', runtimeAddrA, { client: { wallet: ctx.artistA } });
+ try {
+ await artistAccessA.write.musicAccSetPersonhoodLevel([ctx.listener.account.address, PersonhoodLevel.None]);
+ expect.fail('Should have reverted');
+ } catch (e: unknown) {
+ expect((e as Error).message).to.include('Individuality precompile');
+ }
});
});
diff --git a/contracts/evm/tsconfig.json b/contracts/evm/tsconfig.json
index ec2afe1..6efc2d8 100644
--- a/contracts/evm/tsconfig.json
+++ b/contracts/evm/tsconfig.json
@@ -1,14 +1,22 @@
{
- "compilerOptions": {
- "target": "ES2022",
- "module": "NodeNext",
- "moduleResolution": "NodeNext",
- "rootDir": ".",
- "strict": true,
- "esModuleInterop": true,
- "skipLibCheck": true,
- "resolveJsonModule": true,
- "types": ["node", "mocha"]
- },
- "include": ["hardhat.config.ts", "scripts/**/*.ts", "test/**/*.ts"]
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "rootDir": ".",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "types": [
+ "node",
+ "mocha"
+ ]
+ },
+ "include": [
+ "hardhat.config.ts",
+ "scripts/**/*.ts",
+ "test/**/*.ts",
+ "./.cdm/**/*"
+ ]
}
diff --git a/docs/README.md b/docs/README.md
index 9b47fa3..c50c5c6 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-test01.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..4d45a60 100644
--- a/docs/backlog/README.md
+++ b/docs/backlog/README.md
@@ -104,18 +104,22 @@ 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-test01.dot`,
+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, 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
-`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 +164,10 @@ 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 wire real CDM-installed runtime packages through the Product CDM/PAPI
+ 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
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..380a7e4 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,19 @@ 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-test01.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.
+
+Typed runtime ports are now extracted in the follow-up branch. Product-native
+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 +49,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 +144,57 @@ 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.
+- Delivered on the room-beacon branch: Statement Store presence with strict
+ payload, TTL, and signer limits. Host-only publication, per-room last-write-
+ wins channels, 512-byte and 1024-byte budgets enforced before writing, and
+ expiry-based eviction on the reading side. Ships dormant
+ (`VITE_DOTIFY_ROOM_BEACONS=off`) until discovery has a reader and live host
+ evidence exists; joining stays on Socket.IO/WebRTC because moving it would
+ require every guest to hold an identity.
### Phase 4 - Product integration
-Goal: ship Product mode as progressive enhancement.
-
-- 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.
+Goal: deepen the delivered Product mode one adapter at a time.
+
+- 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.
+- Delivered on the follow-up branch: extract typed runtime read/write ports and
+ move the current viem runtime implementation behind `RuntimeReadPort` /
+ `RuntimeWritePort`.
+- 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.
+- 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.
+- 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`.
+- 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. 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
stronger key-custody boundary.
- Keep `.dot`/Playground deployment separate from access enforcement.
@@ -219,8 +265,10 @@ 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, Product DevNet deployment slice, and typed runtime
+ port extraction implemented; keep open for CDM/PAPI 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/context/dotify-technical-memory.md b/docs/context/dotify-technical-memory.md
index a341a1b..11d139b 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 wired for Product frontend protected playback after explicit host-account
+ connection, 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/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.
diff --git a/docs/design/dotify-v2-access-and-streaming.md b/docs/design/dotify-v2-access-and-streaming.md
index 19bb0ab..f794472 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-test01.dot` / `https://dotify-test01.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..d3439a1 100644
--- a/docs/explanation/architecture-overview.md
+++ b/docs/explanation/architecture-overview.md
@@ -74,7 +74,7 @@ Track selected → access checked
│
├── Has access? ──► Content key requested, full audio decrypted and played
│
- └── No access? ──► 42 % preview played, access gate shown
+ └── No access? ──► Unlock/personhood gate shown, no protected audio
│
├── Pay DOT → musicRoyPayAccess() → access granted
└── Prove PoP → registrar confirms personhood → access granted
@@ -102,6 +102,11 @@ src/
│ ├── useSession.ts # WebRTC + Socket.IO room management
│ ├── useArtistConsole.ts # /artists registration, releases, royalties
│ └── useWallet.ts # Wallet tiers: passkey → EIP-6963 extension
+├── features/runtime/
+│ ├── runtimePorts.ts # RuntimeReadPort / RuntimeWritePort contracts
+│ ├── 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
@@ -174,4 +179,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-test01.dot` and resolves
+publicly at `https://dotify-test01.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..c6a7575
--- /dev/null
+++ b/docs/explanation/product-devnet-architecture.md
@@ -0,0 +1,451 @@
+# 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-test01.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-test01.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 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 |
+
+## 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.
+
+### Room Beacons
+
+That presence spike is now implemented, and ships dormant. A host inside the
+Product container can publish a compact beacon to the Statement Store so a room
+is discoverable without Dotify's signaling server. It carries the room code,
+host name, and an aggregate listener count - never SDP, ICE, audio, chat,
+listener identities, or source keys. Now-playing is opt-in per host, because a
+beacon is globally readable and outlives the room by up to the statement TTL.
+
+Joining deliberately does not move here, and cannot. A WebRTC offer is 1.5-4 KB
+against a 512-byte statement ceiling and a 1024-byte per-account total, so a
+peer cannot hold even one. More decisively, a guest would have to publish an
+answer to complete the handshake, which requires an identity and an allowance -
+turning every listener into a registered person. Only the host publishes,
+because the host is already identified.
+
+Beacons are per-room channels for last-write-wins, so one hosted room occupies
+exactly one live statement no matter how often it refreshes. Host mode signs
+through the product's allowance account on the RFC-10 sponsored path, so
+hosting does not require the host to hold an Individuality allowance.
+
+`VITE_DOTIFY_ROOM_BEACONS` is `off` in the tracked Product profile: nothing
+reads beacons yet, so publishing room records would be exposure with no
+consumer, and the publish path has no live host evidence. See the deployment
+runbook for the opt-in build and the evidence procedure.
+
+## 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.
+
+## Runtime Port
+
+The contract integration is split into two typed ports rather than Product
+conditionals throughout feature hooks:
+
+```text
+RuntimeReadPort
+ resolveArtistRuntime()
+ listArtistRuntimes()
+ listRuntimeTracks()
+ canAccess()
+ hasPaid()
+ listRoyaltyPaymentLogs()
+
+RuntimeWritePort
+ createRuntime()
+ installRuntimeStep()
+ registerTrack()
+ setAccessMode()
+ setReleaseActive()
+ payForAccess()
+```
+
+Adapters:
+
+- `ViemRuntimeAdapter`: current standalone EVM implementation behind the typed
+ ports;
+- `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.
+
+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 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.
+
+### Registering `@dotify/*` Without Redeploying
+
+`cdm deploy` builds, deploys, publishes metadata, and registers in one pass.
+Dotify cannot use it: the contracts are already deployed and already hold the
+live catalog, so deploying again would mint new addresses and orphan every
+existing artist runtime.
+
+The registry contract itself provides the operation that is actually needed.
+`publishLatest(contract_name, contract_address, metadata_uri)` binds a name to
+an arbitrary address, and the contract's own comment states the rule: "The
+caller only has permission to publish a new version of `contract_name` if
+either the name is available or they are already the owner of the name." So a
+free name is claimable by anyone, and afterwards only by its owner.
+`metadata_uri` is stored verbatim and never validated - it is a pointer, not a
+checked reference.
+
+`npm run cdm:publish:testnet` (task `cdm:publish`) performs that registration
+for the addresses in `deployments.json`. It is read-only by default: it prints
+the plan and the exact calldata, and stops. Registration is first-writer-owns
+and the registry exposes no release or transfer entry point, so a claimed name
+is permanent - execution therefore requires `--confirm` and an explicit key.
+
+The task refuses to proceed when a target address has no bytecode on the
+connected chain, or when a name is already owned by another account. Publishing
+a name that points at nothing would be worse than not publishing it.
+
+| Registry | Address | Network |
+| --- | --- | --- |
+| `devnet` preset | `0x59b0245778917af55224e5f8fb55f7f8d452619f` | Paseo Asset Hub, para 1000, chain 420420417 |
+
+CDM's own documentation confirms the preset distinction that
+`VITE_DOTIFY_PRODUCT_CHAIN` encodes: "the `paseo` preset targets **paseo-next**
+... para 1500 - not the Paseo testnet. The `devnet` preset targets the Paseo
+testnet Asset Hub (para 1000, EVM chain id 420420417)." Publishing against the
+`paseo` registry would register Dotify's names on a network where its contracts
+do not exist.
+
+Note also that CDM does support Solidity, through a `/// @custom:cdm @org/name`
+NatSpec tag and first-pass Hardhat and Foundry templates. The architecture page
+mentions only PolkaVM bytecode, so this is easy to miss - it means Dotify's
+existing toolchain is not an obstacle to CDM participation.
+
+### Why `cdm deploy` Cannot Be Used, Even With A Fresh Redeploy
+
+The obvious objection to the task above is that a redeploy would avoid all of
+it. Dotify's on-chain data is test data, so that was worth checking properly
+rather than assuming. It does not work, and the reason is a hard chain limit
+rather than a preference.
+
+CDM's Solidity path compiles with `resolc` to PolkaVM, not with `solc` to EVM
+bytecode. `resolc` compiles Dotify's contracts successfully - all 24 files,
+including the diamond's `delegatecall` fallback and every one of its 17 inline
+assembly blocks, with only an informational `extcodesize` warning from
+`LibDiamond`. Feasibility is therefore not the blocker.
+
+Size is. The Asset Hub initcode limit is 49,152 bytes, and `resolc` emits
+roughly 4-10x more bytecode than `solc` for the same source:
+
+| Contract | Deployed EVM | resolc PolkaVM | Against the 48 KB limit |
+| --- | --- | --- | --- |
+| `MusicRegistryPallet` | 8,855 | 71,252 | **over by 45%** |
+| `SmartRuntime` | n/a | 41,142 | under |
+| `DiamondCutPallet` | 4,753 | 39,408 | under |
+| `ArtistRuntimeFactory` | 9,999 | 38,926 | under |
+| `ArtistDirectory` | 1,829 | 17,325 | under |
+| `MusicRightsRegistry` | not deployed | 88,955 | **over by 81%** |
+
+`MusicRegistryPallet` is the pallet that holds the catalog, so this is not an
+optional component. Clearing the limit would mean splitting it into a
+storage-only contract and a logic contract - and the practitioner report that
+documents that workaround also records that diamond-style generic mappings were
+*ineffective* at reducing size, which is precisely Dotify's architecture.
+
+So the ordering is: Asset Hub's `pallet-revive` accepts both EVM bytecode
+through `eth-rpc` and PolkaVM blobs through `resolc`, and for Dotify the EVM
+path is not a legacy compromise - it is the only one that currently fits. The
+existing deployment sits comfortably inside the limit on every contract.
+
+`publishLatest` registration is therefore the correct mechanism, not a
+workaround for an unwillingness to redeploy. Revisit only if `resolc` output
+size improves substantially, or if the registry pallet is split for reasons of
+its own.
+
+### 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**, 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
+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.
+
+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.
+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 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.
+
+### 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.
+
+## 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 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.
+ 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-test01.dot` |
+| Public gateway | `https://dotify-test01.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/)
+- [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/)
+- [Product messaging architecture](https://docs.polkadotcommunity.foundation/architecture/messaging/)
diff --git a/docs/index.html b/docs/index.html
index 3b9ae43..aeeade6 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -1537,11 +1537,16 @@ 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-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 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.
@@ -1601,11 +1606,20 @@ Explore the project
→
+
+
+
+ Product DevNet architecture
+ What the Product host adapts now, what remains on Fly, and how runtime ports prepare CDM/PAPI.
+
+ →
+
+
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..3f7b86a 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-test01.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-test01.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,62 @@ 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-test01.dot` |
+| `VITE_PUBLIC_APP_URL` | `https://dotify-test01.dev-dot.li` |
+| `VITE_DOTIFY_API_URL` | `https://dotify-api.fly.dev` |
+| `VITE_SIGNAL_URL` | `https://dotify-signal.fly.dev` |
+| `VITE_DOTIFY_ROOM_BEACONS` | `off` |
+
+`VITE_DOTIFY_ROOM_BEACONS` is off in the tracked profile, so the standard
+publication announces no rooms on the Statement Store. The capability ships
+dormant on purpose: nothing reads beacons yet, so publishing room records to a
+public chain would be exposure with no consumer, and the publish path has no
+live host evidence. Enabling also adds about 24 KB to every publication, against
+a finite Bulletin quota.
+
+To publish a build that does announce:
+
+```bash
+cd web
+npm run deploy:product-devnet:beacons
+```
+
+Rolling back is a normal republication with the flag absent - the standard
+`npm run deploy:product-devnet` produces the `off` build. Beacons already
+published expire on their own within the statement TTL; there is no revocation
+step, and none is needed.
+
+`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 +191,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-test01.dev-dot.li` |
| `PASEO_ASSET_HUB_RPC` | `https://eth-rpc-testnet.polkadot.io/` |
| `DOTIFY_FACTORY_ADDRESS` | `0xbd1a11cfce8b5ef7a37e507bc5109895f8f42a72` |
| `DOTIFY_DIRECTORY_ADDRESS` | `0xcf1534c6e2b0e43b9436c1e86a076466dc0f2108` |
@@ -145,7 +201,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. |
@@ -175,6 +230,25 @@ 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 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
Open app `dotify-signal`.
@@ -188,12 +262,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-test01.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,9 +301,14 @@ 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 explicit origin rejection evidence, include a denied origin:
+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).
+
+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
new file mode 100644
index 0000000..c93401a
--- /dev/null
+++ b/docs/operations/product-devnet-deployment.md
@@ -0,0 +1,399 @@
+# 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.
+
+## 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.
+
+## Publishing CDM Metadata And Registering `@dotify/*`
+
+Separate from the frontend publish below, and only needed when the contracts or
+their ABIs change.
+
+The registry stores `(name -> address)` and `(name -> metadata_uri)`. Without
+the second, another product can resolve where Dotify's contracts are but not
+what they expose, so `cdm install` fails and the registration is nominal.
+
+**1. Generate the metadata blobs and their CIDs.**
+
+```bash
+cd web
+npm run generate:cdm-metadata
+```
+
+Writes `web/src/generated/contracts/cdm-metadata/` - one JSON blob per package
+plus `cids.json`. Output is deterministic: the same contracts produce the same
+bytes and therefore the same CIDs, so the published blob can always be checked
+against the repository. `published_at` is omitted for that reason; pass
+`--published-at ` if a publication date is wanted, accepting that it
+changes the CID.
+
+**2. Upload the blobs to Bulletin.**
+
+This needs a live Bulletin storage authorization for the uploading account -
+the same finite, expiring quota the frontend publish uses. Upload each JSON file
+from that directory and confirm the returned CID matches `cids.json`. A mismatch
+means the bytes changed in transit and must not be registered.
+
+**3. Register the names.**
+
+```bash
+cd contracts/evm
+npm run cdm:publish:testnet # dry run, prints the plan
+npx hardhat cdm:publish --network polkadotTestnet --confirm
+```
+
+No key needs to be supplied. The task signs with the account hardhat already
+holds for this network, sourced from the encrypted `PRIVATE_KEY` var
+(`npx hardhat vars set PRIVATE_KEY`). Pass `--private-key` only to publish from
+a different account than the deployer.
+
+That account matters beyond paying fees: `publish_latest` records the caller as
+the **permanent owner** of every name it creates, so whoever signs owns
+`@dotify/*` from then on. The dry run prints the resolved publisher for exactly
+that reason - check it before confirming.
+
+The task reads `cids.json`, so the CID published on-chain is derived from the
+generated bytes rather than pasted by hand. It is read-only without `--confirm`.
+
+Registration is first-writer-owns and the registry exposes no release or
+transfer entry point, so **a claimed name is permanent**. Confirm the namespace
+before the first publish. The task refuses to register an address with no
+bytecode, or a name owned by another account.
+
+Verify afterwards from a consuming project:
+
+```bash
+cdm i -n devnet @dotify/artist-directory @dotify/artist-runtime-factory
+```
+
+Use `-n devnet`, never `-n paseo`: the `paseo` preset targets paseo-next
+(para 1500), which holds none of Dotify's contracts.
+
+## Prerequisites
+
+- Node.js 22 and npm 10+
+- a clean build from the intended commit
+- access to the `dotify-test01.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-test01.dot`, or eligibility to register it during deploy.
+
+`dotify.dot` currently requires full personhood on Product DevNet. Until the
+project has that proof level, use `dotify-test01.dot` and
+`https://dotify-test01.dev-dot.li` for operator deployments.
+
+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-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:
+
+```bash
+cd services/api
+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.
+
+## 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-test01.dot
+VITE_PUBLIC_APP_URL=https://dotify-test01.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 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
+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-test01.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-test01.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-test01.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-test01.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 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.
+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.
+
+## Room Beacons (Dormant By Default)
+
+The Statement Store beacon capability ships but is **not enabled** by the
+standard publication. `web/.env.product-devnet` sets
+`VITE_DOTIFY_ROOM_BEACONS=off`, and `npm run deploy:product-devnet` rebuilds in
+that mode, so a normal publish announces no rooms.
+
+That is deliberate. Nothing reads beacons yet, so publishing room records to a
+public chain would be exposure with no consumer, and the publish path has no
+live host evidence. Treat this section as the procedure for collecting that
+evidence, not as part of a routine release.
+
+### Prerequisites
+
+- `VITE_DOTIFY_HOST_MODE` must be `auto` or `required`. The statement store
+ client runs only inside the Product host container, and the production guard
+ refuses `VITE_DOTIFY_ROOM_BEACONS=on` without it.
+- No Individuality allowance is needed for the host account: host mode signs
+ through the product's allowance account on the RFC-10 sponsored path.
+- Expect about 24 KB of extra publication weight against the Bulletin quota.
+
+### Publish an announcing build
+
+```bash
+cd web
+npm run deploy:product-devnet:beacons
+```
+
+### Collect live evidence
+
+Inside the Product host, with the announcing build open:
+
+1. Create a room. Watch the browser console. A refused publish logs
+ `[dotify] room beacon not published (): `; nothing is logged
+ on success.
+2. Record which happened:
+ - **published** - capture the room code and the fact that no warning
+ appeared. This is the first evidence the publish path works end to end;
+ - **`rejected`** - the statement store refused the write. Most often the
+ account-wide quota, which the client cannot observe. Capture the detail
+ line before changing anything;
+ - **`transport`** - the client could not reach the store at all;
+ - **`quota-local`** - this instance's own beacons already fill the
+ 1024-byte account budget.
+3. Confirm hosting is unaffected in every case: the room must still be
+ joinable from its share link by a wallet-free browser. A beacon failure that
+ degrades hosting is a defect, not a limitation.
+4. With a second client in the host, confirm the room appears through
+ `subscribeRoomBeacons` and disappears within roughly the statement TTL plus
+ one sweep after the host stops.
+
+Record the outcome in the release evidence. Until step 2 shows a published
+beacon, treat the capability as unproven regardless of unit coverage.
+
+### Rollback
+
+Republish without the flag:
+
+```bash
+cd web
+npm run deploy:product-devnet
+```
+
+Beacons already published expire on their own within the statement TTL. There
+is no revocation step and none is needed - a beacon carries no key, no
+identity, and no durable claim.
+
+## 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 accepted by the API only through the explicit
+ `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.
+- 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
+ 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.
+- 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/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 e255b55..133bf90 100644
--- a/docs/reference/environment-variables.md
+++ b/docs/reference/environment-variables.md
@@ -36,14 +36,166 @@ 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_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_ROOM_BEACONS`
+
+| Property | Value |
+| ------------ | ------------ |
+| **Type** | `on` or `off` |
+| **Required** | No |
+| **Default** | `off` |
+| **Example** | `off` |
+
+Publishes a small beacon to the Statement Store while hosting a room, so the
+room can be discovered without Dotify's signaling server.
+
+This is **discovery only**. A beacon never carries SDP, ICE, chat, or audio, and
+is never required to join: a share link still works with no wallet, no account,
+and no chain. Joining cannot move here - a WebRTC offer is 1.5-4 KB against a
+512-byte statement ceiling, and a guest would have to publish an answer, which
+needs an identity and an allowance. That would turn every listener into a
+registered person.
+
+Only a host publishes, and only while hosting. Requires `VITE_DOTIFY_HOST_MODE`
+to be `auto` or `required`: the statement store client runs only inside the
+Product host container, so enabling beacons without it would ship chain code
+that can never connect. The production guard rejects that combination.
+
+A beacon carries the room code, host display name, and an aggregate listener
+count - never listener identities. Now-playing is opt-in per host, because a
+beacon is globally readable and outlives the room by up to the retention window,
+which is a different exposure than sharing a link.
+
+**Build size.** Enabling this adds about 24 KB. A build with it `off` still
+carries a ~69 KB statement-store chunk that is never fetched at runtime: Rollup
+emits a chunk for the nested dynamic import before it can prove the build-time
+guard makes it unreachable. That is ~1.5% of the bundle, and the code never
+executes, but it is published weight against the Bulletin quota.
+
+---
+
+### `VITE_DOTIFY_PRODUCT_CHAIN`
+
+| Property | Value |
+| ------------ | -------- |
+| **Type** | `devnet` |
+| **Required** | No |
+| **Default** | `devnet` |
+| **Example** | `devnet` |
+
+Product chain preset used only when `VITE_DOTIFY_RUNTIME_ADAPTER=product-cdm`.
+
+`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.
+
+---
+
+### `VITE_DOTIFY_PRODUCT_ID`
+
+| Property | Value |
+| ------------ | ---------------------- |
+| **Type** | Lowercase `.dot` name |
+| **Required** | Host mode is not `off` |
+| **Default** | `dotify-test01.dot` |
+| **Example** | `dotify-test01.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-test01.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 |
-| ------------ | ----------------- |
-| **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,
@@ -78,8 +230,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.
---
@@ -241,12 +395,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.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
@@ -327,7 +481,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-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
+content-key routes.
---
diff --git a/services/api/.env.example b/services/api/.env.example
index b86aba5..bcdf622 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-test01.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
@@ -30,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/fly.toml b/services/api/fly.toml
index 6b061eb..baa8017 100644
--- a/services/api/fly.toml
+++ b/services/api/fly.toml
@@ -10,6 +10,17 @@ primary_region = "ams"
# checks target the owner-guarded factory/directory pair.
API_PORT = "8790"
NODE_ENV = "production"
+ # 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"
@@ -20,7 +31,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]]
diff --git a/services/api/package-lock.json b/services/api/package-lock.json
index 9a9f4ca..25f9f9b 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 65f0f21..4fa05fc 100644
--- a/services/api/package.json
+++ b/services/api/package.json
@@ -12,13 +12,14 @@
"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",
"@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/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..f61ac66
--- /dev/null
+++ b/services/api/src/cors.test.ts
@@ -0,0 +1,68 @@
+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-test01.dev-dot.li'],
+ });
+ const server = app;
+
+ for (const origin of ['https://muzinga.netlify.app', 'https://dotify-test01.dev-dot.li']) {
+ const response = await server.inject({
+ method: 'GET',
+ url: '/health',
+ headers: { origin },
+ });
+ assert.equal(response.headers['access-control-allow-origin'], origin);
+ }
+
+ const unrelated = await server.inject({
+ method: 'GET',
+ url: '/health',
+ headers: { origin: 'https://unrelated.example' },
+ });
+ 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/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..9089c1e 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,28 @@ 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')
+});
+
+// 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}|[0-9a-fA-F]{130})$/, '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 +119,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..95c8fc4 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,105 @@ 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('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({
+ 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..79a5677 100644
--- a/services/api/src/routes/keys.ts
+++ b/services/api/src/routes/keys.ts
@@ -21,26 +21,46 @@ 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')
+});
+
+// 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}|[0-9a-fA-F]{130})$/, '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 +165,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..3c8943f 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,40 @@ async function signedPayload(overrides: Partial = {}) {
return { payload, signature };
}
+// 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 = {
+ action: 'REQUEST_CONTENT_KEY',
+ purpose: 'individual',
+ contentHash: CONTENT_HASH,
+ requester,
+ chainId: CHAIN_ID,
+ nonce: challenge.nonce,
+ expiresAt: challenge.expiresAt,
+ ...overrides
+ };
+ const raw = signSr25519(productSecretKey, encodeProductPayload(buildSignedRequestMessage(payload), options.envelope ?? 'raw'));
+ return { payload, signature: encodeProductSignature(raw, options.shape ?? 'bare'), productPublicKey: productPublicKeyHex };
+}
+
describe('verifySignedRequest', () => {
beforeEach(() => {
resetNonceStore();
@@ -45,6 +91,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 +120,118 @@ 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({
+ ...payload,
+ contentHash: `0x${'cd'.repeat(32)}`,
+ 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 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 +293,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..a34901b 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,24 @@
// (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.
+//
+// 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';
import { verifyMessage } from 'viem';
import { config } from '../config.js';
import { checkDotifyChainId } from './chainDomain.js';
@@ -20,6 +33,16 @@ 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;
+// Substrate MultiSignature enum tag for sr25519 (0 = ed25519, 1 = sr25519).
+const MULTISIGNATURE_SR25519_TAG = 0x01;
export type NonceChallengeRequest = {
address: string;
@@ -42,10 +65,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 +105,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 +126,196 @@ 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'));
+}
+
+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
+ * 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 addressBytes = isEvmDerivedAccountId(publicKey)
+ ? 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;
+}): 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 = parseProductSignatureBytes(args.signature);
+ } catch {
+ return {
+ valid: false,
+ code: 'PRODUCT_SIGNATURE_INVALID',
+ reason: 'Product signature payload is malformed.'
+ };
+ }
+
+ // 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 {
+ valid: false,
+ code: 'PRODUCT_ADDRESS_MISMATCH',
+ reason: 'Product account public key does not derive to the requester H160 address.'
+ };
+ }
+
+ const signatureValid = productSignedMessageVariants(args.message).some(payload => {
+ try {
+ return verifySr25519Signature(payload, signature, publicKey);
+ } catch {
+ return false;
+ }
+ });
+
+ if (!signatureValid) {
+ // 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 };
+}
+
+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
+ });
+ }
+
+ 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 +331,13 @@ export async function verifySignInRequest(request: SignInRequest): Promise
+
+
=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-statement-store": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/@parity/product-sdk-statement-store/-/product-sdk-statement-store-0.6.2.tgz",
+ "integrity": "sha512-n7FJ2lvKMuJ6oINLVYPlSH8UCVd68XIBqFNmK5VliaJ7i+7HcfB90NyYdD4TDHQAxO5wFZ5lUo3xYfvVCB5ARw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@parity/product-sdk-errors": "0.2.0",
+ "@parity/product-sdk-host": "0.14.1",
+ "@parity/product-sdk-logger": "0.1.1",
+ "@parity/product-sdk-utils": "0.1.1",
+ "@parity/result": "0.2.0",
+ "@polkadot-api/substrate-client": "^0.7.0",
+ "polkadot-api": "^2.1.6"
+ }
+ },
+ "node_modules/@parity/product-sdk-statement-store/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-statement-store/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-statement-store/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-statement-store/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-statement-store/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-statement-store/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-statement-store/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-statement-store/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-statement-store/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-statement-store/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-statement-store/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-statement-store/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-statement-store/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-statement-store/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-statement-store/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-statement-store/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-statement-store/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-statement-store/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-statement-store/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-statement-store/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-statement-store/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-statement-store/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-statement-store/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-statement-store/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-statement-store/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-statement-store/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-utils": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@parity/product-sdk-utils/-/product-sdk-utils-0.1.1.tgz",
+ "integrity": "sha512-vu/af1K7k7p0/aLKALrGVxz7K94xnY800DxmhnOhu6Hy4+y+8e9WHPecMjbYBBJby4FPMOkWLqEVRN/sk3Ucgw==",
+ "dependencies": {
+ "@noble/hashes": "^1.7.1",
+ "@parity/product-sdk-logger": "0.1.1"
+ }
+ },
+ "node_modules/@parity/product-sdk-utils/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/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 +5176,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 +5398,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 +5411,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 +5424,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 +5437,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 +5450,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 +5463,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 +5476,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 +5489,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 +5502,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 +5515,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 +5528,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 +5541,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 +5554,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 +5567,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 +5580,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 +5593,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 +5606,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 +5619,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 +5632,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 +5645,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 +5658,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 +5671,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 +5684,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 +5697,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 +5710,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 +5960,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 +5973,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 +6222,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 +6623,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 +6667,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 +7000,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 +7264,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 +7815,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 +7971,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 +8147,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 +8444,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 +8461,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 +8506,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 +8641,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 +8652,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 +9028,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 +9047,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.11",
+ "nanoid": "^3.3.16",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -5389,6 +9229,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 +9575,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 +9612,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 +9627,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 +9759,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 +10478,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 +10524,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 +11212,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..43b8b5a 100644
--- a/web/package.json
+++ b/web/package.json
@@ -16,17 +16,28 @@
"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",
+ "build:product-devnet:beacons": "VITE_DOTIFY_ROOM_BEACONS=on npm run build: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-test01.dot --env devnet --js-merkle --config ./polkadot-app-deploy.config.ts",
+ "deploy:product-devnet:beacons": "VITE_DOTIFY_ROOM_BEACONS=on npm run deploy:product-devnet",
"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",
"fmt:check": "prettier --check 'src/**/*.{ts,tsx}' 'server/**/*.mjs' 'scripts/**/*.mjs' README.md",
+ "generate:cdm": "node scripts/generate-cdm-manifest.mjs",
+ "generate:cdm-metadata": "node scripts/generate-cdm-metadata.mjs",
+ "upload:cdm-metadata": "node scripts/upload-cdm-metadata.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",
+ "@parity/product-sdk-statement-store": "0.6.2",
"@polkadot-api/descriptors": "file:.papi/descriptors",
"@polkadot-apps/chain-client": "^2.0.5",
"@polkadot-apps/descriptors": "^1.0.1",
@@ -45,6 +56,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..4f90ead
--- /dev/null
+++ b/web/polkadot-app-deploy.config.ts
@@ -0,0 +1,16 @@
+export default {
+ domain: 'dotify-test01.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 0000000..1206ffa
Binary files /dev/null and b/web/product-icon.png differ
diff --git a/web/product-icon.svg b/web/product-icon.svg
new file mode 100644
index 0000000..cb5ed43
--- /dev/null
+++ b/web/product-icon.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/scripts/deploy-bulletin.cjs b/web/scripts/deploy-bulletin.cjs
index 0fbe5df..ba52aa1 100644
--- a/web/scripts/deploy-bulletin.cjs
+++ b/web/scripts/deploy-bulletin.cjs
@@ -41,10 +41,16 @@ function base32lower(bytes) {
return out;
}
+// Multihash code for blake2b-256 is 0xb220, which is 45600 and therefore needs three
+// varint bytes: 0xa0 0xe4 0x02. The single byte 0x1e used here previously is blake3 - an
+// algorithm the Bulletin Chain does not even accept - so the digest was correct but
+// tagged as the wrong function, and the resulting CID resolved nowhere. Verified against
+// @parity/bulletin-sdk `calculateCid(bytes, 0x55, 45600)`, which produces this prefix.
+const BLAKE2B_256_MULTIHASH = [0xa0, 0xe4, 0x02, 0x20];
+
function cidFromBytes(bytes) {
const hash = blake2b(bytes, null, 32);
- const mh = new Uint8Array([0x1e, 0x20, ...hash]); // blake2b-256 multihash
- const cid = new Uint8Array([0x01, 0x55, ...mh]); // CIDv1 + raw codec
+ const cid = new Uint8Array([0x01, 0x55, ...BLAKE2B_256_MULTIHASH, ...hash]); // CIDv1 + raw codec
return 'b' + base32lower(cid);
}
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).`);
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/scripts/generate-cdm-metadata.mjs b/web/scripts/generate-cdm-metadata.mjs
new file mode 100644
index 0000000..573de69
--- /dev/null
+++ b/web/scripts/generate-cdm-metadata.mjs
@@ -0,0 +1,135 @@
+// Build the CDM metadata blobs that `cdm install` fetches, and compute their CIDs.
+//
+// The registry stores `(name -> address)` and `(name -> metadata_uri)`. Without the
+// second, another product can resolve where Dotify's contracts are but not what they
+// expose, so `cdm install` fails and composability is nominal only. These blobs close
+// that gap.
+//
+// Shape comes from CDM's own consumers, not guesswork:
+// - install validates exactly one thing: `abi` must be a non-empty array
+// (contract-dependency-manager `src/lib/contracts/src/install.ts`);
+// - contracts.dot.li additionally renders `description`, `readme`, `homepage`,
+// `repository`, `license`, `keywords`, `authors`, `dependencies`, `published_at`
+// (`src/apps/frontend/src/data/registry-queries.ts`).
+// Everything else is stored verbatim and ignored.
+//
+// Output is deterministic on purpose. A content-addressed blob whose bytes depend on
+// wall-clock time gets a new CID on every run, which makes it impossible to check that
+// what is published still matches the repository. `published_at` is therefore opt-in
+// via --published-at; omitted by default so `npm run generate:cdm-metadata` twice gives
+// byte-identical output and the same CIDs.
+//
+// Run: npm run generate:cdm-metadata
+// Publishing the blobs to Bulletin needs a storage authorization and is a separate,
+// credentialed step - see docs/operations/product-devnet-deployment.md.
+
+import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
+import { dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { calculateCid } from '@parity/product-sdk-cloud-storage';
+
+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/cdm-metadata');
+
+// Bulletin's raw codec + blake2b-256, matching @parity/bulletin-sdk defaults.
+const CID_CODEC_RAW = 0x55;
+const HASH_BLAKE2B_256 = 45600;
+
+const publishedAtArg = process.argv.indexOf('--published-at');
+const publishedAt = publishedAtArg !== -1 ? process.argv[publishedAtArg + 1] : null;
+
+const COMMON = {
+ homepage: 'https://muzinga.netlify.app',
+ repository: 'https://github.com/knzeng-e/dotify',
+ license: 'MIT',
+ authors: ['Dotify'],
+ keywords: ['music', 'dotify', 'artist-runtime', 'access-control']
+};
+
+const PACKAGES = [
+ {
+ name: '@dotify/artist-directory',
+ artifact: 'ArtistDirectory.sol/ArtistDirectory.json',
+ description: 'Registry mapping each artist address to their owned SmartRuntime. Entry point for enumerating the Dotify catalog.',
+ readme: [
+ '# @dotify/artist-directory',
+ '',
+ 'Maps an artist address to the address of the SmartRuntime they own, and enumerates',
+ 'every registered artist.',
+ '',
+ 'Start here to read the Dotify catalog: `artistCount()` and `artistsPage(offset, limit)`',
+ 'enumerate artists with their runtimes, and `runtimeOf(artist)` resolves one directly.',
+ 'Each runtime then exposes its own tracks and access policy.',
+ '',
+ 'Registration is performed by the artist runtime factory, not by callers.'
+ ].join('\n')
+ },
+ {
+ name: '@dotify/artist-runtime-factory',
+ artifact: 'ArtistRuntimeFactory.sol/ArtistRuntimeFactory.json',
+ description: 'Deploys one artist-owned SmartRuntime per artist and registers it in the artist directory.',
+ readme: [
+ '# @dotify/artist-runtime-factory',
+ '',
+ 'Deploys a SmartRuntime for an artist and registers it in `@dotify/artist-directory`.',
+ '',
+ 'A runtime is a diamond: music registry, royalties, access, and NFT pallets are',
+ 'installed as facets, and the artist is set as its owner. Because each artist owns',
+ 'their own runtime, catalog, access policy, and royalty splits stay under the',
+ "artist's control rather than the platform's.",
+ '',
+ 'Creation is staged - `createRuntime()` then `installRuntimeStep()` until',
+ '`pendingRuntimeStageOf(artist)` reports completion - so that installation fits',
+ 'within block limits.'
+ ].join('\n')
+ }
+];
+
+function readAbi(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) || parsed.abi.length === 0) {
+ // install.ts rejects an empty ABI, so fail here rather than publish a blob that
+ // every consumer will reject after it is already immutable on Bulletin.
+ throw new Error(`Artifact ${artifact} has no usable abi array.`);
+ }
+ return parsed.abi;
+}
+
+mkdirSync(outDir, { recursive: true });
+
+const index = {};
+
+for (const pkg of PACKAGES) {
+ const metadata = {
+ name: pkg.name,
+ description: pkg.description,
+ readme: pkg.readme,
+ abi: readAbi(pkg.artifact),
+ ...COMMON,
+ ...(publishedAt ? { published_at: publishedAt } : {})
+ };
+
+ // Two spaces, trailing newline: the bytes are the identity, so the formatting is
+ // part of the contract with the CID and must not drift.
+ const bytes = new TextEncoder().encode(`${JSON.stringify(metadata, null, 2)}\n`);
+ const cid = (await calculateCid(bytes, CID_CODEC_RAW, HASH_BLAKE2B_256)).toString();
+
+ const fileName = `${pkg.name.replace(/^@/, '').replace(/\//g, '-')}.json`;
+ writeFileSync(resolve(outDir, fileName), bytes);
+
+ index[pkg.name] = { file: fileName, cid, bytes: bytes.length };
+ console.log(`${pkg.name}\n file: ${fileName}\n size: ${bytes.length} bytes\n cid: ${cid}\n`);
+}
+
+writeFileSync(resolve(outDir, 'cids.json'), `${JSON.stringify(index, null, 2)}\n`);
+
+console.log(`Wrote ${PACKAGES.length} metadata blobs + cids.json into web/src/generated/contracts/cdm-metadata/`);
+console.log('CIDs are computed locally and are deterministic; publishing the blobs to Bulletin is a separate credentialed step.');
diff --git a/web/scripts/upload-cdm-metadata.mjs b/web/scripts/upload-cdm-metadata.mjs
new file mode 100644
index 0000000..3efd684
--- /dev/null
+++ b/web/scripts/upload-cdm-metadata.mjs
@@ -0,0 +1,179 @@
+// Upload the generated CDM metadata blobs to the Bulletin Chain.
+//
+// Why not the Product SDK: `CloudStorageClient.create()` resolves its chain connection
+// through `getChainAPI`/`createChainClient`, which route exclusively through the Product
+// host container and have no direct-WebSocket fallback. Uploading from a terminal is
+// therefore impossible through that path. The Bulletin chain itself accepts a plain
+// signed `TransactionStorage.store` extrinsic over WebSocket, which is what this uses -
+// the same approach as the existing `deploy-bulletin.cjs`.
+//
+// Read-only by default: it checks the account's authorization and reports what it would
+// upload. Uploading needs --confirm.
+//
+// Run:
+// npm run upload:cdm-metadata # dry run, dev account
+// BULLETIN_SURI='//Alice' npm run upload:cdm-metadata -- --confirm
+// BULLETIN_MNEMONIC='...' npm run upload:cdm-metadata -- --confirm
+
+import { readFileSync, readdirSync } from 'node:fs';
+import { dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { createClient, Binary, Enum } from 'polkadot-api';
+import { withPolkadotSdkCompat } from 'polkadot-api/polkadot-sdk-compat';
+import { getWsProvider } from 'polkadot-api/ws-provider/node';
+import { bulletin } from '@polkadot-api/descriptors';
+import { getPolkadotSigner } from 'polkadot-api/signer';
+import { sr25519CreateDerive } from '@polkadot-labs/hdkd';
+import { DEV_PHRASE, entropyToMiniSecret, mnemonicToEntropy, ss58Address } from '@polkadot-labs/hdkd-helpers';
+import { calculateCid } from '@parity/product-sdk-cloud-storage';
+
+const scriptDir = dirname(fileURLToPath(import.meta.url));
+const metadataDir = resolve(scriptDir, '../src/generated/contracts/cdm-metadata');
+
+// Product DevNet Bulletin (Paseo Bulletin, para 1010).
+const BULLETIN_WS = process.env.VITE_BULLETIN_WS_URL || 'wss://bulletin-paseo.tservices.es:8443';
+const UPLOAD_TIMEOUT_MS = 180_000;
+
+const CID_CODEC_RAW = 0x55;
+const HASH_BLAKE2B_256 = 45600;
+
+const confirm = process.argv.includes('--confirm');
+
+function loadSigner() {
+ const mnemonic = process.env.BULLETIN_MNEMONIC;
+ const suri = process.env.BULLETIN_SURI || '//Alice';
+ const phrase = mnemonic || DEV_PHRASE;
+ const derive = sr25519CreateDerive(entropyToMiniSecret(mnemonicToEntropy(phrase)));
+ const keypair = derive(mnemonic ? '' : suri);
+ return {
+ address: ss58Address(keypair.publicKey),
+ signer: getPolkadotSigner(keypair.publicKey, 'Sr25519', keypair.sign),
+ source: mnemonic ? 'BULLETIN_MNEMONIC' : `dev account ${suri}`
+ };
+}
+
+function loadBlobs() {
+ const index = JSON.parse(readFileSync(resolve(metadataDir, 'cids.json'), 'utf8'));
+ const files = readdirSync(metadataDir).filter(name => name.endsWith('.json') && name !== 'cids.json');
+
+ return files.map(file => {
+ const bytes = new Uint8Array(readFileSync(resolve(metadataDir, file)));
+ const entry = Object.entries(index).find(([, value]) => value.file === file);
+ if (!entry) throw new Error(`${file} is not listed in cids.json. Re-run npm run generate:cdm-metadata.`);
+ return { name: entry[0], file, bytes, expectedCid: entry[1].cid };
+ });
+}
+
+async function main() {
+ const blobs = loadBlobs();
+ const { address, signer, source } = loadSigner();
+
+ // Recompute every CID from the bytes on disk. A blob edited after generation would
+ // otherwise be uploaded under a CID the registry no longer matches, and the registry
+ // entry is immutable once published.
+ let drifted = false;
+ for (const blob of blobs) {
+ const actual = (await calculateCid(blob.bytes, CID_CODEC_RAW, HASH_BLAKE2B_256)).toString();
+ blob.actualCid = actual;
+ if (actual !== blob.expectedCid) drifted = true;
+ }
+
+ console.log(`\nBulletin: ${BULLETIN_WS}`);
+ console.log(`Account: ${address} (${source})`);
+ console.log(`Blobs: ${blobs.length}\n`);
+
+ for (const blob of blobs) {
+ const ok = blob.actualCid === blob.expectedCid;
+ console.log(`${blob.name}`);
+ console.log(` file: ${blob.file} (${blob.bytes.length} bytes)`);
+ console.log(` cid: ${blob.actualCid}${ok ? '' : ` MISMATCH — cids.json says ${blob.expectedCid}`}`);
+ console.log('');
+ }
+
+ if (drifted) {
+ throw new Error('A blob no longer matches its recorded CID. Re-run npm run generate:cdm-metadata before uploading.');
+ }
+
+ const totalBytes = blobs.reduce((sum, blob) => sum + blob.bytes.length, 0);
+ const client = createClient(withPolkadotSdkCompat(getWsProvider(BULLETIN_WS)));
+
+ try {
+ const api = client.getTypedApi(bulletin);
+
+ let authorized = false;
+ let detail = 'no authorization found';
+ try {
+ const auth = await api.query.TransactionStorage.Authorizations.getValue(Enum('Account', address));
+ if (auth) {
+ const haveTx = BigInt(auth.extent.transactions ?? 0n);
+ const haveBytes = BigInt(auth.extent.bytes ?? 0n);
+ authorized = haveTx >= BigInt(blobs.length) && haveBytes >= BigInt(totalBytes);
+ detail = `${haveTx} transactions / ${haveBytes} bytes remaining; need ${blobs.length} / ${totalBytes}`;
+ }
+ } catch (error) {
+ detail = `authorization query failed: ${error.message}`;
+ }
+
+ console.log(`Authorization: ${authorized ? 'OK' : 'INSUFFICIENT'} — ${detail}\n`);
+
+ if (!authorized) {
+ throw new Error(
+ `${address} cannot store ${totalBytes} bytes on Bulletin.\n` +
+ 'Grant a quota first, then re-run:\n' +
+ ' dotns bulletin authorize ' +
+ address +
+ ' --transactions 1000 --bytes 104857600 --env devnet\n' +
+ ' or use the Bulletin console: https://paritytech.github.io/polkadot-bulletin-chain/ (Products Devnet)'
+ );
+ }
+
+ if (!confirm) {
+ console.log('Dry run. Re-run with --confirm to upload.');
+ console.log(
+ 'After uploading, register the names with: cd contracts/evm && npx hardhat cdm:publish --network polkadotTestnet --confirm --private-key '
+ );
+ return;
+ }
+
+ for (const blob of blobs) {
+ process.stdout.write(`Uploading ${blob.name} … `);
+ const tx = api.tx.TransactionStorage.store({ data: Binary.fromBytes(blob.bytes) });
+
+ await new Promise((resolvePromise, rejectPromise) => {
+ const timer = setTimeout(() => {
+ subscription.unsubscribe();
+ rejectPromise(new Error(`upload of ${blob.name} timed out after ${UPLOAD_TIMEOUT_MS / 1000}s`));
+ }, UPLOAD_TIMEOUT_MS);
+
+ const subscription = tx.signSubmitAndWatch(signer).subscribe({
+ next: event => {
+ if (event.type === 'txBestBlocksState' && event.found) {
+ clearTimeout(timer);
+ subscription.unsubscribe();
+ if (event.ok === false) {
+ rejectPromise(new Error(`${blob.name} rejected on chain: ${JSON.stringify(event.dispatchError ?? 'unknown')}`));
+ return;
+ }
+ resolvePromise();
+ }
+ },
+ error: error => {
+ clearTimeout(timer);
+ rejectPromise(error);
+ }
+ });
+ });
+
+ console.log(`stored as ${blob.actualCid}`);
+ }
+
+ console.log('\nAll blobs uploaded. The CIDs above are what cdm:publish will register.');
+ } finally {
+ client.destroy();
+ }
+}
+
+main().catch(error => {
+ console.error(`\n${error.message}`);
+ process.exit(1);
+});
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..5619848 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,25 +39,35 @@ 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);
+function canRequestProtectedPlayback(wallet: ConnectedWallet | null): boolean {
+ return Boolean(wallet?.createEvmClient || wallet?.keyRequestSigner);
+}
+
export function WalletProvider({ children }: { children: ReactNode }) {
const { setTransactionFeedback, setShowWalletModal } = useUiFeedback();
const {
state: walletState,
connectPasskey,
connectExtension,
+ connectProductHost,
switchExtensionNetwork,
disconnect: disconnectWalletOnly,
hasPrfSupport,
hasStoredPasskey,
- forgetPasskey
+ forgetPasskey,
+ productHostMode,
+ productHostStatus
} = useWallet();
const [ethRpcUrl] = useState(getDefaultEthRpcUrl);
@@ -68,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?.evmAddress;
+ const connectedAddress = canRequestProtectedPlayback(connectedWallet) ? connectedWallet?.evmAddress : undefined;
const lastConnectedAddressRef = useRef<`0x${string}` | null>(null);
const disconnect = useCallback(() => {
if (connectedAddress) void signOutOfDotifySession(connectedAddress);
@@ -88,7 +100,10 @@ export function WalletProvider({ children }: { children: ReactNode }) {
const currentBulletinAccount = devAccounts[bulletinAccountIndex];
const activeEvmAddress = connectedWallet?.evmAddress ?? zeroAddress;
- const listenerEvmAddress = 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;
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 +112,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 +184,7 @@ export function WalletProvider({ children }: { children: ReactNode }) {
connectedWallet,
activeEvmAddress,
listenerEvmAddress,
+ activeIdentityAddress,
activeSubstrateAddress,
activeSubstrateSigner,
currentBulletinAccount,
@@ -178,16 +197,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 +222,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..58416bc 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 && (
@@ -118,6 +130,37 @@ export function WalletModal({
)}
+ {wallet.method === 'product-host' && (
+ <>
+
+ 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 && (
+
+
+
+
+
+ {hasStoredPasskey ? 'Use passkey' : 'Create passkey'}
+ Enable paid access and publishing.
+
+
+ )}
+
+
+
+
+
+ Use EVM wallet
+ Enable paid access and publishing.
+
+
+
+ >
+ )}
+
{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' && (
+
+
+
+
+
+ Use Polkadot app
+
+ {productHostStatus === 'checking'
+ ? 'Checking the Product host...'
+ : productHostStatus === 'available'
+ ? 'Use your app-scoped account.'
+ : 'Open Dotify inside the Product host.'}
+
+
+
+ )}
+
{hasPrfSupport && (
-
+
diff --git a/web/src/features/productHost/productHost.test.ts b/web/src/features/productHost/productHost.test.ts
new file mode 100644
index 0000000..051f8ee
--- /dev/null
+++ b/web/src/features/productHost/productHost.test.ts
@@ -0,0 +1,91 @@
+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', () => {
+ expect(resolveProductHostConfig({})).toEqual({
+ mode: 'off',
+ productId: 'dotify-test01.dot'
+ });
+ });
+
+ it('reads an explicit Product DevNet configuration', () => {
+ expect(
+ resolveProductHostConfig({
+ VITE_DOTIFY_HOST_MODE: 'required',
+ VITE_DOTIFY_PRODUCT_ID: 'dotify-preview.dot'
+ })
+ ).toEqual({
+ mode: 'required',
+ productId: 'dotify-preview.dot'
+ });
+ });
+
+ it('fails closed to off for an unknown mode', () => {
+ expect(resolveProductHostConfig({ VITE_DOTIFY_HOST_MODE: 'sometimes' }).mode).toBe('off');
+ });
+});
+
+describe('probeProductHost', () => {
+ it('does not contact the host when integration is disabled', async () => {
+ let called = false;
+ const status = await probeProductHost('off', async () => {
+ called = true;
+ return true;
+ });
+
+ expect(status).toBe('off');
+ expect(called).toBe(false);
+ });
+
+ it('reports host availability without requesting an account', async () => {
+ await expect(probeProductHost('required', async () => true)).resolves.toBe('available');
+ await expect(probeProductHost('auto', async () => false)).resolves.toBe('unavailable');
+ });
+
+ it('treats a failed host handshake as unavailable', async () => {
+ await expect(
+ probeProductHost('required', async () => {
+ throw new Error('host missing');
+ })
+ ).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
new file mode 100644
index 0000000..3e23a4c
--- /dev/null
+++ b/web/src/features/productHost/productHost.ts
@@ -0,0 +1,111 @@
+import { bytesToHex } from '@polkadot-apps/utils';
+
+export type ProductHostMode = 'off' | 'auto' | 'required';
+export type ProductHostStatus = 'off' | 'checking' | 'available' | 'unavailable';
+
+export type ProductHostConfig = {
+ mode: ProductHostMode;
+ productId: string;
+};
+
+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];
+ return value === null || value === undefined ? '' : String(value).trim();
+}
+
+export function resolveProductHostConfig(env: EnvironmentLike): ProductHostConfig {
+ const rawMode = envValue(env, 'VITE_DOTIFY_HOST_MODE').toLowerCase();
+ const mode: ProductHostMode = rawMode === 'auto' || rawMode === 'required' ? rawMode : 'off';
+
+ return {
+ mode,
+ productId: envValue(env, 'VITE_DOTIFY_PRODUCT_ID') || 'dotify-test01.dot'
+ };
+}
+
+async function probeSdkHost(): Promise {
+ const { isInsideContainer } = await import('@parity/product-sdk/host');
+ return isInsideContainer();
+}
+
+export async function probeProductHost(mode: ProductHostMode, probe: () => Promise = probeSdkHost): Promise {
+ if (mode === 'off') return 'off';
+
+ try {
+ return (await probe()) ? 'available' : 'unavailable';
+ } catch {
+ return 'unavailable';
+ }
+}
+
+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) {
+ return String((error as { reason: unknown }).reason);
+ }
+ return String(error);
+}
+
+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 } = deps ?? (await loadProductHostIdentityDeps());
+ const provider = await getAccountsProvider();
+ if (!provider) {
+ throw new Error('Open this build inside the Polkadot Product host, then try again.');
+ }
+
+ const account = await provider.getProductAccount(config.productId, 0).match(
+ value => value,
+ error => {
+ 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),
+ 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/features/rooms/roomBeacon.test.ts b/web/src/features/rooms/roomBeacon.test.ts
new file mode 100644
index 0000000..f70c8d2
--- /dev/null
+++ b/web/src/features/rooms/roomBeacon.test.ts
@@ -0,0 +1,168 @@
+import { describe, expect, it } from 'vitest';
+import {
+ MAX_ACCOUNT_BYTES,
+ MAX_BEACON_BYTES,
+ assertBeaconBudget,
+ beaconByteLength,
+ buildRoomBeacon,
+ parseRoomBeacon,
+ roomBeaconChannel,
+ roomBeaconTopic
+} from './roomBeacon';
+
+const base = { roomCode: 'AB12CD', hostName: 'Kevin', listenerCount: 3 };
+
+describe('buildRoomBeacon', () => {
+ it('builds a compact beacon well inside the statement ceiling', () => {
+ const beacon = buildRoomBeacon(base);
+ expect(beacon).toEqual({ version: 1, room: 'AB12CD', host: 'Kevin', listenerCount: 3 });
+ expect(beaconByteLength(beacon!)).toBeLessThan(MAX_BEACON_BYTES);
+ });
+
+ it('omits now-playing unless the host opted in', () => {
+ // Publishing what someone is listening to is a different exposure than
+ // sharing a link, so it must never appear by default.
+ expect(buildRoomBeacon(base)).not.toHaveProperty('title');
+ expect(buildRoomBeacon({ ...base, nowPlaying: null })).not.toHaveProperty('title');
+
+ const opted = buildRoomBeacon({ ...base, nowPlaying: { title: 'Kwenda', artist: 'Muzinga' } });
+ expect(opted).toMatchObject({ title: 'Kwenda', artist: 'Muzinga' });
+ });
+
+ it('never carries listener identities, only an aggregate count', () => {
+ const beacon = buildRoomBeacon({ ...base, listenerCount: 7 });
+ expect(Object.keys(beacon!).sort()).toEqual(['host', 'listenerCount', 'room', 'version']);
+ expect(beacon!.listenerCount).toBe(7);
+ });
+
+ it('rejects a room code that could not be joined anyway', () => {
+ expect(buildRoomBeacon({ ...base, roomCode: '' })).toBeNull();
+ expect(buildRoomBeacon({ ...base, roomCode: 'AB' })).toBeNull();
+ expect(buildRoomBeacon({ ...base, roomCode: 'not-a-code!' })).toBeNull();
+ });
+
+ it('clamps a hostile listener count instead of publishing it', () => {
+ expect(buildRoomBeacon({ ...base, listenerCount: -5 })!.listenerCount).toBe(0);
+ expect(buildRoomBeacon({ ...base, listenerCount: 10 ** 9 })!.listenerCount).toBe(9999);
+ expect(buildRoomBeacon({ ...base, listenerCount: Number.NaN })!.listenerCount).toBe(0);
+ });
+
+ it('stays inside the ceiling when every field is oversized', () => {
+ const beacon = buildRoomBeacon({
+ roomCode: 'ZZZZZZZZZZZZ',
+ hostName: 'x'.repeat(500),
+ listenerCount: 9999,
+ nowPlaying: { title: 'y'.repeat(500), artist: 'z'.repeat(500) }
+ });
+
+ expect(beacon).not.toBeNull();
+ expect(beaconByteLength(beacon!)).toBeLessThanOrEqual(MAX_BEACON_BYTES);
+ expect(beacon!.room).toBe('ZZZZZZZZZZZZ');
+ });
+
+ it('stays inside the ceiling for multi-byte text, where character bounds are not byte bounds', () => {
+ // A 4-byte emoji clamped to 40 characters is 160 bytes, so the byte check
+ // has to be the authority rather than the length bound.
+ const beacon = buildRoomBeacon({
+ roomCode: 'AB12CD',
+ hostName: '🎧'.repeat(200),
+ listenerCount: 1,
+ nowPlaying: { title: '音楽'.repeat(200), artist: '🎵'.repeat(200) }
+ });
+
+ expect(beacon).not.toBeNull();
+ expect(beaconByteLength(beacon!)).toBeLessThanOrEqual(MAX_BEACON_BYTES);
+ });
+
+ it('sheds now-playing before the room identity when space runs out', () => {
+ const beacon = buildRoomBeacon({
+ roomCode: 'AB12CD',
+ hostName: 'h'.repeat(40),
+ listenerCount: 1,
+ nowPlaying: { title: '🎼'.repeat(60), artist: '🎹'.repeat(60) }
+ });
+
+ // Degrades to "a room exists here" rather than failing to announce at all.
+ expect(beacon!.room).toBe('AB12CD');
+ expect(beaconByteLength(beacon!)).toBeLessThanOrEqual(MAX_BEACON_BYTES);
+ });
+});
+
+describe('assertBeaconBudget', () => {
+ it('accepts what one account can actually hold', () => {
+ const beacons = [buildRoomBeacon(base)!, buildRoomBeacon({ ...base, roomCode: 'EF34GH' })!];
+ const budget = assertBeaconBudget(beacons);
+
+ expect(budget.ok).toBe(true);
+ expect(budget.usedBytes).toBeLessThanOrEqual(MAX_ACCOUNT_BYTES);
+ });
+
+ it('refuses a set that the chain would silently reject', () => {
+ // Over the account total the chain rejects the write, which a user sees as a
+ // room that never appears. An explainable refusal beats a vanishing room.
+ const many = Array.from({ length: 12 }, (_, index) =>
+ buildRoomBeacon({
+ roomCode: `ROOM${String(index).padStart(2, '0')}`,
+ hostName: 'a-fairly-long-host-name-here',
+ listenerCount: index,
+ nowPlaying: { title: 'A reasonably long track title', artist: 'A reasonably long artist name' }
+ })
+ ).filter((beacon): beacon is NonNullable => Boolean(beacon));
+
+ const budget = assertBeaconBudget(many);
+ expect(budget.ok).toBe(false);
+ expect(budget.ok === false && budget.reason).toMatch(/account limit/);
+ });
+});
+
+describe('parseRoomBeacon', () => {
+ it('round-trips a beacon it built', () => {
+ const beacon = buildRoomBeacon({ ...base, nowPlaying: { title: 'Kwenda', artist: 'Muzinga' } })!;
+ expect(parseRoomBeacon(JSON.parse(JSON.stringify(beacon)))).toEqual(beacon);
+ });
+
+ it('rejects anything that is not a version 1 beacon', () => {
+ // Statements come from arbitrary accounts, so shape cannot be assumed.
+ for (const bad of [null, undefined, 42, 'beacon', [], {}, { version: 2, room: 'AB12CD', host: 'K' }]) {
+ expect(parseRoomBeacon(bad)).toBeNull();
+ }
+ });
+
+ it('rejects a malformed room or missing host rather than listing a dead room', () => {
+ expect(parseRoomBeacon({ version: 1, room: '!!', host: 'K', listenerCount: 1 })).toBeNull();
+ expect(parseRoomBeacon({ version: 1, room: 'AB12CD', host: '', listenerCount: 1 })).toBeNull();
+ });
+
+ it('clamps hostile values from a remote publisher', () => {
+ const parsed = parseRoomBeacon({
+ version: 1,
+ room: 'ab12cd',
+ host: 'h'.repeat(500),
+ listenerCount: Number.MAX_SAFE_INTEGER,
+ title: 't'.repeat(500),
+ artist: 'a'.repeat(500)
+ });
+
+ expect(parsed!.room).toBe('AB12CD');
+ expect(parsed!.host.length).toBeLessThanOrEqual(40);
+ expect(parsed!.listenerCount).toBe(9999);
+ expect(parsed!.title!.length).toBeLessThanOrEqual(60);
+ });
+
+ it('ignores a negative count instead of rendering it', () => {
+ expect(parseRoomBeacon({ version: 1, room: 'AB12CD', host: 'K', listenerCount: -10 })!.listenerCount).toBe(0);
+ });
+});
+
+describe('channel and topic naming', () => {
+ it('gives one last-write-wins channel per room', () => {
+ // Without this every heartbeat would be a new statement and the account
+ // budget would be gone within a couple of beats.
+ expect(roomBeaconChannel('ab12cd')).toBe('room/AB12CD');
+ expect(roomBeaconChannel('AB12CD')).toBe(roomBeaconChannel('ab12cd'));
+ });
+
+ it('scopes a subscriber to one room', () => {
+ expect(roomBeaconTopic('ab12cd')).toBe('dotify-room-AB12CD');
+ });
+});
diff --git a/web/src/features/rooms/roomBeacon.ts b/web/src/features/rooms/roomBeacon.ts
new file mode 100644
index 0000000..f101b2a
--- /dev/null
+++ b/web/src/features/rooms/roomBeacon.ts
@@ -0,0 +1,198 @@
+// Room discovery beacons for the Statement Store.
+//
+// What this is and is not:
+//
+// A beacon announces that a room exists and is live, so a listening room can be
+// found without asking Dotify's signaling server. It is *additive discovery*. It
+// never carries SDP, ICE, chat, or audio, and it is never required to join a
+// room - a share link still works with no wallet, no account, and no chain.
+// That invariant is the reason the beacon is deliberately small and dull.
+//
+// Why joining cannot move here:
+//
+// A WebRTC offer is 1.5-4 KB against a 512-byte statement ceiling, and a guest
+// would have to publish an answer to complete a handshake - which requires an
+// identity and an allowance. Moving the handshake here would convert every
+// listener into a registered person and delete the gesture the product exists to
+// protect. The host is already identified, so only the host publishes.
+//
+// The budget is the hard part:
+//
+// MAX_STATEMENT_SIZE 512 bytes per statement
+// MAX_USER_TOTAL 1024 bytes per account, across all live statements
+//
+// One account therefore holds at most two full statements. A beacon is written
+// to a per-room channel so last-write-wins keeps exactly one live statement per
+// hosted room, and `assertBeaconBudget` refuses a set of rooms that would exceed
+// the account total. Publishing blind would get later rooms silently rejected by
+// the chain, which reads as "the room vanished".
+
+/** Mirrors MAX_STATEMENT_SIZE in @parity/product-sdk-statement-store. */
+export const MAX_BEACON_BYTES = 512;
+/** Mirrors MAX_USER_TOTAL. One account across every live statement it holds. */
+export const MAX_ACCOUNT_BYTES = 1024;
+
+/** Field bounds, chosen so a worst-case record still fits the ceiling. */
+const MAX_ROOM_CODE = 12;
+const MAX_HOST_NAME = 40;
+const MAX_TRACK_TEXT = 60;
+const MAX_LISTENERS = 9_999;
+
+// Field names are spelled out rather than single letters. They cost bytes in
+// every published beacon - the full set is roughly 40 bytes of keys against a
+// 512-byte ceiling - but a wire format is read by people debugging a live room,
+// and `n`/`t`/`a` are unreadable for a saving the budget does not need.
+export type RoomBeacon = {
+ /** Schema version, so a reader can reject shapes it does not understand. */
+ version: 1;
+ /** Room code, matching the share link. */
+ room: string;
+ /** Host display name, already public to anyone holding the link. */
+ host: string;
+ /** Aggregate only - never identities. */
+ listenerCount: number;
+ /** Now playing, present only when the host opted in. */
+ title?: string;
+ /** Artist, present only alongside `title`. */
+ artist?: string;
+};
+
+export type RoomBeaconInput = {
+ roomCode: string;
+ hostName: string;
+ listenerCount: number;
+ /**
+ * Now-playing text. Omitted unless the host opted in.
+ *
+ * This is the one field with a real privacy cost. A room link is semi-public -
+ * whoever holds it can look - but a beacon is globally readable and outlives
+ * the room by up to the retention window. Publishing what someone is listening
+ * to is therefore a different exposure than sharing a link, and it stays a
+ * deliberate choice rather than a default.
+ */
+ nowPlaying?: { title: string; artist: string } | null;
+};
+
+function clamp(value: string, max: number): string {
+ // Trim first so a long run of spaces cannot consume the budget.
+ const trimmed = value.trim().replace(/\s+/g, ' ');
+ return trimmed.length <= max ? trimmed : trimmed.slice(0, max);
+}
+
+function byteLength(value: unknown): number {
+ return new TextEncoder().encode(JSON.stringify(value)).length;
+}
+
+/** Serialized size of a beacon, in the bytes the chain will actually count. */
+export function beaconByteLength(beacon: RoomBeacon): number {
+ return byteLength(beacon);
+}
+
+function tooLarge(beacon: RoomBeacon): boolean {
+ return beaconByteLength(beacon) > MAX_BEACON_BYTES;
+}
+
+/**
+ * Build a beacon that is guaranteed to fit MAX_BEACON_BYTES.
+ *
+ * Returns null for an unusable room code rather than publishing a beacon nobody
+ * can join. Optional fields are dropped before required ones if the record is
+ * still too large, so discovery degrades to "a room exists here" instead of
+ * failing outright.
+ */
+export function buildRoomBeacon(input: RoomBeaconInput): RoomBeacon | null {
+ const room = clamp(input.roomCode, MAX_ROOM_CODE).toUpperCase();
+ if (!/^[A-Z0-9]{4,12}$/.test(room)) return null;
+
+ const listeners = Number.isFinite(input.listenerCount) ? Math.max(0, Math.min(MAX_LISTENERS, Math.trunc(input.listenerCount))) : 0;
+
+ const beacon: RoomBeacon = {
+ version: 1,
+ room,
+ host: clamp(input.hostName || 'Host', MAX_HOST_NAME),
+ listenerCount: listeners
+ };
+
+ if (input.nowPlaying) {
+ const title = clamp(input.nowPlaying.title, MAX_TRACK_TEXT);
+ const artist = clamp(input.nowPlaying.artist, MAX_TRACK_TEXT);
+ if (title) beacon.title = title;
+ if (artist) beacon.artist = artist;
+ }
+
+ // Shed optional fields in order of least value until the record fits. The size
+ // has to be re-measured after each removal rather than snapshotted once: every
+ // step changes the serialized bytes, so a cached length would be stale by the
+ // next check. A multi-byte host name can still overflow after clamping by
+ // character count, so bytes are authoritative over the length bounds above.
+ if (tooLarge(beacon)) delete beacon.artist;
+ if (tooLarge(beacon)) delete beacon.title;
+ while (tooLarge(beacon) && beacon.host.length > 1) {
+ beacon.host = beacon.host.slice(0, Math.floor(beacon.host.length / 2));
+ }
+
+ return tooLarge(beacon) ? null : beacon;
+}
+
+/**
+ * Channel name for a room, giving last-write-wins per room.
+ *
+ * Without a per-room channel every heartbeat would be a new statement and the
+ * account budget would be exhausted within a couple of beats.
+ */
+export function roomBeaconChannel(roomCode: string): string {
+ return `room/${roomCode.toUpperCase()}`;
+}
+
+/** Secondary topic, so a subscriber can filter to one room. */
+export function roomBeaconTopic(roomCode: string): string {
+ return `dotify-room-${roomCode.toUpperCase()}`;
+}
+
+export type BeaconBudget = { ok: true; usedBytes: number } | { ok: false; usedBytes: number; reason: string };
+
+/**
+ * Check a set of live beacons against the per-account ceiling.
+ *
+ * The chain enforces this silently by rejecting the write, which surfaces to a
+ * user as a room that never appears. Checking first turns that into an
+ * explainable refusal.
+ */
+export function assertBeaconBudget(beacons: RoomBeacon[]): BeaconBudget {
+ const usedBytes = beacons.reduce((total, beacon) => total + beaconByteLength(beacon), 0);
+ if (usedBytes > MAX_ACCOUNT_BYTES) {
+ return {
+ ok: false,
+ usedBytes,
+ reason: `${beacons.length} live beacons need ${usedBytes} bytes, over the ${MAX_ACCOUNT_BYTES}-byte account limit. Host fewer rooms from one account.`
+ };
+ }
+ return { ok: true, usedBytes };
+}
+
+/**
+ * Parse a received statement payload into a beacon.
+ *
+ * Everything here is untrusted: statements are published by arbitrary accounts,
+ * so a reader must not assume shape, types, or bounds. Returns null rather than
+ * throwing, so one malformed beacon cannot break a discovery list.
+ */
+export function parseRoomBeacon(data: unknown): RoomBeacon | null {
+ if (!data || typeof data !== 'object' || Array.isArray(data)) return null;
+ const record = data as Record;
+ if (record.version !== 1) return null;
+
+ const room = typeof record.room === 'string' ? record.room.toUpperCase() : '';
+ if (!/^[A-Z0-9]{4,12}$/.test(room)) return null;
+
+ const host = typeof record.host === 'string' ? clamp(record.host, MAX_HOST_NAME) : '';
+ if (!host) return null;
+
+ const rawCount = typeof record.listenerCount === 'number' && Number.isFinite(record.listenerCount) ? Math.trunc(record.listenerCount) : 0;
+ const beacon: RoomBeacon = { version: 1, room, host, listenerCount: Math.max(0, Math.min(MAX_LISTENERS, rawCount)) };
+
+ if (typeof record.title === 'string' && record.title.trim()) beacon.title = clamp(record.title, MAX_TRACK_TEXT);
+ if (typeof record.artist === 'string' && record.artist.trim()) beacon.artist = clamp(record.artist, MAX_TRACK_TEXT);
+
+ return beacon;
+}
diff --git a/web/src/features/rooms/roomBeaconPublisher.test.ts b/web/src/features/rooms/roomBeaconPublisher.test.ts
new file mode 100644
index 0000000..d68309f
--- /dev/null
+++ b/web/src/features/rooms/roomBeaconPublisher.test.ts
@@ -0,0 +1,411 @@
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+afterEach(() => {
+ vi.unstubAllEnvs();
+ vi.resetModules();
+});
+
+async function loadModule() {
+ return import('./roomBeaconPublisher');
+}
+
+function fakeClient(overrides: { publishOk?: boolean; publishThrows?: boolean } = {}) {
+ const published: Array<{ data: unknown; options: unknown }> = [];
+ let handler: ((statement: { data: unknown; expiry?: bigint }) => void) | null = null;
+ const client = {
+ publish: vi.fn(async (data: unknown, options: unknown) => {
+ if (overrides.publishThrows) throw new Error('transport down');
+ published.push({ data, options });
+ return { ok: overrides.publishOk ?? true };
+ }),
+ subscribe: vi.fn((callback: (statement: { data: unknown; expiry?: bigint }) => void) => {
+ handler = callback;
+ return { unsubscribe: vi.fn() };
+ }),
+ destroy: vi.fn()
+ };
+ return { client, published, emit: (data: unknown, expiry?: bigint) => handler?.({ data, expiry }) };
+}
+
+const base = { roomCode: 'AB12CD', hostName: 'Kevin', listenerCount: 2 };
+
+describe('createRoomBeaconPublisher', () => {
+ it('is absent unless the build opted in', async () => {
+ // Off by default so no chain code ships to a build that cannot use it.
+ vi.stubEnv('VITE_DOTIFY_ROOM_BEACONS', '');
+ vi.resetModules();
+ const { createRoomBeaconPublisher } = await loadModule();
+
+ const createClient = vi.fn();
+ await expect(createRoomBeaconPublisher({ createClient })).resolves.toBeNull();
+ expect(createClient).not.toHaveBeenCalled();
+ });
+
+ it('returns null instead of throwing when no host container is present', async () => {
+ // Hosting a room must keep working outside the Product host.
+ vi.stubEnv('VITE_DOTIFY_ROOM_BEACONS', 'on');
+ vi.resetModules();
+ const { createRoomBeaconPublisher } = await loadModule();
+
+ const publisher = await createRoomBeaconPublisher({
+ createClient: async () => {
+ throw new Error('no host provider');
+ }
+ });
+
+ expect(publisher).toBeNull();
+ });
+
+ it('publishes a beacon to a per-room last-write-wins channel', async () => {
+ vi.stubEnv('VITE_DOTIFY_ROOM_BEACONS', 'on');
+ vi.resetModules();
+ const { createRoomBeaconPublisher, BEACON_TTL_SECONDS } = await loadModule();
+ const { client, published } = fakeClient();
+
+ const publisher = await createRoomBeaconPublisher({ createClient: async () => client });
+ await expect(publisher!.announce(base)).resolves.toEqual({ ok: true });
+
+ expect(published).toHaveLength(1);
+ expect(published[0].data).toEqual({ version: 1, room: 'AB12CD', host: 'Kevin', listenerCount: 2 });
+ expect(published[0].options).toEqual({
+ channel: 'room/AB12CD',
+ topic2: 'dotify-room-AB12CD',
+ ttlSeconds: BEACON_TTL_SECONDS
+ });
+ });
+
+ it('never publishes SDP, chat, or listener identities', async () => {
+ vi.stubEnv('VITE_DOTIFY_ROOM_BEACONS', 'on');
+ vi.resetModules();
+ const { createRoomBeaconPublisher } = await loadModule();
+ const { client, published } = fakeClient();
+
+ const publisher = await createRoomBeaconPublisher({ createClient: async () => client });
+ await publisher!.announce({ ...base, nowPlaying: { title: 'Kwenda', artist: 'Muzinga' } });
+
+ const keys = Object.keys(published[0].data as object).sort();
+ expect(keys).toEqual(['artist', 'host', 'listenerCount', 'room', 'title', 'version']);
+ });
+
+ it('refuses a room that would push the account over its byte ceiling', async () => {
+ // The chain would reject this silently, which reads as a room that never
+ // appears; refusing locally keeps the cause visible.
+ vi.stubEnv('VITE_DOTIFY_ROOM_BEACONS', 'on');
+ vi.resetModules();
+ const { createRoomBeaconPublisher } = await loadModule();
+ const { client, published } = fakeClient();
+
+ const publisher = await createRoomBeaconPublisher({ createClient: async () => client });
+
+ let accepted = 0;
+ let lastReason = '';
+ for (let index = 0; index < 12; index += 1) {
+ const outcome = await publisher!.announce({
+ roomCode: `ROOM${String(index).padStart(2, '0')}`,
+ hostName: 'a-fairly-long-host-name-here',
+ listenerCount: index,
+ nowPlaying: { title: 'A reasonably long track title', artist: 'A reasonably long artist name' }
+ });
+ if (outcome.ok) accepted += 1;
+ else lastReason = outcome.reason;
+ }
+
+ expect(accepted).toBeGreaterThan(0);
+ expect(accepted).toBeLessThan(12);
+ expect(published).toHaveLength(accepted);
+ // Scoped honestly: this guard only sees writes from this instance.
+ expect(lastReason).toBe('quota-local');
+ });
+
+ it('names an account-wide rejection the client cannot observe locally', async () => {
+ vi.stubEnv('VITE_DOTIFY_ROOM_BEACONS', 'on');
+ vi.resetModules();
+ const { createRoomBeaconPublisher } = await loadModule();
+ const { client } = fakeClient({ publishOk: false });
+
+ const publisher = await createRoomBeaconPublisher({ createClient: async () => client });
+ const outcome = await publisher!.announce(base);
+
+ expect(outcome).toMatchObject({ ok: false, reason: 'rejected' });
+ expect(outcome.ok === false && outcome.detail).toMatch(/account-wide quota/);
+ });
+
+ it('swallows a transport error so hosting is never interrupted', async () => {
+ vi.stubEnv('VITE_DOTIFY_ROOM_BEACONS', 'on');
+ vi.resetModules();
+ const { createRoomBeaconPublisher } = await loadModule();
+ const { client } = fakeClient({ publishThrows: true });
+
+ const publisher = await createRoomBeaconPublisher({ createClient: async () => client });
+ await expect(publisher!.announce(base)).resolves.toMatchObject({ ok: false, reason: 'transport' });
+ });
+
+ it('stops announcing and releases the connection', async () => {
+ vi.stubEnv('VITE_DOTIFY_ROOM_BEACONS', 'on');
+ vi.resetModules();
+ const { createRoomBeaconPublisher } = await loadModule();
+ const { client } = fakeClient();
+
+ const publisher = await createRoomBeaconPublisher({ createClient: async () => client });
+ publisher!.stop();
+
+ expect(client.destroy).toHaveBeenCalled();
+ await expect(publisher!.announce(base)).resolves.toMatchObject({ ok: false, reason: 'stopped' });
+ });
+});
+
+describe('subscribeRoomBeacons', () => {
+ it('collects valid beacons and drops malformed ones', async () => {
+ vi.stubEnv('VITE_DOTIFY_ROOM_BEACONS', 'on');
+ vi.resetModules();
+ const { subscribeRoomBeacons } = await loadModule();
+ const { client, emit } = fakeClient();
+
+ const listener = await subscribeRoomBeacons({}, { createClient: async () => client });
+
+ emit({ version: 1, room: 'AB12CD', host: 'Kevin', listenerCount: 2 });
+ emit({ version: 9, room: 'EF34GH', host: 'Nope', listenerCount: 1 });
+ emit('not a beacon');
+ emit({ version: 1, room: 'EF34GH', host: 'Ada', listenerCount: 5 });
+
+ expect(listener!.beacons()).toEqual([
+ { version: 1, room: 'AB12CD', host: 'Kevin', listenerCount: 2 },
+ { version: 1, room: 'EF34GH', host: 'Ada', listenerCount: 5 }
+ ]);
+ });
+
+ it('keeps only the latest beacon per room', async () => {
+ vi.stubEnv('VITE_DOTIFY_ROOM_BEACONS', 'on');
+ vi.resetModules();
+ const { subscribeRoomBeacons } = await loadModule();
+ const { client, emit } = fakeClient();
+
+ const listener = await subscribeRoomBeacons({}, { createClient: async () => client });
+ emit({ version: 1, room: 'AB12CD', host: 'Kevin', listenerCount: 2 });
+ emit({ version: 1, room: 'AB12CD', host: 'Kevin', listenerCount: 6 });
+
+ expect(listener!.beacons()).toEqual([{ version: 1, room: 'AB12CD', host: 'Kevin', listenerCount: 6 }]);
+ });
+
+ it('filters to one room when asked', async () => {
+ vi.stubEnv('VITE_DOTIFY_ROOM_BEACONS', 'on');
+ vi.resetModules();
+ const { subscribeRoomBeacons } = await loadModule();
+ const { client, emit } = fakeClient();
+
+ const listener = await subscribeRoomBeacons({ roomCode: 'ab12cd' }, { createClient: async () => client });
+ emit({ version: 1, room: 'AB12CD', host: 'Kevin', listenerCount: 2 });
+ emit({ version: 1, room: 'EF34GH', host: 'Ada', listenerCount: 5 });
+
+ expect(listener!.beacons()).toEqual([{ version: 1, room: 'AB12CD', host: 'Kevin', listenerCount: 2 }]);
+ });
+
+ it('is absent unless the build opted in', async () => {
+ vi.stubEnv('VITE_DOTIFY_ROOM_BEACONS', '');
+ vi.resetModules();
+ const { subscribeRoomBeacons } = await loadModule();
+
+ await expect(subscribeRoomBeacons({}, { createClient: vi.fn() })).resolves.toBeNull();
+ });
+
+ it('evicts a room once its statement expires', async () => {
+ // The store drops an expired record on its side but sends no deletion event,
+ // so without a sweep a stopped room would be listed for the page lifetime.
+ vi.useFakeTimers();
+ vi.stubEnv('VITE_DOTIFY_ROOM_BEACONS', 'on');
+ vi.resetModules();
+ const { subscribeRoomBeacons } = await loadModule();
+ const { client, emit } = fakeClient();
+
+ let clock = 1_000_000_000_000;
+ const changes: number[] = [];
+ const listener = await subscribeRoomBeacons({ onChange: beacons => changes.push(beacons.length) }, { createClient: async () => client, now: () => clock });
+
+ // expiry packs seconds in the upper 32 bits.
+ const expiresAtSeconds = BigInt(Math.floor(clock / 1000) + 90);
+ emit({ version: 1, room: 'AB12CD', host: 'Kevin', listenerCount: 2 }, expiresAtSeconds << 32n);
+ expect(listener!.beacons()).toHaveLength(1);
+
+ clock += 91_000;
+ // Reads are filtered immediately, without waiting for the sweep.
+ expect(listener!.beacons()).toHaveLength(0);
+
+ await vi.advanceTimersByTimeAsync(20_000);
+ expect(changes[changes.length - 1]).toBe(0);
+
+ listener!.stop();
+ vi.useRealTimers();
+ });
+
+ it('ignores a statement that is already expired on arrival', async () => {
+ vi.stubEnv('VITE_DOTIFY_ROOM_BEACONS', 'on');
+ vi.resetModules();
+ const { subscribeRoomBeacons } = await loadModule();
+ const { client, emit } = fakeClient();
+
+ const clock = 1_000_000_000_000;
+ const listener = await subscribeRoomBeacons({}, { createClient: async () => client, now: () => clock });
+
+ emit({ version: 1, room: 'AB12CD', host: 'Kevin', listenerCount: 2 }, BigInt(Math.floor(clock / 1000) - 10) << 32n);
+ expect(listener!.beacons()).toHaveLength(0);
+ });
+
+ it('falls back to an assumed lifetime when the transport omits expiry', async () => {
+ // Otherwise a transport without the field would reintroduce never-evicting.
+ vi.stubEnv('VITE_DOTIFY_ROOM_BEACONS', 'on');
+ vi.resetModules();
+ const { subscribeRoomBeacons } = await loadModule();
+ const { client, emit } = fakeClient();
+
+ let clock = 1_000_000_000_000;
+ const listener = await subscribeRoomBeacons({}, { createClient: async () => client, now: () => clock });
+
+ emit({ version: 1, room: 'AB12CD', host: 'Kevin', listenerCount: 2 });
+ expect(listener!.beacons()).toHaveLength(1);
+
+ clock += 121_000;
+ expect(listener!.beacons()).toHaveLength(0);
+ });
+});
+
+describe('startRoomBeaconLoop', () => {
+ it('announces immediately and then on the refresh interval', async () => {
+ vi.stubEnv('VITE_DOTIFY_ROOM_BEACONS', 'on');
+ vi.resetModules();
+ const { startRoomBeaconLoop, BEACON_REFRESH_MS } = await loadModule();
+
+ const announce = vi.fn(async () => ({ ok: true as const }));
+ let tick: (() => void) | null = null;
+
+ const stop = startRoomBeaconLoop(() => base, {
+ createPublisher: async () => ({ announce, stop: vi.fn() }),
+ setInterval: (handler, ms) => {
+ expect(ms).toBe(BEACON_REFRESH_MS);
+ tick = handler;
+ return 1 as never;
+ },
+ clearInterval: vi.fn()
+ });
+
+ await vi.waitFor(() => expect(announce).toHaveBeenCalledTimes(1));
+ tick!();
+ await vi.waitFor(() => expect(announce).toHaveBeenCalledTimes(2));
+ stop();
+ });
+
+ it('re-reads the input each announce, so a changing listener count needs no restart', async () => {
+ vi.stubEnv('VITE_DOTIFY_ROOM_BEACONS', 'on');
+ vi.resetModules();
+ const { startRoomBeaconLoop } = await loadModule();
+
+ const announce = vi.fn(async (_input: unknown) => ({ ok: true as const }));
+ let count = 2;
+ let tick: (() => void) | null = null;
+
+ const stop = startRoomBeaconLoop(() => ({ ...base, listenerCount: count }), {
+ createPublisher: async () => ({ announce, stop: vi.fn() }),
+ setInterval: handler => {
+ tick = handler;
+ return 1 as never;
+ },
+ clearInterval: vi.fn()
+ });
+
+ await vi.waitFor(() => expect(announce).toHaveBeenCalledTimes(1));
+ count = 9;
+ tick!();
+
+ await vi.waitFor(() => expect(announce).toHaveBeenCalledTimes(2));
+ expect(announce.mock.calls[1][0]).toMatchObject({ listenerCount: 9 });
+ stop();
+ });
+
+ it('stops the publisher and clears the timer on stop', async () => {
+ vi.stubEnv('VITE_DOTIFY_ROOM_BEACONS', 'on');
+ vi.resetModules();
+ const { startRoomBeaconLoop } = await loadModule();
+
+ const publisherStop = vi.fn();
+ const clearIntervalSpy = vi.fn();
+ const setIntervalSpy = vi.fn(() => 7 as never);
+
+ const stop = startRoomBeaconLoop(() => base, {
+ createPublisher: async () => ({ announce: vi.fn(async () => ({ ok: true as const })), stop: publisherStop }),
+ setInterval: setIntervalSpy,
+ clearInterval: clearIntervalSpy
+ });
+
+ // Wait until the refresh timer actually exists, otherwise this asserts a
+ // different lifecycle stage than it claims to.
+ await vi.waitFor(() => expect(setIntervalSpy).toHaveBeenCalled());
+ stop();
+
+ expect(publisherStop).toHaveBeenCalled();
+ expect(clearIntervalSpy).toHaveBeenCalledWith(7);
+ });
+
+ it('releases a publisher that finished connecting after stop was called', async () => {
+ // The room can close while the host connection is still being established;
+ // without this the publisher would be left running with no owner.
+ vi.stubEnv('VITE_DOTIFY_ROOM_BEACONS', 'on');
+ vi.resetModules();
+ const { startRoomBeaconLoop } = await loadModule();
+
+ const publisherStop = vi.fn();
+ const announce = vi.fn(async () => ({ ok: true as const }));
+ let release: (() => void) | null = null;
+ const pending = new Promise(resolve => {
+ release = resolve;
+ });
+
+ const stop = startRoomBeaconLoop(() => base, {
+ createPublisher: async () => {
+ await pending;
+ return { announce, stop: publisherStop };
+ }
+ });
+
+ stop();
+ release!();
+ await vi.waitFor(() => expect(publisherStop).toHaveBeenCalled());
+ expect(announce).not.toHaveBeenCalled();
+ });
+
+ it('reports a refused announce without throwing', async () => {
+ vi.stubEnv('VITE_DOTIFY_ROOM_BEACONS', 'on');
+ vi.resetModules();
+ const { startRoomBeaconLoop } = await loadModule();
+
+ const outcomes: unknown[] = [];
+ const stop = startRoomBeaconLoop(() => base, {
+ createPublisher: async () => ({
+ announce: vi.fn(async () => ({ ok: false as const, reason: 'rejected' as const, detail: 'quota' })),
+ stop: vi.fn()
+ }),
+ setInterval: () => 1 as never,
+ clearInterval: vi.fn(),
+ onOutcome: outcome => outcomes.push(outcome)
+ });
+
+ await vi.waitFor(() => expect(outcomes).toHaveLength(1));
+ expect(outcomes[0]).toMatchObject({ reason: 'rejected' });
+ stop();
+ });
+
+ it('skips announcing when the room is no longer hostable', async () => {
+ vi.stubEnv('VITE_DOTIFY_ROOM_BEACONS', 'on');
+ vi.resetModules();
+ const { startRoomBeaconLoop } = await loadModule();
+
+ const announce = vi.fn(async () => ({ ok: true as const }));
+ const stop = startRoomBeaconLoop(() => null, {
+ createPublisher: async () => ({ announce, stop: vi.fn() }),
+ setInterval: () => 1 as never,
+ clearInterval: vi.fn()
+ });
+
+ await new Promise(resolve => setTimeout(resolve, 10));
+ expect(announce).not.toHaveBeenCalled();
+ stop();
+ });
+});
diff --git a/web/src/features/rooms/roomBeaconPublisher.ts b/web/src/features/rooms/roomBeaconPublisher.ts
new file mode 100644
index 0000000..4b3b55f
--- /dev/null
+++ b/web/src/features/rooms/roomBeaconPublisher.ts
@@ -0,0 +1,329 @@
+// Publishes and reads room beacons over the Statement Store.
+//
+// Scope, deliberately narrow:
+//
+// - only a host publishes, and only for rooms it is hosting;
+// - a beacon is discovery, never a join path - the share link stays the way in,
+// with no wallet, no account, and no chain;
+// - a failure here is silent by design. Discovery is an enhancement, so a
+// statement store that is unreachable must degrade to "no beacons" and never
+// interrupt hosting a room.
+//
+// Two constraints from the SDK shape everything below.
+//
+// The client "is designed to run exclusively inside a host container", so this
+// path only works inside the Polkadot Product host. Standalone builds keep
+// Socket.IO discovery and lose nothing they had.
+//
+// Host mode signs through the product's allowance account via the RFC-10
+// sponsored path, so the listener does not need an Individuality allowance of
+// their own. That is what keeps hosting from becoming an identity gate.
+//
+// The client tree is loaded lazily and behind a build-time flag so a build that
+// did not opt in never carries it. Same discipline as the Product contract
+// adapter, for the same reason: unused chain code is not free, it is published
+// weight against a finite Bulletin quota.
+
+import { assertBeaconBudget, buildRoomBeacon, parseRoomBeacon, roomBeaconChannel, roomBeaconTopic, type RoomBeacon, type RoomBeaconInput } from './roomBeacon';
+
+/** Application namespace, hashed into topic1. Scopes Dotify traffic. */
+const BEACON_APP_NAME = 'dotify-rooms';
+
+/**
+ * Republish interval. The statement TTL is a chain-side expiry, so a beacon has
+ * to be refreshed to stay live; refreshing well inside the TTL means one missed
+ * beat does not drop a room from discovery.
+ */
+export const BEACON_TTL_SECONDS = 90;
+export const BEACON_REFRESH_MS = 30_000;
+
+const BEACONS_ENABLED = import.meta.env.VITE_DOTIFY_ROOM_BEACONS === 'on';
+
+/**
+ * A received statement, keeping the metadata the reader needs.
+ *
+ * `expiry` is carried deliberately. The Statement Store drops an expired record
+ * on its side but sends no deletion event, so a subscriber that discards this
+ * would show a room forever after its host stopped. It packs a timestamp in the
+ * upper 32 bits and a sequence number in the lower 32.
+ */
+export type BeaconStatement = { data: unknown; expiry?: bigint };
+
+export type BeaconClient = {
+ publish: (data: unknown, options: { channel: string; topic2: string; ttlSeconds: number }) => Promise<{ ok: boolean }>;
+ subscribe: (callback: (statement: BeaconStatement) => void, options?: { topic2?: string }) => { unsubscribe: () => void };
+ destroy: () => void;
+};
+
+/** Seconds since epoch encoded in a statement expiry, or null when absent. */
+export function beaconExpirySeconds(expiry: bigint | undefined): number | null {
+ if (typeof expiry !== 'bigint') return null;
+ const seconds = Number(expiry >> 32n);
+ return Number.isFinite(seconds) && seconds > 0 ? seconds : null;
+}
+
+export type BeaconDeps = {
+ createClient: () => Promise;
+ /** Injectable clock, so expiry behaviour can be tested without real time. */
+ now?: () => number;
+};
+
+async function createSdkClient(): Promise {
+ const { StatementStoreClient } = await import('@parity/product-sdk-statement-store');
+ const client = new StatementStoreClient({ appName: BEACON_APP_NAME, defaultTtlSeconds: BEACON_TTL_SECONDS });
+
+ // Host mode only. Local mode would need an Sr25519 signer and an allowance,
+ // which is precisely the identity gate this feature must not introduce.
+ await client.connect({ mode: 'host' });
+
+ return {
+ publish: async (data, options) => {
+ const result = await client.publish(data, options);
+ return { ok: result.ok };
+ },
+ subscribe: (callback, options) => client.subscribe(callback as never, options),
+ destroy: () => client.destroy()
+ };
+}
+
+const defaultDeps: BeaconDeps = { createClient: createSdkClient };
+
+/** Why an announce did not land. `quota-local` is this instance only; see below. */
+export type AnnounceOutcome = { ok: true } | { ok: false; reason: 'unbuildable' | 'stopped' | 'quota-local' | 'rejected' | 'transport'; detail: string };
+
+export type RoomBeaconPublisher = {
+ /** Publish or refresh the beacon for one room. */
+ announce: (input: RoomBeaconInput) => Promise;
+ /** Stop announcing and release the connection. */
+ stop: () => void;
+};
+
+/**
+ * Start publishing beacons.
+ *
+ * Returns null when beacons are disabled at build time or the host is
+ * unavailable, so callers can treat discovery as simply absent rather than
+ * branching on an error.
+ */
+export async function createRoomBeaconPublisher(deps: BeaconDeps = defaultDeps): Promise {
+ if (!BEACONS_ENABLED) return null;
+
+ let client: BeaconClient;
+ try {
+ client = await deps.createClient();
+ } catch {
+ // No host container, no allowance, no transport: hosting still works.
+ return null;
+ }
+
+ const live = new Map();
+ let stopped = false;
+
+ return {
+ async announce(input) {
+ if (stopped) return { ok: false, reason: 'stopped', detail: 'publisher already stopped' };
+
+ const beacon = buildRoomBeacon(input);
+ if (!beacon) return { ok: false, reason: 'unbuildable', detail: `room code "${input.roomCode}" is not announceable` };
+
+ // A per-instance guard, not an account-wide preflight.
+ //
+ // `live` only knows about writes this publisher made. It cannot see another
+ // tab, another Dotify instance, or any other use of the same allowance
+ // account, so the real account-wide total can be higher than this and a
+ // write can still be rejected by the chain. Keeping the check is still
+ // worthwhile - it catches the one case this instance can cause - but the
+ // authoritative answer is the network's, which is why `rejected` below is
+ // reported separately rather than folded into this branch.
+ const next = new Map(live);
+ next.set(beacon.room, beacon);
+ const budget = assertBeaconBudget([...next.values()]);
+ if (!budget.ok) {
+ return { ok: false, reason: 'quota-local', detail: budget.reason };
+ }
+
+ try {
+ const result = await client.publish(beacon, {
+ channel: roomBeaconChannel(beacon.room),
+ topic2: roomBeaconTopic(beacon.room),
+ ttlSeconds: BEACON_TTL_SECONDS
+ });
+ if (!result.ok) {
+ // Most likely the account-wide quota this instance cannot observe.
+ return {
+ ok: false,
+ reason: 'rejected',
+ detail: `the statement store rejected the beacon for ${beacon.room}, commonly an account-wide quota this client cannot see`
+ };
+ }
+ live.set(beacon.room, beacon);
+ return { ok: true };
+ } catch (error) {
+ return { ok: false, reason: 'transport', detail: error instanceof Error ? error.message : String(error) };
+ }
+ },
+
+ stop() {
+ stopped = true;
+ live.clear();
+ try {
+ client.destroy();
+ } catch {
+ // Already gone; nothing to release.
+ }
+ }
+ };
+}
+
+export type RoomBeaconListener = {
+ beacons: () => RoomBeacon[];
+ stop: () => void;
+};
+
+/**
+ * How often expired beacons are swept.
+ *
+ * A beacon whose host stopped simply goes quiet; nothing tells the subscriber.
+ * Sweeping is therefore the only way a discovery list stops showing dead rooms.
+ */
+export const BEACON_SWEEP_MS = 15_000;
+
+/**
+ * Fallback lifetime for a beacon that arrives with no usable expiry.
+ *
+ * Without this a transport that omits the field would reintroduce the
+ * never-evicted behaviour. Slightly longer than the publish TTL so a live room
+ * is not swept between refreshes.
+ */
+const ASSUMED_LIFETIME_MS = (BEACON_TTL_SECONDS + 30) * 1000;
+
+/**
+ * Subscribe to beacons, optionally for a single room.
+ *
+ * Everything received is untrusted and re-parsed; a malformed beacon is dropped
+ * rather than allowed to break the list.
+ */
+export async function subscribeRoomBeacons(
+ options: { roomCode?: string; onChange?: (beacons: RoomBeacon[]) => void } = {},
+ deps: BeaconDeps = defaultDeps
+): Promise {
+ if (!BEACONS_ENABLED) return null;
+
+ let client: BeaconClient;
+ try {
+ client = await deps.createClient();
+ } catch {
+ return null;
+ }
+
+ const seen = new Map();
+ const now = () => (deps.now ?? Date.now)();
+
+ function live(): RoomBeacon[] {
+ const current = now();
+ return [...seen.values()].filter(entry => entry.expiresAtMs > current).map(entry => entry.beacon);
+ }
+
+ /** Drop expired entries, and report only when the visible set actually changed. */
+ function sweep(): void {
+ const current = now();
+ let removed = false;
+ for (const [room, entry] of seen) {
+ if (entry.expiresAtMs <= current) {
+ seen.delete(room);
+ removed = true;
+ }
+ }
+ if (removed) options.onChange?.(live());
+ }
+
+ const subscription = client.subscribe(
+ statement => {
+ const beacon = parseRoomBeacon(statement.data);
+ if (!beacon) return;
+ if (options.roomCode && beacon.room !== options.roomCode.toUpperCase()) return;
+
+ const expirySeconds = beaconExpirySeconds(statement.expiry);
+ const expiresAtMs = expirySeconds !== null ? expirySeconds * 1000 : now() + ASSUMED_LIFETIME_MS;
+
+ // An already-expired statement is ignored rather than shown then swept.
+ if (expiresAtMs <= now()) return;
+
+ seen.set(beacon.room, { beacon, expiresAtMs });
+ options.onChange?.(live());
+ },
+ options.roomCode ? { topic2: roomBeaconTopic(options.roomCode) } : undefined
+ );
+
+ const sweeper = setInterval(sweep, BEACON_SWEEP_MS);
+
+ return {
+ beacons: live,
+ stop() {
+ clearInterval(sweeper);
+ try {
+ subscription.unsubscribe();
+ client.destroy();
+ } catch {
+ // Already gone.
+ }
+ }
+ };
+}
+
+export type BeaconLoopDeps = {
+ createPublisher?: () => Promise;
+ setInterval?: (handler: () => void, ms: number) => ReturnType;
+ clearInterval?: (handle: ReturnType) => void;
+ onOutcome?: (outcome: AnnounceOutcome) => void;
+};
+
+/**
+ * Announce a room until stopped, refreshing before the statement expires.
+ *
+ * Extracted from the React hook so the lifecycle - start, refresh, cancellation,
+ * and the race where a caller stops before the publisher finishes connecting -
+ * is testable as plain logic, without a DOM or a renderer.
+ *
+ * `readInput` is called per announce rather than captured once, so a changing
+ * listener count reaches the next beacon without restarting the connection.
+ */
+export function startRoomBeaconLoop(readInput: () => RoomBeaconInput | null, deps: BeaconLoopDeps = {}): () => void {
+ const create = deps.createPublisher ?? (() => createRoomBeaconPublisher());
+ const schedule = deps.setInterval ?? ((handler, ms) => setInterval(handler, ms));
+ const unschedule = deps.clearInterval ?? (handle => clearInterval(handle));
+
+ let cancelled = false;
+ let publisher: RoomBeaconPublisher | null = null;
+ let timer: ReturnType | undefined;
+
+ async function announce(): Promise {
+ const input = readInput();
+ if (!publisher || cancelled || !input) return;
+ const outcome = await publisher.announce(input);
+ if (!outcome.ok) deps.onOutcome?.(outcome);
+ }
+
+ void (async () => {
+ const created = await create();
+ // A caller that stopped while we were connecting must not leave a live
+ // publisher behind; this is the window where a room closes during connect.
+ if (cancelled) {
+ created?.stop();
+ return;
+ }
+ publisher = created;
+ if (!publisher) return;
+
+ await announce();
+ if (cancelled) return;
+ timer = schedule(() => void announce(), BEACON_REFRESH_MS);
+ })();
+
+ return () => {
+ cancelled = true;
+ if (timer !== undefined) unschedule(timer);
+ publisher?.stop();
+ publisher = null;
+ };
+}
diff --git a/web/src/features/rooms/roomState.test.ts b/web/src/features/rooms/roomState.test.ts
index ddae478..71d2735 100644
--- a/web/src/features/rooms/roomState.test.ts
+++ b/web/src/features/rooms/roomState.test.ts
@@ -29,6 +29,10 @@ describe('buildSessionLink', () => {
expect(buildSessionLink('NEW1', 'https://dotify.example/#/rooms/OLD9')).toBe('https://dotify.example/#/rooms/NEW1');
});
+ it('builds a public Product DevNet link instead of leaking a container URL', () => {
+ expect(buildSessionLink('LIVE42', 'https://dotify-test01.dev-dot.li')).toBe('https://dotify-test01.dev-dot.li/#/rooms/LIVE42');
+ });
+
it('returns empty string without a room id or href', () => {
expect(buildSessionLink('', 'https://dotify.example/')).toBe('');
expect(buildSessionLink('AB12CD', '')).toBe('');
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/features/runtime/productCdmContracts.test.ts b/web/src/features/runtime/productCdmContracts.test.ts
new file mode 100644
index 0000000..3bb1a41
--- /dev/null
+++ b/web/src/features/runtime/productCdmContracts.test.ts
@@ -0,0 +1,91 @@
+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: 'devnet' }, 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: '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: 'devnet' }, 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: 'devnet' }, 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: 'devnet' }, 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..fc513da
--- /dev/null
+++ b/web/src/features/runtime/productCdmContracts.ts
@@ -0,0 +1,210 @@
+// 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
+};
+
+/**
+ * The Product chain environment Dotify targets.
+ *
+ * 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 = '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> = {
+ 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 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.`
+ );
+ }
+ }
+
+ 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.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..0ea0956
--- /dev/null
+++ b/web/src/features/runtime/productCdmRuntimeAdapter.ts
@@ -0,0 +1,362 @@
+import { zeroAddress, type Address, type Hash } from 'viem';
+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';
+
+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'));
+ const trackTotal = assertBoundedCount(trackCount, MAX_RUNTIME_TRACKS, `Runtime ${runtimeAddress}`);
+
+ return Promise.all(
+ 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: splitTotal }, 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
+ ]);
+ },
+
+ // 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 }]);
+ },
+
+ 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]);
+ },
+
+ // 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;
+ }
+ };
+}
+
+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);
+ }
+}
diff --git a/web/src/features/runtime/runtimeAdapterConfig.test.ts b/web/src/features/runtime/runtimeAdapterConfig.test.ts
new file mode 100644
index 0000000..152a593
--- /dev/null
+++ b/web/src/features/runtime/runtimeAdapterConfig.test.ts
@@ -0,0 +1,31 @@
+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: 'devnet' });
+ });
+
+ 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', () => {
+ // 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('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
new file mode 100644
index 0000000..7409064
--- /dev/null
+++ b/web/src/features/runtime/runtimeAdapterConfig.ts
@@ -0,0 +1,47 @@
+// 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: 'devnet';
+};
+
+type EnvironmentLike = Record;
+
+const PRODUCT_ENVIRONMENTS = ['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');
+ // 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'])
+ : 'devnet';
+
+ return { kind, productEnvironment };
+}
diff --git a/web/src/features/runtime/runtimePorts.ts b/web/src/features/runtime/runtimePorts.ts
new file mode 100644
index 0000000..5fd7622
--- /dev/null
+++ b/web/src/features/runtime/runtimePorts.ts
@@ -0,0 +1,90 @@
+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;
+};
+
+/**
+ * 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;
+ 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/runtimeReaderProvider.test.ts b/web/src/features/runtime/runtimeReaderProvider.test.ts
new file mode 100644
index 0000000..8d4612f
--- /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: 'devnet' } });
+
+ 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: 'devnet' }
+ });
+
+ 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: 'devnet' }
+ });
+
+ 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/features/runtime/viemRuntimeAdapter.test.ts b/web/src/features/runtime/viemRuntimeAdapter.test.ts
new file mode 100644
index 0000000..e05ef4c
--- /dev/null
+++ b/web/src/features/runtime/viemRuntimeAdapter.test.ts
@@ -0,0 +1,200 @@
+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('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 () => [
+ {
+ 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..a3dc284
--- /dev/null
+++ b/web/src/features/runtime/viemRuntimeAdapter.ts
@@ -0,0 +1,316 @@
+import { parseAbiItem, zeroAddress, type Address, type Hash } from 'viem';
+import {
+ artistDirectoryAbi,
+ artistRuntimeFactoryAbi,
+ getPublicClient,
+ getWalletClient,
+ musicAccessAbi,
+ musicRegistryAbi,
+ musicRoyaltiesAbi
+} from '../../shared/config/contracts';
+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';
+
+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;
+
+ const trackTotal = assertBoundedCount(trackCount, MAX_RUNTIME_TRACKS, `Runtime ${runtimeAddress}`);
+
+ return Promise.all(
+ Array.from({ length: trackTotal }, 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 splitTotal = assertBoundedCount(splitCount, MAX_ROYALTY_SPLITS, `Track ${hash} royalty splits`);
+
+ const royaltySplits = (
+ await Promise.all(
+ Array.from({ length: splitTotal }, 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/generated/contracts/cdm-metadata/cids.json b/web/src/generated/contracts/cdm-metadata/cids.json
new file mode 100644
index 0000000..6582c6e
--- /dev/null
+++ b/web/src/generated/contracts/cdm-metadata/cids.json
@@ -0,0 +1,12 @@
+{
+ "@dotify/artist-directory": {
+ "file": "dotify-artist-directory.json",
+ "cid": "bafk2bzacebiynqo7tjvxa3xlvf6pphgq3nzatqjbn3yo2fszrlvxibtsr4ce6",
+ "bytes": 4430
+ },
+ "@dotify/artist-runtime-factory": {
+ "file": "dotify-artist-runtime-factory.json",
+ "cid": "bafk2bzaceckm27ft3kvt4mjs67hzklylws5fp5d4z4nrvpa34gmzndtqz7lwg",
+ "bytes": 7585
+ }
+}
diff --git a/web/src/generated/contracts/cdm-metadata/dotify-artist-directory.json b/web/src/generated/contracts/cdm-metadata/dotify-artist-directory.json
new file mode 100644
index 0000000..57a0da3
--- /dev/null
+++ b/web/src/generated/contracts/cdm-metadata/dotify-artist-directory.json
@@ -0,0 +1,193 @@
+{
+ "name": "@dotify/artist-directory",
+ "description": "Registry mapping each artist address to their owned SmartRuntime. Entry point for enumerating the Dotify catalog.",
+ "readme": "# @dotify/artist-directory\n\nMaps an artist address to the address of the SmartRuntime they own, and enumerates\nevery registered artist.\n\nStart here to read the Dotify catalog: `artistCount()` and `artistsPage(offset, limit)`\nenumerate artists with their runtimes, and `runtimeOf(artist)` resolves one directly.\nEach runtime then exposes its own tracks and access policy.\n\nRegistration is performed by the artist runtime factory, not by callers.",
+ "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"
+ }
+ ],
+ "homepage": "https://muzinga.netlify.app",
+ "repository": "https://github.com/knzeng-e/dotify",
+ "license": "MIT",
+ "authors": [
+ "Dotify"
+ ],
+ "keywords": [
+ "music",
+ "dotify",
+ "artist-runtime",
+ "access-control"
+ ]
+}
diff --git a/web/src/generated/contracts/cdm-metadata/dotify-artist-runtime-factory.json b/web/src/generated/contracts/cdm-metadata/dotify-artist-runtime-factory.json
new file mode 100644
index 0000000..4e6e1a7
--- /dev/null
+++ b/web/src/generated/contracts/cdm-metadata/dotify-artist-runtime-factory.json
@@ -0,0 +1,333 @@
+{
+ "name": "@dotify/artist-runtime-factory",
+ "description": "Deploys one artist-owned SmartRuntime per artist and registers it in the artist directory.",
+ "readme": "# @dotify/artist-runtime-factory\n\nDeploys a SmartRuntime for an artist and registers it in `@dotify/artist-directory`.\n\nA runtime is a diamond: music registry, royalties, access, and NFT pallets are\ninstalled as facets, and the artist is set as its owner. Because each artist owns\ntheir own runtime, catalog, access policy, and royalty splits stay under the\nartist's control rather than the platform's.\n\nCreation is staged - `createRuntime()` then `installRuntimeStep()` until\n`pendingRuntimeStageOf(artist)` reports completion - so that installation fits\nwithin block limits.",
+ "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"
+ }
+ ],
+ "homepage": "https://muzinga.netlify.app",
+ "repository": "https://github.com/knzeng-e/dotify",
+ "license": "MIT",
+ "authors": [
+ "Dotify"
+ ],
+ "keywords": [
+ "music",
+ "dotify",
+ "artist-runtime",
+ "access-control"
+ ]
+}
diff --git a/web/src/generated/contracts/cdm.d.ts b/web/src/generated/contracts/cdm.d.ts
new file mode 100644
index 0000000..0b157fa
--- /dev/null
+++ b/web/src/generated/contracts/cdm.d.ts
@@ -0,0 +1,73 @@
+// 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 };
+ musicAccPersonhoodInfo: { args: [account: HexString]; response: { status: number; contextAlias: SizedHex<32>; live: boolean } };
+ musicAccPersonhoodLevel: { args: [account: HexString]; response: number };
+ musicAccSetPersonhoodLevel: { args: [arg0: HexString, arg1: 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/musicAccess.ts b/web/src/generated/contracts/musicAccess.ts
index 62a3dcb..0a73bcf 100644
--- a/web/src/generated/contracts/musicAccess.ts
+++ b/web/src/generated/contracts/musicAccess.ts
@@ -125,6 +125,35 @@ export const musicAccessAbi = [
"stateMutability": "view",
"type": "function"
},
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "account",
+ "type": "address"
+ }
+ ],
+ "name": "musicAccPersonhoodInfo",
+ "outputs": [
+ {
+ "internalType": "uint8",
+ "name": "status",
+ "type": "uint8"
+ },
+ {
+ "internalType": "bytes32",
+ "name": "contextAlias",
+ "type": "bytes32"
+ },
+ {
+ "internalType": "bool",
+ "name": "live",
+ "type": "bool"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
{
"inputs": [
{
@@ -148,18 +177,18 @@ export const musicAccessAbi = [
"inputs": [
{
"internalType": "address",
- "name": "account",
+ "name": "",
"type": "address"
},
{
"internalType": "enum LibMusicRegistry.PersonhoodLevel",
- "name": "level",
+ "name": "",
"type": "uint8"
}
],
"name": "musicAccSetPersonhoodLevel",
"outputs": [],
- "stateMutability": "nonpayable",
+ "stateMutability": "pure",
"type": "function"
},
{
diff --git a/web/src/generated/contracts/smartRuntime.ts b/web/src/generated/contracts/smartRuntime.ts
new file mode 100644
index 0000000..7d5f1fc
--- /dev/null
+++ b/web/src/generated/contracts/smartRuntime.ts
@@ -0,0 +1,1123 @@
+// 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": "musicAccPersonhoodInfo",
+ "outputs": [
+ {
+ "internalType": "uint8",
+ "name": "status",
+ "type": "uint8"
+ },
+ {
+ "internalType": "bytes32",
+ "name": "contextAlias",
+ "type": "bytes32"
+ },
+ {
+ "internalType": "bool",
+ "name": "live",
+ "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": "",
+ "type": "address"
+ },
+ {
+ "internalType": "enum LibMusicRegistry.PersonhoodLevel",
+ "name": "",
+ "type": "uint8"
+ }
+ ],
+ "name": "musicAccSetPersonhoodLevel",
+ "outputs": [],
+ "stateMutability": "pure",
+ "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;
diff --git a/web/src/hooks/useArtistConsole.ts b/web/src/hooks/useArtistConsole.ts
index 554e9a0..228b6db 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,10 @@ import {
import { chainMismatchMessage } from '../features/wallet/network';
import { localAudioRef, priceDotForAccessMode, runtimeAddressFromTrackId } from '../features/catalog/trackModel';
import { encodeAccessMode, encodeRequiredPersonhood, manifestRequiredPersonhood } from '../features/runtime/accessEncoding';
+import { createViemRuntimeWriter } from '../features/runtime/viemRuntimeAdapter';
+import { createRuntimeReader } from '../features/runtime/runtimeReaderProvider';
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 +35,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 +233,7 @@ export function useArtistConsole(deps: UseArtistConsoleDeps) {
coverUploadRef
} = deps;
+ const runtimeReader = createRuntimeReader({ ethRpcUrl });
const [artistRuntimeAddress, setArtistRuntimeAddress] = useState<`0x${string}` | null>(null);
const [artistRegistrationStatus, setArtistRegistrationStatus] = useState('Checking artist registration');
const [isRegisteringArtist, setIsRegisteringArtist] = useState(false);
@@ -267,6 +260,9 @@ export function useArtistConsole(deps: UseArtistConsoleDeps) {
if (!connectedWallet) {
throw new Error('Connect a wallet before signing this transaction.');
}
+ if (!connectedWallet.createEvmClient) {
+ throw new Error('Artist publishing 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));
@@ -311,21 +307,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;
@@ -417,7 +408,7 @@ export function useArtistConsole(deps: UseArtistConsoleDeps) {
return;
}
- const factoryExists = await ensureContract(factoryAddress!, ethRpcUrl);
+ const factoryExists = await runtimeReader.ensureContract(factoryAddress!);
if (!factoryExists) {
setTransactionFeedback({
tone: 'error',
@@ -428,19 +419,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',
@@ -449,11 +435,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',
@@ -463,17 +445,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',
@@ -483,22 +458,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];
@@ -510,11 +473,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',
@@ -524,15 +483,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();
@@ -576,42 +530,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
@@ -859,7 +791,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.' });
@@ -877,6 +809,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}`;
@@ -887,27 +820,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');
@@ -917,7 +843,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',
@@ -970,24 +896,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',
@@ -1026,26 +948,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 fc73475..5b35258 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,9 @@ 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 { 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 {
E2E_CLASSIC_AUDIO_URL,
@@ -49,12 +52,10 @@ import type {
AccessGate,
AccessMode,
CatalogTrack,
- OnchainTrackRecord,
PersonhoodLevel,
PlayerState,
RegistryCatalogTrack,
RoomPlaybackMode,
- RoyaltySplit,
TrackInfo,
TransactionFeedback
} from '../shared/types';
@@ -272,6 +273,7 @@ export function useCatalog(deps: UseCatalogDeps) {
setDescription
} = deps;
+ const runtimeReader = createRuntimeReader({ ethRpcUrl });
const usesCatalogApi = isCatalogApiConfigured() && !isClassicUnlockE2e && !isArtistPublishE2e && !isRoomJoinE2e;
const [initialCatalog] = useState(() => {
const cached = usesCatalogApi ? readCachedCatalog() : null;
@@ -379,12 +381,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 +402,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;
}
@@ -481,15 +473,15 @@ export function useCatalog(deps: UseCatalogDeps) {
const cacheKey = contentHash.toLowerCase();
const cached = contentKeysRef.current.get(cacheKey);
if (cached) return cached;
- if (!isKeyServiceConfigured() || !connectedWallet) 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;
@@ -977,7 +969,7 @@ export function useCatalog(deps: UseCatalogDeps) {
}
async function payForTrackAccess(track: CatalogTrack) {
- if (!connectedWallet) {
+ if (!connectedWallet?.createEvmClient) {
setAccessGate(buildAccessGateInfo(track));
setShowWalletModal(true);
return;
@@ -1008,7 +1000,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 +1013,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 +1033,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 });
- }
- }
-
- 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;
- }
- })
- );
+ 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 {
- 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 +1141,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 +1150,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 +1160,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 +1250,6 @@ export function useCatalog(deps: UseCatalogDeps) {
payForTrackAccess,
fetchAndDecryptAudio,
refreshCatalogFromRegistry,
- fetchDirectoryEntries,
fetchRuntimeCatalog,
clearObjectUrls
};
diff --git a/web/src/hooks/useRoomBeacon.ts b/web/src/hooks/useRoomBeacon.ts
new file mode 100644
index 0000000..3aa9d40
--- /dev/null
+++ b/web/src/hooks/useRoomBeacon.ts
@@ -0,0 +1,74 @@
+// Announce a hosted room on the Statement Store, for as long as it is hosted.
+//
+// Kept out of useSession on purpose: the room lifecycle is already the most
+// intricate part of the app, and discovery is an optional enhancement that must
+// never be able to interrupt it.
+//
+// This is a thin adapter. The lifecycle lives in `startRoomBeaconLoop` so it can
+// be tested as plain logic rather than through a renderer.
+
+import { useEffect, useRef } from 'react';
+
+export type UseRoomBeaconInput = {
+ /** True only while this client is hosting the room. */
+ isHosting: boolean;
+ roomCode: string;
+ hostName: string;
+ listenerCount: number;
+ /** Present only when the host opted into announcing what is playing. */
+ nowPlaying?: { title: string; artist: string } | null;
+};
+
+// Build-time constant, so a build that did not opt in lets Rollup prove the
+// dynamic import below is unreachable and drop the statement-store chunks.
+const BEACONS_ENABLED = import.meta.env.VITE_DOTIFY_ROOM_BEACONS === 'on';
+
+export function useRoomBeacon(input: UseRoomBeaconInput): void {
+ const { isHosting, roomCode } = input;
+
+ // Written in an effect rather than during render: a ref mutated while
+ // rendering can be torn between a discarded render and the committed one.
+ const latest = useRef(input);
+ useEffect(() => {
+ latest.current = input;
+ });
+
+ useEffect(() => {
+ if (!BEACONS_ENABLED || !isHosting || !roomCode) return;
+
+ let stop: (() => void) | undefined;
+ let cancelled = false;
+
+ void (async () => {
+ const { startRoomBeaconLoop } = await import('../features/rooms/roomBeaconPublisher');
+ if (cancelled) return;
+ stop = startRoomBeaconLoop(
+ () => {
+ const current = latest.current;
+ if (!current.isHosting || !current.roomCode) return null;
+ return {
+ roomCode: current.roomCode,
+ hostName: current.hostName,
+ listenerCount: current.listenerCount,
+ nowPlaying: current.nowPlaying ?? null
+ };
+ },
+ {
+ // Additive discovery: a refusal must not surface to the listener or
+ // interrupt hosting, but it must not vanish either - otherwise an
+ // operator debugging "my room is not discoverable" has nothing to read.
+ onOutcome: outcome => {
+ if (!outcome.ok) console.warn(`[dotify] room beacon not published (${outcome.reason}): ${outcome.detail}`);
+ }
+ }
+ );
+ })();
+
+ return () => {
+ cancelled = true;
+ stop?.();
+ };
+ // Restart only when the identity of the hosted room changes. Listener count
+ // and now-playing ride the ref, so a busy room does not thrash the connection.
+ }, [isHosting, roomCode]);
+}
diff --git a/web/src/hooks/useSession.ts b/web/src/hooks/useSession.ts
index 3e5de8e..bef1865 100644
--- a/web/src/hooks/useSession.ts
+++ b/web/src/hooks/useSession.ts
@@ -13,6 +13,8 @@ import {
roomJoinE2eIceServers
} from '../e2e/roomJoinMock';
import { buildSessionLink, getInitialRoomCode } from '../features/rooms/roomState';
+import { diagnoseSignalFailure } from '../features/rooms/signalDiagnostics';
+import { useRoomBeacon } from './useRoomBeacon';
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';
@@ -89,6 +91,9 @@ function shouldMaterializeRemoteSource(source: string) {
export type UseSessionDeps = {
signalUrl: string;
+ // Canonical externally reachable app URL. Product-host builds use this
+ // instead of sharing an internal container/gateway location.
+ publicAppUrl?: string | null;
// Optional local identity key used only to remember a display name on this
// browser. It never crosses the anonymous room signaling boundary.
identityAddress?: string | null;
@@ -104,8 +109,18 @@ export type UseSessionDeps = {
};
export function useSession(deps: UseSessionDeps) {
- const { signalUrl, identityAddress, setTrackInfo, setPlayerState, localAudioRef, objectUrlsRef, resolvedAudioSourcesRef, navigateToView, setAudioSource } =
- deps;
+ const {
+ signalUrl,
+ publicAppUrl,
+ identityAddress,
+ setTrackInfo,
+ setPlayerState,
+ localAudioRef,
+ objectUrlsRef,
+ resolvedAudioSourcesRef,
+ navigateToView,
+ setAudioSource
+ } = deps;
const [roomId, setRoomId] = useState('');
const [hostName, setHostName] = useState('');
@@ -293,6 +308,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');
@@ -1018,8 +1040,18 @@ export function useSession(deps: UseSessionDeps) {
return () => clearInterval(timer);
}, [mode, roomId]);
+ // Announce the room on the Statement Store so it can be discovered without
+ // Dotify's signaling server. Additive only: the share link remains the way in,
+ // and this is inert unless the build opted in and a Product host is present.
+ useRoomBeacon({
+ isHosting: mode === 'host' && Boolean(roomId),
+ roomCode: roomId,
+ hostName: displayName,
+ listenerCount
+ });
+
async function copySessionLink() {
- const link = buildSessionLink(roomId);
+ const link = buildSessionLink(roomId, publicAppUrl || window.location.href);
if (!link) return;
try {
await navigator.clipboard.writeText(link);
@@ -1166,6 +1198,6 @@ export function useSession(deps: UseSessionDeps) {
removeRoomRequest,
clearRoomRequests,
destroySession,
- sessionLink: buildSessionLink(roomId)
+ sessionLink: buildSessionLink(roomId, publicAppUrl || window.location.href)
};
}
diff --git a/web/src/hooks/useWallet.ts b/web/src/hooks/useWallet.ts
index 8a86551..54dc9a3 100644
--- a/web/src/hooks/useWallet.ts
+++ b/web/src/hooks/useWallet.ts
@@ -25,8 +25,10 @@ import {
isArtistPublishE2eScenarioRequested,
shouldAutoConnectArtistPublishE2eWallet
} from '../e2e/artistPublishMock';
+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 ────────────────────────────────────────────────────────────────
@@ -39,7 +41,7 @@ const CONNECT_TIMEOUT_MS = 42_000;
// ── Public types ─────────────────────────────────────────────────────────────
-export type WalletMethod = 'passkey' | 'extension';
+export type WalletMethod = 'passkey' | 'extension' | 'product-host';
export type ConnectedWallet = {
method: WalletMethod;
@@ -52,8 +54,10 @@ 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;
+ createEvmClient?: (chain: Chain, rpcUrl: string) => WalletClient;
};
export type WalletState =
@@ -63,6 +67,8 @@ export type WalletState =
| { status: 'connected'; wallet: ConnectedWallet }
| { status: 'error'; message: string };
+const productHostConfig = resolveProductHostConfig(import.meta.env);
+
// ── Internal: key derivation ─────────────────────────────────────────────────
/** Build a ConnectedWallet from a KeyManager (passkey path). */
@@ -268,6 +274,7 @@ export function useWallet() {
if (isClassicUnlockE2e) return { status: 'connected', wallet: createClassicUnlockE2eWallet() };
return { status: 'disconnected' };
});
+ const [productHostStatus, setProductHostStatus] = useState(() => (productHostConfig.mode === 'off' ? 'off' : 'checking'));
const connectPasskey = useCallback(async () => {
if (isArtistPublishE2e) {
@@ -307,6 +314,39 @@ export function useWallet() {
}
}, []);
+ const connectProductHost = useCallback(async () => {
+ setState({ status: 'connecting', via: 'product-host' });
+ try {
+ const identity = await withTimeout(
+ connectProductHostIdentity(productHostConfig),
+ 'The Polkadot Product host did not answer in time. Reopen Dotify from the Product host and try again.'
+ );
+ setProductHostStatus('available');
+ localStorage.removeItem(LAST_METHOD_KEY);
+ setState({
+ status: 'connected',
+ wallet: {
+ method: 'product-host',
+ label: 'Polkadot app',
+ substrateAddress: identity.substrateAddress,
+ evmAddress: identity.evmAddress,
+ keyRequestSigner: {
+ signatureScheme: PRODUCT_SR25519_SIGNATURE_SCHEME,
+ address: identity.evmAddress,
+ productPublicKey: identity.productPublicKey,
+ signMessage: identity.signMessage
+ }
+ }
+ });
+ } catch (error) {
+ setProductHostStatus('unavailable');
+ setState({
+ status: 'error',
+ message: error instanceof Error ? error.message : 'The Polkadot Product account could not be connected.'
+ });
+ }
+ }, []);
+
const switchExtensionNetwork = useCallback(async (chain: Chain) => {
if (isArtistPublishE2e) {
const wallet = { ...createArtistPublishE2eWallet(), chainId: chain.id };
@@ -374,6 +414,16 @@ export function useWallet() {
};
}, []);
+ useEffect(() => {
+ let cancelled = false;
+ void probeProductHost(productHostConfig.mode).then(status => {
+ if (!cancelled) setProductHostStatus(status);
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
useEffect(() => {
if (isClassicUnlockE2e || isArtistPublishE2e) return;
const ethereum = getEthereumProvider();
@@ -437,5 +487,17 @@ export function useWallet() {
setState(current => (current.status === 'needs-reconnect' && current.via === 'passkey' ? { status: 'disconnected' } : current));
}, []);
- return { state, connectPasskey, connectExtension, switchExtensionNetwork, disconnect, hasPrfSupport, hasStoredPasskey, forgetPasskey };
+ return {
+ state,
+ connectPasskey,
+ connectExtension,
+ connectProductHost,
+ switchExtensionNetwork,
+ disconnect,
+ hasPrfSupport,
+ hasStoredPasskey,
+ forgetPasskey,
+ productHostMode: productHostConfig.mode,
+ productHostStatus
+ };
}
diff --git a/web/src/services/gatewayRace.test.ts b/web/src/services/gatewayRace.test.ts
new file mode 100644
index 0000000..22e9960
--- /dev/null
+++ b/web/src/services/gatewayRace.test.ts
@@ -0,0 +1,171 @@
+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('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'));
+
+ 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..cf1e050
--- /dev/null
+++ b/web/src/services/gatewayRace.ts
@@ -0,0 +1,186 @@
+// 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) {
+ 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;
+ }
+
+ 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/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 {
- 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}` });
}
// ---------------------------------------------------------------------------
diff --git a/web/src/shared/config/deploymentSafety.test.ts b/web/src/shared/config/deploymentSafety.test.ts
index 8ff46d8..aec4c08 100644
--- a/web/src/shared/config/deploymentSafety.test.ts
+++ b/web/src/shared/config/deploymentSafety.test.ts
@@ -191,6 +191,108 @@ describe('validateProductionEnvironment', () => {
});
});
+ it('requires a canonical public URL and .dot product id for Product-host builds', () => {
+ expect(
+ validateProductionEnvironment({
+ ...validProductionEnv,
+ VITE_DOTIFY_HOST_MODE: 'required',
+ VITE_DOTIFY_PRODUCT_ID: 'Dotify'
+ }).errors
+ ).toEqual([
+ 'VITE_DOTIFY_PRODUCT_ID must be a lowercase .dot name when Product host integration is enabled.',
+ 'VITE_PUBLIC_APP_URL is required when VITE_DOTIFY_DEPLOYMENT=production.'
+ ]);
+ });
+
+ it('accepts the Product DevNet public identity boundary', () => {
+ expect(
+ validateProductionEnvironment({
+ ...validProductionEnv,
+ 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('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: 'paseo'
+ }).errors
+ ).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', () => {
+ expect(
+ validateProductionEnvironment({
+ ...validProductionEnv,
+ VITE_DOTIFY_RUNTIME_ADAPTER: 'product-cdm',
+ 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'
+ })
+ ).toEqual({ mode: 'production', errors: [], warnings: [] });
+ });
+
+ it('rejects room beacons without a Product host, since they publish only inside it', () => {
+ expect(
+ validateProductionEnvironment({
+ ...validProductionEnv,
+ VITE_DOTIFY_ROOM_BEACONS: 'on'
+ }).errors
+ ).toEqual(['VITE_DOTIFY_ROOM_BEACONS=on requires VITE_DOTIFY_HOST_MODE to be auto or required: room beacons publish only inside the Product host.']);
+ });
+
+ it('rejects an unknown room beacon value', () => {
+ expect(
+ validateProductionEnvironment({
+ ...validProductionEnv,
+ VITE_DOTIFY_ROOM_BEACONS: 'yes'
+ }).errors
+ ).toEqual(['VITE_DOTIFY_ROOM_BEACONS must be on or off.']);
+ });
+
+ it('accepts room beacons alongside an enabled Product host', () => {
+ expect(
+ validateProductionEnvironment({
+ ...validProductionEnv,
+ VITE_DOTIFY_ROOM_BEACONS: 'on',
+ 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 996ebf2..f19a098 100644
--- a/web/src/shared/config/deploymentSafety.ts
+++ b/web/src/shared/config/deploymentSafety.ts
@@ -259,6 +259,49 @@ 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 roomBeacons = readEnvironmentValue(env, 'VITE_DOTIFY_ROOM_BEACONS').toLowerCase() || 'off';
+ if (!['on', 'off'].includes(roomBeacons)) {
+ errors.push('VITE_DOTIFY_ROOM_BEACONS must be on or off.');
+ }
+ if (roomBeacons === 'on' && (readEnvironmentValue(env, 'VITE_DOTIFY_HOST_MODE').toLowerCase() || 'off') === 'off') {
+ // The statement store client runs only inside the Product host container, so
+ // enabling beacons without it would ship chain code that can never connect.
+ errors.push('VITE_DOTIFY_ROOM_BEACONS=on requires VITE_DOTIFY_HOST_MODE to be auto or required: room beacons publish only inside the Product host.');
+ }
+
+ 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() || '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
+ // 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.');
+ }
+ if (productHostMode !== 'off') {
+ const productId = readEnvironmentValue(env, 'VITE_DOTIFY_PRODUCT_ID');
+ if (!/^[a-z0-9][a-z0-9-]*\.dot$/.test(productId)) {
+ errors.push('VITE_DOTIFY_PRODUCT_ID must be a lowercase .dot name when Product host integration is enabled.');
+ }
+ validateUrl(env, 'VITE_PUBLIC_APP_URL', { required: true, protocols: ['https:'], errors });
+ } else {
+ validateUrl(env, 'VITE_PUBLIC_APP_URL', { protocols: ['https:'], errors });
+ }
+
validateUrl(env, 'VITE_SIGNAL_URL', { required: true, protocols: ['https:', 'wss:'], errors });
validateUrl(env, 'VITE_DOTIFY_API_URL', { required: true, protocols: ['https:'], errors });
validateUrl(env, 'VITE_PINATA_GATEWAY', { required: true, protocols: ['https:'], errors });
diff --git a/web/vite.product.config.ts b/web/vite.product.config.ts
new file mode 100644
index 0000000..0de357a
--- /dev/null
+++ b/web/vite.product.config.ts
@@ -0,0 +1,31 @@
+import { defineConfig, loadEnv } from 'vite';
+import react from '@vitejs/plugin-react';
+import path from 'node:path';
+
+import { assertProductionEnvironment } from './src/shared/config/deploymentSafety';
+
+// Product DevNet keeps Vite's normal multi-file output so `pad` can upload
+// changed chunks incrementally on later releases.
+export default defineConfig(({ command, mode }) => {
+ const env = { ...loadEnv(mode, process.cwd(), ''), ...process.env };
+ if (command === 'build') assertProductionEnvironment(env);
+
+ return {
+ base: './',
+ plugins: [react()],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src')
+ }
+ },
+ build: {
+ outDir: 'dist-product',
+ target: 'esnext'
+ },
+ optimizeDeps: {
+ esbuildOptions: {
+ target: 'esnext'
+ }
+ }
+ };
+});