Skip to content

feat: verify Product CDM unlock read-back - #127

Open
knzeng-e wants to merge 1 commit into
devfrom
agent/product-cdm-smoke-validation
Open

feat: verify Product CDM unlock read-back#127
knzeng-e wants to merge 1 commit into
devfrom
agent/product-cdm-smoke-validation

Conversation

@knzeng-e

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

Copy link
Copy Markdown
Owner

Outcome

This PR makes the opt-in Product CDM Classic unlock smoke test falsifiable. In a product-cdm build, Dotify no longer marks a paid Classic track as open immediately after the Product host transaction path returns a hash. It waits for inclusion, then polls the runtime read-back for the same Product-derived H160 account and requires both musicAccHasPaid(contentHash, listenerH160) and musicAccCanAccess(contentHash, listenerH160) to be true.

After review, the post-inclusion path now treats Product read lag as expected: it polls with a bound before failing closed. If a payment was already included but access cannot be verified, the UI reports Payment included, access not verified and keeps the transaction hash visible as the user's receipt instead of collapsing the result into a generic Payment failed state.

Issue and context

Refs #85.

This PR now targets dev directly after #126 merged. #126 made Product CDM writes fail closed before submission when the Product host signer does not match the connected Dotify Product identity. The next risk is a false positive after submission: a host transaction could be included or return a hash, but native value forwarding, account mapping, read-head freshness, or access-state mutation could still be wrong.

That is a money/access boundary. Dotify should not show Work opened, request the protected key, or treat Product writes as ready unless the runtime itself confirms the entitlement for the same H160 account.

Local scope document: docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md.

Architecture and key concepts

The existing runtime port boundary stays intact:

Classic unlock CTA
  -> typed native runtime payment intent
  -> RuntimeWritePort.payForAccess()
  -> RuntimeWritePort.waitForTransaction()
  -> Product CDM only: bounded RuntimeReadPort.hasPaid() + RuntimeReadPort.canAccess() polling
  -> unlock UI state and protected-key request only after read-back passes

The read-back is Product-CDM-only because viem remains the production default and already has the current e2e evidence. product-cdm is explicitly experimental, so it should be stricter: transaction inclusion is necessary, but not sufficient, for a Classic unlock smoke pass.

How it works

paymentReadback.ts adds a typed verification helper that reads hasPaid and canAccess for an executable runtime-native payment intent and the exact listener H160 account. The helper retries briefly because the Product writer and reader use separate host chain clients, so a writer can observe inclusion before the reader observes the same runtime head.

useCatalog.payForTrackAccess() now adds one Product CDM step after transaction inclusion:

  1. show Verifying runtime access;
  2. require a connected Product-derived H160 address;
  3. call verifyRuntimeAccessPayment() with bounded read-back polling;
  4. emit dotify:product-cdm-payment-smoke with txHash, runtimeAddress, contentHash, listenerAddress, hasPaid, canAccess, attempts, ok, and error;
  5. report Payment included, access not verified with the transaction hash if verification fails;
  6. only then set local access state and show Work opened.

Review comments addressed

  • discussion_r3887991845: implemented bounded Product read-back polling after inclusion so a charged first-time unlock is not failed just because the Product read client is one head behind the writer.
  • discussion_r3887991848: separated included-payment/access-verification failure from pre-inclusion payment failure, keeping txHash in the feedback when a transaction was included but read-back evidence failed.

Design decisions and tradeoffs

I kept the verification in the catalog/payment flow rather than adding another runtime write method because the runtime port already has the required read and write primitives. This avoids turning Product smoke evidence into a second transaction API.

I did not change the default adapter. Product CDM remains opt-in behind VITE_DOTIFY_RUNTIME_ADAPTER=product-cdm until a real Product-host run proves native value forwarding, approval UX, and post-payment read-back.

I did not upgrade Product SDK packages in this PR. npm was checked again on 2026-08-30: @parity/product-sdk is now 0.25.0, host 0.18.0, statement-store 0.6.7, descriptors 0.11.0, and polkadot-api 3.0.0. Dotify stays pinned on the current SDK set here because this PR changes smoke semantics, not the dependency graph. The upgrade belongs in a dedicated compatibility PR.

Security, failure, and operations

The runtime remains authoritative. Product CDM payment success now requires the runtime to confirm the entitlement after inclusion. Ambiguous, lagging, or inconsistent read-back fails closed and keeps the shipped Product profile on viem.

No Netlify/Fly/Product env variable changes are required. The operational smoke checklist now asks testers to capture the dotify:product-cdm-payment-smoke browser event, including the number of read-back attempts, when running a Product CDM write build.

Review guide

Suggested order

  1. web/src/features/payments/paymentReadback.ts - verify bounded polling and failure results preserve enough evidence without mutating UI state.
  2. web/src/hooks/useCatalog.ts - check the Product-CDM-only branch after waitForTransaction, especially the ordering before local unlock state is set and the included-payment failure copy.
  3. web/src/features/payments/paymentReadback.test.ts - confirm successful, lagging, transient-error, missing-grant, and inconsistent-state cases are covered.
  4. docs/operations/product-devnet-deployment.md - verify the manual Product host smoke instructions now match the app behavior.
  5. Product architecture/config docs - confirm the docs still say Product writes are not the default path.

Verify carefully

  • A product-cdm build cannot show Work opened when hasPaid or canAccess is false after transaction inclusion.
  • The read-back uses the same runtimeAddress, contentHash, and listener H160 from the payment intent/account.
  • Standalone viem Classic unlock behavior is unchanged.
  • The Product smoke event is evidence-only and does not bypass any access check.
  • Documentation does not imply Product writes are production-ready by default.

Validation

Evidence What it proves
npm view @parity/product-sdk version && npm view @parity/product-sdk-host version && npm view @parity/product-sdk-statement-store version && npm view @parity/product-sdk-descriptors version && npm view @polkadot-community-foundation/polkadot-app-deploy version && npm view polkadot-api version Rechecked the official npm-published Product/PAPI versions before this PR; SDK upgrade remains deliberately deferred.
npm run test:unit -- src/features/payments/paymentReadback.test.ts src/features/runtime/runtimeWriterProvider.test.ts src/features/runtime/productCdmRuntimeAdapter.test.ts src/features/payments/paymentModel.test.ts Focused payment/read-back and Product writer tests pass: 4 files / 27 tests.
npm run test:unit Full web unit suite passes: 42 files / 331 tests.
npm run build Standalone production build compiles; existing Rollup/chunk warnings remain.
npm run build:product-devnet Product build compiles; catalog bootstrap refresh was skipped because sandbox fetch failed and the existing snapshot was kept.
npm run lint ESLint passes with the existing 3 React hook dependency warnings in App.tsx and ArtistShell.tsx.
npm run test:e2e -- classic-unlock.spec.ts Existing deterministic Classic unlock path still opens after payment in the standalone/e2e route.
git diff --check No whitespace errors.

Known limitations and follow-ups

This PR still does not close #85. Remaining evidence:

  • run the real Product host-signed Classic unlock inside the Product container;
  • confirm the host approval UX is explicit;
  • prove the native pricePlanck value reaches musicRoyPayAccess;
  • capture a successful dotify:product-cdm-payment-smoke event;
  • confirm the backend releases the protected key through the same Product identity;
  • run a dedicated Product SDK 0.25.x compatibility PR after this smoke path is stable;
  • confirm the Product-supported CASH settlement model before exposing CASH conversion, bridge, or receipt UI.

Metadata checklist

  • Backlog issue linked with correct close/reference semantics
  • Local backlog document linked
  • Added to Project 5 (Dotify sprints)
  • Project Priority, Track, Phase, Type, and Backlog doc mirror the issue
  • Workflow status matches draft/review state
  • Assignee set
  • Applicable labels set
  • Applicable milestone set, or confirmed none exists
  • Reviewers requested when ownership is known
  • Draft/ready state is intentional

@knzeng-e knzeng-e added dotify-backlog Tracked by docs/backlog/backlog.json and Project 5 product-sdk Polkadot Product SDK / Host / Playground integration labels Aug 29, 2026
@knzeng-e knzeng-e self-assigned this Aug 29, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 29, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-29T23:49:12.936148Z d319103 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@knzeng-e knzeng-e moved this from Todo to In Review in Dotify sprints Aug 29, 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: d319103e97

ℹ️ 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".

Comment on lines +17 to +20
const [hasPaid, canAccess] = await Promise.all([
input.reader.hasPaid(input.intent.runtimeAddress, input.intent.contentHash, input.listenerAddress),
input.reader.canAccess(input.intent.runtimeAddress, input.intent.contentHash, input.listenerAddress)
]);

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 Retry read-back against the included chain state

In a product-cdm build, the reader and writer are backed by independently created Product chain clients (runtimeReaderProvider.ts:41 and runtimeWriterProvider.ts:139), while this helper performs only one immediate read after the writer reports best-block inclusion. If the reader is still on the preceding head—or either query fails transiently—a successfully charged first-time unlock is reported as failed; retrying the payment then reverts as already paid. Poll against the included state with a bound before failing closed.

AGENTS.md reference: AGENTS.md:L69-L70

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. The Product CDM post-inclusion read-back now uses bounded polling via verifyRuntimeAccessPayment() before failing closed, because the Product writer and reader can observe different heads. The smoke event now includes attempts plus the last observed read-back/error.

Comment thread web/src/hooks/useCatalog.ts Outdated
Comment on lines +1197 to +1200
if (readbackError) {
throw new Error(
`Product CDM payment smoke failed: ${readbackError} Keep Product writes disabled until native value forwarding and account mapping are verified in the Product host.`
);

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 Preserve the confirmed transaction in read-back errors

When either post-inclusion value is false, this throw reaches the existing catch, which replaces the feedback with title: 'Payment failed' and drops txHash. At this point waitForTransaction has already completed and the payment may have been distributed—for example, hasPaid can be true while current access is denied—so the message misstates the financial outcome and hides the receipt users need to verify it. Report an included-payment/access-verification failure separately and retain the hash.

AGENTS.md reference: AGENTS.md:L69-L71

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. The payment flow now separates pre-inclusion payment failure from post-inclusion access-verification failure. If a transaction was included but Product CDM read-back cannot verify access, the UI reports Payment included, access not verified and preserves the included txHash as the user receipt.

Base automatically changed from agent/product-cdm-write-smoke to dev August 30, 2026 00:05
@knzeng-e
knzeng-e force-pushed the agent/product-cdm-smoke-validation branch from d319103 to e069b4f Compare August 30, 2026 14:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dotify-backlog Tracked by docs/backlog/backlog.json and Project 5 product-sdk Polkadot Product SDK / Host / Playground integration

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

1 participant