feat: wire V10 Publishing Conviction NFT through SDK + daemon#527
Conversation
Add V10ConvictionAccountInfo + the seven optional V10 PCA signatures to the ChainAdapter interface (replacing the V9 ConvictionAccountInfo DTO), and implement createConvictionAccount + getConvictionAccountInfo on EVMChainAdapter against DKGPublishingConvictionNFT. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ConvictionAgent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Owner-gated topUp/register revert propagates (not swallowed) so the daemon can map NotAccountOwner to HTTP 403. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… reconcile guards - Remove the unused legacy V9 `publishingConvictionAccount` Contract cache slot + its init resolve (DKGPublishingConvictionNFT is the only PCA contract on a V10 deploy); existing V10 read methods preserved. - v8-v9-archive guard: stop guarding `createConvictionAccount` and `getConvictionAccountInfo` — those names are reclaimed by the V10 DKGPublishingConvictionNFT surface (PRD §6). - mock-adapter-parity: exempt the seven V10 PCA methods + the private requireConvictionNFT helper; TB-0002 mirrors them on the mock and removes the exemptions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
In-memory account map with incrementing id and owner = signer; the V10 read shape mirrors DKGPublishingConvictionNFT.getAccountInfo. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…nt/isConvictionAgent Bidirectional agent map mirrors agentToAccountId; re-registration throws AgentAlreadyRegistered for N28 parity. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
topUp accumulates the persistent buffer; settle is a permissionless no-op sweep on the mock. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…Registered) topUp/register/deregister are owner-gated; the owner revert is surfaced, never swallowed (daemon maps it to 403). settle stays permissionless. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
All seven DKGPublishingConvictionNFT methods reject via the shared noChain() helper instead of returning a half-set 0n/null. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drop the seven TB-0001 exemptions now that MockChainAdapter mirrors the DKGPublishingConvictionNFT write+read surface; full chain suite green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ress/baseEpochAllowance) createConvictionAccount/topUp reject zero, negative and uint96-overflow amounts (InvalidAmount); registerConvictionAgent rejects the zero address (ZeroAgentAddress); getAccountInfo.baseEpochAllowance is committedTRAC / lockDurationEpochs — matching DKGPublishingConvictionNFT. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Delete the five return-null V9 PCA stubs (createPublishingConvictionAccount, addPublishingConvictionAccountFunds, addPCAAuthorizedKey, isPCAAuthorizedKey, getPublishingConvictionAccountInfo) and replace them with thin delegating wrappers over the V10 chain-adapter surface: createConvictionAccount (no lockEpochs), topUpConvictionAccount, registerConvictionAgent, deregisterConvictionAgent, isConvictionAgent, settleConvictionAccount, getConvictionAccountInfo. Owner reverts propagate (not swallowed); a null return means the adapter has no V10 PCA surface. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Update the only live caller of the deleted V9 PCA facade names so `pnpm -r build` stays green: createConvictionAccount/topUpConvictionAccount/ registerConvictionAgent/isConvictionAgent/getConvictionAccountInfo, TxResult .hash field, and a V10 serializeAccountInfo shape. Route paths/semantics (authorize, settle, owner-revert 403) are rewritten in TB-0004. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Address review blockers:
- POST /api/pca no longer requires/echoes the removed V9 lockEpochs
field; a V10 body with only { tokens } now returns 200, not 400.
- Owner-gated register/top-up reverts (NotAccountOwner from the V10
contract and MockChainAdapter) now map to HTTP 403 via a shared
isOwnerRevert() helper instead of falling through as HTTP 500.
Adds packages/cli/test/daemon-pca-routes.test.ts covering both.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
NoChainAdapter implements the V10 PCA methods but throws noChain() rather than returning null, so no-chain/pre-chain daemons were regressing to HTTP 500 instead of the documented 503 unavailable contract. Add isNoChain() and short-circuit every PCA route catch (create/authorize/funds/info) to 503 before the generic 500. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The /api/pca rewire dropped lockEpochs and now returns the V10 getAccountInfo shape. Update the operator caller surface to match: - createPca request/response drops lockEpochs; `pca create` no longer takes --epochs or prints lockEpochs. - getPcaInfo response and `pca info` print the V10 fields (owner, committedTRAC, topUpBuffer, baseEpochAllowance, epoch window, agentCount, lastSettledWindow, fullySwept, discountBps). - admin→owner / authorizedKeys→conviction-agent wording. Calibrated via tsc: api-client V10 types first (build RED on the old cli.ts field reads), then cli.ts updated (build GREEN). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
POST /api/pca/:id/agent registers a publishing agent against the V10 DKGPublishingConvictionNFT, owner-gated (NotAccountOwner -> 403). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Owner-gated V10 agent deregistration; NotAccountOwner -> 403, no-chain -> 503. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Permissionless V10 lazy-settlement sweep; no-chain -> 503. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the V9 PublishingConvictionAccount narrative with the V10 DKGPublishingConvictionNFT contract description and the full owner-gating / permissionless-settle route contract; drop stale authorizedKeys and V10.1 wording. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
api-client.ts authorizePcaKey (POST /api/pca/:id/authorize, { key })
is a V9 PCA DTO. Replace with registerPcaAgent (POST /api/pca/:id/agent,
{ agent }) matching the V10 daemon route contract, and rewire the
`dkg pca` command (authorize -> register-agent).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Maps to DELETE /api/pca/:id/agent/:address. Adds a private del() helper (no DELETE verb existed) and the `dkg pca deregister-agent` command. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Maps to the permissionless POST /api/pca/:id/settle route and the `dkg pca settle` command. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…e lifecycle Standalone on-chain assertions for the DKGPublishingConvictionNFT lifecycle: createAccount mints + records the discount tier, topUp is owner-gated and leaves committedTRAC/expiry untouched, register/ deregister maintain the agent reverse map + agentCount, and settle advances the lazy-settlement cursor then marks the account fully swept post-expiry. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ish() Registers an agent, publishes through the real KAV10.publish() with p.epochs == lockDurationEpochs, and asserts on chain that the staker- pool distribution equals the DISCOUNTED cost (tokenAmount * (1 - discountBps/1e4)) while the KC still records the full tokenAmount — proving the conviction discount branch executed, not the direct-spend fallthrough. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
publish() gates the discount off and falls through to direct spend on an expired PCA (no brick). The expiry revert lives in the conviction funding entrypoint coverPublishingCost; drive it via an EOA standing in for KnowledgeAssetsV10 and assert AccountExpired once block.timestamp passes expiresAtTimestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Review blocker: the expired-account criterion requires a real publish() attempt, not a direct coverPublishingCost() call against a Hub-rewired EOA. Rewrite test 3 to advance past expiresAtTimestamp and call the real KAV10.publish(): the conviction discount is gated off, publish() falls through to the direct-spend branch, _addTokens reverts TooLowAllowance (the registered agent was only funded for the up-front committedTRAC, never approved KAV10 for a direct spend), and no KC is minted (atomic rollback). The same unfunded agent publishing the same params pre-expiry succeeds via the NFT-funded discount branch (test 2), so the revert is expiry-driven. Shared publisher/agent/CG setup extracted into setupRegisteredAgentPublish(). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Issue #519 TB-0007 gate 5. The PCA HTTP smoke must assert the discounted cost on chain, not just an HTTP 200 — KnowledgeAssetsV10 silently demotes to the no-discount branch when the publishing wallet is not a registered agent / epochs != lockDurationEpochs. Add the pure assertion helper + un-ignore the .devnet smoke entrypoint trio so the literal test_command can reach .devnet/run.mjs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Issue #519 TB-0007 acceptance criterion 3 — the scripted PCA flow must write round-trip evidence to .scratch/issue-519/verify.md. Pure table builder, one row per step, PASS/FAIL verdict. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Review blocker 2: the smoke registered only publisher-wallets[0], but the daemon publisher rotates operational wallets, so the wallet that actually signed the publish tx may not be a registered agent — KnowledgeAssetsV10 then silently demotes to the no-discount branch and the failure surfaces as an opaque discount-math error. This pure helper checks the publish tx signer's on-chain agentToAccountId against the smoke's account and reports a distinct, actionable failure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses both review blockers: 1. ensureDevnetLive no longer reuses an already-green devnet — it ALWAYS stop+wipe+boots a clean devnet, so the on-chain agentToAccountId map and the daemon's publish-signer rotation start from a deterministic empty state (no AgentAlreadyRegistered on re-run, no drifted signer). 2. readNode1 now returns the full operational+publisher wallet union; the smoke registers every candidate idempotently via classifyAgentRegistration (skips wallets already bound to this account, hard-fails on a foreign binding) and, after publish, binds the actual tx signer (receipt.from) to the account via assertPublishSignerBound BEFORE the discount math — a silent demotion now fails with an actionable message instead of an opaque discount-math error. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reflect the forced clean restart, idempotent multi-wallet registration, and publish-signer binding; unit suite now 12/12. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Post-wiring architecture section: call path, ChainAdapter V10 PCA surface, the V9->V10 semantic break, owner-gating, the daemon HTTP contract, and the clean-devnet on-chain discount assertion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The PCA devnet smoke (.devnet/run.mjs + pca-smoke-lib*) and the .scratch/issue-519 verify note are verification scaffolding, not feature code — they do not belong in the PR. Reverts the .gitignore negation hack that force-tracked them. Runtime verification is done locally and the evidence lives in the PR description instead. Also gitignores .scratch/ so local scratch can't leak into a PR again. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pca.ts header 23→4 lines, chain-adapter + dkg-agent JSDoc condensed. Keeps the load-bearing why (owner-revert→403, no-chain→503); drops prose now covered by ARCHITECTURE.md § #519. Comment-only; build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review feedback: 1. Naming: `*ConvictionAccount`/`*ConvictionAgent` were ambiguous with the Staking Conviction NFT (both are "conviction accounts"). Restore the explicit `*PublishingConviction*` qualifier the V9 names carried for exactly this disambiguation, across the chain-adapter interface, evm/mock/no-chain impls, the DKGAgent facade, daemon routes, api-client, tests, and ARCHITECTURE.md. The bare V9 `Conviction` names go back into the v8-v9-archive guard (V9-only again). 2. Comments: every multi-line V10 PCA comment block trimmed to <=2 lines (load-bearing why kept; prose lives in ARCHITECTURE.md § #519). Build green; chain/agent/cli suites green; devnet HTTP round-trip re-verified 6/6 PASS post-rename (HTTP API unchanged). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rity Addresses codex review on #527: - A: EVMChainAdapter throws typed PcaUnavailableError when DKGPublishingConvictionNFT is undeployed (write + read paths); daemon maps it → 503. getPublishingConvictionAccountInfo throws for undeployed, null only for account-missing (→ 404), removing the old 500/404 ambiguity on pre-V10 hubs. - C: deterministic PCA reverts now map to 4xx — InvalidAmount→400, Agent{Already,Not}Registered→409, AccountExpired→409 — across all write handlers (was opaque 500). - D: api-client probe field authorized→registered (V10 agent terminology; drops retired V9 "authorized key" wording). - B (mock parity): MockChainAdapter computes the exact contract getDiscountBps tier ladder + persists createdAtEpoch / derives expiresAtEpoch. Settlement cursor / fullySwept deliberately not modeled (covered by hardhat + devnet against the real NFT). Build green; chain 360 / cli 1123 suites green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Codex review findings addressed in
Verified: build + chain (360) / cli (1123) suites green; devnet HTTP |
- classifyPcaRevert now covers all route-reachable deterministic PCA errors: ZeroAgentAddress→400, TokenTransferFailed→400, AgentCapReached→409, AccountAlreadyFullySettled→409 (was 500). Enumerated from DKGPublishingConvictionNFT.sol:164-179 to end the incremental review loop. - Fast zero-address reject (400) in POST and DELETE /api/pca/:id/agent before any RPC (ethers.isAddress accepts 0x00..0). - MockChainAdapter enforces maxAgentsPerAccount (default 100, mirrors contract :208/:711-712), throws AgentCapReached past the cap so mock-backed tests catch the regression. TDD: each behavior driven red→green (6 cli route tests, 3 mock tests). Build green; chain 363 / cli 1129 suites green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Codex round-2 findings addressed in
Built test-first (TDD, red→green per behavior): 6 new daemon-route tests, 3 new mock tests. Verified: |
codex round-3: - NoChainAdapter no longer defines the 7 optional #519 PCA methods, so the DKGAgent facade's `typeof` guard returns the documented `null` (was: throwing noChain() to direct SDK callers in no-chain mode). Daemon still 503 (facade null → existing path); GET capability probe yields 503 not 404. Interface marks them optional so omission is valid. - Mock: lastSettledWindow/fullySwept marked STATIC STUBS and settlePublishingConvictionAccount a DELIBERATE NO-OP. Settlement is intentionally out of mock-parity scope (contract accounting, verified on-chain by hardhat + devnet) — honest scoping, not emulation. Pinning test locks the intentional stub so it can't silently half-model. TDD red→green. Build green; chain 365 / cli 1130 suites green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Codex round-3 addressed in
TDD red→green (facade-null contract test + no-chain GET 503 test + mock stub-pin test). Build green; chain 365 / cli 1130 suites green. Skipped a devnet re-run by design — these changes touch only the no-chain/mock adapters + comments; the devnet path uses the EVM adapter (unchanged, verified at the prior SHA). Only remaining red CI lane is still the #500-inherited coverage ratchet. |
…round 4) - classifyPcaRevert: map OZ v5 ERC721NonexistentToken (+ legacy string fallback) to 404 UnknownAccount, so topUp/register/deregister/settle return a client error for an unminted PCA id instead of HTTP 500 (GET already 404s — writes are now consistent). +3 route tests. - mock-adapter: correct two comments that overclaimed contract parity; the mock is boundary-aligned (no wall clock) so it does not model the contract's mid-epoch expiry round-up. Comment-only, zero logic change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex review round 4 — addressed in
|
…l (codex round 5) - register route: wrap the post-write isPublishingConvictionAgent probe in its own try/catch so a probe failure can never turn a mined registration tx into HTTP 500 (caller would retry → AgentAlreadyRegistered). Response is now 200 + txHash whenever the tx mined; registered/adapterSupported is tri-state, matching the GET route's probedKey shape. - DKGAgent: add public supportsPublishingConvictionNft getter; GET route uses it for 404-vs-503 instead of casting into the private chain field. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ound 6) PCA write methods rethrew raw ethers CALL_EXCEPTIONs; providers report custom errors as opaque "unknown custom error"+data, so the daemon's message-based classifier never saw NotAccountOwner/InvalidAmount/ERC721- NonexistentToken and returned 500 instead of 403/4xx. Wrap the 5 writes in a pcaWrite() helper that runs enrichEvmError() on throw then rethrows, mirroring isContractMissingRevert/translateRandomSamplingError. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5ea5440
into
feat/archive-non-v10-contracts
Codex review rounds 5 & 6 — addressed (pushed:
|
| // committedTRAC) — the signer must allow the NFT to pull the TRAC. | ||
| if (this.contracts.token) { | ||
| const allowance: bigint = await this.contracts.token.allowance(this.signer.address, nftAddress); | ||
| if (allowance < committedTRAC) { |
There was a problem hiding this comment.
🔴 Bug: approve() happens before we validate that the requested amount fits the contract's uint96 parameter. If a caller passes something larger than 2^96-1, the approval tx will still go through and then createAccount/topUp reverts with InvalidAmount, leaving an unintended unlimited allowance behind. Please reject out-of-range values before the allowance branch here and in topUpPublishingConvictionAccount() below.
| authorized: verified === true, | ||
| txHash: result.txHash, | ||
| agent: agentAddr, | ||
| registered: verified === true, |
There was a problem hiding this comment.
🔴 Bug: this reports registered: false whenever the post-write probe is unavailable or transiently fails, even though the tx already mined successfully. That makes the HTTP contract contradict the actual write result and can trigger pointless retries that then hit AgentAlreadyRegistered. The success field should stay true after a successful tx, with probe/verification exposed separately.
| async topUpPublishingConvictionAccount(accountId: bigint, amount: bigint): Promise<TxResult> { | ||
| const acct = this.requireConvictionOwner(accountId); | ||
| this.requireValidConvictionAmount(amount); | ||
| acct.topUpBuffer += amount; |
There was a problem hiding this comment.
🟡 Issue: the mock only validates each individual top-up amount, not the accumulated topUpBuffer. On chain topUpBalance is uint96, so repeated top-ups past 2^96-1 revert; here they keep succeeding and diverge from EVM behavior. Add a cumulative overflow check before += and cover it with a parity test.
Summary
Wires the V10
DKGPublishingConvictionNFTwrite+read surface end-to-end through the SDK so operators can create, fund, and manage Publishing Conviction Accounts via the chain-adapter, theDKGAgentfacade, and the daemon HTTP API.POST /api/pcaand the funds/agent/settle routes now return 200 with real tx hashes instead of 503.Closes #519. Sibling cleanup tracked in #520.
Stacked on #500 (
feat/archive-non-v10-contracts) — base of this PR is that branch, notmain. #519's "drop the V9 null stubs" only makes sense on top of #500's archive. #500 is still open (basemain); rebase this ontomainonce #500 merges.Why
The V10 PCA NFT contract is deployed and invoked by
KnowledgeAssetsV10.publish()on-chain, but the SDK exposed only read shims — all PCA write methods returnednull, so the daemon/api/pca/*routes returned HTTP 503. The V9PublishingConvictionAccountpredecessor was archived in #500. This PR closes the SDK gap (the follow-up ADR-0001 explicitly named).What changed (transitive — every consumer in one PR, the #500 lesson)
packages/chain/src): new V10 interface surface —createConvictionAccount(committedTRAC),topUpConvictionAccount,registerConvictionAgent,deregisterConvictionAgent,isConvictionAgent,settleConvictionAccount, V10 12-tuplegetConvictionAccountInfo. EVM impl +mock-adapter/no-chain-adapterparity. Dead V9publishingConvictionAccountcache slot removed.packages/agent/src/dkg-agent.ts): fivereturn nullPCA stubs deleted, delegate to the new adapter methods,lockEpochsdropped, key→agent terminology, callers fixed.packages/cli/src/daemon/routes/pca.ts): V10-shapedserializeAccountInfo;POST /api/pcabody dropslockEpochs;:id/authorizereplaced byPOST :id/agent+DELETE :id/agent/:address;POST :id/settleadded;:id/funds→topUp; owner revert → 403.packages/cli/src/api-client.ts): V10 request/response shapes;registerPcaAgent/deregisterPcaAgent/settlePcamethods + commands.packages/evm-module/test/v10-pca-lifecycle.test.ts(create → topUp → registerAgent → discounted publish via realKnowledgeAssetsV10.publish()→ expiry revert); chain adapter + mock↔EVM parity; daemon route round-trip; devnet HTTP smoke.V9 → V10 semantic break (DTOs changed shape — not a rename)
lockEpochsargauthorizedKeys+adminregisterAgent+agentToAccountId(1 account/agent)adminfieldownerOf(accountId)addFunds→ balancetopUp→ persistenttopUpBalancegetAccountInfoBreaking HTTP contract is acceptable: the V9 routes only ever returned 503 on V10 deployments — no working client to break. The same reasoning covers the
ApiClientTypeScript surface (createPcadropslockEpochs; PCA probe fieldauthorized→registered; method renames): an intentional, coordinated break for the10.0.0-rcmajor — V9 is archived in stacked base #500, so a deprecated shim would only call a removed contract. Broader V9 SDK excision is tracked by #520.Owner-gating (curation trust model)
createConvictionAccountmints to the signer;topUp/register/deregisterare owner-only on chain (msg.sender == ownerOf(accountId)). The SDK surfaces theNotAccountOwnerrevert (not swallowed); the daemon maps it to 403 (distinct from 503 = no-chain). Agents publish only.Verification
Built via 8 tracer-bullet slices (one commit-set each, TDD red→green, independent reviewer gating each slice — 37 commits). Reviewer caught and forced fixes for real defects, notably:
InvalidAmount/ZeroAgentAddress/baseEpochAllowance)lockEpochsleak)KnowledgeAssetsV10.publish()0 < discountedCost < baseCoston chain (guards the silent-demotion risk: KAv10 takes the discount branch only whenpublishEpochs == lockDurationEpochs)Gates executed in-run (per slice, reviewer-gated):
pnpm -r build;pnpm --filter @origintrail-official/dkg-chain test;pnpm --filter @origintrail-official/dkg-evm-module exec hardhat test; daemon route tests; clean-devnet HTTP/api/pcaround-trip with on-chain discount assertion. CI (Tornado lanes) is the remaining gate this PR triggers.Out of scope (followups)
evm-adaptermethods, non-PCA interface cleanup).DKGStakingConvictionNFTSDK surface (createConviction/claim) — separate feature.🤖 Generated with Claude Code
Post-review cleanup (2026-05-15)
Addressed reviewer feedback:
25ca4530): removed.devnet/run.mjs+pca-smoke-lib*and.scratch/issue-519/verify.mdfrom the PR, reverted the.gitignorenegation hack that force-tracked them, and added.scratch/to.gitignore. The devnet smoke is verification scaffolding, not feature code — it runs locally; evidence lives here in the PR body. (Root cause: the orchestratortest_commandwascd .devnet && node run.mjs, which forced the harness into the tree.)1bd98146):pca.tsheader 23→4 lines;chain-adapter.ts+dkg-agent.tsJSDoc condensed (−43 net comment lines). Kept the load-bearing why (owner-revert→403, no-chain→503); prose moved toARCHITECTURE.md § #519.Net PR diff is now
packages/*(feature) +.gitignore(policy) +ARCHITECTURE.mdonly.pnpm -r buildgreen.Independent devnet runtime verification
Booted a clean 2-node devnet and drove the full operator path through the live daemon (not the in-run subagent — independently re-run end-to-end):
POST /api/pcaPOST /api/pca/:id/agentGET /api/pca/:idCostCoveredemitted for account 1; discountedCost ==baseCost·(10000−discountBps)/10000(exact on-chain tier formula)The
CostCoveredevent firing fromDKGPublishingConvictionNFTfor the account proves the publish routed throughcoverPublishingCost(a demoted/direct-spend publish emits none). Devnet zeroes token economics (base cost = 1 wei), so the discounted value floors to 0 — the assertion verifies the discount formula was applied exactly, robust to dust-sized base costs.Known blocker — deferred to #500
CI is green except one job: Tornado: Solidity coverage (push safety net) — ratchet failure (lines 54.84% < 60%, branches 45.38% < 48%, functions 54.85% < 65%).
This is not introduced by #519. It is inherited from the base branch: #500 (
feat/archive-non-v10-contracts) archived ~14 kLOC of V8/V9 Solidity tests, dropping evm-module coverage below the repo ratchet. #519 only adds tests (it cannot raise coverage above what #500 removed). Evidence the wiring itself is sound:CostCoveredon chain → discount per exact tier formula → GET round-trip)Resolution owner: #500. The coverage ratchet (lower thresholds to the post-archive reality, or add V10 contract tests) belongs in the archive PR's scope, not this feature PR. #519 rebases onto
mainonce #500 lands with coverage resolved. Tracked via a comment on #500.Post-review round 2 (2026-05-15)
c7283678): the V10 SDK methods were renamed*ConvictionAccount/*ConvictionAgent→*PublishingConviction*(createPublishingConvictionAccount,topUpPublishingConvictionAccount,registerPublishingConvictionAgent,deregisterPublishingConvictionAgent,isPublishingConvictionAgent,settlePublishingConvictionAccount,getPublishingConvictionAccountInfo, typeV10PublishingConvictionAccountInfo). "Conviction account" is ambiguous in this codebase — there are two: publishing (DKGPublishingConvictionNFT, this PR) and staking (DKGStakingConvictionNFT). The V9 names carried thePublishingqualifier for exactly this reason; the bare names were a regression I introduced in the spec. Applied transitively (interface, evm/mock/no-chain impls, facade, daemon routes, api-client, tests, ARCHITECTURE.md); bareConvictionnames returned to the v8-v9-archive guard as V9-only.c7283678): every multi-line V10 PCA comment block trimmed; load-bearing why kept, prose in ARCHITECTURE.md § SDK + daemon PCA write surface is wired to archived V9 contract; V10 PCA NFT has no SDK path #519.Verification after rename:
pnpm -r buildgreen; chain (352✓) / agent (461✓) / cli (1117✓) suites green; devnet HTTP round-trip re-verified 6/6 PASS (HTTP API surface unchanged — rename is internal). Note: the two pre-existing read methodsgetConvictionAgentAccountId/getConvictionAccountLockDurationEpochsare on the base branch (not introduced here) and left as-is to avoid scope creep — minor pre-existing naming debt.Post-review round 3 — codex findings addressed (
c5762e2d)PcaUnavailableError(codePCA_UNAVAILABLE);evm-adapterthrows it on write (requireConvictionNFT) and read (getPublishingConvictionAccountInfo) when the NFT is absent; daemon maps → 503.nullnow means account-missing only → 404. No more 500/404 on pre-V10 hubs.InvalidAmount→400,Agent{Already,Not}Registered→409,AccountExpired→409 across all 5 write handlers.authorized(V9 wording)authorized→registered(route + client type + tests + a stalecli.tsref).MockChainAdaptermirrors the contract's exactgetDiscountBpstier ladder; persistscreatedAtEpoch, derivesexpiresAtEpoch. Settlement cursor /fullySweptdeliberately not modeled in the mock (that is contract accounting — covered by the evm-module hardhat suite + the devnet smoke against the real NFT); documented in-code.Verification at
c5762e2d:pnpm -r build✓; chain 360✓ / cli 1123✓ suites green; devnet HTTP round-trip re-verified 6/6 PASS (happy path unaffected — A/C/D are error-path/serialization, B is mock-only).Post-review round 4 — codex round-2 findings addressed (
351c2614)classifyPcaRevertincomplete (ZeroAgentAddress / AgentCapReached → 500)DKGPublishingConvictionNFT.sol:164-179):ZeroAgentAddress→400,TokenTransferFailed→400,AgentCapReached→409,AccountAlreadyFullySettled→409. Also a fast zero-address reject (400, pre-RPC) in POST + DELETE/api/pca/:id/agent.maxAgentsPerAccountMockChainAdapterenforces the contract default cap 100 (mirrors:208/:711-712), throwsAgentCapReachedpast the cap so mock-backed tests catch the regression.Enumerated the contract's full custom-error set to end the incremental review loop (covered 4 reachable errors, not just the 2 flagged). TDD: each behavior driven red→green (6 cli route tests, 3 mock tests). Verification at
351c2614:pnpm -r build✓; chain 363 / cli 1129 suites green; devnet HTTP round-trip re-verified 6/6 PASS.