From 652a9749522fbd8cea34752ab19e264a150398e0 Mon Sep 17 00:00:00 2001 From: Kevin Nzeng Essimengane Date: Sun, 30 Aug 2026 00:57:11 +0200 Subject: [PATCH] feat: guard product cdm signer mapping --- docs/backlog/24-access-streaming-v2.md | 5 +- docs/backlog/README.md | 9 +-- docs/backlog/improvement-plan.md | 4 +- ...oduct-readiness-and-killer-dapp-roadmap.md | 29 +++++--- .../product-devnet-architecture.md | 26 ++++--- docs/index.html | 5 +- docs/operations/dependency-security-status.md | 24 ++++--- docs/operations/deployment-configuration.md | 20 +++--- docs/operations/product-devnet-deployment.md | 50 ++++++++++---- .../features/runtime/productCdmContracts.ts | 2 +- .../runtime/runtimeWriterProvider.test.ts | 68 +++++++++++++++++-- .../features/runtime/runtimeWriterProvider.ts | 29 +++++++- web/src/hooks/useCatalog.ts | 1 + 13 files changed, 203 insertions(+), 69 deletions(-) diff --git a/docs/backlog/24-access-streaming-v2.md b/docs/backlog/24-access-streaming-v2.md index 33c4e79..4d7f807 100644 --- a/docs/backlog/24-access-streaming-v2.md +++ b/docs/backlog/24-access-streaming-v2.md @@ -139,10 +139,11 @@ 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 adaptation note (updated 2026-08-26): +Product SDK adaptation note (updated 2026-08-30): - Product SDK 0.23.0 and deploy tooling 0.13.1 remain - prototype/reference/unaudited. + prototype/reference/unaudited in Dotify's pinned baseline. npm now publishes + Product SDK 0.25.0, which is deferred to a dedicated compatibility PR. - Root PAPI 3.0.0 is not adopted yet because the current Product SDK graph uses PAPI 2.2.x while `@polkadot-apps` still uses PAPI 1.23.x. - Host APIs are progressive enhancement for Product containers; standalone web diff --git a/docs/backlog/README.md b/docs/backlog/README.md index fae56a2..051e051 100644 --- a/docs/backlog/README.md +++ b/docs/backlog/README.md @@ -115,12 +115,13 @@ decisions. Product-signed key/session verification now exists through explicit Product account connection. Live host evidence remains required before Product identity can be treated as broadly proven across devices. -The Product SDK evidence snapshot used for this replanning is now +The Product SDK evidence snapshot used for this replanning currently pins `@parity/product-sdk` 0.23.0 and -`@polkadot-community-foundation/polkadot-app-deploy` 0.13.1, -last aligned with npm on 2026-08-26. The Product SDK set remains prototype / +`@polkadot-community-foundation/polkadot-app-deploy` 0.13.1. npm was rechecked +on 2026-08-30 and now publishes Product SDK 0.25.0; that migration is deferred +to a dedicated compatibility PR. The Product SDK set remains prototype / reference / unaudited code. Root `polkadot-api` 3.0.0 exists but is not yet -adopted because the current Product SDK packages use PAPI 2.2.x and +adopted because the current pinned Product SDK packages use PAPI 2.2.x and `@polkadot-apps` packages use PAPI 1.23.x, so a single PAPI 3 root breaks Dotify's Bulletin/wallet type seams. Product SDK contracts target `pallet-revive` / PolkaVM CDM flows, not Dotify's diff --git a/docs/backlog/improvement-plan.md b/docs/backlog/improvement-plan.md index bfb7653..2de1734 100644 --- a/docs/backlog/improvement-plan.md +++ b/docs/backlog/improvement-plan.md @@ -68,7 +68,9 @@ sound. Only the baseline rows marked delivered may be presented as live. Product SDK snapshot used for this plan: - `@parity/product-sdk` 0.23.0, host 0.16.0, statement-store 0.6.5, and - descriptors 0.10.0 (aligned with npm on 2026-08-26); + descriptors 0.10.0 (pinned baseline; npm rechecked on 2026-08-30 and now + publishes Product SDK 0.25.0 / host 0.18.0 / statement-store 0.6.7 / + descriptors 0.11.0, deferred to a dedicated compatibility PR); - `@polkadot-community-foundation/polkadot-app-deploy` 0.13.1; - deferred PAPI drift: root `polkadot-api` 3.0.0 is published, but the current Product SDK graph uses PAPI 2.2.x and `@polkadot-apps` uses PAPI 1.23.x, so 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 591b33e..541c2a6 100644 --- a/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md +++ b/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md @@ -3,12 +3,14 @@ Status: active execution note; the Product DevNet baseline is delivered on `dev` and remains tracked through Product compatibility issue #85. -Last Product SDK package check: 2026-08-26. Dotify now pins the current -published Product SDK set: `@parity/product-sdk` 0.23.0, host 0.16.0, -statement-store 0.6.5, descriptors 0.10.0, and -`@polkadot-community-foundation/polkadot-app-deploy` 0.13.1. npm also -publishes `polkadot-api` 3.0.0, but Dotify keeps root PAPI on 1.23.3 because -the current Product SDK packages use PAPI 2.2.x and `@polkadot-apps` +Last Product SDK package check: 2026-08-30. Dotify currently pins +`@parity/product-sdk` 0.23.0, host 0.16.0, statement-store 0.6.5, +descriptors 0.10.0, and +`@polkadot-community-foundation/polkadot-app-deploy` 0.13.1. npm now publishes +Product SDK 0.25.0, host 0.18.0, statement-store 0.6.7, and descriptors 0.11.0; +that upgrade is deferred to a dedicated compatibility PR. npm also publishes +`polkadot-api` 3.0.0, but Dotify keeps root PAPI on 1.23.3 because the current +Product SDK packages use PAPI 2.2.x and `@polkadot-apps` chain-client/keys/signer use PAPI 1.23.x. Root PAPI 3 remains blocked until the official SDK graph converges. @@ -160,8 +162,8 @@ Goal: prove the Product host path with small spikes before committing the app. - 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. -- Delivered: align the Product SDK set with the 2026-08-26 npm latest while - keeping standalone/Product builds green. +- Delivered: align the Product SDK set with the 2026-08-26 npm latest at that + time while keeping standalone/Product builds green. - Remaining: prototype host transaction signing and resource allocation. - Compare Dotify's Hardhat/EVM runtime with Product SDK PolkaVM/CDM contracts. - Delivered on the room-beacon branch: Statement Store presence with strict @@ -205,6 +207,11 @@ Goal: deepen the delivered Product mode one adapter at a time. now has an explicit unsupported `product-cash` rail, so future CASH work can add receipt/bridge settlement without pretending CASH is already executable through `msg.value`. +- Delivered on the next follow-up branch: Product CDM writes now verify both + the selected host signer public key and its derived `pallet-revive` H160 + address against the Product account Dotify connected for key/session requests. + This prevents a Product-host transaction from marking access for a different + runtime account than the one the UI and backend authenticated. - 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, @@ -212,8 +219,10 @@ Goal: deepen the delivered Product mode one adapter at a time. 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. +- Next: real host-signed transaction smoke tests before Product writes can + replace the EVM wallet path. The remaining evidence is native value + forwarding, host approval UX, and post-payment `musicAccCanAccess` reads from + inside the Product container. - Next: revisit root `polkadot-api` 3 after Product SDK and `@polkadot-apps` packages publish on a compatible PAPI major line. - Next: confirm the Product-supported CASH settlement model before exposing any diff --git a/docs/explanation/product-devnet-architecture.md b/docs/explanation/product-devnet-architecture.md index a75edec..9d1522b 100644 --- a/docs/explanation/product-devnet-architecture.md +++ b/docs/explanation/product-devnet-architecture.md @@ -380,12 +380,14 @@ selection as reads: `viem` uses the connected EVM/passkey wallet, and default remains `viem`, 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, including native value forwarding for -Classic unlock. The chain question is settled, the manifest and types exist, -and reads/writes share one port. Until that write evidence exists, -`VITE_DOTIFY_RUNTIME_ADAPTER` defaults to `viem`. +The remaining gate for Product contract _writes_ is now narrow. Dotify verifies +the selected host signer public key and derived `pallet-revive` H160 address +against the Product account that the UI and backend use for key/session +requests before any Product CDM write can be submitted. The remaining evidence +must come from inside the container: native value forwarding for Classic unlock, +host approval UX, and post-payment `musicAccCanAccess` reads. The chain question +is settled, the manifest and types exist, and reads/writes share one port. 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 @@ -466,11 +468,13 @@ The current baseline is: | Public gateway | `https://dotify-test01.dev-dot.li` | | Asset Hub EVM chain ID | `420420417` | -Checked against npm on 2026-08-26: the Product SDK set above is current. npm -also publishes `polkadot-api` `3.0.0`, but Dotify keeps root PAPI on `1.23.3` -for now. The current official Product SDK packages bring their own PAPI -`2.2.x` tree, while `@polkadot-apps` chain-client/keys/signer still depend on -PAPI `1.23.x`. A direct root PAPI 3 trial removed the public +Checked against npm on 2026-08-30: npm now publishes Product SDK `0.25.0`, +host `0.18.0`, statement-store `0.6.7`, and descriptors `0.11.0`. Dotify keeps +the baseline above for this branch and defers that migration to a dedicated SDK +compatibility PR. npm also publishes `polkadot-api` `3.0.0`, but Dotify keeps +root PAPI on `1.23.3` for now. The current pinned Product SDK packages bring +their own PAPI `2.2.x` tree, while `@polkadot-apps` chain-client/keys/signer +still depend on PAPI `1.23.x`. A direct root PAPI 3 trial removed the public `PolkadotSigner` export and broke `ChainDefinition` / `TypedApi` compatibility at the Bulletin and wallet seams. Root PAPI 3 therefore remains a blocked compatibility migration until the upstream SDK graph converges. diff --git a/docs/index.html b/docs/index.html index b8d8c53..303106b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1547,8 +1547,9 @@

Product DevNet now, sovereignty adapters next

current viem implementation, API-side Product sr25519 verification with frontend Product proof submission for protected key/session requests, and an opt-in CDM/PAPI path that can route runtime reads and write submissions through the Product host signer. The tracked - deployment still defaults to viem until account mapping, native payment forwarding, - host-signed transaction evidence, and Product CASH receipt design are verified. Product + deployment still defaults to viem; Product writes now fail closed on Product public-key / + pallet-revive H160 mismatch, and still need native payment forwarding, host-signed + transaction evidence, and Product CASH receipt design before becoming default. Product Mobile in-app live audio pending a host-exposed WebRTC capability, Humanity/Individuality proofs, consented provenance, and ambassador mechanics remain sequenced behind verified security and privacy boundaries. diff --git a/docs/operations/dependency-security-status.md b/docs/operations/dependency-security-status.md index ad849a2..547d998 100644 --- a/docs/operations/dependency-security-status.md +++ b/docs/operations/dependency-security-status.md @@ -1,6 +1,6 @@ # Dependency and Security Status -Last checked: 2026-08-26. +Last checked: 2026-08-30. This record captures the dependency/security evidence for the maintenance pass that follows the Product payment-label work. It is not a blanket policy change: @@ -20,14 +20,16 @@ inside their reviewed scope. ## Official Version Drift -Checked against npm published versions on 2026-08-26. +Checked against npm published versions on 2026-08-30. | Package | Current pinned/locked value | Latest published value | Decision | | --- | --- | --- | --- | -| `@parity/product-sdk` | `0.23.0` | `0.23.0` | Current. | -| `@parity/product-sdk-host` | `0.16.0` | `0.16.0` | Current. | -| `@parity/product-sdk-statement-store` | `0.6.5` | `0.6.5` | Current. | -| `@parity/product-sdk-descriptors` | `0.10.0` | `0.10.0` | Current. | +| `@parity/product-sdk` | `0.23.0` | `0.25.0` | Defer to a dedicated Product compatibility PR; this PR changes signer/account mapping, not the SDK graph. | +| `@parity/product-sdk-host` | `0.16.0` | `0.18.0` | Defer with the Product SDK compatibility PR. | +| `@parity/product-sdk-statement-store` | `0.6.5` | `0.6.7` | Defer with the Product SDK compatibility PR. | +| `@parity/product-sdk-descriptors` | `0.10.0` | `0.11.0` | Defer with the Product SDK compatibility PR. | +| `@parity/product-sdk-signer` | `0.14.0` transitively via `@parity/product-sdk/wallet` | `0.14.2` | No direct dependency; import through the Product SDK export and upgrade with the SDK set. | +| `@parity/product-sdk-address` | `0.2.0` transitively via `@parity/product-sdk/address` | `0.2.0` | Current through the Product SDK export. | | `polkadot-api` | `1.23.3` | `3.0.0` | Blocked as a root migration: Product SDK `0.23.0` currently depends on PAPI `2.2.x`, while `@polkadot-apps/chain-client` / keys / signer depend on PAPI `1.23.x`; a direct root PAPI 3 trial removes `PolkadotSigner` and breaks `ChainDefinition` / `TypedApi` compatibility. | | `@polkadot-community-foundation/polkadot-app-deploy` | `0.13.1` | `0.13.1` | Current. | | `react` / `react-dom` | `18.3.1` | `19.2.8` | Defer as a UI/runtime migration. | @@ -52,13 +54,15 @@ Checked against npm published versions on 2026-08-26. - `npm audit --omit=dev --audit-level=moderate` reports the same 26 Product / PAPI chain findings because those packages are runtime dependencies. npm does not offer a non-breaking fix for the `deepmerge-ts` path. -- Product SDK latest does not remove that audit chain. A root +- Product SDK `0.25.0` has not yet been tested against Dotify's Product + manifest, host permissions, CDM resolver, and mobile fallback. A root `polkadot-api@3.0.0` trial also did not produce a deployable graph because the official Product SDK and `@polkadot-apps` packages still use different PAPI major lines. - Next safe action: Product host-signed transaction/resource-allocation smoke - tests on this SDK set, plus upstream monitoring for a Product SDK / - `@polkadot-apps` PAPI 3 convergence release. Do not run + tests on this SDK set, then a dedicated Product SDK `0.25.x` compatibility + PR, plus upstream monitoring for a Product SDK / `@polkadot-apps` PAPI 3 + convergence release. Do not run `npm audit fix --force` on the Product stack. `contracts/evm`: @@ -73,6 +77,8 @@ Checked against npm published versions on 2026-08-26. - Run real Product host smoke tests for Product sr25519 key/session requests and the opt-in `product-cdm` runtime adapter with the SDK `0.23.0` set. +- Test the Product SDK `0.25.x` line in a dedicated compatibility PR before + changing the published Product profile. - Track root `polkadot-api` `3.0.0` separately until Product SDK and `@polkadot-apps` publish compatible packages on the same PAPI major line. - Plan a Hardhat 3 migration separately from app/runtime changes. diff --git a/docs/operations/deployment-configuration.md b/docs/operations/deployment-configuration.md index 1775258..e336dfd 100644 --- a/docs/operations/deployment-configuration.md +++ b/docs/operations/deployment-configuration.md @@ -165,19 +165,21 @@ when deciding whether to refresh a previously opened app. Current Product host SDK dependencies: -| Package | Current value | Latest checked 2026-08-26 | +| Package | Current value | Latest checked 2026-08-30 | | -------------------------------------------- | ------------- | ------------------------- | -| `@parity/product-sdk` | `0.23.0` | `0.23.0` | -| `@parity/product-sdk-host` | `0.16.0` | `0.16.0` | -| `@parity/product-sdk-statement-store` | `0.6.5` | `0.6.5` | -| `@parity/product-sdk-descriptors` | `0.10.0` | `0.10.0` | +| `@parity/product-sdk` | `0.23.0` | `0.25.0` | +| `@parity/product-sdk-host` | `0.16.0` | `0.18.0` | +| `@parity/product-sdk-statement-store` | `0.6.5` | `0.6.7` | +| `@parity/product-sdk-descriptors` | `0.10.0` | `0.11.0` | | `polkadot-api` | `1.23.3` | `3.0.0` | | `@polkadot-community-foundation/polkadot-app-deploy` | `0.13.1` | `0.13.1` | | `engine.io-client` | `6.6.6` | `6.6.6` | Keep the Product SDK packages pinned exactly during Product DevNet hardening. Recheck npm and the official Product docs before changing them because the -mobile host API is still moving quickly. `polkadot-api` remains on `1.23.3` at +mobile host API is still moving quickly. The 2026-08-30 check found a newer +Product SDK line; upgrade it in a dedicated compatibility PR rather than mixing +it into Product write-signer mapping work. `polkadot-api` remains on `1.23.3` at the Dotify root even though npm publishes `3.0.0`: the current official Product SDK packages bring their own PAPI `2.2.x` tree, while `@polkadot-apps` chain-client/keys/signer still depend on PAPI `1.23.x`. A direct root PAPI 3 @@ -356,8 +358,10 @@ an explicit Product-host account connection. including Classic unlock payments, through the Product CDM contract adapter. The tracked Product profile does not enable that flag yet. Keep `viem` as the production default until Product-host transaction evidence proves account -mapping, fees/native value handling, and user approval for real writes. Validate -Product protected playback through host smoke tests after each Product +mapping, fees/native value handling, and user approval for real writes. Product +CDM writes now fail closed unless the host signer public key maps to the same +pallet-revive H160 address that Dotify connected for key/session requests. +Validate Product protected playback through host smoke tests after each Product publication before treating Product identity as production-ready for gated listening. diff --git a/docs/operations/product-devnet-deployment.md b/docs/operations/product-devnet-deployment.md index 9125ac3..d22decd 100644 --- a/docs/operations/product-devnet-deployment.md +++ b/docs/operations/product-devnet-deployment.md @@ -493,17 +493,38 @@ Then verify in the Product host: In every rejected case, playback must stop and offer a passkey/EVM wallet. No path may release a key without a verified signature. -6. A Product-origin host creates a room and copies a +6. Only for an explicit Product CDM write smoke build, set + `VITE_DOTIFY_RUNTIME_ADAPTER=product-cdm` and use a funded Product account + that has not already paid for the target Classic track. Do not use this as + the default `dotify-test01.dot` release gate until it has passed once end to + end. Verify: + - the connected Dotify Product account and the host-selected signer expose + the same public key; + - deriving `pallet-revive` H160 from that public key gives the same H160 + address shown by Dotify and used in key/session requests; + - clicking **Support and open** triggers an explicit host transaction + approval, not a silent write; + - the submitted `musicRoyPayAccess(contentHash)` forwards the exact + `pricePlanck` native value; + - after inclusion, `musicAccHasPaid(contentHash, listenerH160)` and + `musicAccCanAccess(contentHash, listenerH160)` both read `true`; + - the backend then releases the full key through the same Product identity. + + If any mapping check fails, the expected behavior is a fail-closed + **Payment signer unavailable** error before submission. If native value, + host approval UX, or post-payment access evidence is missing, keep the + shipped profile on `viem`. +7. A Product-origin host creates a room and copies a `https://dotify-test01.dev-dot.li/#/rooms/` link. -7. A wallet-free browser joins that link from outside the Product host. -8. The outside listener reaches `In sync` and hears the host stream; staying on +8. A wallet-free browser joins that link from outside the Product host. +9. The outside listener reaches `In sync` and hears the host stream; staying on `Connecting...` means host capture or WebRTC negotiation is still failing, not room creation. -9. A Netlify-origin host and Product-origin guest also connect. -10. Briefly interrupting the mobile network preserves and resumes the same room +10. A Netlify-origin host and Product-origin guest also connect. +11. Briefly interrupting the mobile network preserves and resumes the same room within 120 seconds; it must disappear from public discovery while the host is offline and return with the same code after reconnecting. -11. Explicitly leaving ends the room immediately. Force-closing the host leaves +12. Explicitly leaving ends the room immediately. Force-closing the host leaves the room private until the 120-second resume window expires. Inspect the browser console and Fly logs for CORS, catalog, Socket.IO, and @@ -601,14 +622,17 @@ active. 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. + host signature verifies regardless of which shape it uses. The validation + step for `product-sr25519-v1` 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. + selected is real host-signed transaction evidence. Dotify now validates that + the selected Product host signer public key maps to the same `pallet-revive` + H160 account used by the connected Product identity before a CDM write can be + submitted. - 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. @@ -617,5 +641,5 @@ active. - Product contract mode (`VITE_DOTIFY_RUNTIME_ADAPTER=product-cdm`) covers catalog reads and runtime write submissions inside the Product host. The tracked deployment still keeps the default `viem` adapter until - `pallet-revive` account mapping, native value forwarding, and host-signed - transaction evidence are captured. + native value forwarding, host approval UX, and post-payment access evidence + are captured. diff --git a/web/src/features/runtime/productCdmContracts.ts b/web/src/features/runtime/productCdmContracts.ts index fc513da..9c280fb 100644 --- a/web/src/features/runtime/productCdmContracts.ts +++ b/web/src/features/runtime/productCdmContracts.ts @@ -71,7 +71,7 @@ export type ProductChainEnvironment = 'devnet'; export type ProductCdmContractsOptions = { environment: ProductChainEnvironment; - /** Signer manager from @parity/product-sdk-signer, when transactions are in scope. */ + /** Signer manager from @parity/product-sdk/wallet, when transactions are in scope. */ signerManager?: unknown; }; diff --git a/web/src/features/runtime/runtimeWriterProvider.test.ts b/web/src/features/runtime/runtimeWriterProvider.test.ts index 4543949..7d18d96 100644 --- a/web/src/features/runtime/runtimeWriterProvider.test.ts +++ b/web/src/features/runtime/runtimeWriterProvider.test.ts @@ -7,6 +7,8 @@ const runtime = '0xcccccccccccccccccccccccccccccccccccccccc' as const; const hash = '0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd' as const; const productPublicKey = `0x${'11'.repeat(32)}` as const; const differentProductPublicKey = `0x${'22'.repeat(32)}` as const; +const productH160Address = '0x9999999999999999999999999999999999999999' as const; +const differentProductH160Address = '0x8888888888888888888888888888888888888888' as const; const viemWriter = { createRuntime: vi.fn(async () => txHash), @@ -41,14 +43,15 @@ afterEach(() => { vi.unstubAllEnvs(); vi.resetModules(); vi.doUnmock('./productCdmContracts'); - vi.doUnmock('@parity/product-sdk-signer'); + vi.doUnmock('@parity/product-sdk/wallet'); + vi.doUnmock('@parity/product-sdk/address'); }); async function loadProvider() { return (await import('./runtimeWriterProvider')).createRuntimeWriter; } -function productAccount(publicKey = productPublicKey) { +function productAccount(publicKey = productPublicKey, h160Address = h160ForPublicKey(publicKey)) { return { publicKey: Uint8Array.from( publicKey @@ -57,7 +60,7 @@ function productAccount(publicKey = productPublicKey) { .map(byte => Number.parseInt(byte, 16)) ), address: '5ProductAccount', - h160Address: '0x9999999999999999999999999999999999999999' + h160Address }; } @@ -73,8 +76,10 @@ function mockProductSigner(selectedAccount = productAccount()) { return signerManager; }); const HostProvider = vi.fn(() => ({ type: 'host' })); - vi.doMock('@parity/product-sdk-signer', () => ({ SignerManager, HostProvider })); - return { signerManager, signerManagerOptions, SignerManager, HostProvider }; + const deriveH160 = vi.fn((publicKey: Uint8Array) => h160ForPublicKey(hexFromBytes(publicKey))); + vi.doMock('@parity/product-sdk/wallet', () => ({ SignerManager, HostProvider })); + vi.doMock('@parity/product-sdk/address', () => ({ deriveH160 })); + return { signerManager, signerManagerOptions, SignerManager, HostProvider, deriveH160 }; } function accessIntent(amountPlanck: bigint) { @@ -125,7 +130,7 @@ describe('createRuntimeWriter', () => { it('routes Product writes through CDM setup without falling back to viem', async () => { vi.stubEnv('VITE_DOTIFY_RUNTIME_ADAPTER', 'product-cdm'); - const { signerManager, signerManagerOptions, SignerManager, HostProvider } = mockProductSigner(); + const { signerManager, signerManagerOptions, SignerManager, HostProvider, deriveH160 } = mockProductSigner(); const verifyDeployment = vi.fn(async () => undefined); const resolver = { getRuntimeContract: vi.fn() }; vi.doMock('./productCdmContracts', () => ({ @@ -143,7 +148,7 @@ describe('createRuntimeWriter', () => { ethRpcUrl: 'https://rpc.example', getViemWalletClient, config: { kind: 'product-cdm', productEnvironment: 'devnet' }, - productAccount: { productId: 'dotify-test01.dot', publicKey: productPublicKey } + productAccount: { productId: 'dotify-test01.dot', evmAddress: productH160Address, publicKey: productPublicKey } }); await expect(writer.createRuntime(factory)).resolves.toBe(txHash); @@ -165,6 +170,7 @@ describe('createRuntimeWriter', () => { } }); expect(signerManager.connect).toHaveBeenCalledWith('host'); + expect(deriveH160).toHaveBeenCalledWith(productAccount().publicKey); expect(createProductCdmContracts).toHaveBeenCalledTimes(1); expect(createProductCdmContracts).toHaveBeenCalledWith({ environment: 'devnet', signerManager }); expect(verifyDeployment).toHaveBeenCalledTimes(1); @@ -195,4 +201,52 @@ describe('createRuntimeWriter', () => { expect(signerManager.destroy).toHaveBeenCalledTimes(1); expect(createProductCdmContracts).not.toHaveBeenCalled(); }); + + it('rejects Product writes when the host signer maps to a different pallet-revive H160 address', async () => { + vi.stubEnv('VITE_DOTIFY_RUNTIME_ADAPTER', 'product-cdm'); + const { signerManager } = mockProductSigner(productAccount(productPublicKey, productH160Address)); + const createProductCdmContracts = vi.fn(); + vi.doMock('./productCdmContracts', () => ({ createProductCdmContracts })); + vi.resetModules(); + const createRuntimeWriter = await loadProvider(); + + const writer = createRuntimeWriter({ + ethRpcUrl: 'https://rpc.example', + getViemWalletClient: vi.fn(async () => ({}) as never), + config: { kind: 'product-cdm', productEnvironment: 'devnet' }, + productAccount: { productId: 'dotify-test01.dot', evmAddress: differentProductH160Address, publicKey: productPublicKey } + }); + + await expect(writer.payForAccess(accessIntent(1n))).rejects.toThrow(/maps to/); + expect(signerManager.destroy).toHaveBeenCalledTimes(1); + expect(createProductCdmContracts).not.toHaveBeenCalled(); + }); + + it('rejects Product writes when the signer-reported H160 disagrees with the derived public-key mapping', async () => { + vi.stubEnv('VITE_DOTIFY_RUNTIME_ADAPTER', 'product-cdm'); + const { signerManager } = mockProductSigner(productAccount(productPublicKey, differentProductH160Address)); + const createProductCdmContracts = vi.fn(); + vi.doMock('./productCdmContracts', () => ({ createProductCdmContracts })); + vi.resetModules(); + const createRuntimeWriter = await loadProvider(); + + const writer = createRuntimeWriter({ + ethRpcUrl: 'https://rpc.example', + getViemWalletClient: vi.fn(async () => ({}) as never), + config: { kind: 'product-cdm', productEnvironment: 'devnet' }, + productAccount: { productId: 'dotify-test01.dot', evmAddress: productH160Address, publicKey: productPublicKey } + }); + + await expect(writer.payForAccess(accessIntent(1n))).rejects.toThrow(/account mapping mismatch/); + expect(signerManager.destroy).toHaveBeenCalledTimes(1); + expect(createProductCdmContracts).not.toHaveBeenCalled(); + }); }); + +function hexFromBytes(bytes: Uint8Array): `0x${string}` { + return `0x${Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('')}` as `0x${string}`; +} + +function h160ForPublicKey(publicKey: `0x${string}`): `0x${string}` { + return publicKey === productPublicKey ? productH160Address : differentProductH160Address; +} diff --git a/web/src/features/runtime/runtimeWriterProvider.ts b/web/src/features/runtime/runtimeWriterProvider.ts index 64b7be1..53f1993 100644 --- a/web/src/features/runtime/runtimeWriterProvider.ts +++ b/web/src/features/runtime/runtimeWriterProvider.ts @@ -16,6 +16,8 @@ type ViemWalletClient = Awaited>; export type ProductRuntimeSignerAccount = { productId: string; derivationIndex?: number; + /** H160 address Dotify uses for runtime access checks and pallet-revive calls. */ + evmAddress?: `0x${string}`; /** Product account public key already approved by Dotify's wallet flow. */ publicKey?: `0x${string}`; }; @@ -48,16 +50,24 @@ type ProductSignerModule = { HostProvider: new (options?: unknown) => unknown; }; +type ProductAddressModule = { + deriveH160: (publicKey: Uint8Array) => `0x${string}`; +}; + async function createViemWriter(deps: RuntimeWriterDeps): Promise { const walletClient = await deps.getViemWalletClient(); return createViemRuntimeWriter({ ethRpcUrl: deps.ethRpcUrl, walletClient }); } async function createProductSignerManager(account?: ProductRuntimeSignerAccount): Promise { - const { SignerManager, HostProvider } = (await import('@parity/product-sdk-signer')) as unknown as ProductSignerModule; + const [{ SignerManager, HostProvider }, { deriveH160 }] = await Promise.all([ + import('@parity/product-sdk/wallet') as Promise, + import('@parity/product-sdk/address') as Promise + ]); const productId = account?.productId?.trim() || productIdFromEnv(); const derivationIndex = account?.derivationIndex ?? 0; const expectedPublicKey = normalizeHex(account?.publicKey); + const expectedH160Address = normalizeHex(account?.evmAddress); const manager = new SignerManager({ dappName: productId, @@ -89,6 +99,23 @@ async function createProductSignerManager(account?: ProductRuntimeSignerAccount) } const selectedPublicKey = hexFromBytes(selectedAccount.publicKey); + const derivedH160Address = normalizeHex(deriveH160(selectedAccount.publicKey)); + const selectedH160Address = normalizeHex(selectedAccount.h160Address); + + if (selectedH160Address && selectedH160Address !== derivedH160Address) { + manager.destroy(); + throw new Error( + `Product CDM signer account mapping mismatch: the host signer reports H160 ${selectedH160Address}, but deriving pallet-revive H160 from its Product public key gives ${derivedH160Address}. Refusing to submit a runtime transaction.` + ); + } + + if (expectedH160Address && derivedH160Address !== expectedH160Address) { + manager.destroy(); + throw new Error( + `Product CDM signer account mismatch: the connected Dotify account maps to ${expectedH160Address}, but the Product host signer maps to ${derivedH160Address}. Reconnect the Product account before submitting a runtime transaction.` + ); + } + if (expectedPublicKey && selectedPublicKey.toLowerCase() !== expectedPublicKey) { manager.destroy(); throw new Error( diff --git a/web/src/hooks/useCatalog.ts b/web/src/hooks/useCatalog.ts index d960126..56b95cf 100644 --- a/web/src/hooks/useCatalog.ts +++ b/web/src/hooks/useCatalog.ts @@ -297,6 +297,7 @@ export function useCatalog(deps: UseCatalogDeps) { const signer = connectedWallet.keyRequestSigner; return { productId: productHostConfig.productId, + evmAddress: connectedWallet.evmAddress, publicKey: signer && 'productPublicKey' in signer ? signer.productPublicKey : undefined }; }, [connectedWallet, productHostConfig.productId]);