Skip to content

Model track access payment intents - #121

Merged
knzeng-e merged 4 commits into
devfrom
agent/payment-asset-model
Aug 24, 2026
Merged

Model track access payment intents#121
knzeng-e merged 4 commits into
devfrom
agent/payment-asset-model

Conversation

@knzeng-e

@knzeng-e knzeng-e commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Outcome

This PR makes Dotify's Classic unlock payment path explicit before the next Product payment work: listener unlocks now build a typed native runtime payment intent before they reach RuntimeWritePort, and Product CASH is represented as a separate unsupported rail instead of being conflated with runtime msg.value.

Issue and context

Refs #85 and the Product roadmap in docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md.

PR #120 moved Classic unlock writes behind RuntimeWritePort, which was the right seam for viem vs Product CDM contract submission. The remaining ambiguity was the value flowing through that seam: (runtimeAddress, contentHash, value) was mechanically correct, but it did not encode whether the value was native runtime settlement or future Product CASH settlement.

That matters because Dotify wants DOT/CASH actions to run inside the Product environment, but CASH lives on People chain while Dotify runtime entitlement is granted by Asset Hub SmartRuntimes. Until Product confirms the settlement model, the app must not imply CASH can be silently converted into native runtime value or that access can be marked paid without runtime evidence.

Architecture and key concepts

The new payment model has two rails:

  • runtime-native: executable today. It carries asset DOT, Asset Hub runtime address, content hash, and 18-decimal native amount. RuntimeWritePort.payForAccess() accepts only this executable intent.
  • product-cash: modeled but unsupported. It records the future CASH path and explains why it cannot execute until there is a Product-confirmed receipt or bridge design.

The runtime write adapters stay behaviorally unchanged at the contract boundary:

sequenceDiagram
  participant UI as useCatalog
  participant Model as paymentModel
  participant Port as RuntimeWritePort
  participant RT as SmartRuntime

  UI->>Model: createNativeRuntimeAccessPaymentIntent(track price)
  Model-->>UI: runtime-native intent
  UI->>Port: payForAccess(intent)
  Port->>RT: musicRoyPayAccess(contentHash) + msg.value
  RT-->>Port: tx hash / failure
Loading

The viem adapter still submits writeContract({ functionName: 'musicRoyPayAccess', args: [contentHash], value }). The Product CDM adapter still submits musicRoyPayAccess.tx(contentHash, { value }). The difference is that both receive a named intent, not loose positional payment data.

How it works

  1. useCatalog.payForTrackAccess() converts the track price with dotToPlanck() and builds a native runtime intent.
  2. Invalid or zero-value Classic payments fail before wallet submission.
  3. The writer provider forwards the intent to the configured write adapter.
  4. On success, the user is explicitly returned to the player and the selected track is reloaded with full access.
  5. CASH settlement helpers exist only as an unsupported rail with a clear reason string.

Design decisions and tradeoffs

The implementation keeps the contract call unchanged because the deployed runtime still grants access through musicRoyPayAccess() and native msg.value. Changing the contract or adding a fake CASH conversion here would overstate Product readiness.

The alternative was to make RuntimeWritePort accept a union of native and CASH intents. I rejected that for this seam: adapters that call SmartRuntime msg.value should not even type-check with CASH. A later Product payment/receipt port can consume product-cash once the settlement model is confirmed.

The other small behavior change is intentional: after a successful unlock, Dotify navigates back to the player before reselecting the track. That matches the unlock promise and stabilizes the Classic unlock e2e flow.

Security, failure, and operations

  • Runtime contracts remain authoritative for access grants.
  • Zero-value Classic payment intents fail closed before submission.
  • CASH remains non-executable; no bridge, conversion, entitlement credit, or optimistic paid state is introduced.
  • No production env vars, hosted dashboard settings, CORS origins, storage mounts, or deployment config changed. docs/operations/deployment-configuration.md was checked and does not need an update for this PR.
  • Product CDM mode is still opt-in behind VITE_DOTIFY_RUNTIME_ADAPTER=product-cdm; Product host transaction smoke evidence remains a follow-up.

Review guide

Suggested order

  1. web/src/features/payments/paymentModel.ts and .test.ts - verify the rail model and fail-closed intent creation.
  2. web/src/features/runtime/runtimePorts.ts - verify that executable runtime writes accept only native runtime intents.
  3. web/src/features/runtime/viemRuntimeAdapter.ts and productCdmRuntimeAdapter.ts - verify the actual contract calls are unchanged except for reading from the intent.
  4. web/src/features/runtime/runtimeWriterProvider.ts - verify adapter selection still stays lazy and fail-closed.
  5. web/src/hooks/useCatalog.ts - verify Classic unlock creates the intent, handles setup errors clearly, and returns to the player after success.
  6. Documentation files - verify Product CASH is described as future/unsupported, not delivered.

Verify carefully

  • A CASH intent cannot be submitted to RuntimeWritePort.payForAccess().
  • The viem adapter still sends musicRoyPayAccess(contentHash) with the same native value.
  • The Product CDM adapter still sends musicRoyPayAccess.tx(contentHash, { value }) and does not claim Product CASH support.
  • A malformed or zero Classic amount fails before wallet confirmation.
  • The docs do not imply DOT/CASH conversion is already available.

Validation

Evidence What it proves
npm run test:unit -- paymentModel runtimeWriterProvider viemRuntimeAdapter productCdmRuntimeAdapter Payment model and runtime writer/adapters preserve native payment behavior and reject unsupported assumptions.
npm run test:e2e -- e2e/classic-unlock.spec.ts Classic user unlock flow still moves from locked track to full playback after payment.
npm run build Standalone web build type-checks and bundles. Vite reports existing chunk/dynamic-import warnings only.
npm run build:product-devnet Product DevNet build type-checks and bundles. The catalog bootstrap fetch was unavailable locally, so the script kept the existing bootstrap snapshot.
npm run lint ESLint passes with the existing three React hook dependency warnings in App.tsx and ArtistShell.tsx.
git diff --check No whitespace errors.

Known limitations and follow-ups

  • Product CASH settlement is not implemented. The next design gate remains PCF/Product confirmation of the receipt/bridge model between People-chain CASH and Asset Hub runtime entitlement.
  • Product CDM runtime writes still need real host-signed transaction smoke evidence before replacing the default viem path.
  • No DOT/CASH conversion UI is included in this PR.

Metadata checklist

  • Backlog issue linked with correct reference semantics: Refs [Product SDK][P1] Prove Product host compatibility before integration #85
  • Local backlog document linked
  • Added to Project 5 (Dotify sprints)
  • Project Priority, Track, Phase, Type, and Backlog doc mirror the issue
  • Workflow status matches review state: In Review
  • Assignee set: knzeng-e
  • Applicable labels set: dotify-backlog, product-sdk, polkadot, contracts, frontend, documentation
  • Applicable milestone set, or confirmed none exists: none
  • Reviewers requested when ownership is known
  • Draft/ready state is intentional

@knzeng-e knzeng-e added product-sdk Polkadot Product SDK / Host / Playground integration polkadot contracts frontend documentation Improvements or additions to documentation dotify-backlog Tracked by docs/backlog/backlog.json and Project 5 labels Aug 22, 2026
@knzeng-e knzeng-e self-assigned this Aug 22, 2026
@knzeng-e knzeng-e moved this from Todo to In Review in Dotify sprints Aug 22, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b2d2195c9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


export const DOTIFY_NATIVE_RUNTIME_ASSET = {
kind: 'native',
symbol: 'DOT',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Label the native Paseo asset as PAS rather than DOT

In the configured Paseo Asset Hub/Product DevNet environment, msg.value spends the native PAS fee token, not DOT; the repository's own stack description confirms this at docs/design/dotify-product-stack-alignment.md:166-168. Because useCatalog now displays paymentIntent.asset.symbol in the confirmation message, this constant tells listeners they are approving DOT while their wallet spends PAS and makes the new settlement model encode the wrong asset. Derive the symbol from the configured chain or identify this rail as PAS/generic native currency.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 4c5e5c5.

The runtime-native asset is no longer hard-coded as DOT. paymentModel now requires the native asset on the payment intent, useCatalog derives it from the configured EVM chain via resolveEvmChain, and the shared chain config labels chain 420420417 as PAS. The access gate and transaction feedback now use that derived symbol, with UNIT only as a generic fallback for local/unknown chains.

I also updated the payment docs to describe this as runtime-native/PAS on Product DevNet rather than DOT, and added tests for both the payment asset derivation and the chain currency mapping.

Validation:

  • npm run test:unit -- paymentModel runtimeWriterProvider viemRuntimeAdapter productCdmRuntimeAdapter contracts -> 33 passed
  • npm run build -> passed, existing chunk warnings only
  • npm run lint -> passed with the existing 3 React Hook warnings
  • npm run test:e2e -- e2e/classic-unlock.spec.ts -> passed
  • npm run build:product-devnet -> passed; bootstrap refresh kept the existing snapshot because fetch failed
  • git diff --check -> clean

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 5816286. The implementation now makes the boundary explicit: EVM JSON-RPC is used to fetch chainId, but native symbol metadata is not available through a standard on-chain/RPC method. Dotify maps known Polkadot Hub EVM chain ids to native currencies instead, including 420420419 -> DOT, 420420418 -> KSM, and the current Product DevNet/Paseo rail 420420417 -> PAS. Added unit coverage and docs so this does not silently drift back to a hard-coded DOT/PAS assumption.

@knzeng-e

knzeng-e commented Aug 22, 2026

Copy link
Copy Markdown
Owner Author

Claude feedback assessment and follow-up commit: 96e2bba

I reviewed the five points from Claude and treated them as review feedback, not automatic instructions. The valuable parts are now implemented:

  1. High / accepted: Classic unlock amount no longer comes from the lossy display string. Registry/API tracks now carry pricePlanck, and payment intent construction uses that authoritative bigint when available. Added regression tests for more than 9 fractional decimals and tiny positive prices that display as 0.000000000.

  2. Medium / accepted: post-unlock reselection now preserves room callbacks. The player passes socketEmit, setLocalStreamReady, and closeHostPeers into payForTrackAccess, so an in-room Classic unlock can update room metadata and refresh the local stream just like other track-open paths.

  3. Low / accepted: success navigation is guarded. If the user changes track or view while the payment confirmation is pending, access is still recorded, but the app does not force-navigate back to the player or reselect an old track.

  4. Low / accepted: missing runtime address no longer returns silently. It now fails closed with user-facing transaction feedback.

  5. Low / accepted with scope: removed the decorative chain field from the native DOT asset because it was not verified at runtime. Kept CASH as an explicit unsupported product-cash design marker, with an implementation comment and docs explaining that it must not execute until Product confirms a receipt/bridge model.

Validation run:

  • npm run test:unit -- paymentModel runtimeWriterProvider viemRuntimeAdapter productCdmRuntimeAdapter -> 24 tests passed
  • npm run build -> passed, existing chunk warnings only
  • npm run lint -> passed with the existing 3 React Hook warnings
  • npm run build:product-devnet -> passed; catalog bootstrap refresh could not fetch and kept the existing generated snapshot
  • npm run test:e2e -- e2e/classic-unlock.spec.ts -> passed
  • git diff --check -> clean

@knzeng-e knzeng-e left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reviewed. A question asked to chain the confg/contracts.ts *nativeCurrncyForChain* function

@knzeng-e
knzeng-e merged commit e102d23 into dev Aug 24, 2026
5 checks passed
@github-project-automation github-project-automation Bot moved this from In Review to Done in Dotify sprints Aug 24, 2026
@knzeng-e knzeng-e mentioned this pull request Aug 24, 2026
14 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contracts documentation Improvements or additions to documentation dotify-backlog Tracked by docs/backlog/backlog.json and Project 5 frontend polkadot product-sdk Polkadot Product SDK / Host / Playground integration

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant