feat: devnet (moutai) support + evo-sdk 4.2.0-dev.2 - #304
Conversation
Pins the exact dev.2 build, which carries the protocol-14 additions Layer 2 needs (refersTo / ReferencedEntityNotFound / timeRange / requiredSince).
Verified before landing: the standing byte-array regression check (memory evo-sdk-4-1-byte-array-write-regression) serializes a like create transition built through Document.fromObject and the byte field lands as 0a 20 + 32 raw bytes (Value::Bytes); the new Document({properties}) negative control still produces the broken 15 20 02 marker, so the check itself is sound. Followed by a real testnet write on the staging contract as bot0: bookmark create, read-back with postId bytes intact, delete.
NEXT_PUBLIC_NETWORK=devnet now builds an EvoSDK against an explicit DAPI address pool (NEXT_PUBLIC_DAPI_ADDRESSES) for the devnet named by NEXT_PUBLIC_DEVNET_NAME. The typed constructor is used rather than EvoSDK.devnetTrusted() because that factory has no addresses parameter, and devnets publish no masternode list to discover them from.
Proof verification is not optional on devnet: wasm-sdk 4.2.0-dev.2 panics on proofs:false ('queries without proofs are not supported yet') and rejects non-trusted proof verification outright ('Non-trusted mode is not supported in WASM'). Every devnet read therefore needs a trusted context, prefetched from a quorum service at NEXT_PUBLIC_QUORUM_URL (/quorums, /previous and /masternodes); the built-in default host quorums.<devnetName>.networks.dash.org does not exist for moutai.
Devnets reuse testnet address and WIF prefixes, so network now splits in two: getConfiguredNetwork() is what the SDK connects to, keyNetwork() is what key material is encoded for and collapses devnet to testnet. Every key-derivation, signer-matching and secure-storage call site moves to keyNetwork(); only SDK construction sees 'devnet'. DPNS_CONTRACT_ID and the Insight base URL become overridable per deployment.
scripts/sdk-env.mjs replaces the hardcoded EvoSDK.testnetTrusted in the six operational scripts. NETWORK (plus DEVNET_NAME, DAPI_ADDRESSES, QUORUM_URL, read from .env.devnet when not in the environment) selects the chain; testnet stays the default, so every existing invocation is unchanged. provision-test-identity gains a devnet mode. InstantSend asset-lock proofs are refused there: rs-dapi on the devnet build never forwards the IS lock, so an InstantSend-funded lock silently burns the funds (dashpay/platform#4399). Instead the asset-lock address is funded out of band and handed back as --funding-outpoint <txid>:<vout>; the script polls Insight for the funding tx's block height and DAPI getStatus for coreChainLockedHeight, with a 600s budget, then builds a ChainLock proof. --gen-asset-lock-key now prints the P2PKH address to pay, and every one-shot asset-lock key is appended to the gitignored .devnet-locks.local before anything is broadcast. register-test-contracts can clone from another chain (--source-network, --from-social, --from-profile) and now carries the source contract's tokens block across. tokenCost lives in the document schemas and always followed them, so dropping the token configuration left the copy costing a token that did not exist. set-yapp-price takes --contract and a seed-derived --owner-index so the same price can be set on a copy.
npm run build:devnet sources the checked-in .env.devnet and builds with BASE_PATH=/devnet, mirroring build:testing. deploy.yml rebuilds the staging checkout a second time with it, so /devnet always tracks the staging commit; client-side storage is scoped by base path, so it stays isolated from /, /staging and /testing. The step is continue-on-error: the devnet chain is disposable and re-genesises without notice, and a broken /devnet must never hold back the /staging deploy. .env.devnet carries the moutai wiring (DAPI pool, insight, devnet name) and documents that NEXT_PUBLIC_QUORUM_URL must be filled in before /devnet can read anything.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Deploying yappr with
|
| Latest commit: |
a7fe12d
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://3fff712b.yappr.pages.dev |
| Branch Preview URL: | https://feat-devnet-moutai-support.yappr.pages.dev |
SdkProvider hardcoded network: 'testnet'. It wraps the whole app and usually wins the race against the on-demand callers, so on a /devnet build every useSdk() consumer — the store, checkout, blog, DPNS and homepage paths — would have read testnet through the shared singleton until some later caller forced a reinit. evoSdkService.initialize() also compared only network and contractId when deciding whether the existing instance could be reused, so a devnet config differing solely in its address pool or quorum URL would have been silently ignored; it now compares the whole config.
…ock survives
new DataContract({ schemas, tokens }) only accepts live TokenConfiguration handles, so the tokens option is unusable for anything read back from JSON, and the schemas-only form silently dropped tokens, groups, config, keywords and description. DataContract.fromJSON round-trips all of it, so the copy is a faithful clone; publishContract now also asserts the published contract came back with the token count it was given.
Verified against testnet: the re-owned JSON builds, signs and passes Drive's state transition validation, failing only on the owner's balance (a 16-doctype contract with the YAPP token costs ~93,000,100,000 credits and the e2e bots hold less).
Also: connectSdk resolved the network twice, insightUrl stripped its trailing slash on two separate paths, keyNetwork carried a parameter no caller passed, and owner-keys.mjs reported 'not found on testnet' regardless of the network it had connected to.
Deploying yappr-v2 with
|
| Latest commit: |
a7fe12d
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://43532637.yappr-v2.pages.dev |
| Branch Preview URL: | https://feat-devnet-moutai-support.yappr-v2.pages.dev |
|
🔍 Review in progress — actively reviewing now (commit a7fe12d) |
thepastaclaw
left a comment
There was a problem hiding this comment.
Preliminary review — Codex only
The network split and contract-cloning changes are structurally sound, but four in-scope blockers remain. The ChainLock provisioner reads a nonexistent SDK field, devnet payment polling targets testnet, the cloned contract owner cannot access token moderation, and the generated price-setting command silently switches back to testnet.
Source: Codex reviewer backend model was not provided in the supplied evidence; final Claude verifier backend model was not exposed to this agent; orchestration-only and not reviewer evidence: openclaw-agent/cliproxy/gpt-5.6-sol.
Validated blockers were found in the Codex precheck. Opus is deferred until a fresh Codex revalidation clears the blocker gate.
Review provenance
- Codex reviewers:
gpt-5.6-sol— general (completed) - Verifier:
gpt-5.6-sol— verifier - Sonnet: not run (deferred by blocker gate)
🔴 4 blocking
2 additional finding(s) omitted (not in diff).
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `scripts/provision-test-identity.mjs`:
- [BLOCKING] scripts/provision-test-identity.mjs:204-205: Read the SDK's snake-case ChainLock height
`sdk.system.status()` returns the WASM `StatusResponse`. In the installed `@dashevo/wasm-sdk` 4.2.0-dev.2 declaration and runtime implementation, `StatusChain` exposes `core_chain_locked_height`; it has no `coreChainLockedHeight` getter. This expression therefore always falls back to zero, so every `--funding-outpoint` run waits for 600 seconds and times out even after the funding block is chain-locked, preventing the new devnet identity provisioning path from completing.
In `lib/constants.ts`:
- [BLOCKING] lib/constants.ts:10-12: Configure the token authority per deployment
The cloned token configuration authorizes `ContractOwner` for freeze, unfreeze, destruction, emergency actions, and price changes. On devnet that owner is the newly provisioned maker identity, whose ID differs from the hard-coded testnet authority because it is derived from a different asset-lock outpoint. `app/settings/page.tsx` compares the logged-in identity against this constant, so the actual devnet contract owner cannot see or use the moderation controls. Make the authority publicly configurable and populate the corresponding `.env.devnet` value alongside the maker and contract IDs after provisioning.
In `lib/services/insight-api-service.ts`:
- [BLOCKING] lib/services/insight-api-service.ts:132-137: Route devnet tDASH polling to the configured Insight API
Devnet uses testnet address prefixes and consequently the existing `tdash:` payment scheme, but this helper always converts that scheme to `testnet`. `use-dash-transaction-watcher.ts` passes the returned network explicitly to `waitForUtxo`, overriding its new `getConfiguredNetwork()` default. A `/devnet` checkout therefore polls the testnet Insight host instead of `NEXT_PUBLIC_INSIGHT_API_URL`, so native devnet payments are never detected.
In `scripts/register-test-contracts.mjs`:
- [BLOCKING] scripts/register-test-contracts.mjs:226-232: Preserve the target network in the generated price command
The documented `NETWORK=devnet node scripts/register-test-contracts.mjs ...` assignment applies only to the registration process and does not persist in the shell. The emitted follow-up command omits `NETWORK`, while `set-yapp-price.mjs` defaults to testnet through `sdk-env.mjs`. Following the generated instructions therefore connects to testnet and cannot fetch or update the newly published devnet contract, leaving the cloned YAPP token without its required direct-purchase price.
| if (published.NEXT_PUBLIC_YAPPR_CONTRACT_ID) { | ||
| console.log(''); | ||
| console.log('Then set the YAPP direct-purchase price on the new social contract:'); | ||
| console.log( | ||
| ` node scripts/set-yapp-price.mjs --contract ${published.NEXT_PUBLIC_YAPPR_CONTRACT_ID}` + | ||
| ` --owner ${ownerId} --owner-index ${args.ownerIndex}` | ||
| ); |
There was a problem hiding this comment.
🔴 Blocking: Preserve the target network in the generated price command
The documented NETWORK=devnet node scripts/register-test-contracts.mjs ... assignment applies only to the registration process and does not persist in the shell. The emitted follow-up command omits NETWORK, while set-yapp-price.mjs defaults to testnet through sdk-env.mjs. Following the generated instructions therefore connects to testnet and cannot fetch or update the newly published devnet contract, leaving the cloned YAPP token without its required direct-purchase price.
| if (published.NEXT_PUBLIC_YAPPR_CONTRACT_ID) { | |
| console.log(''); | |
| console.log('Then set the YAPP direct-purchase price on the new social contract:'); | |
| console.log( | |
| ` node scripts/set-yapp-price.mjs --contract ${published.NEXT_PUBLIC_YAPPR_CONTRACT_ID}` + | |
| ` --owner ${ownerId} --owner-index ${args.ownerIndex}` | |
| ); | |
| console.log( | |
| ` NETWORK=${network()} node scripts/set-yapp-price.mjs --contract ${published.NEXT_PUBLIC_YAPPR_CONTRACT_ID}` + | |
| ` --owner ${ownerId} --owner-index ${args.ownerIndex}` | |
| ); |
source: ['codex']
…g requires
Faucets pay plain P2PKH, but Platform rejects such outpoints ('Funding transaction must have an Asset Lock Special Transaction Payload'). build-asset-lock.mjs spends the faucet UTXO into a DIP-2 type-8 transaction shaped per v4.2-dev's validate_asset_lock_transaction_structure_v0 (OP_RETURN burn output, P2PKH credit output in the payload, proof outpoint txid:0). Note: moutai's nodes run a low -maxtxfee, so the fee is 500 duffs.
.env.devnet now carries the live moutai state: v3-draft social contract (with refersTo + YAPP price set), profile clone, maker + bot identities registered 2026-08-27 via ChainLock asset locks.
thepastaclaw
left a comment
There was a problem hiding this comment.
Preliminary review — Codex only
The SDK network/key-network split and static devnet deployment are directionally sound, but eight in-scope blockers remain. All four previously verified blockers are still present, and the current head also selects testnet identities during bare devnet script runs, drops devnet between the new asset-lock commands, and exposes features backed only by testnet contract IDs. Source: Codex reviewer backend model was not provided in the supplied evidence; final Claude verifier backend model was not exposed to this agent; orchestration-only and not reviewer evidence: openclaw-agent/cliproxy/gpt-5.6-sol.
Validated blockers were found in the Codex precheck. Opus is deferred until a fresh Codex revalidation clears the blocker gate.
Review provenance
- Codex reviewers:
gpt-5.6-sol— general (completed) - Verifier:
gpt-5.6-sol— verifier - Sonnet: not run (deferred by blocker gate)
🔴 7 blocking
4 additional finding(s) omitted (not in diff).
2 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `lib/constants.ts`:
- [BLOCKING] lib/constants.ts:10-12: Configure the token authority per deployment
The cloned token authorizes its `ContractOwner` to freeze, unfreeze, destroy frozen balances, and perform the other administrative actions. The devnet contract maker recorded in `.env.devnet` is `DuqE3...`, but this UI gate remains fixed to the testnet authority `hbGEc...`. Because `app/settings/page.tsx` uses this constant to render the moderation section, the actual devnet contract owner cannot access those controls. Make the authority deployment-configurable and set `NEXT_PUBLIC_YAPP_TOKEN_AUTHORITY_ID` to the devnet maker identity in `.env.devnet`.
- [BLOCKING] lib/constants.ts:22-43: Do not route devnet features through testnet contract IDs
The devnet environment configures only the social and profile contracts, while the DM, storefront, encrypted-backup, key-exchange, vault, auth-vault, blog, and Pollr constants remain nonempty testnet fallbacks. The `/devnet` export still exposes messages, store, checkout, blog, polls, and vault-backed authentication, so their services query IDs that do not exist on moutai. Preloading merely logs several missing-contract failures, and checks such as `authVaultService.isConfigured()` incorrectly report that the fallback contract is available. Provision and configure devnet copies of these contracts, or make these values empty on devnet and gate the corresponding routes and controls.
In `lib/services/insight-api-service.ts`:
- [BLOCKING] lib/services/insight-api-service.ts:132-137: Route devnet tDASH polling to the configured Insight API
Devnet addresses use the testnet prefix and therefore retain the `tdash:` payment scheme, but this helper always maps that scheme to the SDK network `testnet`. `use-dash-transaction-watcher.ts` passes the result explicitly to `waitForUtxo`, overriding its deployment-aware default. A `/devnet` checkout consequently polls the testnet Insight host instead of `NEXT_PUBLIC_INSIGHT_API_URL`, so native devnet payments are never detected.
In `scripts/derive-identities.mjs`:
- [BLOCKING] scripts/derive-identities.mjs:137-140: Load the devnet identity pool for devnet script runs
`loadIdentityIds()` always falls back to `.env.testing`, even when `NETWORK=devnet`. The new `sdk-env.mjs` reads `.env.devnet` lazily but does not export its values into `process.env`, so a bare devnet invocation still loads the testnet IDs `GENx...` and `BSHD...` instead of the devnet IDs checked into `.env.devnet`. As a result, `provision-test-identity.mjs --check-balances`, `verify-poll-interop.mjs`, bot-based owner resolution, and the default registration owner query testnet identities through the devnet SDK unless the operator separately exports `E2E_IDENTITY_IDS`, contradicting the documented bare `NETWORK=devnet` workflow.
In `scripts/build-asset-lock.mjs`:
- [BLOCKING] scripts/build-asset-lock.mjs:19-23: Keep devnet selected for the provisioning command
The first command's `NETWORK=devnet` assignment is process-local and does not persist after `build-asset-lock.mjs` exits. The immediately following provisioning command omits it, so `provision-test-identity.mjs` defaults to testnet and attempts to use a moutai asset-lock outpoint against testnet. The saved key permits recovery and retry, but the newly documented end-to-end provisioning sequence cannot complete as written.
In `scripts/provision-test-identity.mjs`:
- [BLOCKING] scripts/provision-test-identity.mjs:204-205: Read the SDK's snake-case ChainLock height
(existing thread: https://github.com/PastaPastaPasta/yappr/pull/304#discussion_r3868677011)
`sdk.system.status()` directly returns the WASM `StatusResponse`. In the installed `@dashevo/wasm-sdk` 4.2.0-dev.2 declaration, `StatusResponse.chain` is a `StatusChain` whose accessor is `core_chain_locked_height`; there is no `coreChainLockedHeight` accessor. The current expression therefore always falls back to zero, so automatic `--funding-outpoint` provisioning waits for the full 600-second timeout even after the funding block has been chain-locked.
In `scripts/register-test-contracts.mjs`:
- [BLOCKING] scripts/register-test-contracts.mjs:226-232: Preserve the target network in the generated price command
(existing thread: https://github.com/PastaPastaPasta/yappr/pull/304#discussion_r3868677032)
The `NETWORK=devnet` assignment used to invoke contract registration applies only to that process. The generated follow-up command omits `NETWORK`, while `set-yapp-price.mjs` defaults to testnet through `sdk-env.mjs`. Following the printed reset/provisioning workflow therefore looks for the newly published contract on testnet and leaves the cloned devnet YAPP token without its direct-purchase price.
| * Run: | ||
| * NETWORK=devnet node scripts/build-asset-lock.mjs --key-file <keyfile> --outpoint <txid>:<vout> | ||
| * then: | ||
| * node scripts/provision-test-identity.mjs <idx> --asset-lock-key-file <keyfile> \ | ||
| * --funding-outpoint <printedTxid>:0 [--chain-lock <height>] |
There was a problem hiding this comment.
🔴 Blocking: Keep devnet selected for the provisioning command
The first command's NETWORK=devnet assignment is process-local and does not persist after build-asset-lock.mjs exits. The immediately following provisioning command omits it, so provision-test-identity.mjs defaults to testnet and attempts to use a moutai asset-lock outpoint against testnet. The saved key permits recovery and retry, but the newly documented end-to-end provisioning sequence cannot complete as written.
| * Run: | |
| * NETWORK=devnet node scripts/build-asset-lock.mjs --key-file <keyfile> --outpoint <txid>:<vout> | |
| * then: | |
| * node scripts/provision-test-identity.mjs <idx> --asset-lock-key-file <keyfile> \ | |
| * --funding-outpoint <printedTxid>:0 [--chain-lock <height>] | |
| * Run: | |
| * NETWORK=devnet node scripts/build-asset-lock.mjs --key-file <keyfile> --outpoint <txid>:<vout> | |
| * then: | |
| * NETWORK=devnet node scripts/provision-test-identity.mjs <idx> --asset-lock-key-file <keyfile> \ | |
| * --funding-outpoint <printedTxid>:0 [--chain-lock <height>] |
source: ['codex']
1) The chain-lock wait read coreChainLockedHeight off the live StatusChain class, which only exposes the snake_case getter - the value was always undefined, so every --funding-outpoint run timed out at 600s (this, not status caching, was the provisioning failure; verified live: camelCase=undefined, snake_case=41272). Accept both shapes. 2) YAPP_TOKEN_AUTHORITY_ID is now env-overridable and .env.devnet points it at the devnet contract owner so moderation controls gate correctly per deployment. 3) getNetworkFromScheme resolves tdash: to the configured network (devnets share testnet prefixes), so /devnet polls its own Insight host instead of testnet's. 4) register-test-contracts' emitted follow-up command now carries NETWORK= so the price step targets the network the contract was published on.
|
Triage of the four review findings — all four are valid, all fixed in 8aff4f7: 1. snake_case ChainLock height — valid, and it explains a live incident. Verified against moutai: 2. Token authority per deployment — valid, fixed. 3. tdash: → testnet hardcode — valid, fixed. 4. Emitted price command loses NETWORK — valid, fixed. The follow-up command now carries Validation: lint + build clean; snake_case getter verified live against moutai. 🤖 Posted autonomously by Claude on behalf of pasta. |
…y pool Optional contract constants now use ?? so .env.devnet can explicitly blank the eight contracts that have no moutai copy (DM, storefront, key backup, key exchange, vault, auth vault, blog, pollr) - preload skips them and every isConfigured() gate fails closed instead of querying testnet ids that do not exist on this chain. loadIdentityIds() picks .env.devnet under NETWORK=devnet (env var still wins), so bot-based scripts stop silently using the testnet pool. build-asset-lock's doc header repeats NETWORK=devnet on the provisioning command since the assignment is process-local.
|
Second review round triage — four of the seven findings were already fixed in 8aff4f7 (this round reviewed adfe64a): token authority (env-overridable, .env.devnet set), tdash: Insight routing, the snake_case ChainLock getter, and the price command's NETWORK prefix — see the earlier triage comment for details. The three new findings are all valid, fixed in the follow-up commit: Testnet fallbacks on /devnet — fixed. The eight optional contract constants switched from loadIdentityIds() ignoring NETWORK=devnet — fixed. It now reads build-asset-lock's doc header — fixed. The provisioning example now repeats Validation: lint + build clean; devnet identity loading verified. 🤖 Posted autonomously by Claude on behalf of pasta. |
Plan A of
PLAN_DEVNET_STAGING.md: bump the SDK, teach the app and the operational scripts to talk to a Dash devnet, and add a/devnetbuild of the staging commit.The provisioning half of the plan (A4/A5) could not be executed — moutai is currently unreadable from the WASM SDK. Details and evidence below. The code that would do it is here and ready;
.env.devnetcarries the wiring with the contract and identity slots left blank rather than filled with invented ids.A1 — evo-sdk 4.2.0-dev.2
Pinned exact, mirroring #295. Verified before landing:
evo-sdk-4-1-byte-array-write-regression). Serialized alikecreate transition built throughDocument.fromObjectand hexdumped it: the 32-bytepostIdlands as… 49 64 | 0a 20 | <32 raw bytes>—Value::Bytes, correct. Negative control: the same document built withnew Document({ properties })still produces the broken15 20 02…(Value::ArrayofU64) marker and the raw bytes are absent, so the check itself is sound and dev.2 has not regressed.9oDC6xdg8WRixTD2j3FCBq3vtsrf6bRGjXSJbhtFoma9as bot0 (GENx4QYZ4UT3KGDbtxc2E7pvECfbf8AP8kk33ULcAPXr): created abookmark, read it back withpostIdbytes intact, then deleted it. This is the gate that chore: bump @dashevo/evo-sdk to 4.2.0-dev.1 #295's regression slipped past.npm run lintandnpm run buildclean.A2 — devnet in the app
NEXT_PUBLIC_NETWORK=devnetnow builds an SDK against an explicit DAPI address pool for the devnet named byNEXT_PUBLIC_DEVNET_NAME. The typednew EvoSDK({ network: 'devnet', … })constructor is used rather thanEvoSDK.devnetTrusted(), because that factory takes noaddressesand a devnet publishes no masternode list to discover them from.The important structural change is that
networksplits in two:getConfiguredNetwork()'devnet'keyNetwork()'testnet'Devnets reuse testnet's address and WIF version bytes — moutai's own Insight reports
"network":"testnet". Every key-derivation, signer-matching and secure-storage call site moves tokeyNetwork(); only SDK construction ever sees'devnet'. Without this the eleven existing(process.env.NEXT_PUBLIC_NETWORK as 'testnet' | 'mainnet') || 'testnet'casts would have quietly passed the string'devnet'into key derivation.DPNS_CONTRACT_IDand the Insight base URL become overridable per deployment.A3–A5 — scripts
scripts/sdk-env.mjsreplaces the hardcodedEvoSDK.testnetTrustedin all six operational scripts.NETWORK(plusDEVNET_NAME,DAPI_ADDRESSES,QUORUM_URL, read from.env.devnetwhen absent from the environment) picks the chain. Testnet remains the default, so every existing invocation is byte-for-byte unchanged — verified withnode scripts/provision-test-identity.mjs --check-balances.provision-test-identity.mjsgains a devnet mode that refuses InstantSend asset-lock proofs. On the devnet build rs-dapi's bloom matcher never forwards the IS lock, so an InstantSend-funded lock silently burns the funds (feat: register funded identities via a ChainLock asset lock dashpay/platform#4399). The devnet path is ChainLock only: fund the address printed by--gen-asset-lock-key, pass the outpoint back as--funding-outpoint <txid>:<vout>, and the script polls Insight for the funding tx's block height and DAPIgetStatus().chain.coreChainLockedHeightfor the lock (600s budget) before buildingAssetLockProof.createChainAssetLockProof. Every one-shot asset-lock key is appended to the gitignored.devnet-locks.localbefore anything is broadcast, so funds are never stranded behind a key that only lived in memory.register-test-contracts.mjscan clone from another chain (--source-network,--from-social,--from-profile) and now carries the source contract'stokensblock across.tokenCostlives inside the document schemas and always followed them, so dropping the token configuration left the copy costing a token that did not exist. The clone goes throughDataContract.fromJSONrather thannew DataContract({ schemas, tokens }): the constructor'stokensoption only accepts liveTokenConfigurationhandles (a plain object read from JSON is rejected with "JS object constructor name mismatch"), and the JSON form additionally round-tripsgroups,config,keywordsanddescription, so the copy is faithful rather than schemas-only.publishContractasserts the published contract came back with the token count it was given.set-yapp-price.mjstakes--contractand a seed-derived--owner-indexso the same price can be set on a copy.A6 —
/devnetdeploymentnpm run build:devnetsources the checked-in.env.devnetand builds withBASE_PATH=/devnet, mirroringbuild:testing.deploy.ymlrebuilds the staging checkout a second time with it, so/devnetalways tracks the staging commit. Storage scoping is automatic via base path (devnet:prefix), so it is isolated from/,/stagingand/testing— confirmed in the built bundle. The step iscontinue-on-error: the devnet chain is disposable and re-genesises without notice, and a broken/devnetmust never hold back the/stagingdeploy.Moutai facts (verified live 2026-08-27)
https://seed-{1..5}.moutai.networks.dash.org:1443, valid public TLS, reachable.getStatus: dapi/drive4.2.0-dev.2, tenderdash1.7.0, drive protocol 14, chain iddash-devnet-moutai, platform height ~17 (re-genesised today),coreChainLockedHeight40550.https://insight.moutai.networks.dash.org/insight-apiworks and reports"network":"testnet". Faucet addressyhJHMkBAT2TF6D8GHc4v9bMfBh3V2Z6megholds ~247k DASH.What could not be completed, and why
Moutai cannot currently be read by the WASM SDK at all, which blocks identity provisioning (A4) and contract registration (A5) — both need
identities.fetch/contracts.fetch/ a nonce before they can write anything.Three independent findings, each reproduced against evo-sdk 4.2.0-dev.2:
proofs: falseis not a fallback. The first proofless query panics inside rs-sdk:packages/rs-sdk/src/platform/query.rs:232 — not implemented: queries without proofs are not supported yet. So the plan's non-trusted +proofs:falseshape is not viable.trusted: falsewith proofs on is refused outright:Context provider error: Non-trusted mode is not supported in WASM. Please construct a WasmTrustedContext via prefetchMainnet/prefetchTestnet/prefetchDevnet/prefetchLocal.EvoSDK.devnetTrusted('moutai')defaults tohttps://quorums.moutai.networks.dash.org, which is NXDOMAIN. No alternative host serves it either (checkedmoutai.networks.dash.org,quorum.moutai…, the seed hosts on 443 and 1443, insight, and the faucet).I did confirm the plumbing works, so this is purely a missing service and not an SDK gap: pointing
quorumUrlat a local HTTP server showed the SDK requesting exactly three paths —GET /quorums,GET /previous,GET /masternodes— in that order, and mirroring testnet's real responses got it past prefetch. Producing moutai's own quorum and masternode data needs Core RPC (quorum listextended/protx list), which on moutai is VPN-only; only :1443 is publicly open.Hence
NEXT_PUBLIC_QUORUM_URLin.env.devnetand inscripts/sdk-env.mjs— that is the one thing standing between this branch and a working/devnet. Once a sidecar exists (dashmate can produce the data), the follow-up is mechanical and fully scripted:Two smaller deviations from the plan, both forced:
dashpay/dash-network-configs/devnet-moutai.yml, fetched at runtime. That repository is not publicly readable (raw.githubusercontent.comreturns 404 unauthenticated; it resolves only with an authenticatedgh apicall), so an unattended runtime fetch is not possible. Separately, neither@dashevo/evo-sdknor this repo has a Dash Core transaction builder, so spending that UTXO would mean a new dependency. The script therefore takes the funding outpoint from the operator instead, which works with the moutai MultiFaucet's web UI as-is. No key material was printed, stored, or committed at any point..env.devnetships with the contract and identity slots commented out rather than populated. They are public data and would have been checked in, but nothing was registered, so there is nothing truthful to put there yet.Validation
npm run lintnpm run buildnpm run build:devnet/devnetbase path,devnet:storage scope and the moutai DAPI pool baked inprovision-test-identity.mjs --check-balanceson testnet.env.devnetReview pass
Two findings from review were fixed on this branch rather than left for the PR:
contexts/sdk-context.tsxhardcodednetwork: 'testnet'. It is the app-wide SDK bootstrap and normally wins the race against the on-demand callers, so on a/devnetbuild everyuseSdk()consumer (store, checkout, blog, DPNS, homepage) would have read testnet through the shared singleton until something else forced a reinit. Pre-existing code that thekeyNetwork()sweep did not match, because it never used the cast pattern being replaced.evoSdkService.initialize()compared onlynetworkandcontractIdwhen deciding to reuse the existing instance, so a devnet config differing solely in its address pool or quorum URL would have been silently ignored.🤖 Generated with Claude Code