Isolated CHIP-0035 Chia DataLayer store coin driver, compiled to WebAssembly, with a Next.js demo app that lists, mints, updates, and deletes DataLayer stores using the Sage wallet over WalletConnect.
The driver was extracted from DataLayer-Driver and depends only on upstream chia-* crates — no networking, no signing, no key derivation inside the WASM. It builds the coin spends; the consumer (the demo app) handles keys, coin selection, signing (Sage), and broadcast (coinset.org). Spend-bundle output is byte-for-byte identical to DataLayer-Driver (both rest on chia-sdk-driver 0.30 chip-0035).
chip35_dl_coin/
core/ chip35-dl-coin — Rust driver: mint/update/ownership/melt/oracle + addFee + serialization
wasm/ chip35-dl-coin-wasm — wasm-bindgen bindings (the published module the app imports)
app/ chip35-dl-coin-app — Next.js demo UI (Sage + WalletConnect + coinset.org)
puzzles/ delegation_layer.clsp / writer_filter.clsp — canonical store puzzle sources (reference only;
the compiled logic ships inside chia-sdk-driver — see puzzles/README.md)
docs/superpowers/ design spec + implementation plan
- Connect the Sage wallet via WalletConnect (QR pairing).
- Mint a new DataLayer store (a singleton owned by your wallet key).
- List the stores you've created (tracked in browser localStorage) with on-chain liveness.
- Update a store's metadata (root hash / label / description).
- Delete (melt) a store.
Each mint / update / delete: builds coin spends in WASM → Sage signs (chip0002_signCoinSpends) → pushed to coinset.org /push_tx → the app waits for on-chain confirmation.
- Rust (stable) + the wasm target:
rustup target add wasm32-unknown-unknown - wasm-pack:
cargo install wasm-pack --locked - LLVM/Clang on PATH (the
blstBLS dependency compiles C to wasm). On Windows install LLVM and ensureC:\Program Files\LLVM\binis on PATH. (wasm-packusually sets this up itself; only needed if a build complains aboutclang.) - Node.js 20+ and npm
- Sage wallet (desktop) set to the network you'll use (this demo targets mainnet)
- A free WalletConnect / Reown project ID from https://cloud.reown.com
⚠️ Mainnet, real funds. Mint/update/delete spend real XCH and pay real fees. Use small amounts. There is no automated end-to-end test of the wallet/chain flow — it is verified manually.
From the repo root (PowerShell shown; the commands are the same on bash):
# 1. Build the WASM package → wasm/pkg (the app depends on it via file:../wasm/pkg)
wasm-pack build wasm --target bundler --release --no-opt
# (equivalent: cd wasm; npm run build:bundler; cd ..)
# 2. Configure the app
cd app
Copy-Item .env.example .env.local
# then edit .env.local and set your project id:
# NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=<your reown project id>
npm install
# 3. Run the dev server
npm run dev
# open http://localhost:3000--no-opt is required: the bundled wasm-opt rejects BLST's bulk-memory ops. The crate also sets wasm-opt = false in wasm/Cargo.toml, so a plain wasm-pack build wasm --target bundler --release works too.
# Same project id you'd use for any Sage/WalletConnect dapp (https://cloud.reown.com)
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your_reown_project_id
# coinset.org RPC (reads + /push_tx). Default is fine.
NEXT_PUBLIC_COINSET_BASE_URL=https://api.coinset.orgNEXT_PUBLIC_* values are inlined when the dev server (or build) starts — restart npm run dev after editing .env.local.
cd app
npm run build # → app/out (Next.js static export)
npm start # serves app/out on http://localhost:3000- Open http://localhost:3000. Make sure Sage is running and set to mainnet, with a small spendable XCH balance.
- Click Connect Wallet, scan the QR with Sage, approve.
- Mint: set a label/description, a 32-byte root hash (or click random), and a fee (default
1,000,000mojos). Submit, approve in Sage. Watch the status: Pushing → waiting for on-chain confirmation → Confirmed. - Update / Delete: pick a store from the list, set the new root hash (update) or confirm the fee (delete), approve in Sage, wait for confirmation.
The store list lives in your browser's localStorage; "Refresh status" checks each store's current coin on coinset.org.
# Rust driver unit + parity tests
cargo test -p chip35-dl-coin
# Coverage (what CI gates). Requires cargo-llvm-cov: `cargo install cargo-llvm-cov`.
# CI fails the build below the floor (>=90% lines / >=85% regions for the core crate).
cargo llvm-cov -p chip35-dl-coin --fail-under-lines 90 --fail-under-regions 85
# WASM functional test (builds the nodejs target, runs mint/update/melt/oracle + native↔wasm golden parity)
cd wasm; npm test; cd ..
# App typecheck / build
cd app; npx tsc --noEmit; npm run build; cd ..Coverage is measured per-push by the coverage job in .github/workflows/ci.yml. The wasm crate is
publish = false thin bindings over the core crate and is exercised by the wasm JS-shape job, so the
coverage gate targets the core crate's own logic.
-
chip35-dl-coin-wasmexposes:- DataStore spend builders:
mintStore,updateStoreMetadata,updateStoreOwnership,meltStore,oracleSpend, plusaddFee,digstoreOwnerHint,dataStoreFromSpend, andspendBundleToHex/hexSpendBundleToCoinSpends. - Per-capsule $DIG payment (task #111):
buildDigStorePayment(the canonical per-capsule store payment — pay $DIG to the treasury),digTreasuryPaymentCoin(the exact treasury coin a payment emits, for verification), anddigConstants(the DIG asset id + treasury inner puzzle hash). Minting a store is FREE of $DIG —mintStoreonly launches the on-chain singleton (XCH network fee only) and carries NO DIG payment. $DIG is paid ONLY when a CAPSULE is created — every commit / root-advance: the consumer concatenatesbuildDigStorePayment's CAT spends withupdateStoreMetadata's singleton spend into ONE atomic bundle, signs them together, and pushes once. The amount is the dynamic, USD-pegged per-capsule price (dig_amount = target_usd ÷ live DIG price) passed in as an INPUT — never hardcoded here. - DataStore delegation (hub Teams #43 + revocable deploy tokens #17):
adminDelegatedPuzzleFromKey,writerDelegatedPuzzleFromKey,oracleDelegatedPuzzle— build the delegate entry to add to a store's delegated-puzzle set (viamintStore/updateStoreOwnership). An admin updates the store + changes delegation; a writer advances the root (deploy) but cannot change delegation — a deploy token is a revocable writer key; an oracle lets anyone spend for a fixed fee. The delegate advances the root viaupdateStoreMetadatawith thewriterPublicKey/adminPublicKeyargument (no owner seed). - Asset toolkit (roadmap #33/#34/#35/#36):
mintNft(NFT with URN + https-fallback media URIs and computed hashes),bulkMint+generateItemMetadata(collection bulk mint from a parsed traits manifest),createDid(creator identity),issueCat(fixed-supply CAT),encodeOffer/decodeOffer(offer codec), and the CHIP-0007 helpersbuildChip0007Metadata,validateChip0007,sha256. - In-dapp monetization (roadmap #46) — a dapp deployed on DIG can EARN:
buildPayment/buildCatPayment(a buyer pays the dapp owner in XCH or any CAT incl. DIG, settling to the owner's address),verifyPaymentReceipt(paywall / pay-to-unlock: verify a confirmed payment of ≥ amount to the owner, with a replay-proof nonce) +paymentNonce, and the NFT-gating readsproveNftOwnership/proveCollectionMembership/readNftOwnership(prove a wallet holds an NFT / a collection member — read/verify, not a spend). Recurring subscriptions are scaffolded (clear TODO; need a time-locked/delegated puzzle) — until then model recurring billing as one payment per period. - Trustless lazy mint / mint-on-claim (roadmap #40):
buildLazyMintCommit(the creator DID spends ONCE to precommit a whole collection; returns the per-item launcher ids + an opaquedescriptorhandle) andbuildLazyMintClaim(afterwards ANYONE mints an individual NFT on demand with NO further DID involvement, funding 1 mojo). The flat secure-the-bag layout is simulator-validated (commit + free claim as a different party, NFT owned by the claimer, lineage to the creator DID). Allowlist (merkle) gating is ENFORCED OFF-CHAIN: when a commit declares anallowlistRoot,buildLazyMintClaimthrowsALLOWLIST_DENIEDunless amerkleProofproves the claimer's own puzzle hash is a member;verifyMerkleMembership(leaf, proof, root)validates a proof without building a spend (for off-chain gating). Trustless ON-CHAIN allowlist enforcement andpaymentGatedatomic payment enforcement remain DEFERRED with precise blockers (each needs a compiled/audited claim puzzle, or a wallet-assembled offer, respectively — seeDESIGN.md#40). Ported from secure-the-mint (Apache-2.0; seeNOTICE+puzzles/LICENSE-APACHE). - Runtime self-description (agent-friendly):
version()returns the package version string (= the npm package version), andcapabilities()returns a machine-readable descriptor{ name, version, builders, errorCodes }— the full builder catalogue + the stable error-code list — so a consumer or agent can introspect the loaded surface at runtime with zero out-of-band knowledge.
It returns
CoinSpend[](and a result summary such as the updatedDataStore, the minted NFT/DID launcher id, or the CAT asset id) — it never signs, derives keys, or touches the network. SeeDESIGN.mdfor the full asset-toolkit + delegation design. - DataStore spend builders:
-
Real TypeScript types. The published
.d.tstypes every builder's inputs/outputs with concrete interfaces (Coin,CoinSpend,DataStore,SuccessResponse,NftMintParams,PaymentReceipt,NftOwnershipProof,Capabilities, …) instead ofany, including discriminated unions for the "exactly one of" shapes:Proof(lineageProof|eveProof),DelegatedPuzzle(admin | writer | oracle), andPaymentAsset({ xch:true }|{ assetId }). Encoding rules: 32/48/96-byte hashes/keys/signatures areUint8Array(raw bytes, not hex);u64/amounts arebigint; keys arecamelCase. -
Stable machine error codes. Every failure carries a stable
UPPER_SNAKEcode an automated caller branches on — never parse the human message. A throwing export rejects with a structuredChipErrorobject{ code, message }; the result-shaped helpers carry the samecodeas a field:verifyPaymentReceipt→{ ok, code?, error? }(aPaywallErrorcode on denial), and the gating reads →{ ok, proof?, code?, error? }(aGatingErrorcode on failure). The full catalogue (ChipErrorCode) is in the.d.tsand discoverable at runtime viacapabilities().errorCodes:Code Where it comes from INVALID_ARGUMENTa wasm argument is the wrong length/shape (bad key, non-32-byte hash, missing "exactly one of" selector) SERDE_ERRORa JS value failed to (de)serialize at the boundary DRIVER_ERRORthe underlying chia driver failed to construct the spend PARSE_ERRORthe builder rejected its inputs (e.g. empty coin selection, insufficient funds) PERMISSION_DENIEDthe puzzle can't perform the requested action ALLOWLIST_DENIEDan allowlist-gated lazy-mint claim was missing a merkle membership proof, or its proof did not prove the claimer's own puzzle hash is in the committed allowlist root (off-chain gate) METADATA_ERRORCHIP-0007 metadata failed schema validation / serialization NOT_AN_NFT·WRONG_OWNER·WRONG_COLLECTION·WRONG_NFTNFT-gating denial ( GatingError)WRONG_RECIPIENT·INSUFFICIENT_AMOUNT·WRONG_ASSET·NONCE_MISMATCHpaywall denial ( PaywallError) -
The app uses
chia-wallet-sdk-wasmfor wallet utilities the driver omits: bech32m address decode, and uncurrying a coin's puzzle reveal to recover its synthetic public key. -
Sage (over WalletConnect) provides spendable coins (
chip0002_getAssetCoins) and signs (chip0002_signCoinSpends,partial:false, auto_submit:false). -
coinset.org broadcasts (
/push_tx) and provides confirmation/liveness reads. -
A store is owned by the synthetic key of the coin that funded its mint (the key Sage signs with), so its whole mint → update → melt lifecycle stays self-consistent.
- Demo registry is local. "List" shows stores minted/updated in this browser (localStorage). Importing arbitrary pre-existing stores by launcher id is out of scope.
- Update/delete assume this app is the sole spender of a tracked store (its cached
DataStoreis the latest state). - WASM import discipline: the module is only loaded via
app/app/lib/wasm.tsgetWasm()insidedynamic(..., { ssr: false })components — never a top-level import.next.config.tsenablesasyncWebAssembly. Don't change this pattern or the build breaks.
| Symptom | Cause / fix |
|---|---|
WalletConnect client could not be initialised. Check NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID |
.env.local missing the project id, or the dev server wasn't restarted after adding it. Set the id and restart npm run dev. |
Loading chunk … failed |
Stale dev-server chunks after source changed under a running npm run dev. Restart the dev server and hard-refresh (Ctrl+Shift+R). |
/push_tx rejected: … DOUBLE_SPEND |
The selected coin was already spent / still in the mempool. The app marks it and you can retry to pick a different coin; or wait for a prior tx to confirm. |
/push_tx rejected: … WRONG_PUZZLE_HASH |
A store minted by an older build is owned by a key the app didn't store. Clear the old list (localStorage.clear() in the console) and mint a fresh store with the current build. |
No spendable XCH coin found covering N mojos |
The wallet has no single unlocked coin ≥ fee+1 (mint) or ≥ fee (update/delete). Fund the wallet or lower the fee. |
next build fails with EPERM … .next\trace |
A npm run dev is already running and locking .next. Stop it before npm run build. |
MIT. See LICENSE (driver code extracted from DataLayer-Driver, also MIT).