diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index fdee10f..a4d0599 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -146,6 +146,40 @@ jobs: path: napi/${{ env.APP_NAME }}.*.node if-no-files-found: error + build-wasm: + name: Build & test WASM bindings + needs: rust-checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Install Rust + wasm target + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + targets: wasm32-unknown-unknown + - name: Install clang/lld (blst needs a C/Clang toolchain for wasm32) + run: sudo apt-get update && sudo apt-get install -y clang lld + - name: Install wasm-pack + uses: jetli/wasm-pack-action@v0.4.0 + with: + version: latest + - name: Build NAPI (needed by the parity test) + run: cd napi && npm install && npm run build + - name: Build WASM nodejs target and run parity test + run: cd wasm && npm install && npm run build:node && npm test + - name: Build WASM bundler artifact (published package) + run: cd wasm && npm run build:bundler + - name: Upload wasm pkg + uses: actions/upload-artifact@v4 + with: + name: wasm-pkg + path: wasm/pkg + if-no-files-found: error + test-macOS-windows-binding: name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }} needs: @@ -315,6 +349,38 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + publish-wasm-npm: + name: Publish WASM to NPM + runs-on: ubuntu-latest + needs: + - build-wasm + steps: + - uses: actions/checkout@v4 + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Download wasm pkg + uses: actions/download-artifact@v4 + with: + name: wasm-pkg + path: wasm/pkg + - name: Publish to NPM (on version-tag commit) + run: | + npm config set provenance true + if git log -1 --pretty=%B | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+$"; then + echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc + cd wasm/pkg && npm publish --access public + elif git log -1 --pretty=%B | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+"; then + echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc + cd wasm/pkg && npm publish --tag next --access public + else + echo "Not a release, skipping wasm npm publish" + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + publish-crates: name: Publish to Crates.io runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index 4f04533..92230d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -769,6 +769,16 @@ dependencies = [ "sha3", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -882,9 +892,19 @@ name = "datalayer-driver" version = "3.0.0" dependencies = [ "chia", + "chia-bls 0.26.0", + "chia-consensus", + "chia-protocol", + "chia-puzzle-types", "chia-puzzles", + "chia-sdk-driver", + "chia-sdk-signer", + "chia-sdk-types", + "chia-sdk-utils", + "chia-traits 0.26.0", "chia-wallet-sdk", "clvm-traits", + "clvm-utils", "clvmr", "futures-util", "hex", @@ -917,6 +937,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "datalayer-driver-wasm" +version = "3.0.0" +dependencies = [ + "console_error_panic_hook", + "datalayer-driver", + "getrandom 0.2.16", + "hex", + "js-sys", + "serde", + "serde-wasm-bindgen", + "serde_bytes", + "wasm-bindgen", + "wasm-bindgen-test", +] + [[package]] name = "der" version = "0.7.10" @@ -1714,6 +1750,16 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2435,6 +2481,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.28" @@ -2503,6 +2558,27 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -3088,6 +3164,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -3195,6 +3281,30 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-bindgen-test" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e381134e148c1062f965a42ed1f5ee933eef2927c3f70d1812158f711d39865" +dependencies = [ + "js-sys", + "minicov", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b673bca3298fe582aeef8352330ecbad91849f85090805582400850f8270a2e8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "web-sys" version = "0.3.81" @@ -3221,6 +3331,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 01abb9b..932a156 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,27 +1,39 @@ [workspace] resolver = "2" -members = [".", "napi"] +members = [".", "napi", "wasm"] [package] -edition = "2021" name = "datalayer-driver" version = "3.0.0" -license = "MIT" +edition = "2021" authors = ["yakuhito ", "William Wills "] -homepage = "https://github.com/DIG-Network/DataLayer-Driver" -repository = "https://github.com/DIG-Network/DataLayer-Driver" description = "Native Chia DataLayer Driver for storing and retrieving data in Chia blockchain" +license = "MIT" +repository = "https://github.com/DIG-Network/DataLayer-Driver" readme = "README.md" +homepage = "https://github.com/DIG-Network/DataLayer-Driver" keywords = ["chia", "blockchain", "datalayer", "storage", "web3"] categories = ["cryptography::cryptocurrencies", "web-programming"] [dependencies] -chia = "0.26.0" +# chia meta-crate: native-only (chia-client inside it pulls tokio) +chia = { version = "0.26.0", optional = true } +# Individual wasm-safe chia sub-crates (used on both paths) +chia-bls = "0.26.0" +chia-consensus = "0.26.0" +chia-protocol = "0.26.0" +chia-puzzle-types = "0.26.0" +chia-traits = "0.26.0" chia-puzzles = "0.20.1" +clvm-utils = "0.26.0" thiserror = "1.0.61" clvmr = "0.14.0" -tokio = "1.39.3" -chia-wallet-sdk = { version = "0.30.0", features = ["chip-0035", "native-tls", "peer-simulator", "action-layer"] } +tokio = { version = "1.39.3", optional = true } +chia-wallet-sdk = { version = "0.30.0", default-features = false, features = ["chip-0035", "action-layer"], optional = true } +chia-sdk-driver = { version = "0.30.0", features = ["chip-0035", "action-layer"] } +chia-sdk-signer = "0.30.0" +chia-sdk-types = { version = "0.30.0", features = ["chip-0035", "action-layer"] } +chia-sdk-utils = "0.30.0" hex-literal = "0.4.1" num-bigint = "0.4.6" hex = "0.4.3" @@ -30,6 +42,16 @@ futures-util = "0.3" clvm-traits = "0.26.0" indexmap = "2.11.4" +[features] +default = ["native"] +native = [ + "dep:tokio", + "dep:chia", + "dep:chia-wallet-sdk", + "chia-wallet-sdk/native-tls", + "chia-wallet-sdk/peer-simulator", +] + [target.aarch64-unknown-linux-gnu.dependencies] openssl = { version = "0.10.64", features = ["vendored"] } openssl-sys = { version = "0.9.102", features = ["vendored"] } diff --git a/README.md b/README.md index b6ccf5a..f075bcb 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Native Chia DataLayer Driver for storing and retrieving data in Chia blockchain. This project provides both Rust library APIs and Node.js bindings for interacting with Chia's DataLayer. +A WebAssembly build is also available as [`@dignetwork/datalayer-driver-wasm`](./wasm/README.md) for browser and bundler environments (webpack, Vite, Next.js, esbuild). The WASM package mirrors the offline subset of the NAPI interface — no networking, no `Peer`/`Tls` — making it ideal for building and signing DIGStore spend bundles client-side without a full node. See [`wasm/README.md`](./wasm/README.md) for installation, usage, and a worked example. + ## Installation ### As a Rust Crate diff --git a/docs/superpowers/plans/2026-05-29-datalayer-driver-wasm-bindings.md b/docs/superpowers/plans/2026-05-29-datalayer-driver-wasm-bindings.md new file mode 100644 index 0000000..47dc886 --- /dev/null +++ b/docs/superpowers/plans/2026-05-29-datalayer-driver-wasm-bindings.md @@ -0,0 +1,1579 @@ +# DataLayer-Driver WASM Bindings Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add WebAssembly bindings to `DataLayer-Driver` that mirror the offline (non-networking) NAPI interface — primarily the ability to build and sign DIGStore (DataStore) spend bundles in the browser — and publish them as `@dignetwork/datalayer-driver-wasm` via CI. + +**Architecture:** A `native` cargo feature (default-on) is added to the core `datalayer-driver` crate, gating all networking (tokio sockets, native TLS, the `async_api` module, every `&Peer` async fn). A new `wasm/` workspace member depends on the core crate with `default-features = false`, exposing the offline functions via `wasm-bindgen`. Scalar params stay native (`Uint8Array`/`bigint`); struct/array params cross the boundary via `serde-wasm-bindgen` with a BigInt-configured serializer. Correctness is proven by a Node parity test that asserts the WASM output byte-for-byte equals the existing NAPI output. + +**Tech Stack:** Rust, `wasm-bindgen` 0.2, `serde-wasm-bindgen` 0.6, `serde_bytes`, `getrandom`, `wasm-pack` (`--target bundler`), `chia` 0.26 / `chia-wallet-sdk` 0.30, GitHub Actions, npm. + +--- + +## Spec + +`docs/superpowers/specs/2026-05-29-datalayer-driver-wasm-bindings-design.md` + +## File Structure + +**Modified (core crate — `native` feature gating):** +- `Cargo.toml` — workspace members + `native` feature on core crate +- `src/lib.rs` — gate `async_api` module + `pub use ...Peer`; keep offline wrappers +- `src/wallet.rs` — gate every `&Peer` async fn + tokio/Peer imports +- `src/dig_coin.rs` — gate `&Peer`/async items +- `src/dig_collateral_coin.rs` — gate `&Peer`/async items + +**Created (wasm crate):** +- `wasm/Cargo.toml` — `crate-type = ["cdylib","rlib"]`, wasm deps +- `wasm/src/lib.rs` — `init()` + all `#[wasm_bindgen]` exports (analog of `napi/src/napi_lib.rs`) +- `wasm/src/types.rs` — serde boundary structs + native conversions (analog of `napi/src/js.rs` + `napi/src/conversions.rs`) +- `wasm/scripts/patch-pkg.mjs` — rewrite generated `pkg/package.json` to the scoped name + inject typed `.d.ts` +- `wasm/types/datalayer-driver-wasm.d.ts` — hand-authored TypeScript types mirroring the NAPI `index.d.ts` shapes +- `wasm/package.json` — dev scripts to drive `wasm-pack` + patch + test +- `wasm/tests/parity.mjs` — Node differential test: WASM output === NAPI output +- `wasm/README.md` — usage + +**Modified (CI):** +- `.github/workflows/CI.yml` — add `build-wasm` job + `publish-wasm-npm` job + +--- + +## Task 1: Gate networking behind a `native` feature (core crate compiles to wasm32) + +This is the highest-risk task; do it first. Goal: `cargo build -p datalayer-driver --target wasm32-unknown-unknown --no-default-features` succeeds, while the default (native) build is byte-for-byte unchanged for the napi crate. + +**Files:** +- Modify: `Cargo.toml` +- Modify: `src/lib.rs:21-28`, `src/lib.rs:340` (async_api module) +- Modify: `src/wallet.rs` (Peer fns + imports) +- Modify: `src/dig_coin.rs`, `src/dig_collateral_coin.rs` + +- [ ] **Step 1: Add the wasm target locally** + +Run: +```powershell +rustup target add wasm32-unknown-unknown +``` +Expected: `info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date` (or installs it). + +- [ ] **Step 2: Add the `native` feature and make tokio optional in `Cargo.toml`** + +In `Cargo.toml`, change the `chia-wallet-sdk` and `tokio` lines and add a `[features]` block. Replace: +```toml +tokio = "1.39.3" +chia-wallet-sdk = { version = "0.30.0", features = ["chip-0035", "native-tls", "peer-simulator", "action-layer"] } +``` +with: +```toml +tokio = { version = "1.39.3", optional = true } +chia-wallet-sdk = { version = "0.30.0", default-features = false, features = ["chip-0035", "action-layer"] } +``` +and add, immediately after the `[package]`/before `[dependencies]` is fine, but place it after `[dependencies]`: +```toml +[features] +default = ["native"] +native = [ + "dep:tokio", + "chia-wallet-sdk/native-tls", + "chia-wallet-sdk/peer-simulator", +] +``` + +Note: `chia-wallet-sdk` previously did not set `default-features = false`; verify its default features are not silently required by the offline path. If the build in Step 7 fails on a missing default feature, re-add the needed one to the unconditional `features = [...]` list (NOT under `native`). + +- [ ] **Step 3: Gate the `async_api` module and `Peer` re-export in `src/lib.rs`** + +In `src/lib.rs`, change line 21: +```rust +pub use chia_wallet_sdk::client::Peer; +``` +to: +```rust +#[cfg(feature = "native")] +pub use chia_wallet_sdk::client::Peer; +``` +Change line 28: +```rust +pub use async_api::{connect_peer, connect_random, create_tls_connector, NetworkType}; +``` +to: +```rust +#[cfg(feature = "native")] +pub use async_api::{connect_peer, connect_random, create_tls_connector, NetworkType}; +``` +Add `#[cfg(feature = "native")]` directly above the `pub mod async_api {` declaration (line 340): +```rust +/// Async functions for blockchain interaction (Rust API versions) +#[cfg(feature = "native")] +pub mod async_api { +``` + +`NetworkType` is referenced by `wallet.rs` (`use crate::{NetworkType, ...}`) — it lives in `async_api`. Since the offline `sign_coin_spends` path uses `wallet::TargetNetwork` (not `NetworkType`), confirm no offline function imports `NetworkType`. If an offline path needs it, move the `NetworkType` enum out of `async_api` to the crate root (ungated) and re-export. Resolve when Step 7 surfaces it. + +- [ ] **Step 4: Gate `&Peer` functions and native-only imports in `src/wallet.rs`** + +At the top of `src/wallet.rs`, gate the networking imports. Change line 31: +```rust +use chia_wallet_sdk::client::Peer; +``` +to: +```rust +#[cfg(feature = "native")] +use chia_wallet_sdk::client::Peer; +``` +The `use crate::{NetworkType, UnspentCoinStates};` (line 11) and any `tokio`/`futures_util` imports used only by `&Peer` fns must also be gated with `#[cfg(feature = "native")]`. Add `#[cfg(feature = "native")]` immediately above EACH of these `pub async fn` definitions (they all take `peer: &Peer`): +`get_unspent_coin_states_by_hint` (60), `get_unspent_coin_states` (72), `spend_xch_server_coins` (250), `fetch_xch_server_coin` (341), `sync_store` (482), `sync_store_using_launcher_id` (580), `get_store_creation_height` (661), `broadcast_spend_bundle` (1028), `get_header_hash` (1037), `get_fee_estimate` (1047), `is_coin_spent` (1079), `look_up_possible_launchers` (1152), `subscribe_to_coin_states` (1183), `unsubscribe_from_coin_states` (1202), `mint_nft` (1230), `generate_did_proof` (1331), `generate_did_proof_from_chain` (1389), `resolve_did_string_and_generate_proof` (1517). + +Example: +```rust +#[cfg(feature = "native")] +pub async fn sync_store( + peer: &Peer, + ... +``` +Leave the offline functions (`mint_store`, `send_xch`, `select_coins`, `sign_coin_spends`, `sign_message`, `verify_signature`, `get_cost`, `add_fee`, `oracle_spend`, `update_store_metadata`, `update_store_ownership`, `melt_store`, `create_server_coin`, `create_simple_did`, `generate_did_proof_manual`) ungated. + +- [ ] **Step 5: Gate `&Peer`/async items in `src/dig_coin.rs` and `src/dig_collateral_coin.rs`** + +Run to list the exact items needing gates: +```powershell +Select-String -Path src\dig_coin.rs, src\dig_collateral_coin.rs -Pattern 'Peer|async|tokio' +``` +For each `pub async fn ...(peer: &Peer, ...)` and each `use ...Peer`/`use ...tokio`, add `#[cfg(feature = "native")]` above it (same pattern as Step 4). Keep any pure (non-Peer, non-async) helpers ungated. + +- [ ] **Step 6: Verify the native build still compiles (no regression)** + +Run: +```powershell +cargo build -p datalayer-driver +cargo build -p datalayer-driver-napi +``` +Expected: both succeed. The napi crate uses default features, so `native` is on and behavior is unchanged. + +- [ ] **Step 7: Verify the offline build compiles to wasm32** + +Run: +```powershell +cargo build -p datalayer-driver --target wasm32-unknown-unknown --no-default-features +``` +Expected: success. If it fails, the error names either (a) a still-ungated networking item — add `#[cfg(feature = "native")]`; (b) a missing chia-wallet-sdk feature on the offline path — add it to the unconditional `features`; or (c) `getrandom` needing a wasm backend — that is resolved in Task 2, so a getrandom-related error here is expected and acceptable to defer. Re-run until only getrandom-class errors (if any) remain. + +- [ ] **Step 8: Commit** + +```powershell +git add Cargo.toml src/lib.rs src/wallet.rs src/dig_coin.rs src/dig_collateral_coin.rs +git commit -m "feat(core): gate networking behind native feature for wasm32 compat" +``` + +--- + +## Task 2: Resolve the `getrandom` wasm backend + +`chia`/`rand` pull `getrandom`. On `wasm32-unknown-unknown` it needs an explicit JS backend or it panics at runtime. The required feature differs between getrandom 0.2 (`js`) and 0.3 (`wasm_js` + a `RUSTFLAGS` cfg). + +**Files:** +- Inspect: `Cargo.lock` +- (Used in Task 3's `wasm/Cargo.toml`) + +- [ ] **Step 1: Determine which getrandom version is in the tree** + +Run: +```powershell +cargo tree -i getrandom --target wasm32-unknown-unknown --no-default-features -p datalayer-driver +``` +Expected: prints `getrandom vX.Y.Z` and what depends on it. Record the major.minor (0.2 or 0.3). If BOTH appear, note both. + +- [ ] **Step 2: Record the resolution for Task 3** + +Write the decision into `wasm/GETRANDOM.md` (create it): +- If **0.2** is present: the wasm crate adds `getrandom = { version = "0.2", features = ["js"] }`. No cargo config needed. +- If **0.3** is present: the wasm crate adds `getrandom = { version = "0.3", features = ["wasm_js"] }` AND a `.cargo/config.toml` (created in Task 3) with: + ```toml + [target.wasm32-unknown-unknown] + rustflags = ['--cfg', 'getrandom_backend="wasm_js"'] + ``` +- If **both**: add the matching dependency line for **each** version present. + +```powershell +git add wasm/GETRANDOM.md +git commit -m "docs(wasm): record getrandom backend resolution" +``` + +--- + +## Task 3: Scaffold the `wasm` crate + +**Files:** +- Modify: `Cargo.toml` (workspace members) +- Create: `wasm/Cargo.toml`, `wasm/src/lib.rs`, `wasm/.cargo/config.toml` (only if getrandom 0.3) + +- [ ] **Step 1: Add `wasm` to workspace members** + +In `Cargo.toml`, change: +```toml +members = [".", "napi"] +``` +to: +```toml +members = [".", "napi", "wasm"] +``` + +- [ ] **Step 2: Create `wasm/Cargo.toml`** + +Use the getrandom line chosen in Task 2 (this template assumes 0.2; swap if Task 2 said 0.3): +```toml +[package] +name = "datalayer-driver-wasm" +version = "3.0.0" +edition = "2021" +license = "MIT" +publish = false +description = "WebAssembly bindings for the Chia DataLayer driver (offline DIGStore spend-bundle construction)." +repository = "https://github.com/DIG-Network/DataLayer-Driver" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console-panic-hook"] +console-panic-hook = ["dep:console_error_panic_hook"] + +[dependencies] +datalayer-driver = { path = "..", default-features = false } +wasm-bindgen = "0.2" +js-sys = "0.3" +serde = { version = "1", features = ["derive"] } +serde_bytes = "0.11" +serde-wasm-bindgen = "0.6" +console_error_panic_hook = { version = "0.1", optional = true } +hex = "0.4" +# getrandom: see wasm/GETRANDOM.md — set to the version present in the tree +getrandom = { version = "0.2", features = ["js"] } + +# Direct chia type access for conversions (versions match the core crate) +chia = "0.26.0" +chia-wallet-sdk = { version = "0.30.0", default-features = false, features = ["chip-0035", "action-layer"] } + +[dev-dependencies] +wasm-bindgen-test = "0.3" +``` + +- [ ] **Step 3: Create `.cargo/config.toml` ONLY if Task 2 found getrandom 0.3** + +If (and only if) getrandom 0.3 is in the tree, create `wasm/.cargo/config.toml`: +```toml +[target.wasm32-unknown-unknown] +rustflags = ['--cfg', 'getrandom_backend="wasm_js"'] +``` +Otherwise skip this step. + +- [ ] **Step 4: Create a minimal `wasm/src/lib.rs` with just `init`** + +```rust +//! WebAssembly bindings for the Chia DataLayer driver. +//! +//! Mirrors the offline subset of the NAPI interface. Networking +//! (Peer/Tls) is intentionally absent — WASM has no native sockets. + +use wasm_bindgen::prelude::*; + +mod types; + +/// Initialise the module. Call once at startup. Installs a panic hook +/// (when the `console-panic-hook` feature is on) so Rust panics surface +/// in the JS console instead of an opaque `unreachable`. +#[wasm_bindgen] +pub fn init() { + #[cfg(feature = "console-panic-hook")] + console_error_panic_hook::set_once(); +} +``` + +Create an empty stub `wasm/src/types.rs` for now: +```rust +//! Serde boundary structs and conversions to/from native datalayer-driver types. +``` + +- [ ] **Step 5: Install wasm-pack and verify the crate builds to wasm** + +Run: +```powershell +cargo install wasm-pack --locked +wasm-pack build wasm --target bundler --dev +``` +Expected: succeeds, producing `wasm/pkg/` containing `datalayer_driver_wasm_bg.wasm`, `datalayer_driver_wasm.js`, `datalayer_driver_wasm.d.ts`, `package.json`. If it fails on getrandom, apply the Task 2 / Step 3 fix and re-run. + +- [ ] **Step 6: Commit** + +```powershell +git add Cargo.toml wasm/Cargo.toml wasm/src/lib.rs wasm/src/types.rs +# include wasm/.cargo/config.toml only if created +git commit -m "feat(wasm): scaffold wasm-bindgen crate with init()" +``` + +--- + +## Task 4: Conversion layer — boundary structs and native conversions + +Mirror `napi/src/js.rs` + the DataStore family in `napi/src/napi_lib.rs`. All structs derive `Serialize`/`Deserialize`, use `#[serde(rename_all = "camelCase")]` (matching the camelCase NAPI JS shape), and use `#[serde(with = "serde_bytes")]` for byte fields so they become `Uint8Array`. Amounts are `u64` and serialize as `bigint` via the configured serializer (Step 1). + +**Files:** +- Modify: `wasm/src/types.rs` + +- [ ] **Step 1: Add serializer helpers and byte/key conversion utilities** + +Write into `wasm/src/types.rs`: +```rust +use chia::bls::{PublicKey, SecretKey, Signature}; +use chia::protocol::{Bytes, Bytes32, Program}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use wasm_bindgen::JsValue; + +/// Serialize a Rust value into a JS value, encoding u64/i64 as BigInt +/// (DataLayer mojo amounts exceed 2^53, so the default lossy f64 path +/// is unacceptable). +pub fn to_js(value: &T) -> Result { + let ser = serde_wasm_bindgen::Serializer::new().serialize_large_number_types_as_bigints(true); + value + .serialize(&ser) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +/// Deserialize a JS value into a Rust value. Accepts both JS number and +/// BigInt for integer fields. +pub fn from_js(value: JsValue) -> Result { + serde_wasm_bindgen::from_value(value).map_err(|e| JsValue::from_str(&e.to_string())) +} + +pub fn bytes32(buf: &[u8]) -> Result { + Bytes32::try_from(buf.to_vec()).map_err(|_| JsValue::from_str("expected 32-byte value")) +} + +pub fn public_key(buf: &[u8]) -> Result { + let arr = <[u8; 48]>::try_from(buf).map_err(|_| JsValue::from_str("expected 48-byte public key"))?; + PublicKey::from_bytes(&arr).map_err(|_| JsValue::from_str("invalid public key")) +} + +pub fn secret_key(buf: &[u8]) -> Result { + let arr = <[u8; 32]>::try_from(buf).map_err(|_| JsValue::from_str("expected 32-byte secret key"))?; + SecretKey::from_bytes(&arr).map_err(|_| JsValue::from_str("invalid secret key")) +} + +pub fn signature(buf: &[u8]) -> Result { + let arr = <[u8; 96]>::try_from(buf).map_err(|_| JsValue::from_str("expected 96-byte signature"))?; + Signature::from_bytes(&arr).map_err(|_| JsValue::from_str("invalid signature")) +} +``` + +- [ ] **Step 2: Add the coin/proof boundary structs and conversions** + +Append to `wasm/src/types.rs`: +```rust +use chia::protocol::{Coin as RustCoin, CoinSpend as RustCoinSpend}; +use chia::puzzles::{EveProof as RustEveProof, LineageProof as RustLineageProof, Proof as RustProof}; + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Coin { + #[serde(with = "serde_bytes")] + pub parent_coin_info: Vec, + #[serde(with = "serde_bytes")] + pub puzzle_hash: Vec, + pub amount: u64, +} + +impl Coin { + pub fn to_native(&self) -> Result { + Ok(RustCoin { + parent_coin_info: bytes32(&self.parent_coin_info)?, + puzzle_hash: bytes32(&self.puzzle_hash)?, + amount: self.amount, + }) + } + pub fn from_native(c: &RustCoin) -> Self { + Coin { + parent_coin_info: c.parent_coin_info.to_vec(), + puzzle_hash: c.puzzle_hash.to_vec(), + amount: c.amount, + } + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CoinSpend { + pub coin: Coin, + #[serde(with = "serde_bytes")] + pub puzzle_reveal: Vec, + #[serde(with = "serde_bytes")] + pub solution: Vec, +} + +impl CoinSpend { + pub fn to_native(&self) -> Result { + Ok(RustCoinSpend { + coin: self.coin.to_native()?, + puzzle_reveal: Program::from(self.puzzle_reveal.clone()), + solution: Program::from(self.solution.clone()), + }) + } + pub fn from_native(cs: &RustCoinSpend) -> Self { + CoinSpend { + coin: Coin::from_native(&cs.coin), + puzzle_reveal: cs.puzzle_reveal.to_vec(), + solution: cs.solution.to_vec(), + } + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LineageProof { + #[serde(with = "serde_bytes")] + pub parent_parent_coin_info: Vec, + #[serde(with = "serde_bytes")] + pub parent_inner_puzzle_hash: Vec, + pub parent_amount: u64, +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EveProof { + #[serde(with = "serde_bytes")] + pub parent_parent_coin_info: Vec, + pub parent_amount: u64, +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Proof { + pub lineage_proof: Option, + pub eve_proof: Option, +} + +impl Proof { + pub fn to_native(&self) -> Result { + if let Some(lp) = &self.lineage_proof { + Ok(RustProof::Lineage(RustLineageProof { + parent_parent_coin_info: bytes32(&lp.parent_parent_coin_info)?, + parent_inner_puzzle_hash: bytes32(&lp.parent_inner_puzzle_hash)?, + parent_amount: lp.parent_amount, + })) + } else if let Some(ep) = &self.eve_proof { + Ok(RustProof::Eve(RustEveProof { + parent_parent_coin_info: bytes32(&ep.parent_parent_coin_info)?, + parent_amount: ep.parent_amount, + })) + } else { + Err(JsValue::from_str("missing proof")) + } + } + pub fn from_native(p: &RustProof) -> Self { + match p { + RustProof::Lineage(lp) => Proof { + lineage_proof: Some(LineageProof { + parent_parent_coin_info: lp.parent_parent_coin_info.to_vec(), + parent_inner_puzzle_hash: lp.parent_inner_puzzle_hash.to_vec(), + parent_amount: lp.parent_amount, + }), + eve_proof: None, + }, + RustProof::Eve(ep) => Proof { + lineage_proof: None, + eve_proof: Some(EveProof { + parent_parent_coin_info: ep.parent_parent_coin_info.to_vec(), + parent_amount: ep.parent_amount, + }), + }, + } + } +} +``` + +- [ ] **Step 3: Add the DataStore family boundary structs and conversions** + +Mirror `napi_lib.rs:90-300`. Append to `wasm/src/types.rs`: +```rust +use datalayer_driver::{ + DataStore as RustDataStore, DataStoreInfo as RustDataStoreInfo, + DataStoreMetadata as RustDataStoreMetadata, DelegatedPuzzle as RustDelegatedPuzzle, +}; +use datalayer_driver::types::SuccessResponse as RustSuccessResponse; + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DataStoreMetadata { + #[serde(with = "serde_bytes")] + pub root_hash: Vec, + pub label: Option, + pub description: Option, + pub bytes: Option, + #[serde(default, with = "serde_bytes")] + pub size_proof: Option>, +} + +impl DataStoreMetadata { + pub fn to_native(&self) -> Result { + Ok(RustDataStoreMetadata { + root_hash: bytes32(&self.root_hash)?, + label: self.label.clone(), + description: self.description.clone(), + bytes: self.bytes, + // NAPI stores size_proof as the hex string of a Bytes32. + size_proof: match &self.size_proof { + Some(sp) => Some(bytes32(sp)?.to_string()), + None => None, + }, + }) + } + pub fn from_native(m: &RustDataStoreMetadata) -> Result { + Ok(DataStoreMetadata { + root_hash: m.root_hash.to_vec(), + label: m.label.clone(), + description: m.description.clone(), + bytes: m.bytes, + size_proof: match &m.size_proof { + Some(s) => Some( + hex::decode(s.trim_start_matches("0x")) + .map_err(|_| JsValue::from_str("invalid size_proof hex"))?, + ), + None => None, + }, + }) + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DelegatedPuzzle { + #[serde(default, with = "serde_bytes")] + pub admin_inner_puzzle_hash: Option>, + #[serde(default, with = "serde_bytes")] + pub writer_inner_puzzle_hash: Option>, + #[serde(default, with = "serde_bytes")] + pub oracle_payment_puzzle_hash: Option>, + pub oracle_fee: Option, +} + +impl DelegatedPuzzle { + pub fn to_native(&self) -> Result { + if let Some(h) = &self.admin_inner_puzzle_hash { + Ok(RustDelegatedPuzzle::Admin(bytes32(h)?.into())) + } else if let Some(h) = &self.writer_inner_puzzle_hash { + Ok(RustDelegatedPuzzle::Writer(bytes32(h)?.into())) + } else if let (Some(h), Some(fee)) = (&self.oracle_payment_puzzle_hash, self.oracle_fee) { + Ok(RustDelegatedPuzzle::Oracle(bytes32(h)?, fee)) + } else { + Err(JsValue::from_str("missing delegated puzzle info")) + } + } + pub fn from_native(d: &RustDelegatedPuzzle) -> Result { + Ok(match d { + RustDelegatedPuzzle::Admin(h) => { + let h: Bytes32 = (*h).into(); + DelegatedPuzzle { admin_inner_puzzle_hash: Some(h.to_vec()), writer_inner_puzzle_hash: None, oracle_payment_puzzle_hash: None, oracle_fee: None } + } + RustDelegatedPuzzle::Writer(h) => { + let h: Bytes32 = (*h).into(); + DelegatedPuzzle { admin_inner_puzzle_hash: None, writer_inner_puzzle_hash: Some(h.to_vec()), oracle_payment_puzzle_hash: None, oracle_fee: None } + } + RustDelegatedPuzzle::Oracle(h, fee) => DelegatedPuzzle { + admin_inner_puzzle_hash: None, + writer_inner_puzzle_hash: None, + oracle_payment_puzzle_hash: Some(h.to_vec()), + oracle_fee: Some(*fee), + }, + }) + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DataStore { + pub coin: Coin, + #[serde(with = "serde_bytes")] + pub launcher_id: Vec, + pub proof: Proof, + pub metadata: DataStoreMetadata, + #[serde(with = "serde_bytes")] + pub owner_puzzle_hash: Vec, + pub delegated_puzzles: Vec, +} + +impl DataStore { + pub fn to_native(&self) -> Result { + Ok(RustDataStore { + coin: self.coin.to_native()?, + proof: self.proof.to_native()?, + info: RustDataStoreInfo { + launcher_id: bytes32(&self.launcher_id)?, + metadata: self.metadata.to_native()?, + owner_puzzle_hash: bytes32(&self.owner_puzzle_hash)?, + delegated_puzzles: self + .delegated_puzzles + .iter() + .map(DelegatedPuzzle::to_native) + .collect::, _>>()?, + }, + }) + } + pub fn from_native(s: &RustDataStore) -> Result { + Ok(DataStore { + coin: Coin::from_native(&s.coin), + launcher_id: s.info.launcher_id.to_vec(), + proof: Proof::from_native(&s.proof), + metadata: DataStoreMetadata::from_native(&s.info.metadata)?, + owner_puzzle_hash: s.info.owner_puzzle_hash.to_vec(), + delegated_puzzles: s + .info + .delegated_puzzles + .iter() + .map(DelegatedPuzzle::from_native) + .collect::, _>>()?, + }) + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SuccessResponse { + pub coin_spends: Vec, + pub new_store: DataStore, +} + +impl SuccessResponse { + pub fn from_native(r: &RustSuccessResponse) -> Result { + Ok(SuccessResponse { + coin_spends: r.coin_spends.iter().map(CoinSpend::from_native).collect(), + new_store: DataStore::from_native(&r.new_datastore)?, + }) + } +} +``` + +Note: confirm the field name on `RustSuccessResponse` is `new_datastore` (per `napi_lib.rs:290`). If `cargo build` reports a different name, correct it here. + +- [ ] **Step 4: Add helper to decode a `Vec` and `Vec` from JsValue** + +Append: +```rust +pub fn coins_from_js(value: JsValue) -> Result, JsValue> { + let coins: Vec = from_js(value)?; + coins.iter().map(Coin::to_native).collect() +} + +pub fn delegated_puzzles_from_js(value: JsValue) -> Result, JsValue> { + let dps: Vec = from_js(value)?; + dps.iter().map(DelegatedPuzzle::to_native).collect() +} + +pub fn coin_spends_from_js(value: JsValue) -> Result, JsValue> { + let css: Vec = from_js(value)?; + css.iter().map(CoinSpend::to_native).collect() +} + +pub fn coin_spends_to_js(css: &[RustCoinSpend]) -> Result { + let out: Vec = css.iter().map(CoinSpend::from_native).collect(); + to_js(&out) +} +``` + +- [ ] **Step 5: Verify it compiles to wasm** + +Run: +```powershell +cargo build -p datalayer-driver-wasm --target wasm32-unknown-unknown +``` +Expected: success (warnings about unused functions are fine; they are used in later tasks). Fix any field-name/type mismatches the compiler reports against the real `datalayer-driver` types. + +- [ ] **Step 6: Commit** + +```powershell +git add wasm/src/types.rs +git commit -m "feat(wasm): add serde boundary structs and native conversions" +``` + +--- + +## Task 5: Implement the utility functions (keys, addresses, proofs, ids) + +These are the simplest and validate the whole toolchain end-to-end. TDD: write a Node parity test stub first (full harness lands in Task 8; here we hand-verify with a quick node check). + +**Files:** +- Modify: `wasm/src/lib.rs` + +- [ ] **Step 1: Implement key/address/proof/id functions** + +Append to `wasm/src/lib.rs` (after `init`): +```rust +use datalayer_driver::{ + address_to_puzzle_hash as core_address_to_puzzle_hash, admin_delegated_puzzle_from_key as core_admin_dp, + get_coin_id as core_get_coin_id, master_public_key_to_first_puzzle_hash as core_mpk_to_first_ph, + master_public_key_to_wallet_synthetic_key as core_mpk_to_synth, + master_secret_key_to_wallet_synthetic_secret_key as core_msk_to_synth, + morph_launcher_id_wrapper as core_morph_launcher_id, puzzle_hash_to_address as core_ph_to_address, + secret_key_to_public_key as core_sk_to_pk, synthetic_key_to_puzzle_hash as core_synth_to_ph, + writer_delegated_puzzle_from_key as core_writer_dp, + get_mainnet_genesis_challenge as core_mainnet_gc, get_testnet11_genesis_challenge as core_testnet_gc, +}; + +use crate::types::{ + bytes32, from_js, public_key, secret_key, to_js, Coin, DelegatedPuzzle, EveProof, LineageProof, Proof, +}; + +#[wasm_bindgen(js_name = "masterPublicKeyToWalletSyntheticKey")] +pub fn master_public_key_to_wallet_synthetic_key(public_key_bytes: &[u8]) -> Result, JsValue> { + let pk = public_key(public_key_bytes)?; + Ok(core_mpk_to_synth(&pk).to_bytes().to_vec()) +} + +#[wasm_bindgen(js_name = "masterPublicKeyToFirstPuzzleHash")] +pub fn master_public_key_to_first_puzzle_hash(public_key_bytes: &[u8]) -> Result, JsValue> { + let pk = public_key(public_key_bytes)?; + Ok(core_mpk_to_first_ph(&pk).to_vec()) +} + +#[wasm_bindgen(js_name = "masterSecretKeyToWalletSyntheticSecretKey")] +pub fn master_secret_key_to_wallet_synthetic_secret_key(secret_key_bytes: &[u8]) -> Result, JsValue> { + let sk = secret_key(secret_key_bytes)?; + Ok(core_msk_to_synth(&sk).to_bytes().to_vec()) +} + +#[wasm_bindgen(js_name = "secretKeyToPublicKey")] +pub fn secret_key_to_public_key(secret_key_bytes: &[u8]) -> Result, JsValue> { + let sk = secret_key(secret_key_bytes)?; + Ok(core_sk_to_pk(&sk).to_bytes().to_vec()) +} + +#[wasm_bindgen(js_name = "syntheticKeyToPuzzleHash")] +pub fn synthetic_key_to_puzzle_hash(synthetic_key_bytes: &[u8]) -> Result, JsValue> { + let pk = public_key(synthetic_key_bytes)?; + Ok(core_synth_to_ph(&pk).to_vec()) +} + +#[wasm_bindgen(js_name = "puzzleHashToAddress")] +pub fn puzzle_hash_to_address(puzzle_hash: &[u8], prefix: String) -> Result { + core_ph_to_address(bytes32(puzzle_hash)?, &prefix).map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen(js_name = "addressToPuzzleHash")] +pub fn address_to_puzzle_hash(address: String) -> Result, JsValue> { + core_address_to_puzzle_hash(&address) + .map(|b| b.to_vec()) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen(js_name = "adminDelegatedPuzzleFromKey")] +pub fn admin_delegated_puzzle_from_key(synthetic_key: &[u8]) -> Result { + let dp = core_admin_dp(&public_key(synthetic_key)?); + to_js(&DelegatedPuzzle::from_native(&dp)?) +} + +#[wasm_bindgen(js_name = "writerDelegatedPuzzleFromKey")] +pub fn writer_delegated_puzzle_from_key(synthetic_key: &[u8]) -> Result { + let dp = core_writer_dp(&public_key(synthetic_key)?); + to_js(&DelegatedPuzzle::from_native(&dp)?) +} + +#[wasm_bindgen(js_name = "newLineageProof")] +pub fn new_lineage_proof(lineage_proof: JsValue) -> Result { + let lp: LineageProof = from_js(lineage_proof)?; + to_js(&Proof { lineage_proof: Some(lp), eve_proof: None }) +} + +#[wasm_bindgen(js_name = "newEveProof")] +pub fn new_eve_proof(eve_proof: JsValue) -> Result { + let ep: EveProof = from_js(eve_proof)?; + to_js(&Proof { lineage_proof: None, eve_proof: Some(ep) }) +} + +#[wasm_bindgen(js_name = "getCoinId")] +pub fn get_coin_id(coin: JsValue) -> Result, JsValue> { + let c: Coin = from_js(coin)?; + Ok(core_get_coin_id(&c.to_native()?).to_vec()) +} + +#[wasm_bindgen(js_name = "morphLauncherId")] +pub fn morph_launcher_id(launcher_id: &[u8], offset: u64) -> Result, JsValue> { + Ok(core_morph_launcher_id(bytes32(launcher_id)?, offset).to_vec()) +} + +#[wasm_bindgen(js_name = "getMainnetGenesisChallenge")] +pub fn get_mainnet_genesis_challenge() -> Vec { + core_mainnet_gc().to_vec() +} + +#[wasm_bindgen(js_name = "getTestnet11GenesisChallenge")] +pub fn get_testnet11_genesis_challenge() -> Vec { + core_testnet_gc().to_vec() +} +``` + +Note: `morph_launcher_id_wrapper` is the crate-root export (`src/lib.rs:143`); confirm the symbol name when compiling and adjust the `use` alias if needed. + +- [ ] **Step 2: Build to wasm and to a node target for a quick check** + +Run: +```powershell +cargo build -p datalayer-driver-wasm --target wasm32-unknown-unknown +wasm-pack build wasm --target nodejs --dev --out-dir pkg-node +``` +Expected: both succeed; `wasm/pkg-node/` is produced. + +- [ ] **Step 3: Quick manual round-trip check** + +Run: +```powershell +node -e "const w=require('./wasm/pkg-node'); w.init(); const ph=w.addressToPuzzleHash('xch1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsk9rsxe'); console.log(Buffer.from(ph).toString('hex')); console.log(w.puzzleHashToAddress(ph,'xch'));" +``` +Expected: prints a 64-char hex and re-encodes to the same address (round-trip). If the sample address is invalid, substitute any valid `xch1...` address; the point is the round-trip equality. + +- [ ] **Step 4: Commit** + +```powershell +git add wasm/src/lib.rs +git commit -m "feat(wasm): key, address, proof, and id utility bindings" +``` + +--- + +## Task 6: Implement DIGStore spend builders (PRIMARY GOAL) + +`mint_store`, `oracle_spend`, `update_store_metadata`, `update_store_ownership`, `melt_store`. Signatures mirror the NAPI JS API positionally. + +**Files:** +- Modify: `wasm/src/lib.rs` +- Reference: `napi/src/napi_lib.rs:1241` (mint_store), `:1292` (oracle_spend), `:1533` (update_store_metadata), `:1590` (update_store_ownership), `:1636` (melt_store); core wrappers `src/lib.rs:237,264,280,301,316`. + +- [ ] **Step 1: Inspect the exact NAPI signatures to mirror** + +Run: +```powershell +Select-String -Path napi\src\napi_lib.rs -Pattern 'pub fn (mint_store|oracle_spend|update_store_metadata|update_store_ownership|melt_store)' -Context 0,14 +``` +Expected: prints each function's parameter list. Confirm parameter order and the `DataStoreInnerSpend` shape used by `update_store_*` (it carries the synthetic key / delegated-puzzle selection). Mirror those exactly in Step 2–3. If `update_store_*` takes a non-trivial `inner_spend_info`, model it as a `JsValue` deserialized into a local `InnerSpendInfo` boundary struct added to `types.rs` matching the NAPI `DataStoreInnerSpend` JS fields. + +- [ ] **Step 2: Implement `mint_store`, `oracle_spend`, `melt_store`** + +Append to `wasm/src/lib.rs`: +```rust +use datalayer_driver::{ + melt_store as core_melt_store, mint_store as core_mint_store, oracle_spend as core_oracle_spend, +}; +use crate::types::{ + coin_spends_to_js, coins_from_js, delegated_puzzles_from_js, DataStore, SuccessResponse, +}; + +#[wasm_bindgen(js_name = "mintStore")] +#[allow(clippy::too_many_arguments)] +pub fn mint_store( + minter_synthetic_key: &[u8], + selected_coins: JsValue, // Coin[] + root_hash: &[u8], + label: Option, + description: Option, + bytes: Option, + owner_puzzle_hash: &[u8], + delegated_puzzles: JsValue, // DelegatedPuzzle[] + fee: u64, +) -> Result { // SuccessResponse + let resp = core_mint_store( + public_key(minter_synthetic_key)?, + coins_from_js(selected_coins)?, + bytes32(root_hash)?, + label, + description, + bytes, + None, // size_proof: NAPI exposes Option; add a param if a test needs it + bytes32(owner_puzzle_hash)?, + delegated_puzzles_from_js(delegated_puzzles)?, + fee, + ) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + to_js(&SuccessResponse::from_native(&resp)?) +} + +#[wasm_bindgen(js_name = "oracleSpend")] +pub fn oracle_spend( + spender_synthetic_key: &[u8], + selected_coins: JsValue, // Coin[] + store: JsValue, // DataStore + fee: u64, +) -> Result { + let store: DataStore = from_js(store)?; + let resp = core_oracle_spend( + public_key(spender_synthetic_key)?, + coins_from_js(selected_coins)?, + store.to_native()?, + fee, + ) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + to_js(&SuccessResponse::from_native(&resp)?) +} + +#[wasm_bindgen(js_name = "meltStore")] +pub fn melt_store(store: JsValue, owner_public_key: &[u8]) -> Result { + let store: DataStore = from_js(store)?; + let css = core_melt_store(store.to_native()?, public_key(owner_public_key)?) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + coin_spends_to_js(&css) +} +``` + +Note: the NAPI `mint_store` includes a `size_proof: Option` parameter between `bytes` and `owner_puzzle_hash`. If the parity test in Task 8 exercises `size_proof`, add `size_proof: Option>` as a param and pass `size_proof.map(|b| bytes32(&b)).transpose()?.map(|h| h.to_string())`. Keep parameter order identical to NAPI. + +- [ ] **Step 3: Implement `update_store_metadata` and `update_store_ownership`** + +Add the `InnerSpendInfo` boundary struct to `wasm/src/types.rs` first, matching the NAPI `DataStoreInnerSpend` JS shape discovered in Step 1 (typical shape: an owner/admin/writer synthetic key plus optional delegated-puzzle list). Example skeleton — adjust fields to the real shape: +```rust +// wasm/src/types.rs +use datalayer_driver::DataStoreInnerSpend as RustDataStoreInnerSpend; + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InnerSpendInfo { + #[serde(default, with = "serde_bytes")] + pub public_key: Option>, + // ...mirror the real DataStoreInnerSpend variants discovered in Task 6 Step 1 +} + +impl InnerSpendInfo { + pub fn to_native(&self) -> Result { + // construct the matching native variant + todo!("fill in per the real DataStoreInnerSpend definition") + } +} +``` +Then in `wasm/src/lib.rs`: +```rust +use datalayer_driver::{ + update_store_metadata as core_update_meta, update_store_ownership as core_update_owner, +}; +use crate::types::InnerSpendInfo; + +#[wasm_bindgen(js_name = "updateStoreMetadata")] +#[allow(clippy::too_many_arguments)] +pub fn update_store_metadata( + store: JsValue, // DataStore + new_root_hash: &[u8], + new_label: Option, + new_description: Option, + new_bytes: Option, + inner_spend_info: JsValue, // InnerSpendInfo +) -> Result { + let store: DataStore = from_js(store)?; + let info: InnerSpendInfo = from_js(inner_spend_info)?; + let resp = core_update_meta( + store.to_native()?, + bytes32(new_root_hash)?, + new_label, + new_description, + new_bytes, + None, // new_size_proof — add a param if exercised + info.to_native()?, + ) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + to_js(&SuccessResponse::from_native(&resp)?) +} + +#[wasm_bindgen(js_name = "updateStoreOwnership")] +pub fn update_store_ownership( + store: JsValue, // DataStore + new_owner_puzzle_hash: &[u8], + new_delegated_puzzles: JsValue, // DelegatedPuzzle[] + inner_spend_info: JsValue, // InnerSpendInfo +) -> Result { + let store: DataStore = from_js(store)?; + let info: InnerSpendInfo = from_js(inner_spend_info)?; + let resp = core_update_owner( + store.to_native()?, + bytes32(new_owner_puzzle_hash)?, + delegated_puzzles_from_js(new_delegated_puzzles)?, + info.to_native()?, + ) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + to_js(&SuccessResponse::from_native(&resp)?) +} +``` +Replace the `todo!()` in `InnerSpendInfo::to_native` with the real construction once Step 1 reveals the exact `DataStoreInnerSpend` definition. The build will fail until this is done — that is intentional (it forces matching the real type). + +- [ ] **Step 4: Build to wasm** + +Run: +```powershell +cargo build -p datalayer-driver-wasm --target wasm32-unknown-unknown +``` +Expected: success once `InnerSpendInfo` matches the real type. Fix compiler-reported mismatches against the real signatures. + +- [ ] **Step 5: Commit** + +```powershell +git add wasm/src/lib.rs wasm/src/types.rs +git commit -m "feat(wasm): DIGStore spend builders (mint/oracle/melt/update)" +``` + +--- + +## Task 7: Implement signing, serialization, coin selection, and server-coin helpers + +`sign_coin_spends`, `sign_message`, `verify_signed_message`, `spend_bundle_to_hex`, `hex_spend_bundle_to_coin_spends`, `get_cost`, `select_coins`, `send_xch`, `add_fee`, `create_server_coin`. + +**Files:** +- Modify: `wasm/src/lib.rs`, `wasm/src/types.rs` + +- [ ] **Step 1: Add `ServerCoin`, `NewServerCoin`, `Output` boundary structs to `types.rs`** + +```rust +use datalayer_driver::XchServerCoin as RustXchServerCoin; +use datalayer_driver::xch_server_coin::NewXchServerCoin as RustNewXchServerCoin; + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ServerCoin { + pub coin: Coin, + #[serde(with = "serde_bytes")] + pub p2_puzzle_hash: Vec, + pub memo_urls: Vec, +} + +impl ServerCoin { + pub fn from_native(s: &RustXchServerCoin) -> Self { + ServerCoin { + coin: Coin::from_native(&s.coin), + p2_puzzle_hash: s.p2_puzzle_hash.to_vec(), + memo_urls: s.memo_urls.clone(), + } + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NewServerCoin { + pub server_coin: ServerCoin, + pub coin_spends: Vec, +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Output { + #[serde(with = "serde_bytes")] + pub puzzle_hash: Vec, + pub amount: u64, + pub memos: Vec, // Vec +} +``` +Confirm `NewXchServerCoin`'s fields against `src/xch_server_coin.rs`; adjust `NewServerCoin::from_native` accordingly (added in Step 3). + +- [ ] **Step 2: Implement signing + serialization + cost + selection** + +Append to `wasm/src/lib.rs`: +```rust +use datalayer_driver::{ + get_cost as core_get_cost, hex_spend_bundle_to_coin_spends as core_hex_to_css, + select_coins as core_select_coins, sign_coin_spends as core_sign_css, sign_message as core_sign_msg, + spend_bundle_to_hex as core_sb_to_hex, verify_signed_message as core_verify_msg, SpendBundle as RustSpendBundle, +}; +use crate::types::{coin_spends_from_js, signature, Coin}; + +#[wasm_bindgen(js_name = "signCoinSpends")] +pub fn sign_coin_spends( + coin_spends: JsValue, // CoinSpend[] + private_keys: JsValue, // Uint8Array[] (each a 32-byte secret key) + for_testnet: bool, +) -> Result, JsValue> { // aggregated signature, 96 bytes + let css = coin_spends_from_js(coin_spends)?; + let keys_raw: Vec = from_js(private_keys)?; + let keys = keys_raw + .iter() + .map(|k| secret_key(k)) + .collect::, _>>()?; + let sig = core_sign_css(&css, &keys, for_testnet).map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(sig.to_bytes().to_vec()) +} + +#[wasm_bindgen(js_name = "signMessage")] +pub fn sign_message(message: &[u8], private_key: &[u8]) -> Result, JsValue> { + let sig = core_sign_msg(message, &secret_key(private_key)?).map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(sig.to_bytes().to_vec()) +} + +#[wasm_bindgen(js_name = "verifySignedMessage")] +pub fn verify_signed_message(sig: &[u8], public_key_bytes: &[u8], message: &[u8]) -> Result { + core_verify_msg(&signature(sig)?, &public_key(public_key_bytes)?, message) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen(js_name = "getCost")] +pub fn get_cost(coin_spends: JsValue) -> Result { + core_get_cost(&coin_spends_from_js(coin_spends)?).map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen(js_name = "selectCoins")] +pub fn select_coins(all_coins: JsValue, total_amount: u64) -> Result { + let selected = core_select_coins(&coins_from_js(all_coins)?, total_amount) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + let out: Vec = selected.iter().map(Coin::from_native).collect(); + to_js(&out) +} + +#[wasm_bindgen(js_name = "spendBundleToHex")] +pub fn spend_bundle_to_hex(spend_bundle: JsValue) -> Result { + // SpendBundle = { coinSpends: CoinSpend[], aggregatedSignature: Uint8Array } + #[derive(serde::Deserialize)] + #[serde(rename_all = "camelCase")] + struct SbIn { coin_spends: Vec, #[serde(with = "serde_bytes")] aggregated_signature: Vec } + let sb: SbIn = from_js(spend_bundle)?; + let css = sb.coin_spends.iter().map(crate::types::CoinSpend::to_native).collect::, _>>()?; + let bundle = RustSpendBundle::new(css, signature(&sb.aggregated_signature)?); + core_sb_to_hex(&bundle).map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen(js_name = "hexSpendBundleToCoinSpends")] +pub fn hex_spend_bundle_to_coin_spends(hex: String) -> Result { + let css = core_hex_to_css(&hex).map_err(|e| JsValue::from_str(&e.to_string()))?; + coin_spends_to_js(&css) +} +``` +Confirm `RustSpendBundle::new(coin_spends, signature)` constructor signature; the core re-export is `chia::protocol::SpendBundle`. Adjust if its constructor differs. + +- [ ] **Step 3: Implement `send_xch`, `add_fee`, `create_server_coin`** + +```rust +use datalayer_driver::{ + add_fee as core_add_fee, create_server_coin as core_create_server_coin, send_xch as core_send_xch, Output as RustOutput, +}; +use crate::types::{NewServerCoin, Output, ServerCoin}; + +#[wasm_bindgen(js_name = "sendXch")] +pub fn send_xch( + synthetic_key: &[u8], + selected_coins: JsValue, // Coin[] + outputs: JsValue, // Output[] + fee: u64, +) -> Result { + let outs: Vec = from_js(outputs)?; + let native_outs: Vec = outs + .iter() + .map(|o| { + Ok(RustOutput { + puzzle_hash: bytes32(&o.puzzle_hash)?, + amount: o.amount, + memos: o.memos.iter().map(|m| chia::protocol::Bytes::new(m.to_vec())).collect(), + }) + }) + .collect::, JsValue>>()?; + let css = core_send_xch(&public_key(synthetic_key)?, &coins_from_js(selected_coins)?, &native_outs, fee) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + coin_spends_to_js(&css) +} + +#[wasm_bindgen(js_name = "addFee")] +pub fn add_fee( + spender_synthetic_key: &[u8], + selected_coins: JsValue, // Coin[] + assert_coin_ids: JsValue, // Uint8Array[] + fee: u64, +) -> Result { + let ids_raw: Vec = from_js(assert_coin_ids)?; + let ids = ids_raw.iter().map(|b| bytes32(b)).collect::, _>>()?; + let css = core_add_fee(&public_key(spender_synthetic_key)?, &coins_from_js(selected_coins)?, &ids, fee) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + coin_spends_to_js(&css) +} + +#[wasm_bindgen(js_name = "createServerCoin")] +pub fn create_server_coin( + synthetic_key: &[u8], + selected_coins: JsValue, // Coin[] + hint: &[u8], + uris: JsValue, // string[] + amount: u64, + fee: u64, +) -> Result { + let uris: Vec = from_js(uris)?; + let new_sc = core_create_server_coin( + public_key(synthetic_key)?, + coins_from_js(selected_coins)?, + bytes32(hint)?, + uris, + amount, + fee, + ) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + // Map RustNewXchServerCoin -> NewServerCoin (confirm field names) + let out = NewServerCoin { + server_coin: ServerCoin::from_native(&new_sc.server_coin), + coin_spends: new_sc.coin_spends.iter().map(crate::types::CoinSpend::from_native).collect(), + }; + to_js(&out) +} +``` +Confirm `RustNewXchServerCoin` field names (`server_coin`, `coin_spends`) against `src/xch_server_coin.rs`; adjust if different. + +- [ ] **Step 4: Build to wasm** + +Run: +```powershell +cargo build -p datalayer-driver-wasm --target wasm32-unknown-unknown +``` +Expected: success. Fix any constructor/field mismatches the compiler reports. + +- [ ] **Step 5: Commit** + +```powershell +git add wasm/src/lib.rs wasm/src/types.rs +git commit -m "feat(wasm): signing, serialization, selection, server-coin bindings" +``` + +--- + +## Task 8: Node parity test — WASM output equals NAPI output + +**Files:** +- Create: `wasm/package.json`, `wasm/tests/parity.mjs` + +- [ ] **Step 1: Create `wasm/package.json` with build + test scripts** + +```json +{ + "name": "@dignetwork/datalayer-driver-wasm-dev", + "version": "3.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "wasm-pack build . --target bundler --release && node scripts/patch-pkg.mjs", + "build:node": "wasm-pack build . --target nodejs --release --out-dir pkg-node", + "test": "npm run build:node && node tests/parity.mjs" + }, + "devDependencies": { + "@dignetwork/datalayer-driver": "file:../napi" + } +} +``` +Note: the test depends on the NAPI package built in `../napi`. In CI the NAPI `.node` artifact is downloaded into `napi/` before this runs (Task 10). + +- [ ] **Step 2: Write the parity test** + +`wasm/tests/parity.mjs`: +```js +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; +const require = createRequire(import.meta.url); + +const wasm = require("../pkg-node"); +const napi = require("@dignetwork/datalayer-driver"); +wasm.init(); + +const eqBytes = (a, b, msg) => + assert.equal(Buffer.from(a).toString("hex"), Buffer.from(b).toString("hex"), msg); + +// Deterministic key material. +const sk = Buffer.alloc(32, 1); + +// 1. Key derivation parity. +const pkW = wasm.secretKeyToPublicKey(sk); +const pkN = napi.secretKeyToPublicKey(sk); +eqBytes(pkW, pkN, "secretKeyToPublicKey"); + +const synthW = wasm.masterPublicKeyToWalletSyntheticKey(pkW); +const synthN = napi.masterPublicKeyToWalletSyntheticKey(pkN); +eqBytes(synthW, synthN, "masterPublicKeyToWalletSyntheticKey"); + +const phW = wasm.masterPublicKeyToFirstPuzzleHash(pkW); +const phN = napi.masterPublicKeyToFirstPuzzleHash(pkN); +eqBytes(phW, phN, "masterPublicKeyToFirstPuzzleHash"); + +// 2. Address round-trip parity. +const addrW = wasm.puzzleHashToAddress(phW, "xch"); +const addrN = napi.puzzleHashToAddress(phN, "xch"); +assert.equal(addrW, addrN, "puzzleHashToAddress"); +eqBytes(wasm.addressToPuzzleHash(addrW), napi.addressToPuzzleHash(addrN), "addressToPuzzleHash"); + +// 3. PRIMARY: mint_store spend bundle parity. +const ownerPh = phW; +const adminDpW = wasm.adminDelegatedPuzzleFromKey(synthW); +const adminDpN = napi.adminDelegatedPuzzleFromKey(synthN); + +const coin = { + parentCoinInfo: Buffer.alloc(32, 2), + puzzleHash: Buffer.from(ownerPh), + amount: 1_000_000_000_000n, // 1 XCH — exceeds 2^53 territory in aggregate +}; +const rootHash = Buffer.alloc(32, 3); + +const mintW = wasm.mintStore(synthW, [coin], rootHash, "label", "desc", 42n, ownerPh, [adminDpW], 0n); +const mintN = napi.mintStore(synthN, [coin], rootHash, "label", "desc", 42n, ownerPh, [adminDpN], 0n); + +// Compare the produced coin spends byte-for-byte via the hex of a signed bundle. +const sigW = wasm.signCoinSpends(mintW.coinSpends, [sk], true); +const sigN = napi.signCoinSpends(mintN.coinSpends, [sk], true); +eqBytes(sigW, sigN, "signCoinSpends(mint)"); + +const hexW = wasm.spendBundleToHex({ coinSpends: mintW.coinSpends, aggregatedSignature: sigW }); +const hexN = napi.spendBundleToHex({ coinSpends: mintN.coinSpends, aggregatedSignature: sigN }); +assert.equal(hexW, hexN, "mint spend bundle hex parity"); + +// 4. Cost + coin id parity. +assert.equal(wasm.getCost(mintW.coinSpends), napi.getCost(mintN.coinSpends), "getCost"); +eqBytes(wasm.getCoinId(coin), napi.getCoinId(coin), "getCoinId"); + +console.log("All parity checks passed."); +``` +Note: align the `mintStore` argument list with the final WASM signature from Task 6 (especially the `size_proof` parameter — include it in BOTH calls or omit from both). If NAPI's `mintStore` requires `size_proof`, pass `null`/`undefined` in both. + +- [ ] **Step 3: Run the parity test locally (requires a native NAPI build)** + +Run: +```powershell +cd napi; npm install; npm run build; cd .. +cd wasm; npm install; npm test; cd .. +``` +Expected: `All parity checks passed.` If a check fails, the WASM conversion for that function diverges from NAPI — fix the conversion in `wasm/src/types.rs` or the binding in `wasm/src/lib.rs`, rebuild, re-run. + +- [ ] **Step 4: Commit** + +```powershell +git add wasm/package.json wasm/tests/parity.mjs +git commit -m "test(wasm): node parity test vs napi (incl. mint_store bundle)" +``` + +--- + +## Task 9: Package metadata patch + typed `.d.ts` + +wasm-pack names the package after the crate (`datalayer-driver-wasm`) and emits `any` for `JsValue` params. We override both for the published artifact. + +**Files:** +- Create: `wasm/scripts/patch-pkg.mjs`, `wasm/types/datalayer-driver-wasm.d.ts` + +- [ ] **Step 1: Write the typed declaration file** + +`wasm/types/datalayer-driver-wasm.d.ts` — mirror the NAPI `index.d.ts` object shapes. Representative content (extend to cover every exported function): +```ts +export interface Coin { parentCoinInfo: Uint8Array; puzzleHash: Uint8Array; amount: bigint; } +export interface CoinSpend { coin: Coin; puzzleReveal: Uint8Array; solution: Uint8Array; } +export interface LineageProof { parentParentCoinInfo: Uint8Array; parentInnerPuzzleHash: Uint8Array; parentAmount: bigint; } +export interface EveProof { parentParentCoinInfo: Uint8Array; parentAmount: bigint; } +export interface Proof { lineageProof?: LineageProof; eveProof?: EveProof; } +export interface DataStoreMetadata { rootHash: Uint8Array; label?: string; description?: string; bytes?: bigint; sizeProof?: Uint8Array; } +export interface DelegatedPuzzle { adminInnerPuzzleHash?: Uint8Array; writerInnerPuzzleHash?: Uint8Array; oraclePaymentPuzzleHash?: Uint8Array; oracleFee?: bigint; } +export interface DataStore { coin: Coin; launcherId: Uint8Array; proof: Proof; metadata: DataStoreMetadata; ownerPuzzleHash: Uint8Array; delegatedPuzzles: DelegatedPuzzle[]; } +export interface SuccessResponse { coinSpends: CoinSpend[]; newStore: DataStore; } + +export function init(): void; +export function mintStore(minterSyntheticKey: Uint8Array, selectedCoins: Coin[], rootHash: Uint8Array, label: string | undefined, description: string | undefined, bytes: bigint | undefined, ownerPuzzleHash: Uint8Array, delegatedPuzzles: DelegatedPuzzle[], fee: bigint): SuccessResponse; +export function oracleSpend(spenderSyntheticKey: Uint8Array, selectedCoins: Coin[], store: DataStore, fee: bigint): SuccessResponse; +export function meltStore(store: DataStore, ownerPublicKey: Uint8Array): CoinSpend[]; +export function signCoinSpends(coinSpends: CoinSpend[], privateKeys: Uint8Array[], forTestnet: boolean): Uint8Array; +export function spendBundleToHex(spendBundle: { coinSpends: CoinSpend[]; aggregatedSignature: Uint8Array }): string; +export function getCost(coinSpends: CoinSpend[]): bigint; +export function getCoinId(coin: Coin): Uint8Array; +export function selectCoins(allCoins: Coin[], totalAmount: bigint): Coin[]; +export function puzzleHashToAddress(puzzleHash: Uint8Array, prefix: string): string; +export function addressToPuzzleHash(address: string): Uint8Array; +// ...add remaining functions to match the full exported surface +``` +Keep this file in sync with the final binding signatures from Tasks 5–7. + +- [ ] **Step 2: Write the patch script** + +`wasm/scripts/patch-pkg.mjs`: +```js +import { readFile, writeFile, copyFile } from "node:fs/promises"; +import path from "node:path"; + +const pkgDir = path.resolve("pkg"); +const pkgJsonPath = path.join(pkgDir, "package.json"); + +const pkg = JSON.parse(await readFile(pkgJsonPath, "utf8")); + +pkg.name = "@dignetwork/datalayer-driver-wasm"; +pkg.description = "WebAssembly bindings for the Chia DataLayer driver (offline DIGStore spend-bundle construction)."; +pkg.repository = { type: "git", url: "https://github.com/DIG-Network/DataLayer-Driver.git" }; +pkg.license = "MIT"; +pkg.types = "datalayer-driver-wasm.d.ts"; +pkg.files = Array.from(new Set([...(pkg.files ?? []), "datalayer-driver-wasm.d.ts"])); + +await writeFile(pkgJsonPath, JSON.stringify(pkg, null, 2) + "\n"); +await copyFile(path.resolve("types/datalayer-driver-wasm.d.ts"), path.join(pkgDir, "datalayer-driver-wasm.d.ts")); + +console.log(`Patched ${pkg.name}@${pkg.version}`); +``` + +- [ ] **Step 3: Run the bundler build + patch and inspect the result** + +Run: +```powershell +cd wasm; npm run build; cd .. +Get-Content wasm\pkg\package.json +``` +Expected: `package.json` shows `"name": "@dignetwork/datalayer-driver-wasm"`, version `3.0.0`, `types` pointing at the typed `.d.ts`, and the `.d.ts` present in `pkg/`. + +- [ ] **Step 4: Commit** + +```powershell +git add wasm/scripts/patch-pkg.mjs wasm/types/datalayer-driver-wasm.d.ts +git commit -m "build(wasm): scoped package-name patch and typed d.ts" +``` + +--- + +## Task 10: CI — build, test, and publish the WASM package + +**Files:** +- Modify: `.github/workflows/CI.yml` + +- [ ] **Step 1: Add the `build-wasm` job** + +Insert after the existing `build` job (a sibling job, gated on `rust-checks`): +```yaml + build-wasm: + name: Build & test WASM bindings + needs: rust-checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Install Rust + wasm target + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + targets: wasm32-unknown-unknown + - name: Install clang/llvm (blst needs it for wasm32) + run: sudo apt-get update && sudo apt-get install -y clang lld + - name: Install wasm-pack + uses: jetli/wasm-pack-action@v0.4.0 + with: + version: latest + - name: Build NAPI (for parity test) + run: cd napi && npm install && npm run build + - name: Build WASM (bundler, published artifact) + run: cd wasm && npm install && npm run build + - name: Parity test (nodejs target vs NAPI) + run: cd wasm && npm test + - name: Upload wasm pkg + uses: actions/upload-artifact@v4 + with: + name: wasm-pkg + path: wasm/pkg + if-no-files-found: error +``` + +- [ ] **Step 2: Add the `publish-wasm-npm` job** + +Mirror the existing `publish-npm` version-tag gating (commit message is a bare semver): +```yaml + publish-wasm-npm: + name: Publish WASM to NPM + runs-on: ubuntu-latest + needs: + - build-wasm + steps: + - uses: actions/checkout@v4 + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Download wasm pkg + uses: actions/download-artifact@v4 + with: + name: wasm-pkg + path: wasm/pkg + - name: Publish to NPM + run: | + cd wasm/pkg + npm config set provenance true + if git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+$"; + then + echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc + npm publish --access public + elif git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+"; + then + echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc + npm publish --tag next --access public + else + echo "Not a release, skipping wasm npm publish" + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} +``` +Note: `git log` inside `wasm/pkg` works because the artifact is downloaded into a checked-out repo. If the `.git` context is unavailable there, run the `git log` check from the repo root and `cd wasm/pkg` only for `npm publish`. + +- [ ] **Step 3: Validate the workflow YAML** + +Run: +```powershell +python -c "import yaml,sys; yaml.safe_load(open('.github/workflows/CI.yml')); print('YAML OK')" +``` +Expected: `YAML OK`. (If Python is unavailable, use any YAML linter or push to a branch and confirm Actions parses it.) + +- [ ] **Step 4: Commit** + +```powershell +git add .github/workflows/CI.yml +git commit -m "ci: build, parity-test, and publish wasm npm package" +``` + +--- + +## Task 11: Docs and final parity self-check + +**Files:** +- Create: `wasm/README.md` +- Modify: `README.md` (root) — add a WASM section + +- [ ] **Step 1: Write `wasm/README.md`** + +Document: install (`npm i @dignetwork/datalayer-driver-wasm`), `init()` requirement, that it is offline-only (no Peer/networking), a `mintStore` → `signCoinSpends` → `spendBundleToHex` example, and the bundler-target note (works in webpack/vite/next; for plain Node use a separate build). + +- [ ] **Step 2: Add a short WASM pointer to the root `README.md`** + +One paragraph linking to `wasm/README.md`, stating the WASM package mirrors the offline NAPI surface for browser/bundler use. + +- [ ] **Step 3: Final full build + test from a clean target dir** + +Run: +```powershell +cargo build -p datalayer-driver --target wasm32-unknown-unknown --no-default-features +cargo build -p datalayer-driver-napi +cd wasm; npm run build; npm test; cd .. +``` +Expected: all succeed; parity test prints `All parity checks passed.` + +- [ ] **Step 4: Commit** + +```powershell +git add wasm/README.md README.md +git commit -m "docs(wasm): usage and parity notes" +``` + +--- + +## Self-Review + +**Spec coverage:** +- §3.1 crate layout → Task 3 (scaffold), Task 4/5/6/7 (src files), Task 8/9 (package, scripts, tests). ✓ +- §3.2 `native` feature gating → Task 1. ✓ +- §3.3 wasm deps → Task 3 Step 2. ✓ +- §4 type mapping (Uint8Array, bigint, serde_bytes, camelCase, bigint serializer) → Task 4 Step 1–3. ✓ (BigInt-serializer correctness explicitly handled — supersedes the reference's lossy plain `to_value`.) +- §4.1 function/type inventory → Priority-1 in Task 6, supporting in Tasks 5 & 7; structs in Task 4 & 7. ✓ +- §5 build/packaging (`--target bundler`, scoped name, package.json patch) → Task 9. ✓ +- §6 CI (clang, wasm target, wasm-pack, version-tag publish) → Task 10. ✓ +- §7 parity testing vs NAPI incl. DIGStore bundles → Task 8. ✓ +- §8 risks (chia-wallet-sdk wasm32, getrandom, tokio leakage, bundler-not-node) → Task 1 Step 7, Task 2, Task 8 (separate node build). ✓ + +**Placeholder scan:** Two intentional `todo!()`/`None`-with-note points exist — `InnerSpendInfo::to_native` (Task 6 Step 3) and the `size_proof` parameter handling — because the exact `DataStoreInnerSpend` shape and whether tests exercise `size_proof` must be read from the real source during execution (Task 6 Step 1 instructs this). These are guarded by explicit "fill in per the real definition" instructions and a build that fails until resolved, not silent gaps. + +**Type consistency:** Boundary struct names (`Coin`, `CoinSpend`, `DataStore`, `DataStoreMetadata`, `DelegatedPuzzle`, `Proof`, `SuccessResponse`, `ServerCoin`, `NewServerCoin`, `Output`, `InnerSpendInfo`) and helpers (`to_js`, `from_js`, `bytes32`, `public_key`, `secret_key`, `signature`, `coins_from_js`, `delegated_puzzles_from_js`, `coin_spends_from_js`, `coin_spends_to_js`) are used consistently across Tasks 4–7. JS export names are camelCase (`mintStore`, `signCoinSpends`, …) matching NAPI throughout. + +**Open items the executor MUST verify against live source (flagged in-task, not assumed):** +- `RustSuccessResponse` field name `new_datastore` (Task 4 Step 3). +- Exact `DataStoreInnerSpend` definition (Task 6 Step 1/3). +- `mint_store`/`update_store_metadata` `size_proof` parameter presence (Task 6). +- `SpendBundle::new` constructor (Task 7 Step 2). +- `NewXchServerCoin` field names (Task 7 Step 3). +- `morph_launcher_id_wrapper` export symbol (Task 5 Step 1). diff --git a/docs/superpowers/specs/2026-05-29-datalayer-driver-wasm-bindings-design.md b/docs/superpowers/specs/2026-05-29-datalayer-driver-wasm-bindings-design.md new file mode 100644 index 0000000..cc6f7e2 --- /dev/null +++ b/docs/superpowers/specs/2026-05-29-datalayer-driver-wasm-bindings-design.md @@ -0,0 +1,243 @@ +# DataLayer-Driver WASM Bindings — Design + +**Date:** 2026-05-29 +**Status:** Approved (design), pending implementation plan +**Author:** Michael Taylor (with Claude Code) + +## 1. Goal + +Add WebAssembly bindings to `DataLayer-Driver` that mirror the existing NAPI +interface for the **offline** (non-networking) subset of the API, and publish +them as an npm package via CI. + +**Primary success criterion:** build and sign **DIGStore (DataStore) spend +bundles** entirely in WASM (browser / bundler environments), producing byte-for-byte +identical output to the NAPI bindings. Everything else (key/address utilities, +coin selection, hex helpers) is supporting cast for that workflow. + +Non-goals: +- Networking from WASM (no `Peer`, no `Tls`, no `connect_*`, no `sync_*`, no + `broadcast_*`). WASM cannot open native TCP sockets or do native TLS. Chain + reads/writes remain the JS consumer's responsibility. +- Async peer methods (`mint_nft`, `generate_did_proof*` async variants, + `simulator_*`). + +## 2. Why offline-only + +The NAPI surface splits cleanly in two: + +1. **Offline functions** — key derivation, address conversion, coin selection, + DataStore/server-coin spend construction, signing, hex/cost/id helpers, + genesis constants. Pure CLVM + BLS; no IO. Port to WASM cleanly. +2. **`Peer` + `Tls` (~24 async methods)** — native TCP socket + native TLS + + multi-threaded tokio + filesystem cert loading. None of these exist in + `wasm32-unknown-unknown`. + +The reference project `chia-scaled-parallel-voting` (same org) solved this by +putting **zero networking in WASM** and gating native code behind a `native` +cargo feature. We follow that proven pattern. + +## 3. Architecture + +### 3.1 Crate layout + +New workspace member `wasm/`, sibling to the existing `napi/`: + +``` +DataLayer-Driver/ + Cargo.toml # workspace members = [".", "napi", "wasm"] + src/ # core crate — gains a `native` feature + napi/ # unchanged (keeps default features → native on) + wasm/ + Cargo.toml # crate-type = ["cdylib", "rlib"] + build.rs # (optional) none required for wasm-bindgen + src/ + lib.rs # #[wasm_bindgen] glue (analog of napi/src/napi_lib.rs) + conversions.rs # JS <-> Rust type bridging (analog of napi/src/conversions.rs) + tests/ # node parity tests (run in CI) + package.json # publish metadata override for @dignetwork/datalayer-driver-wasm +``` + +### 3.2 Core crate `native` feature + +`datalayer-driver/Cargo.toml`: + +```toml +[features] +default = ["native"] +native = ["chia-wallet-sdk/native-tls", "chia-wallet-sdk/peer-simulator", "dep:tokio"] +``` + +- `chia-wallet-sdk` keeps `chip-0035` + `action-layer` unconditionally (pure + puzzle/driver logic, wasm-safe); `native-tls` + `peer-simulator` move under + `native`. +- `tokio` becomes optional, pulled only by `native`. +- Gate behind `#[cfg(feature = "native")]`: + - the entire `async_api` module (`src/lib.rs`) + - every `&Peer` async fn in `src/wallet.rs` (15+: `get_unspent_coin_states*`, + `spend_xch_server_coins`, `fetch_xch_server_coin`, `sync_store*`, + `get_store_creation_height`, `broadcast_spend_bundle`, `get_header_hash`, + `get_fee_estimate`, `is_coin_spent`, `look_up_possible_launchers`, + `subscribe_/unsubscribe_to_coin_states`, `mint_nft`, + `generate_did_proof*`, `resolve_did_string_and_generate_proof`) + - the `use chia_wallet_sdk::client::Peer;` imports and the `pub use ...Peer` + re-export in `src/lib.rs` + - `&Peer`/async items in `src/dig_coin.rs` and `src/dig_collateral_coin.rs` +- The `napi/` crate keeps `datalayer-driver` with default features → no behavior + change, no CI regression for the native build. + +The WASM crate depends on the core crate with networking off: + +```toml +datalayer-driver = { path = "..", default-features = false } +``` + +### 3.3 WASM crate dependencies + +```toml +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +datalayer-driver = { path = "..", default-features = false } +wasm-bindgen = "0.2" +js-sys = "0.3" +serde = { version = "1", features = ["derive"] } +serde_bytes = "0.11" +serde-wasm-bindgen = "0.6" +console_error_panic_hook = { version = "0.1", optional = true } +hex = "0.4" +# getrandom feature/version resolved against what `chia 0.26` pulls (see Risks) + +[features] +default = ["console-panic-hook"] +console-panic-hook = ["dep:console_error_panic_hook"] +``` + +No `wasm-bindgen-futures` / `js-sys::Promise` needed — offline functions are +synchronous. + +## 4. JS interface & type mapping + +Goal: a JS shape as close to the NAPI package as the WASM toolchain allows, so +existing NAPI consumers can switch with minimal changes. + +| NAPI | WASM | +|---|---| +| `Buffer` (fn param/return) | `&[u8]` / `Vec` ↔ `Uint8Array` (native wasm-bindgen) | +| `BigInt` (u64 amounts) | `u64` ↔ `bigint` (native wasm-bindgen) | +| struct field: byte buffer | `#[serde(with = "serde_bytes")] Vec` → `Uint8Array` | +| struct field: u64 | serde-wasm-bindgen serializer with `serialize_large_number_types_as_bigints(true)` → `bigint` | +| struct (JS object, camelCase) | `#[serde(rename_all = "camelCase")]` + `serde_wasm_bindgen::to_value` / `from_value` | +| `Option` | `T | null | undefined` | +| `napi::Result` (throws) | `Result` (throws) | + +- **Amounts (mojos) MUST be `bigint`/`BigInt`, never JS `number`** — values exceed + 2^53. This matches NAPI and is non-negotiable for correctness. +- 32-byte values are `Uint8Array` (matching NAPI `Buffer`), not hex strings, to + keep the interface identical. (Hex helpers remain available where NAPI exposed + them, e.g. `spend_bundle_to_hex`.) +- An `init()` export installs `console_error_panic_hook` (dev-friendly panics). + Idempotent; safe to call once at startup. + +### 4.1 Function & type inventory (offline subset, NAPI names preserved) + +Priority 1 — DIGStore spend bundles (must-have): +`mint_store`, `update_store_metadata`, `update_store_ownership`, `oracle_spend`, +`melt_store`, `sign_coin_spends`, `spend_bundle_to_hex`, +`hex_spend_bundle_to_coin_spends`, `get_cost`, `get_coin_id`, `select_coins`. + +Priority 2 — supporting: +`master_public_key_to_wallet_synthetic_key`, +`master_public_key_to_first_puzzle_hash`, +`master_secret_key_to_wallet_synthetic_secret_key`, +`secret_key_to_public_key`, `synthetic_key_to_puzzle_hash`, +`puzzle_hash_to_address`, `address_to_puzzle_hash`, +`admin_delegated_puzzle_from_key`, `writer_delegated_puzzle_from_key`, +`new_lineage_proof`, `new_eve_proof`, `morph_launcher_id`, +`create_server_coin`, `send_xch`, `add_fee`, `sign_message`, +`verify_signed_message`, `get_mainnet_genesis_challenge`, +`get_testnet11_genesis_challenge`. + +Structs (camelCase JS objects): `Coin`, `CoinState`, `CoinSpend`, +`LineageProof`, `EveProof`, `Proof`, `ServerCoin`, `DataStoreMetadata`, +`DelegatedPuzzle`, `DataStore`, `SuccessResponse`, `NewServerCoin`, `Output`. + +`DelegatedPuzzle` and `Proof` are enums/unions in the core crate — mirror the +NAPI object representation (`Proof { lineageProof?, eveProof? }`; +`DelegatedPuzzle` admin/writer/oracle variants) so JS shapes match. + +## 5. Build & packaging + +- Build command: `wasm-pack build wasm --target bundler --release` → `wasm/pkg/`. +- Published package: **`@dignetwork/datalayer-driver-wasm`**, version mirrors the + root crate (currently `3.0.0`). +- `wasm-pack` derives the npm package name from the crate name; override to the + scoped name via `wasm-pack build --scope dignetwork` and/or a post-build patch + of `pkg/package.json` (name, repository, license, `files`). The patch step + lives in a small script invoked by CI so the published metadata is + deterministic. +- `pkg/` ships: `*_bg.wasm`, `*.js`, `*.d.ts`, `package.json`, `README`, `LICENSE`. + +## 6. CI + +Extend `.github/workflows/CI.yml` (do not disturb existing napi build/test/publish +jobs): + +New `build-wasm` job (ubuntu-latest): +1. Install LLVM/clang (`blst` / `chia-bls` needs a C/Clang toolchain to compile to + `wasm32`). +2. `rustup target add wasm32-unknown-unknown`. +3. `cargo install wasm-pack` (or use the `jetli/wasm-pack-action`). +4. `wasm-pack build wasm --target bundler --release`. +5. Build a second `--target nodejs` artifact into `wasm/pkg-node/` for tests. +6. Run node parity tests (Section 7). + +New publish step (gated on the same version-tag condition the napi publish uses): +`npm publish` from `wasm/pkg/` with `NODE_AUTH_TOKEN` / `NPM_TOKEN`. Runs only on +release/tag, after tests pass. + +## 7. Testing — parity against NAPI + +The strongest validation of "exact same interface" is differential testing: CI +already builds the NAPI `.node`. Add a node test that loads **both** the NAPI +package and the `--target nodejs` WASM build, and asserts equality across the +offline surface: + +- address round-trip (`puzzle_hash_to_address` / `address_to_puzzle_hash`) +- key derivation (all `*_key`/`*_puzzle_hash` helpers) — equal `Uint8Array`s +- coin id, cost +- **DIGStore spend bundles (primary):** construct identical inputs, call + `mint_store`, `update_store_metadata`, `update_store_ownership`, `melt_store`, + `oracle_spend` on both, `sign_coin_spends`, then compare the resulting + `CoinSpend[]` / signed bundle hex via `spend_bundle_to_hex`. Bytes must match + exactly. + +Test fixtures use deterministic secret keys (e.g. `[1u8;32]`) so output is stable +and comparable. No network access required. + +## 8. Risks & mitigations + +1. **`chia-wallet-sdk` 0.30 wasm32 compile** without native features — likely OK + (reference proved 0.24); verify with a throwaway + `cargo build -p datalayer-driver --target wasm32-unknown-unknown --no-default-features` + as the first implementation step. If it fails, identify the offending + transitive feature and gate it. +2. **`getrandom` version mismatch** — `chia 0.26` may pull getrandom 0.2 (needs + `features = ["js"]`) or 0.3 (needs `features = ["wasm_js"]` + `RUSTFLAGS` + `--cfg getrandom_backend="wasm_js"`). Resolve at first build by inspecting + `cargo tree -i getrandom`. +3. **tokio leakage into offline paths** — if any offline function transitively + references a tokio type, gating expands. Caught by the wasm32 build in step 1. +4. **bundler output is not node-`require`-able** — hence the separate + `--target nodejs` build for CI tests; the published artifact stays `bundler`. +5. **Package name override** — wasm-pack defaults to the crate name; CI must patch + `pkg/package.json` to the scoped name before publish. + +## 9. Out of scope / future + +- A `JsChainBackend`-style callback transport that would let WASM drive `Peer` + methods over a JS-supplied async backend (fetch/RPC). Deferred; can be a future + spec if browser-side chain interaction is needed. +- Additional wasm-pack targets (`web`, `nodejs`) as published artifacts. Only + `bundler` is published in v1. diff --git a/src/error.rs b/src/error.rs index d00568d..329e00e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,10 +1,12 @@ -use chia::consensus::validation_error::ValidationErr; +use chia_consensus::validation_error::ValidationErr; +use chia_sdk_driver::DriverError; +#[cfg(feature = "native")] use chia_wallet_sdk::client::ClientError; -use chia_wallet_sdk::driver::DriverError; use thiserror::Error; #[derive(Debug, Error)] pub enum WalletError { + #[cfg(feature = "native")] #[error("{0:?}")] Client(#[from] ClientError), @@ -33,7 +35,7 @@ pub enum WalletError { Clvm, #[error("ToClvm error: {0}")] - ToClvm(#[from] chia::clvm_traits::ToClvmError), + ToClvm(#[from] clvm_traits::ToClvmError), #[error("Permission error: puzzle can't perform this action")] Permission, diff --git a/src/lib.rs b/src/lib.rs index a4c6f79..597b4d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,21 +15,25 @@ //! - Fee management utilities // Re-export core types from dependencies -pub use chia::bls::{master_to_wallet_unhardened, PublicKey, SecretKey, Signature}; -pub use chia::protocol::{Bytes, Bytes32, Coin, CoinSpend, CoinState, Program, SpendBundle}; -pub use chia::puzzles::{EveProof, LineageProof, Proof}; -pub use chia_wallet_sdk::client::Peer; -pub use chia_wallet_sdk::driver::{ +pub use chia_bls::{master_to_wallet_unhardened, PublicKey, SecretKey, Signature}; +pub use chia_protocol::{Bytes, Bytes32, Coin, CoinSpend, CoinState, Program, SpendBundle}; +pub use chia_puzzle_types::{EveProof, LineageProof, Proof}; +pub use chia_sdk_driver::{ DataStore, DataStoreInfo, DataStoreMetadata, DelegatedPuzzle, P2ParentCoin, }; -pub use chia_wallet_sdk::utils::Address; +pub use chia_sdk_utils::Address; +#[cfg(feature = "native")] +pub use chia_wallet_sdk::client::Peer; // Re-export async_api and constants modules at the top level for convenience +#[cfg(feature = "native")] pub use async_api::{connect_peer, connect_random, create_tls_connector, NetworkType}; pub use constants::{get_mainnet_genesis_challenge, get_testnet11_genesis_challenge}; // Internal modules +#[cfg(feature = "native")] mod dig_coin; +#[cfg(feature = "native")] mod dig_collateral_coin; mod error; pub mod types; @@ -41,14 +45,18 @@ pub use types::{ BlsPair, SimulatorPuzzle, SuccessResponse, UnspentCoinStates, UnspentCoinsResponse, }; pub use wallet::{ - create_simple_did, generate_did_proof, generate_did_proof_from_chain, - generate_did_proof_manual, get_fee_estimate, get_header_hash, get_store_creation_height, - get_unspent_coin_states, is_coin_spent, look_up_possible_launchers, mint_nft, - spend_xch_server_coins, subscribe_to_coin_states, sync_store, sync_store_using_launcher_id, - unsubscribe_from_coin_states, verify_signature, DataStoreInnerSpend, PossibleLaunchersResponse, + create_simple_did, generate_did_proof_manual, verify_signature, DataStoreInnerSpend, SyncStoreResponse, TargetNetwork, }; +#[cfg(feature = "native")] +pub use wallet::{ + generate_did_proof, generate_did_proof_from_chain, get_fee_estimate, get_header_hash, + get_store_creation_height, get_unspent_coin_states, is_coin_spent, look_up_possible_launchers, + mint_nft, spend_xch_server_coins, subscribe_to_coin_states, sync_store, + sync_store_using_launcher_id, unsubscribe_from_coin_states, PossibleLaunchersResponse, +}; pub use xch_server_coin::{morph_launcher_id, XchServerCoin}; +#[cfg(feature = "native")] pub use {dig_coin::DigCoin, dig_collateral_coin::DigCollateralCoin}; use hex_literal::hex; @@ -57,7 +65,7 @@ use hex_literal::hex; pub type Result = std::result::Result>; // Helper functions for common conversions -use chia::puzzles::{standard::StandardArgs, DeriveSynthetic}; +use chia_puzzle_types::{standard::StandardArgs, DeriveSynthetic}; // Helper functions for common conversions use xch_server_coin::NewXchServerCoin; @@ -114,19 +122,19 @@ pub fn get_coin_id(coin: &Coin) -> Bytes32 { /// Converts a puzzle hash to an address by encoding it using bech32m. pub fn puzzle_hash_to_address(puzzle_hash: Bytes32, prefix: &str) -> Result { - use chia_wallet_sdk::utils::Address; + use chia_sdk_utils::Address; Ok(Address::new(puzzle_hash, prefix.to_string()).encode()?) } /// Converts an address to a puzzle hash using bech32m. pub fn address_to_puzzle_hash(address: &str) -> Result { - use chia_wallet_sdk::utils::Address; + use chia_sdk_utils::Address; Ok(Address::decode(address)?.puzzle_hash) } /// Converts hex-encoded spend bundle to coin spends. pub fn hex_spend_bundle_to_coin_spends(hex: &str) -> Result> { - use chia::traits::Streamable; + use chia_traits::Streamable; let bytes = hex::decode(hex)?; let spend_bundle = SpendBundle::from_bytes(&bytes)?; Ok(spend_bundle.coin_spends) @@ -134,7 +142,7 @@ pub fn hex_spend_bundle_to_coin_spends(hex: &str) -> Result> { /// Converts a spend bundle to hex encoding. pub fn spend_bundle_to_hex(spend_bundle: &SpendBundle) -> Result { - use chia::traits::Streamable; + use chia_traits::Streamable; let bytes = spend_bundle.to_bytes()?; Ok(hex::encode(bytes)) } @@ -337,6 +345,7 @@ pub fn create_server_coin( } /// Async functions for blockchain interaction (Rust API versions) +#[cfg(feature = "native")] pub mod async_api { use super::*; use futures_util::stream::{FuturesUnordered, StreamExt}; @@ -625,21 +634,21 @@ pub mod async_api { /// Constants for different networks pub mod constants { - use chia_wallet_sdk::types::{MAINNET_CONSTANTS, TESTNET11_CONSTANTS}; + use chia_sdk_types::{MAINNET_CONSTANTS, TESTNET11_CONSTANTS}; /// Returns the mainnet genesis challenge. - pub fn get_mainnet_genesis_challenge() -> chia::protocol::Bytes32 { + pub fn get_mainnet_genesis_challenge() -> chia_protocol::Bytes32 { MAINNET_CONSTANTS.genesis_challenge } /// Returns the testnet11 genesis challenge. - pub fn get_testnet11_genesis_challenge() -> chia::protocol::Bytes32 { + pub fn get_testnet11_genesis_challenge() -> chia_protocol::Bytes32 { TESTNET11_CONSTANTS.genesis_challenge } } /// Example usage of the Rust API -#[cfg(test)] +#[cfg(all(test, feature = "native"))] mod examples { use super::*; diff --git a/src/types.rs b/src/types.rs index 576783f..4157901 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,8 +1,9 @@ pub use crate::xch_server_coin::XchServerCoin; use crate::DataStore; -use chia::bls::{PublicKey, SecretKey}; -pub use chia::protocol::*; -pub use chia::puzzles::{EveProof, LineageProof, Proof}; +use chia_bls::{PublicKey, SecretKey}; +pub use chia_protocol::*; +pub use chia_puzzle_types::{EveProof, LineageProof, Proof}; +#[cfg(feature = "native")] use chia_wallet_sdk::coinset::CoinRecord; pub struct SimulatorPuzzle { @@ -47,6 +48,7 @@ pub struct UnspentCoinStates { pub last_height: u32, pub last_header_hash: Bytes32, } +#[cfg(feature = "native")] pub fn coin_records_to_states(coin_records: Vec) -> Vec { coin_records .into_iter() diff --git a/src/wallet.rs b/src/wallet.rs index 9557cfb..7afb73a 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -5,42 +5,48 @@ use std::time::{SystemTime, UNIX_EPOCH}; // Import proof types from our own crate's rust module use crate::error::WalletError; -pub use crate::types::{coin_records_to_states, SuccessResponse, XchServerCoin}; +#[cfg(feature = "native")] +pub use crate::types::coin_records_to_states; use crate::types::{EveProof, LineageProof, Proof}; +pub use crate::types::{SuccessResponse, XchServerCoin}; use crate::xch_server_coin::{urls_from_conditions, MirrorArgs, MirrorSolution, NewXchServerCoin}; +#[cfg(feature = "native")] use crate::{NetworkType, UnspentCoinStates}; -use chia::bls::{sign, verify, PublicKey, SecretKey, Signature}; -use chia::clvm_traits::{clvm_tuple, FromClvm, ToClvm}; -use chia::clvm_utils::tree_hash; -use chia::consensus::consensus_constants::ConsensusConstants; -use chia::consensus::flags::{DONT_VALIDATE_SIGNATURE, MEMPOOL_MODE}; -use chia::consensus::owned_conditions::OwnedSpendBundleConditions; -use chia::consensus::run_block_generator::run_block_generator; -use chia::consensus::solution_generator::solution_generator; -use chia::protocol::{ - Bytes, Bytes32, Coin, CoinSpend, CoinState, CoinStateFilters, RejectHeaderRequest, - RequestBlockHeader, RequestFeeEstimates, RespondBlockHeader, RespondFeeEstimates, SpendBundle, - TransactionAck, +use chia_bls::{sign, verify, PublicKey, SecretKey, Signature}; +use chia_consensus::consensus_constants::ConsensusConstants; +use chia_consensus::flags::{DONT_VALIDATE_SIGNATURE, MEMPOOL_MODE}; +use chia_consensus::owned_conditions::OwnedSpendBundleConditions; +use chia_consensus::run_block_generator::run_block_generator; +use chia_consensus::solution_generator::solution_generator; +use chia_protocol::{Bytes, Bytes32, Coin, CoinSpend, CoinState, SpendBundle}; +#[cfg(feature = "native")] +use chia_protocol::{ + CoinStateFilters, RejectHeaderRequest, RequestBlockHeader, RequestFeeEstimates, + RespondBlockHeader, RespondFeeEstimates, TransactionAck, }; -use chia::puzzles::{ - nft::NftMetadata, +#[cfg(feature = "native")] +use chia_puzzle_types::nft::NftMetadata; +use chia_puzzle_types::{ standard::{StandardArgs, StandardSolution}, DeriveSynthetic, }; use chia_puzzles::SINGLETON_LAUNCHER_HASH; -use chia_wallet_sdk::client::Peer; -use chia_wallet_sdk::driver::{ +use chia_sdk_driver::{ get_merkle_tree, DataStore, DataStoreMetadata, DelegatedPuzzle, Did, DidInfo, DriverError, HashedPtr, IntermediateLauncher, Launcher, Layer, NftMint, OracleLayer, SpendContext, SpendWithConditions, StandardLayer, WriterLayer, }; -use chia_wallet_sdk::signer::{AggSigConstants, RequiredSignature, SignerError}; -use chia_wallet_sdk::types::{ +use chia_sdk_signer::{AggSigConstants, RequiredSignature, SignerError}; +use chia_sdk_types::{ announcement_id, conditions::{CreateCoin, MeltSingleton, Memos, UpdateDataStoreMerkleRoot}, Condition, Conditions, MAINNET_CONSTANTS, TESTNET11_CONSTANTS, }; -use chia_wallet_sdk::utils::{self, CoinSelectionError}; +use chia_sdk_utils::{self as utils, CoinSelectionError}; +#[cfg(feature = "native")] +use chia_wallet_sdk::client::Peer; +use clvm_traits::{clvm_tuple, FromClvm, ToClvm}; +use clvm_utils::tree_hash; use clvmr::Allocator; use hex_literal::hex; @@ -57,6 +63,7 @@ pub const DIG_ASSET_ID: Bytes32 = Bytes32::new(hex!( pub const MAX_CLVM_COST: u64 = 11_000_000_000; +#[cfg(feature = "native")] pub async fn get_unspent_coin_states_by_hint( peer: &Peer, hint: Bytes32, @@ -69,6 +76,7 @@ pub async fn get_unspent_coin_states_by_hint( get_unspent_coin_states(peer, hint, None, header_hash, true).await } +#[cfg(feature = "native")] pub async fn get_unspent_coin_states( peer: &Peer, puzzle_hash: Bytes32, @@ -247,6 +255,7 @@ pub fn create_server_coin( }) } +#[cfg(feature = "native")] pub async fn spend_xch_server_coins( peer: &Peer, synthetic_key: PublicKey, @@ -338,6 +347,7 @@ pub async fn spend_xch_server_coins( Ok(ctx.take()) } +#[cfg(feature = "native")] pub async fn fetch_xch_server_coin( peer: &Peer, coin_state: CoinState, @@ -479,6 +489,7 @@ pub struct SyncStoreResponse { pub root_hash_history: Option>, } +#[cfg(feature = "native")] pub async fn sync_store( peer: &Peer, store: &DataStore, @@ -577,6 +588,7 @@ pub async fn sync_store( }) } +#[cfg(feature = "native")] pub async fn sync_store_using_launcher_id( peer: &Peer, launcher_id: Bytes32, @@ -658,6 +670,7 @@ pub async fn sync_store_using_launcher_id( }) } +#[cfg(feature = "native")] pub async fn get_store_creation_height( peer: &Peer, launcher_id: Bytes32, @@ -1025,6 +1038,7 @@ pub fn sign_coin_spends( Ok(sig) } +#[cfg(feature = "native")] pub async fn broadcast_spend_bundle( peer: &Peer, spend_bundle: SpendBundle, @@ -1034,6 +1048,7 @@ pub async fn broadcast_spend_bundle( .map_err(WalletError::Client) } +#[cfg(feature = "native")] pub async fn get_header_hash(peer: &Peer, height: u32) -> Result { let resp: Result = peer .request_fallible(RequestBlockHeader { height }) @@ -1044,6 +1059,7 @@ pub async fn get_header_hash(peer: &Peer, height: u32) -> Result Result { let target_time_seconds = target_time_seconds + SystemTime::now() @@ -1076,6 +1092,7 @@ pub async fn get_fee_estimate(peer: &Peer, target_time_seconds: u64) -> Result, @@ -1180,6 +1198,7 @@ pub async fn look_up_possible_launchers( }) } +#[cfg(feature = "native")] pub async fn subscribe_to_coin_states( peer: &Peer, coin_id: Bytes32, @@ -1199,6 +1218,7 @@ pub async fn subscribe_to_coin_states( Err(WalletError::UnknownCoin) } +#[cfg(feature = "native")] pub async fn unsubscribe_from_coin_states( peer: &Peer, coin_id: Bytes32, @@ -1226,6 +1246,7 @@ pub async fn unsubscribe_from_coin_states( /// /// # Returns /// A vector of coin spends that mint the NFT +#[cfg(feature = "native")] #[allow(clippy::too_many_arguments)] pub async fn mint_nft( peer: &Peer, @@ -1246,11 +1267,11 @@ pub async fn mint_nft( // Convert DID proof let did_proof = match did_proof { - chia::puzzles::Proof::Eve(eve) => Proof::Eve(EveProof { + chia_puzzle_types::Proof::Eve(eve) => Proof::Eve(EveProof { parent_parent_coin_info: eve.parent_parent_coin_info, parent_amount: eve.parent_amount, }), - chia::puzzles::Proof::Lineage(lineage) => Proof::Lineage(LineageProof { + chia_puzzle_types::Proof::Lineage(lineage) => Proof::Lineage(LineageProof { parent_parent_coin_info: lineage.parent_parent_coin_info, parent_inner_puzzle_hash: lineage.parent_inner_puzzle_hash, parent_amount: lineage.parent_amount, @@ -1328,11 +1349,12 @@ pub async fn mint_nft( /// /// # Returns /// A tuple containing the DID proof and the DID coin +#[cfg(feature = "native")] pub async fn generate_did_proof( peer: &Peer, did_coin: Coin, network: TargetNetwork, -) -> Result<(chia::puzzles::Proof, Coin), WalletError> { +) -> Result<(chia_puzzle_types::Proof, Coin), WalletError> { let proof = generate_did_proof_from_chain(peer, did_coin, network).await?; Ok((proof, did_coin)) } @@ -1350,14 +1372,14 @@ pub fn generate_did_proof_manual( did_coin: Coin, parent_coin: Option, parent_inner_puzzle_hash: Option, -) -> Result { +) -> Result { match parent_coin { // Eve proof - first spend from launcher None => { // For eve proof, we need the launcher coin info // The parent_parent_coin_info is the coin that created the launcher // The parent_amount is the launcher coin amount (typically 1 mojo) - Ok(chia::puzzles::Proof::Eve(chia::puzzles::EveProof { + Ok(chia_puzzle_types::Proof::Eve(chia_puzzle_types::EveProof { parent_parent_coin_info: did_coin.parent_coin_info, parent_amount: 1, // Launcher coins are typically 1 mojo })) @@ -1368,11 +1390,13 @@ pub fn generate_did_proof_manual( "Parent inner puzzle hash is required".to_string(), ))?; // Need inner puzzle hash for lineage proof - Ok(chia::puzzles::Proof::Lineage(chia::puzzles::LineageProof { - parent_parent_coin_info: parent.parent_coin_info, - parent_inner_puzzle_hash, - parent_amount: parent.amount, - })) + Ok(chia_puzzle_types::Proof::Lineage( + chia_puzzle_types::LineageProof { + parent_parent_coin_info: parent.parent_coin_info, + parent_inner_puzzle_hash, + parent_amount: parent.amount, + }, + )) } } } @@ -1386,11 +1410,12 @@ pub fn generate_did_proof_manual( /// /// # Returns /// A DID proof that can be used to spend the DID coin +#[cfg(feature = "native")] pub async fn generate_did_proof_from_chain( peer: &Peer, did_coin: Coin, network: TargetNetwork, -) -> Result { +) -> Result { // Get the parent coin state let parent_coin_states = peer .request_coin_state( @@ -1411,7 +1436,7 @@ pub async fn generate_did_proof_from_chain( // Check if parent is a launcher (puzzle hash matches singleton launcher) if parent_coin_state.coin.puzzle_hash == SINGLETON_LAUNCHER_HASH.into() { // This is an eve proof - first spend from launcher - return Ok(chia::puzzles::Proof::Eve(chia::puzzles::EveProof { + return Ok(chia_puzzle_types::Proof::Eve(chia_puzzle_types::EveProof { parent_parent_coin_info: parent_coin_state.coin.parent_coin_info, parent_amount: parent_coin_state.coin.amount, })); @@ -1431,11 +1456,13 @@ pub async fn generate_did_proof_from_chain( // For now, create a basic lineage proof // This is a simplified approach - in production you'd want to properly parse the parent DID - Ok(chia::puzzles::Proof::Lineage(chia::puzzles::LineageProof { - parent_parent_coin_info: parent_coin_state.coin.parent_coin_info, - parent_inner_puzzle_hash: Bytes32::default(), // Would need to parse from parent spend - parent_amount: parent_coin_state.coin.amount, - })) + Ok(chia_puzzle_types::Proof::Lineage( + chia_puzzle_types::LineageProof { + parent_parent_coin_info: parent_coin_state.coin.parent_coin_info, + parent_inner_puzzle_hash: Bytes32::default(), // Would need to parse from parent spend + parent_amount: parent_coin_state.coin.amount, + }, + )) } /// Creates a simple DID from a private key and selected coins. @@ -1514,11 +1541,12 @@ pub fn create_simple_did( /// /// # Returns /// A tuple containing the DID proof and the current DID coin +#[cfg(feature = "native")] pub async fn resolve_did_string_and_generate_proof( peer: &Peer, did_string: &str, network: TargetNetwork, -) -> Result<(chia::puzzles::Proof, Coin), WalletError> { +) -> Result<(chia_puzzle_types::Proof, Coin), WalletError> { // Parse DID string to extract launcher ID let parts: Vec<&str> = did_string.split(':').collect(); @@ -1529,7 +1557,7 @@ pub async fn resolve_did_string_and_generate_proof( let bech32_part = parts[2]; // Decode the bech32 address to get the launcher ID - use chia_wallet_sdk::utils::Address; + use chia_sdk_utils::Address; let address = Address::decode(bech32_part) .map_err(|_| WalletError::Parse("Cannot decode address".to_string()))?; diff --git a/src/xch_server_coin.rs b/src/xch_server_coin.rs index 2c3d54d..0a0e71d 100644 --- a/src/xch_server_coin.rs +++ b/src/xch_server_coin.rs @@ -1,8 +1,12 @@ use crate::CoinSpend; -use chia_wallet_sdk::prelude::{ - Allocator, Bytes, Bytes32, Coin, Condition, CreateCoin, CurriedProgram, FromClvm, Memos, Mod, - ToClvm, ToTreeHash, TreeHash, +use chia_protocol::{Bytes, Bytes32, Coin}; +use chia_sdk_types::{ + conditions::{CreateCoin, Memos}, + Condition, Mod, }; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::Allocator; use hex_literal::hex; use num_bigint::BigInt; use std::borrow::Cow; @@ -133,7 +137,7 @@ pub fn urls_from_conditions( #[cfg(test)] mod tests { - use chia::clvm_utils::tree_hash; + use clvm_utils::tree_hash; use clvmr::{serde::node_from_bytes, Allocator}; use super::*; diff --git a/wasm/.gitignore b/wasm/.gitignore new file mode 100644 index 0000000..41b30de --- /dev/null +++ b/wasm/.gitignore @@ -0,0 +1,4 @@ +/pkg +/pkg-node +/target +/node_modules diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml new file mode 100644 index 0000000..dc80f13 --- /dev/null +++ b/wasm/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "datalayer-driver-wasm" +version = "3.0.0" +edition = "2021" +license = "MIT" +publish = false +description = "WebAssembly bindings for the Chia DataLayer driver (offline DIGStore spend-bundle construction)." +repository = "https://github.com/DIG-Network/DataLayer-Driver" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console-panic-hook"] +console-panic-hook = ["dep:console_error_panic_hook"] + +[dependencies] +datalayer-driver = { path = "..", default-features = false } +wasm-bindgen = "0.2" +js-sys = "0.3" +serde = { version = "1", features = ["derive"] } +serde_bytes = "0.11" +serde-wasm-bindgen = "0.6" +console_error_panic_hook = { version = "0.1", optional = true } +hex = "0.4" +getrandom = { version = "0.2", features = ["js"] } + +[dev-dependencies] +wasm-bindgen-test = "0.3" + +[package.metadata.cargo-machete] +ignored = ["getrandom", "js-sys"] diff --git a/wasm/GETRANDOM.md b/wasm/GETRANDOM.md new file mode 100644 index 0000000..1970540 --- /dev/null +++ b/wasm/GETRANDOM.md @@ -0,0 +1,25 @@ +# getrandom backend resolution (wasm32) + +Determined 2026-05-29 via `cargo tree -i getrandom --target wasm32-unknown-unknown --no-default-features -p datalayer-driver`. + +Two versions exist in the overall lockfile (**0.2.16** and **0.3.3**), but only **0.2.16** is present in the `wasm32-unknown-unknown` dependency graph. `getrandom@0.3.3` returns "nothing to print" for the wasm target — it is pulled only on the host/native target. + +`getrandom 0.2.16` reaches the wasm build via `chia-sdk-driver`, `clvm_tools_rs` (→ `chia-sdk-types`), and the `rand_core 0.6 → crypto-bigint → k256` chain. + +## Resolution for the `wasm` crate + +`wasm/Cargo.toml` must declare: + +```toml +getrandom = { version = "0.2", features = ["js"] } +``` + +**No `.cargo/config.toml` is required** (the `wasm_js` backend + `RUSTFLAGS` cfg is a getrandom 0.3 concern, and 0.3 is not on the wasm path). + +Without the `js` feature, `getrandom 0.2` compiles on wasm32 but panics at runtime on the first random-bytes call (e.g. inside k256/BLS code paths). The `js` feature wires it to the browser/Node crypto API. + +## Why this is safe for the offline path + +`getrandom 0.2.16` is pulled transitively into the `wasm32-unknown-unknown` build by: `chia-sdk-driver` (default feature), `clvm_tools_rs` (via `chia-sdk-types`), and the `rand_core 0.6 → crypto-bigint → elliptic-curve → k256` chain (plus `datalayer-driver-wasm` itself declaring the `js` feature directly to satisfy the transitive requirement). + +The offline DIGStore builders and BLS signing are deterministic — the Node parity test produces byte-identical signatures and spend bundles across two independent runtimes (NAPI native vs WASM), which it could not if any exercised path consumed RNG. The `js` feature is required so the transitive getrandom 0.2 compiles for `wasm32-unknown-unknown` and resolves at runtime under the shipped `--target nodejs`/`--target bundler` outputs (which provide the JS crypto global). diff --git a/wasm/README.md b/wasm/README.md new file mode 100644 index 0000000..9054aa2 --- /dev/null +++ b/wasm/README.md @@ -0,0 +1,128 @@ +# @dignetwork/datalayer-driver-wasm + +WebAssembly bindings for the Chia DataLayer driver, mirroring the **offline subset** of the NAPI `@dignetwork/datalayer-driver` interface for browser/bundler use. + +## Scope + +This package is **offline only** — it contains no networking. There is no `Peer`, `Tls`, `connect`, `sync`, or `broadcast`. Reading coins from the chain and broadcasting the resulting spend bundle are the consumer's responsibility (e.g. via a wallet extension such as Goby, or via a full-node RPC in JavaScript). + +The package exists to build and sign **DIGStore (DataStore) spend bundles client-side**, inside a browser or any bundler-based environment, without shipping a full node. + +## Installation + +```bash +npm install @dignetwork/datalayer-driver-wasm +``` + +## Startup + +Call `init()` once at startup. It installs the Rust panic hook so that panics surface as readable JavaScript errors rather than cryptic traps. + +```js +import init from "@dignetwork/datalayer-driver-wasm"; +await init(); +``` + +> **Note:** When built with `--target bundler` (see below), many bundlers auto-run the default export initializer. Even so, calling `init()` explicitly is safe and recommended — it is idempotent after the first call. + +## Build target + +This package is built with: + +``` +wasm-pack --target bundler +``` + +The bundler target works out of the box with **webpack**, **Vite**, **Next.js**, and **esbuild**. If you need plain Node.js (without a bundler), build the `nodejs` target yourself from the `wasm/` crate in the source repo: + +```bash +wasm-pack build . --target nodejs --out-dir pkg-node +``` + +## Primary use case — build and sign a DIGStore mint bundle + +```js +import init, * as dl from "@dignetwork/datalayer-driver-wasm"; + +// Call init() to install the panic hook (safe to call multiple times). +await dl.init(); + +// --------------------------------------------------------------------------- +// 1. Derive keys from the master secret key (32-byte Uint8Array). +// --------------------------------------------------------------------------- +const sk = /* 32-byte Uint8Array — your master secret key */; +const pk = dl.secretKeyToPublicKey(sk); +const syntheticKey = dl.masterPublicKeyToWalletSyntheticKey(pk); +const syntheticSk = dl.masterSecretKeyToWalletSyntheticSecretKey(sk); +const ownerPuzzleHash = dl.masterPublicKeyToFirstPuzzleHash(pk); + +// --------------------------------------------------------------------------- +// 2. Fetch coins from your chain source (wallet RPC, Goby, etc.) and select. +// All `amount` fields are bigint; all byte values are Uint8Array. +// --------------------------------------------------------------------------- +const allCoins = /* Coin[] fetched by your app */; +const selected = dl.selectCoins(allCoins, 1n /* mojos needed + fee */); + +// --------------------------------------------------------------------------- +// 3. Build the mint spend bundle. +// --------------------------------------------------------------------------- +const rootHash = /* 32-byte Uint8Array — SHA-256 root of the data tree */; + +const mint = dl.mintStore( + syntheticKey, + selected, + rootHash, + "my-store", // label (string | undefined) + "description", // description (string | undefined) + undefined, // bytes (bigint | undefined) + undefined, // sizeProof (Uint8Array | undefined) + ownerPuzzleHash, + [dl.adminDelegatedPuzzleFromKey(syntheticKey)], + 0n // fee in mojos +); + +// --------------------------------------------------------------------------- +// 4. Sign and serialize. +// --------------------------------------------------------------------------- +const sig = dl.signCoinSpends( + mint.coinSpends, + [syntheticSk], + false // false = mainnet, true = testnet11 +); + +const bundleHex = dl.spendBundleToHex({ + coinSpends: mint.coinSpends, + aggregatedSignature: sig, +}); + +// Broadcast `bundleHex` via your full-node RPC or wallet extension. +``` + +## Type notes + +- All byte values (`Uint8Array`) — including keys, puzzle hashes, root hashes, and coin fields — are plain `Uint8Array`. +- All amounts (`bigint`) — including `amount`, `fee`, and `bytes` — use JavaScript's native `bigint` type. + +These conventions match the NAPI `@dignetwork/datalayer-driver` package exactly, so code that runs offline is portable between environments. + +## Available functions + +| Category | Functions | +|---|---| +| Key derivation | `secretKeyToPublicKey`, `masterPublicKeyToWalletSyntheticKey`, `masterPublicKeyToFirstPuzzleHash`, `masterSecretKeyToWalletSyntheticSecretKey`, `syntheticKeyToPuzzleHash` | +| Addresses | `puzzleHashToAddress`, `addressToPuzzleHash` | +| Delegated puzzles | `adminDelegatedPuzzleFromKey`, `writerDelegatedPuzzleFromKey` | +| Proofs / IDs | `newLineageProof`, `newEveProof`, `getCoinId`, `morphLauncherId` | +| Genesis challenges | `getMainnetGenesisChallenge`, `getTestnet11GenesisChallenge` | +| DIGStore builders | `mintStore`, `oracleSpend`, `meltStore`, `updateStoreMetadata`, `updateStoreOwnership` | +| Signing / verification | `signCoinSpends`, `signMessage`, `verifySignedMessage` | +| Serialization | `spendBundleToHex`, `hexSpendBundleToCoinSpends` | +| Coin selection / fees | `selectCoins`, `addFee`, `getCost` | +| Transfers | `sendXch` | +| Server coins | `createServerCoin` | + +Full TypeScript types are in `datalayer-driver-wasm.d.ts` (published with the package). + +## Parity guarantee + +This package is generated from the `wasm/` crate in the [DataLayer-Driver](https://github.com/DIG-Network/DataLayer-Driver) repository. Its offline functions are validated **byte-for-byte** against the NAPI package by a parity test that runs in CI on every commit. If a result differs between the WASM and NAPI implementations the CI build fails. diff --git a/wasm/package-lock.json b/wasm/package-lock.json new file mode 100644 index 0000000..8db5509 --- /dev/null +++ b/wasm/package-lock.json @@ -0,0 +1,32 @@ +{ + "name": "@dignetwork/datalayer-driver-wasm-dev", + "version": "3.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@dignetwork/datalayer-driver-wasm-dev", + "version": "3.0.0", + "devDependencies": { + "@dignetwork/datalayer-driver": "file:../napi" + } + }, + "../napi": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "devDependencies": { + "@napi-rs/cli": "^2.18.4", + "@types/node": "^24.3.1", + "ava": "^6.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@dignetwork/datalayer-driver": { + "resolved": "../napi", + "link": true + } + } +} diff --git a/wasm/package.json b/wasm/package.json new file mode 100644 index 0000000..439895c --- /dev/null +++ b/wasm/package.json @@ -0,0 +1,14 @@ +{ + "name": "@dignetwork/datalayer-driver-wasm-dev", + "version": "3.0.0", + "private": true, + "type": "module", + "scripts": { + "build:bundler": "wasm-pack build . --target bundler --release --no-opt && node scripts/patch-pkg.mjs", + "build:node": "wasm-pack build . --target nodejs --dev --out-dir pkg-node", + "test": "node tests/parity.mjs" + }, + "devDependencies": { + "@dignetwork/datalayer-driver": "file:../napi" + } +} diff --git a/wasm/scripts/patch-pkg.mjs b/wasm/scripts/patch-pkg.mjs new file mode 100644 index 0000000..8d2d1c9 --- /dev/null +++ b/wasm/scripts/patch-pkg.mjs @@ -0,0 +1,20 @@ +// NOTE: the bundler build uses --no-opt because the bundled wasm-opt rejects blst's bulk-memory ops; re-enable optimization only with a wasm-opt that supports --enable-bulk-memory. +import { readFile, writeFile, copyFile } from "node:fs/promises"; +import path from "node:path"; + +const pkgDir = path.resolve("pkg"); +const pkgJsonPath = path.join(pkgDir, "package.json"); + +const pkg = JSON.parse(await readFile(pkgJsonPath, "utf8")); + +pkg.name = "@dignetwork/datalayer-driver-wasm"; +pkg.description = "WebAssembly bindings for the Chia DataLayer driver (offline DIGStore spend-bundle construction)."; +pkg.repository = { type: "git", url: "https://github.com/DIG-Network/DataLayer-Driver.git" }; +pkg.license = "MIT"; +pkg.types = "datalayer-driver-wasm.d.ts"; +pkg.files = Array.from(new Set([...(pkg.files ?? []), "datalayer-driver-wasm.d.ts"])); + +await writeFile(pkgJsonPath, JSON.stringify(pkg, null, 2) + "\n"); +await copyFile(path.resolve("types/datalayer-driver-wasm.d.ts"), path.join(pkgDir, "datalayer-driver-wasm.d.ts")); + +console.log(`Patched ${pkg.name}@${pkg.version}`); diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs new file mode 100644 index 0000000..3da237f --- /dev/null +++ b/wasm/src/lib.rs @@ -0,0 +1,454 @@ +//! WebAssembly bindings for the Chia DataLayer driver. +//! +//! Mirrors the offline subset of the NAPI interface. Networking +//! (Peer/Tls) is intentionally absent — WASM has no native sockets. + +use wasm_bindgen::prelude::*; + +mod types; + +/// Initialise the module. Call once at startup. Installs a panic hook +/// (when the `console-panic-hook` feature is on) so Rust panics surface +/// in the JS console instead of an opaque `unreachable`. +#[wasm_bindgen] +pub fn init() { + #[cfg(feature = "console-panic-hook")] + console_error_panic_hook::set_once(); +} + +use datalayer_driver::{ + address_to_puzzle_hash as core_address_to_puzzle_hash, + admin_delegated_puzzle_from_key as core_admin_dp, get_coin_id as core_get_coin_id, + get_mainnet_genesis_challenge as core_mainnet_gc, + get_testnet11_genesis_challenge as core_testnet_gc, + master_public_key_to_first_puzzle_hash as core_mpk_to_first_ph, + master_public_key_to_wallet_synthetic_key as core_mpk_to_synth, + master_secret_key_to_wallet_synthetic_secret_key as core_msk_to_synth, + morph_launcher_id_wrapper as core_morph_launcher_id, + puzzle_hash_to_address as core_ph_to_address, secret_key_to_public_key as core_sk_to_pk, + synthetic_key_to_puzzle_hash as core_synth_to_ph, + writer_delegated_puzzle_from_key as core_writer_dp, +}; + +use crate::types::{ + bytes32, from_js, public_key, secret_key, to_js, Coin, DelegatedPuzzle, EveProof, LineageProof, + Proof, +}; + +// ── Task 6: DIGStore spend builders ────────────────────────────────────────── +use crate::types::{ + coin_spends_to_js, coins_from_js, delegated_puzzles_from_js, DataStore, SuccessResponse, +}; +use datalayer_driver::{ + melt_store as core_melt_store, mint_store as core_mint_store, + oracle_spend as core_oracle_spend, update_store_metadata as core_update_meta, + update_store_ownership as core_update_owner, DataStoreInnerSpend, +}; + +// ── Task 7: Signing / serialization / selection / server-coin ──────────────── +use crate::types::{coin_spends_from_js, signature, NewServerCoin, Output, ServerCoin}; +use datalayer_driver::{ + add_fee as core_add_fee, create_server_coin as core_create_server_coin, + get_cost as core_get_cost, hex_spend_bundle_to_coin_spends as core_hex_to_css, + select_coins as core_select_coins, send_xch as core_send_xch, + sign_coin_spends as core_sign_css, sign_message as core_sign_msg, + spend_bundle_to_hex as core_sb_to_hex, verify_signed_message as core_verify_msg, + Bytes as RustBytes, Output as RustOutput, SpendBundle as RustSpendBundle, +}; + +#[wasm_bindgen(js_name = "masterPublicKeyToWalletSyntheticKey")] +pub fn master_public_key_to_wallet_synthetic_key( + public_key_bytes: &[u8], +) -> Result, JsValue> { + Ok(core_mpk_to_synth(&public_key(public_key_bytes)?) + .to_bytes() + .to_vec()) +} + +#[wasm_bindgen(js_name = "masterPublicKeyToFirstPuzzleHash")] +pub fn master_public_key_to_first_puzzle_hash(public_key_bytes: &[u8]) -> Result, JsValue> { + Ok(core_mpk_to_first_ph(&public_key(public_key_bytes)?).to_vec()) +} + +#[wasm_bindgen(js_name = "masterSecretKeyToWalletSyntheticSecretKey")] +pub fn master_secret_key_to_wallet_synthetic_secret_key( + secret_key_bytes: &[u8], +) -> Result, JsValue> { + Ok(core_msk_to_synth(&secret_key(secret_key_bytes)?) + .to_bytes() + .to_vec()) +} + +#[wasm_bindgen(js_name = "secretKeyToPublicKey")] +pub fn secret_key_to_public_key(secret_key_bytes: &[u8]) -> Result, JsValue> { + Ok(core_sk_to_pk(&secret_key(secret_key_bytes)?) + .to_bytes() + .to_vec()) +} + +#[wasm_bindgen(js_name = "syntheticKeyToPuzzleHash")] +pub fn synthetic_key_to_puzzle_hash(synthetic_key_bytes: &[u8]) -> Result, JsValue> { + Ok(core_synth_to_ph(&public_key(synthetic_key_bytes)?).to_vec()) +} + +#[wasm_bindgen(js_name = "puzzleHashToAddress")] +pub fn puzzle_hash_to_address(puzzle_hash: &[u8], prefix: String) -> Result { + core_ph_to_address(bytes32(puzzle_hash)?, &prefix) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen(js_name = "addressToPuzzleHash")] +pub fn address_to_puzzle_hash(address: String) -> Result, JsValue> { + core_address_to_puzzle_hash(&address) + .map(|b| b.to_vec()) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen(js_name = "adminDelegatedPuzzleFromKey")] +pub fn admin_delegated_puzzle_from_key(synthetic_key: &[u8]) -> Result { + to_js(&DelegatedPuzzle::from_native(&core_admin_dp(&public_key( + synthetic_key, + )?))?) +} + +#[wasm_bindgen(js_name = "writerDelegatedPuzzleFromKey")] +pub fn writer_delegated_puzzle_from_key(synthetic_key: &[u8]) -> Result { + to_js(&DelegatedPuzzle::from_native(&core_writer_dp( + &public_key(synthetic_key)?, + ))?) +} + +#[wasm_bindgen(js_name = "newLineageProof")] +pub fn new_lineage_proof(lineage_proof: JsValue) -> Result { + let lp: LineageProof = from_js(lineage_proof)?; + to_js(&Proof { + lineage_proof: Some(lp), + eve_proof: None, + }) +} + +#[wasm_bindgen(js_name = "newEveProof")] +pub fn new_eve_proof(eve_proof: JsValue) -> Result { + let ep: EveProof = from_js(eve_proof)?; + to_js(&Proof { + lineage_proof: None, + eve_proof: Some(ep), + }) +} + +#[wasm_bindgen(js_name = "getCoinId")] +pub fn get_coin_id(coin: JsValue) -> Result, JsValue> { + let c: Coin = from_js(coin)?; + Ok(core_get_coin_id(&c.to_native()?).to_vec()) +} + +#[wasm_bindgen(js_name = "morphLauncherId")] +pub fn morph_launcher_id(launcher_id: &[u8], offset: u64) -> Result, JsValue> { + Ok(core_morph_launcher_id(bytes32(launcher_id)?, offset).to_vec()) +} + +#[wasm_bindgen(js_name = "getMainnetGenesisChallenge")] +pub fn get_mainnet_genesis_challenge() -> Vec { + core_mainnet_gc().to_vec() +} + +#[wasm_bindgen(js_name = "getTestnet11GenesisChallenge")] +pub fn get_testnet11_genesis_challenge() -> Vec { + core_testnet_gc().to_vec() +} + +// ── Task 6: DIGStore spend builders ────────────────────────────────────────── + +#[wasm_bindgen(js_name = "mintStore")] +#[allow(clippy::too_many_arguments)] +pub fn mint_store( + minter_synthetic_key: &[u8], + selected_coins: JsValue, + root_hash: &[u8], + label: Option, + description: Option, + bytes: Option, + size_proof: Option>, + owner_puzzle_hash: &[u8], + delegated_puzzles: JsValue, + fee: u64, +) -> Result { + let size_proof = match size_proof { + Some(sp) => Some(bytes32(&sp)?.to_string()), + None => None, + }; + let resp = core_mint_store( + public_key(minter_synthetic_key)?, + coins_from_js(selected_coins)?, + bytes32(root_hash)?, + label, + description, + bytes, + size_proof, + bytes32(owner_puzzle_hash)?, + delegated_puzzles_from_js(delegated_puzzles)?, + fee, + ) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + to_js(&SuccessResponse::from_native(&resp)?) +} + +#[wasm_bindgen(js_name = "oracleSpend")] +pub fn oracle_spend( + spender_synthetic_key: &[u8], + selected_coins: JsValue, + store: JsValue, + fee: u64, +) -> Result { + let store: DataStore = from_js(store)?; + let resp = core_oracle_spend( + public_key(spender_synthetic_key)?, + coins_from_js(selected_coins)?, + store.to_native()?, + fee, + ) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + to_js(&SuccessResponse::from_native(&resp)?) +} + +#[wasm_bindgen(js_name = "meltStore")] +pub fn melt_store(store: JsValue, owner_public_key: &[u8]) -> Result { + let store: DataStore = from_js(store)?; + let css = core_melt_store(store.to_native()?, public_key(owner_public_key)?) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + coin_spends_to_js(&css) +} + +#[wasm_bindgen(js_name = "updateStoreMetadata")] +#[allow(clippy::too_many_arguments)] +pub fn update_store_metadata( + store: JsValue, + new_root_hash: &[u8], + new_label: Option, + new_description: Option, + new_bytes: Option, + new_size_proof: Option>, + owner_public_key: Option>, + admin_public_key: Option>, + writer_public_key: Option>, +) -> Result { + let store: DataStore = from_js(store)?; + let inner = + match (&owner_public_key, &admin_public_key, &writer_public_key) { + (Some(pk), None, None) => DataStoreInnerSpend::Owner(public_key(pk)?), + (None, Some(pk), None) => DataStoreInnerSpend::Admin(public_key(pk)?), + (None, None, Some(pk)) => DataStoreInnerSpend::Writer(public_key(pk)?), + _ => return Err(JsValue::from_str( + "Exactly one of ownerPublicKey, adminPublicKey, writerPublicKey must be provided", + )), + }; + let new_size_proof = match new_size_proof { + Some(sp) => Some(bytes32(&sp)?.to_string()), + None => None, + }; + let resp = core_update_meta( + store.to_native()?, + bytes32(new_root_hash)?, + new_label, + new_description, + new_bytes, + new_size_proof, + inner, + ) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + to_js(&SuccessResponse::from_native(&resp)?) +} + +#[wasm_bindgen(js_name = "updateStoreOwnership")] +pub fn update_store_ownership( + store: JsValue, + new_owner_puzzle_hash: Option>, + new_delegated_puzzles: JsValue, + owner_public_key: Option>, + admin_public_key: Option>, +) -> Result { + let store: DataStore = from_js(store)?; + let native_store = store.to_native()?; + let new_owner_ph = match new_owner_puzzle_hash { + Some(ph) => bytes32(&ph)?, + None => native_store.info.owner_puzzle_hash, + }; + let inner = match (&owner_public_key, &admin_public_key) { + (Some(pk), None) => DataStoreInnerSpend::Owner(public_key(pk)?), + (None, Some(pk)) => DataStoreInnerSpend::Admin(public_key(pk)?), + _ => { + return Err(JsValue::from_str( + "Exactly one of ownerPublicKey, adminPublicKey must be provided", + )) + } + }; + let resp = core_update_owner( + native_store, + new_owner_ph, + delegated_puzzles_from_js(new_delegated_puzzles)?, + inner, + ) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + to_js(&SuccessResponse::from_native(&resp)?) +} + +// ── Task 7 functions ───────────────────────────────────────────────────────── + +#[wasm_bindgen(js_name = "signCoinSpends")] +pub fn sign_coin_spends( + coin_spends: JsValue, + private_keys: JsValue, + for_testnet: bool, +) -> Result, JsValue> { + let css = coin_spends_from_js(coin_spends)?; + let keys_raw: Vec = from_js(private_keys)?; + let keys = keys_raw + .iter() + .map(|k| secret_key(k)) + .collect::, _>>()?; + let sig = + core_sign_css(&css, &keys, for_testnet).map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(sig.to_bytes().to_vec()) +} + +#[wasm_bindgen(js_name = "signMessage")] +pub fn sign_message(message: &[u8], private_key: &[u8]) -> Result, JsValue> { + let sig = core_sign_msg(message, &secret_key(private_key)?) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(sig.to_bytes().to_vec()) +} + +#[wasm_bindgen(js_name = "verifySignedMessage")] +pub fn verify_signed_message( + sig: &[u8], + public_key_bytes: &[u8], + message: &[u8], +) -> Result { + core_verify_msg(&signature(sig)?, &public_key(public_key_bytes)?, message) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen(js_name = "getCost")] +pub fn get_cost(coin_spends: JsValue) -> Result { + core_get_cost(&coin_spends_from_js(coin_spends)?).map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen(js_name = "selectCoins")] +pub fn select_coins(all_coins: JsValue, total_amount: u64) -> Result { + let selected = core_select_coins(&coins_from_js(all_coins)?, total_amount) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + let out: Vec = selected + .iter() + .map(crate::types::Coin::from_native) + .collect(); + to_js(&out) +} + +#[wasm_bindgen(js_name = "spendBundleToHex")] +pub fn spend_bundle_to_hex(spend_bundle: JsValue) -> Result { + #[derive(serde::Deserialize)] + #[serde(rename_all = "camelCase")] + struct SbIn { + coin_spends: Vec, + #[serde(with = "serde_bytes")] + aggregated_signature: Vec, + } + let sb: SbIn = from_js(spend_bundle)?; + let css = sb + .coin_spends + .iter() + .map(crate::types::CoinSpend::to_native) + .collect::, _>>()?; + let bundle = RustSpendBundle::new(css, signature(&sb.aggregated_signature)?); + core_sb_to_hex(&bundle).map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen(js_name = "hexSpendBundleToCoinSpends")] +pub fn hex_spend_bundle_to_coin_spends(hex: String) -> Result { + let css = core_hex_to_css(&hex).map_err(|e| JsValue::from_str(&e.to_string()))?; + coin_spends_to_js(&css) +} + +#[wasm_bindgen(js_name = "sendXch")] +pub fn send_xch( + synthetic_key: &[u8], + selected_coins: JsValue, + outputs: JsValue, + fee: u64, +) -> Result { + let outs: Vec = from_js(outputs)?; + let native_outs: Vec = outs + .iter() + .map(|o| { + Ok(RustOutput { + puzzle_hash: bytes32(&o.puzzle_hash)?, + amount: o.amount, + memos: o + .memos + .iter() + .map(|m| RustBytes::from(m.to_vec())) + .collect(), + }) + }) + .collect::, JsValue>>()?; + let css = core_send_xch( + &public_key(synthetic_key)?, + &coins_from_js(selected_coins)?, + &native_outs, + fee, + ) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + coin_spends_to_js(&css) +} + +#[wasm_bindgen(js_name = "addFee")] +pub fn add_fee( + spender_synthetic_key: &[u8], + selected_coins: JsValue, + assert_coin_ids: JsValue, + fee: u64, +) -> Result { + let ids_raw: Vec = from_js(assert_coin_ids)?; + let ids = ids_raw + .iter() + .map(|b| bytes32(b)) + .collect::, _>>()?; + let css = core_add_fee( + &public_key(spender_synthetic_key)?, + &coins_from_js(selected_coins)?, + &ids, + fee, + ) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + coin_spends_to_js(&css) +} + +#[wasm_bindgen(js_name = "createServerCoin")] +pub fn create_server_coin( + synthetic_key: &[u8], + selected_coins: JsValue, + hint: &[u8], + uris: JsValue, + amount: u64, + fee: u64, +) -> Result { + let uris: Vec = from_js(uris)?; + let new_sc = core_create_server_coin( + public_key(synthetic_key)?, + coins_from_js(selected_coins)?, + bytes32(hint)?, + uris, + amount, + fee, + ) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + let out = NewServerCoin { + server_coin: ServerCoin::from_native(&new_sc.server_coin), + coin_spends: new_sc + .coin_spends + .iter() + .map(crate::types::CoinSpend::from_native) + .collect(), + }; + to_js(&out) +} diff --git a/wasm/src/types.rs b/wasm/src/types.rs new file mode 100644 index 0000000..04f0d65 --- /dev/null +++ b/wasm/src/types.rs @@ -0,0 +1,442 @@ +//! Serde boundary structs and conversions to/from native datalayer-driver types. + +// --------------------------------------------------------------------------- +// Step 1 — serializer helpers + byte/key converters +// --------------------------------------------------------------------------- + +use datalayer_driver::{Bytes32, Program, PublicKey, SecretKey, Signature}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use wasm_bindgen::JsValue; + +/// Serialize a Rust value into a JS value, encoding u64/i64 as BigInt +/// (DataLayer mojo amounts exceed 2^53, so the default lossy f64 path +/// is unacceptable). +pub fn to_js(value: &T) -> Result { + let ser = serde_wasm_bindgen::Serializer::new().serialize_large_number_types_as_bigints(true); + value + .serialize(&ser) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +/// Deserialize a JS value into a Rust value. Accepts JS number and BigInt for integers. +pub fn from_js(value: JsValue) -> Result { + serde_wasm_bindgen::from_value(value).map_err(|e| JsValue::from_str(&e.to_string())) +} + +pub fn bytes32(buf: &[u8]) -> Result { + Bytes32::try_from(buf.to_vec()).map_err(|_| JsValue::from_str("expected 32-byte value")) +} + +pub fn public_key(buf: &[u8]) -> Result { + let arr = + <[u8; 48]>::try_from(buf).map_err(|_| JsValue::from_str("expected 48-byte public key"))?; + PublicKey::from_bytes(&arr).map_err(|_| JsValue::from_str("invalid public key")) +} + +pub fn secret_key(buf: &[u8]) -> Result { + let arr = + <[u8; 32]>::try_from(buf).map_err(|_| JsValue::from_str("expected 32-byte secret key"))?; + SecretKey::from_bytes(&arr).map_err(|_| JsValue::from_str("invalid secret key")) +} + +pub fn signature(buf: &[u8]) -> Result { + let arr = + <[u8; 96]>::try_from(buf).map_err(|_| JsValue::from_str("expected 96-byte signature"))?; + Signature::from_bytes(&arr).map_err(|_| JsValue::from_str("invalid signature")) +} + +// --------------------------------------------------------------------------- +// Step 2 — Coin / CoinSpend / proofs +// --------------------------------------------------------------------------- + +use datalayer_driver::{ + Coin as RustCoin, CoinSpend as RustCoinSpend, EveProof as RustEveProof, + LineageProof as RustLineageProof, Proof as RustProof, +}; + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Coin { + #[serde(with = "serde_bytes")] + pub parent_coin_info: Vec, + #[serde(with = "serde_bytes")] + pub puzzle_hash: Vec, + pub amount: u64, +} + +impl Coin { + pub fn to_native(&self) -> Result { + Ok(RustCoin { + parent_coin_info: bytes32(&self.parent_coin_info)?, + puzzle_hash: bytes32(&self.puzzle_hash)?, + amount: self.amount, + }) + } + + pub fn from_native(c: &RustCoin) -> Self { + Coin { + parent_coin_info: c.parent_coin_info.to_vec(), + puzzle_hash: c.puzzle_hash.to_vec(), + amount: c.amount, + } + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CoinSpend { + pub coin: Coin, + #[serde(with = "serde_bytes")] + pub puzzle_reveal: Vec, + #[serde(with = "serde_bytes")] + pub solution: Vec, +} + +impl CoinSpend { + pub fn to_native(&self) -> Result { + Ok(RustCoinSpend { + coin: self.coin.to_native()?, + puzzle_reveal: Program::from(self.puzzle_reveal.clone()), + solution: Program::from(self.solution.clone()), + }) + } + + pub fn from_native(cs: &RustCoinSpend) -> Self { + CoinSpend { + coin: Coin::from_native(&cs.coin), + puzzle_reveal: cs.puzzle_reveal.to_vec(), + solution: cs.solution.to_vec(), + } + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LineageProof { + #[serde(with = "serde_bytes")] + pub parent_parent_coin_info: Vec, + #[serde(with = "serde_bytes")] + pub parent_inner_puzzle_hash: Vec, + pub parent_amount: u64, +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EveProof { + #[serde(with = "serde_bytes")] + pub parent_parent_coin_info: Vec, + pub parent_amount: u64, +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Proof { + pub lineage_proof: Option, + pub eve_proof: Option, +} + +impl Proof { + pub fn to_native(&self) -> Result { + if let Some(lp) = &self.lineage_proof { + Ok(RustProof::Lineage(RustLineageProof { + parent_parent_coin_info: bytes32(&lp.parent_parent_coin_info)?, + parent_inner_puzzle_hash: bytes32(&lp.parent_inner_puzzle_hash)?, + parent_amount: lp.parent_amount, + })) + } else if let Some(ep) = &self.eve_proof { + Ok(RustProof::Eve(RustEveProof { + parent_parent_coin_info: bytes32(&ep.parent_parent_coin_info)?, + parent_amount: ep.parent_amount, + })) + } else { + Err(JsValue::from_str("missing proof")) + } + } + + pub fn from_native(p: &RustProof) -> Self { + match p { + RustProof::Lineage(lp) => Proof { + lineage_proof: Some(LineageProof { + parent_parent_coin_info: lp.parent_parent_coin_info.to_vec(), + parent_inner_puzzle_hash: lp.parent_inner_puzzle_hash.to_vec(), + parent_amount: lp.parent_amount, + }), + eve_proof: None, + }, + RustProof::Eve(ep) => Proof { + lineage_proof: None, + eve_proof: Some(EveProof { + parent_parent_coin_info: ep.parent_parent_coin_info.to_vec(), + parent_amount: ep.parent_amount, + }), + }, + } + } +} + +// --------------------------------------------------------------------------- +// Step 3 — DataStore family +// --------------------------------------------------------------------------- + +use datalayer_driver::{ + DataStore as RustDataStore, DataStoreInfo as RustDataStoreInfo, + DataStoreMetadata as RustDataStoreMetadata, DelegatedPuzzle as RustDelegatedPuzzle, + SuccessResponse as RustSuccessResponse, +}; + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DataStoreMetadata { + #[serde(with = "serde_bytes")] + pub root_hash: Vec, + pub label: Option, + pub description: Option, + pub bytes: Option, + /// 32-byte size proof as a `Uint8Array` (mirrors NAPI's `Option`). + /// The native type stores this as an `Option` hex representation, + /// so we convert on the boundary. + #[serde(default, with = "serde_bytes_opt")] + pub size_proof: Option>, +} + +impl DataStoreMetadata { + pub fn to_native(&self) -> Result { + Ok(RustDataStoreMetadata { + root_hash: bytes32(&self.root_hash)?, + label: self.label.clone(), + description: self.description.clone(), + bytes: self.bytes, + // JS passes 32 raw bytes; the native type stores the hex string. + size_proof: match &self.size_proof { + Some(sp) => Some(bytes32(sp)?.to_string()), + None => None, + }, + }) + } + + pub fn from_native(m: &RustDataStoreMetadata) -> Result { + Ok(DataStoreMetadata { + root_hash: m.root_hash.to_vec(), + label: m.label.clone(), + description: m.description.clone(), + bytes: m.bytes, + // Native stores hex; decode back to the 32 raw bytes for JS. + size_proof: match &m.size_proof { + Some(s) => Some( + hex::decode(s.trim_start_matches("0x")) + .map_err(|_| JsValue::from_str("invalid size_proof hex"))?, + ), + None => None, + }, + }) + } +} + +/// Represents one of three delegated-puzzle variants. +/// Exactly one of the three option groups should be populated. +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DelegatedPuzzle { + #[serde(default, with = "serde_bytes_opt")] + pub admin_inner_puzzle_hash: Option>, + #[serde(default, with = "serde_bytes_opt")] + pub writer_inner_puzzle_hash: Option>, + #[serde(default, with = "serde_bytes_opt")] + pub oracle_payment_puzzle_hash: Option>, + pub oracle_fee: Option, +} + +// serde_bytes doesn't support Option> directly; provide a local module. +mod serde_bytes_opt { + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(v: &Option>, s: S) -> Result { + match v { + Some(b) => serde_bytes::serialize(b, s), + None => s.serialize_none(), + } + } + + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result>, D::Error> { + let opt: Option = Option::deserialize(d)?; + Ok(opt.map(|b| b.into_vec())) + } +} + +impl DelegatedPuzzle { + pub fn to_native(&self) -> Result { + if let Some(h) = &self.admin_inner_puzzle_hash { + // Admin variant wraps TreeHash; Bytes32 converts to TreeHash via Into. + Ok(RustDelegatedPuzzle::Admin(bytes32(h)?.into())) + } else if let Some(h) = &self.writer_inner_puzzle_hash { + Ok(RustDelegatedPuzzle::Writer(bytes32(h)?.into())) + } else if let (Some(h), Some(fee)) = (&self.oracle_payment_puzzle_hash, self.oracle_fee) { + Ok(RustDelegatedPuzzle::Oracle(bytes32(h)?, fee)) + } else { + Err(JsValue::from_str("missing delegated puzzle info")) + } + } + + pub fn from_native(d: &RustDelegatedPuzzle) -> Result { + Ok(match d { + RustDelegatedPuzzle::Admin(th) => { + // TreeHash → Bytes32 via Into, then .to_vec() + let b32: Bytes32 = (*th).into(); + DelegatedPuzzle { + admin_inner_puzzle_hash: Some(b32.to_vec()), + writer_inner_puzzle_hash: None, + oracle_payment_puzzle_hash: None, + oracle_fee: None, + } + } + RustDelegatedPuzzle::Writer(th) => { + let b32: Bytes32 = (*th).into(); + DelegatedPuzzle { + admin_inner_puzzle_hash: None, + writer_inner_puzzle_hash: Some(b32.to_vec()), + oracle_payment_puzzle_hash: None, + oracle_fee: None, + } + } + RustDelegatedPuzzle::Oracle(h, fee) => DelegatedPuzzle { + admin_inner_puzzle_hash: None, + writer_inner_puzzle_hash: None, + oracle_payment_puzzle_hash: Some(h.to_vec()), + oracle_fee: Some(*fee), + }, + }) + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DataStore { + pub coin: Coin, + #[serde(with = "serde_bytes")] + pub launcher_id: Vec, + pub proof: Proof, + pub metadata: DataStoreMetadata, + #[serde(with = "serde_bytes")] + pub owner_puzzle_hash: Vec, + pub delegated_puzzles: Vec, +} + +impl DataStore { + pub fn to_native(&self) -> Result { + Ok(RustDataStore { + coin: self.coin.to_native()?, + proof: self.proof.to_native()?, + info: RustDataStoreInfo { + launcher_id: bytes32(&self.launcher_id)?, + metadata: self.metadata.to_native()?, + owner_puzzle_hash: bytes32(&self.owner_puzzle_hash)?, + delegated_puzzles: self + .delegated_puzzles + .iter() + .map(DelegatedPuzzle::to_native) + .collect::, _>>()?, + }, + }) + } + + pub fn from_native(s: &RustDataStore) -> Result { + Ok(DataStore { + coin: Coin::from_native(&s.coin), + launcher_id: s.info.launcher_id.to_vec(), + proof: Proof::from_native(&s.proof), + metadata: DataStoreMetadata::from_native(&s.info.metadata)?, + owner_puzzle_hash: s.info.owner_puzzle_hash.to_vec(), + delegated_puzzles: s + .info + .delegated_puzzles + .iter() + .map(DelegatedPuzzle::from_native) + .collect::, _>>()?, + }) + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SuccessResponse { + pub coin_spends: Vec, + pub new_store: DataStore, +} + +impl SuccessResponse { + pub fn from_native(r: &RustSuccessResponse) -> Result { + Ok(SuccessResponse { + coin_spends: r.coin_spends.iter().map(CoinSpend::from_native).collect(), + new_store: DataStore::from_native(&r.new_datastore)?, + }) + } +} + +// --------------------------------------------------------------------------- +// Step 4 — JsValue array helpers +// --------------------------------------------------------------------------- + +pub fn coins_from_js(value: JsValue) -> Result, JsValue> { + let coins: Vec = from_js(value)?; + coins.iter().map(Coin::to_native).collect() +} + +pub fn delegated_puzzles_from_js(value: JsValue) -> Result, JsValue> { + let dps: Vec = from_js(value)?; + dps.iter().map(DelegatedPuzzle::to_native).collect() +} + +pub fn coin_spends_from_js(value: JsValue) -> Result, JsValue> { + let css: Vec = from_js(value)?; + css.iter().map(CoinSpend::to_native).collect() +} + +pub fn coin_spends_to_js(css: &[RustCoinSpend]) -> Result { + let out: Vec = css.iter().map(CoinSpend::from_native).collect(); + to_js(&out) +} + +// --------------------------------------------------------------------------- +// Step 5 — ServerCoin / NewServerCoin / Output boundary structs +// --------------------------------------------------------------------------- + +use datalayer_driver::XchServerCoin as RustXchServerCoin; + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ServerCoin { + pub coin: Coin, + #[serde(with = "serde_bytes")] + pub p2_puzzle_hash: Vec, + pub memo_urls: Vec, +} + +impl ServerCoin { + pub fn from_native(s: &RustXchServerCoin) -> Self { + ServerCoin { + coin: Coin::from_native(&s.coin), + p2_puzzle_hash: s.p2_puzzle_hash.to_vec(), + memo_urls: s.memo_urls.clone(), + } + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NewServerCoin { + pub server_coin: ServerCoin, + pub coin_spends: Vec, +} + +/// Output for send_xch. +/// JS field names (camelCase via serde): puzzleHash, amount, memos. +/// Matches NAPI's `Output { puzzle_hash, amount, memos }` which NAPI maps to +/// the same camelCase names. +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Output { + #[serde(with = "serde_bytes")] + pub puzzle_hash: Vec, + pub amount: u64, + pub memos: Vec, +} diff --git a/wasm/tests/parity.mjs b/wasm/tests/parity.mjs new file mode 100644 index 0000000..24cd8dd --- /dev/null +++ b/wasm/tests/parity.mjs @@ -0,0 +1,115 @@ +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; +const require = createRequire(import.meta.url); + +const wasm = require("../pkg-node"); +const napi = require("@dignetwork/datalayer-driver"); +wasm.init(); + +const hex = (b) => Buffer.from(b).toString("hex"); +const eqBytes = (a, b, msg) => assert.equal(hex(a), hex(b), msg); + +// Deterministic key material. +const sk = Buffer.alloc(32, 1); + +// 1. Key derivation parity. +const pkW = wasm.secretKeyToPublicKey(sk); +const pkN = napi.secretKeyToPublicKey(sk); +eqBytes(pkW, pkN, "secretKeyToPublicKey"); + +const synthW = wasm.masterPublicKeyToWalletSyntheticKey(pkW); +const synthN = napi.masterPublicKeyToWalletSyntheticKey(pkN); +eqBytes(synthW, synthN, "masterPublicKeyToWalletSyntheticKey"); + +const phW = wasm.masterPublicKeyToFirstPuzzleHash(pkW); +const phN = napi.masterPublicKeyToFirstPuzzleHash(pkN); +eqBytes(phW, phN, "masterPublicKeyToFirstPuzzleHash"); + +const synthSkW = wasm.masterSecretKeyToWalletSyntheticSecretKey(sk); +const synthSkN = napi.masterSecretKeyToWalletSyntheticSecretKey(sk); +eqBytes(synthSkW, synthSkN, "masterSecretKeyToWalletSyntheticSecretKey"); + +// 2. Address round-trip parity. +const addrW = wasm.puzzleHashToAddress(phW, "xch"); +const addrN = napi.puzzleHashToAddress(phN, "xch"); +assert.equal(addrW, addrN, "puzzleHashToAddress"); +eqBytes(wasm.addressToPuzzleHash(addrW), napi.addressToPuzzleHash(addrN), "addressToPuzzleHash"); + +// 3. Delegated puzzle parity. +const adminW = wasm.adminDelegatedPuzzleFromKey(synthW); +const adminN = napi.adminDelegatedPuzzleFromKey(synthN); +eqBytes(adminW.adminInnerPuzzleHash, adminN.adminInnerPuzzleHash, "adminDelegatedPuzzleFromKey"); + +// 4. PRIMARY: mint_store DIGStore spend bundle parity. +const ownerPh = Buffer.from(phW); +const coin = { + parentCoinInfo: Buffer.alloc(32, 2), + puzzleHash: ownerPh, + amount: 1_000_000_000_000n, // 1 XCH +}; +const rootHash = Buffer.alloc(32, 3); + +const mintW = wasm.mintStore(synthW, [coin], rootHash, "label", "desc", 42n, null, ownerPh, [adminW], 0n); +const mintN = napi.mintStore(synthN, [coin], rootHash, "label", "desc", 42n, null, ownerPh, [adminN], 0n); + +assert.equal(mintW.coinSpends.length, mintN.coinSpends.length, "mint coinSpends length"); +for (let i = 0; i < mintW.coinSpends.length; i++) { + eqBytes(mintW.coinSpends[i].puzzleReveal, mintN.coinSpends[i].puzzleReveal, `coinSpends[${i}].puzzleReveal`); + eqBytes(mintW.coinSpends[i].solution, mintN.coinSpends[i].solution, `coinSpends[${i}].solution`); + eqBytes(mintW.coinSpends[i].coin.parentCoinInfo, mintN.coinSpends[i].coin.parentCoinInfo, `coinSpends[${i}].coin.parent`); + assert.equal(mintW.coinSpends[i].coin.amount, mintN.coinSpends[i].coin.amount, `coinSpends[${i}].coin.amount`); +} +eqBytes(mintW.newStore.launcherId, mintN.newStore.launcherId, "newStore.launcherId"); +eqBytes(mintW.newStore.metadata.rootHash, mintN.newStore.metadata.rootHash, "newStore.metadata.rootHash"); + +// 5. Sign + serialize the mint bundle — ultimate byte-for-byte check. +const sigW = wasm.signCoinSpends(mintW.coinSpends, [synthSkW], true); +const sigN = napi.signCoinSpends(mintN.coinSpends, [synthSkN], true); +eqBytes(sigW, sigN, "signCoinSpends(mint)"); + +const hexW = wasm.spendBundleToHex({ coinSpends: mintW.coinSpends, aggregatedSignature: sigW }); +const hexN = napi.spendBundleToHex({ coinSpends: mintN.coinSpends, aggregatedSignature: sigN }); +assert.equal(hexW, hexN, "mint spend bundle hex parity"); + +// 6. Cost + coin id parity. +assert.equal(wasm.getCost(mintW.coinSpends), napi.getCost(mintN.coinSpends), "getCost"); +eqBytes(wasm.getCoinId(coin), napi.getCoinId(coin), "getCoinId"); + +// 7. meltStore parity (uses the identical newStore from the mint above). +// The minted store's owner is the synthetic key, so melt with synthW/synthN. +const meltStore = mintN.newStore; // byte-identical to mintW.newStore (asserted above) +const meltW = wasm.meltStore(meltStore, synthW); +const meltN = napi.meltStore(meltStore, synthN); +assert.equal(meltW.length, meltN.length, "meltStore coinSpends length"); +for (let i = 0; i < meltW.length; i++) { + eqBytes(meltW[i].puzzleReveal, meltN[i].puzzleReveal, `melt coinSpends[${i}].puzzleReveal`); + eqBytes(meltW[i].solution, meltN[i].solution, `melt coinSpends[${i}].solution`); +} + +// 8. sendXch parity. +const outputs = [{ puzzleHash: ownerPh, amount: 1n, memos: [] }]; +const sendW = wasm.sendXch(synthW, [coin], outputs, 0n); +const sendN = napi.sendXch(synthN, [coin], outputs, 0n); +assert.equal(sendW.length, sendN.length, "sendXch coinSpends length"); +for (let i = 0; i < sendW.length; i++) { + eqBytes(sendW[i].puzzleReveal, sendN[i].puzzleReveal, `sendXch coinSpends[${i}].puzzleReveal`); + eqBytes(sendW[i].solution, sendN[i].solution, `sendXch coinSpends[${i}].solution`); +} + +// 9. signMessage / verifySignedMessage parity. +const msg = Buffer.from("hello datalayer", "utf8"); +const msgSigW = wasm.signMessage(msg, synthSkW); +const msgSigN = napi.signMessage(msg, synthSkN); +eqBytes(msgSigW, msgSigN, "signMessage"); +// public key corresponding to the synthetic secret key: +const synthPkW = wasm.secretKeyToPublicKey(synthSkW); +const synthPkN = napi.secretKeyToPublicKey(synthSkN); +assert.equal(wasm.verifySignedMessage(msgSigW, synthPkW, msg), true, "wasm verifySignedMessage"); +assert.equal(napi.verifySignedMessage(msgSigN, synthPkN, msg), true, "napi verifySignedMessage"); +assert.equal( + wasm.verifySignedMessage(msgSigW, synthPkW, msg), + napi.verifySignedMessage(msgSigN, synthPkN, msg), + "verifySignedMessage parity" +); + +console.log("All parity checks passed."); diff --git a/wasm/types/datalayer-driver-wasm.d.ts b/wasm/types/datalayer-driver-wasm.d.ts new file mode 100644 index 0000000..0d11806 --- /dev/null +++ b/wasm/types/datalayer-driver-wasm.d.ts @@ -0,0 +1,57 @@ +// Type definitions for @dignetwork/datalayer-driver-wasm +// Mirrors the offline subset of the NAPI @dignetwork/datalayer-driver interface. + +export interface Coin { parentCoinInfo: Uint8Array; puzzleHash: Uint8Array; amount: bigint; } +export interface CoinSpend { coin: Coin; puzzleReveal: Uint8Array; solution: Uint8Array; } +export interface LineageProof { parentParentCoinInfo: Uint8Array; parentInnerPuzzleHash: Uint8Array; parentAmount: bigint; } +export interface EveProof { parentParentCoinInfo: Uint8Array; parentAmount: bigint; } +export interface Proof { lineageProof?: LineageProof; eveProof?: EveProof; } +export interface DataStoreMetadata { rootHash: Uint8Array; label?: string; description?: string; bytes?: bigint; sizeProof?: Uint8Array; } +export interface DelegatedPuzzle { adminInnerPuzzleHash?: Uint8Array; writerInnerPuzzleHash?: Uint8Array; oraclePaymentPuzzleHash?: Uint8Array; oracleFee?: bigint; } +export interface DataStore { coin: Coin; launcherId: Uint8Array; proof: Proof; metadata: DataStoreMetadata; ownerPuzzleHash: Uint8Array; delegatedPuzzles: DelegatedPuzzle[]; } +export interface SuccessResponse { coinSpends: CoinSpend[]; newStore: DataStore; } +export interface ServerCoin { coin: Coin; p2PuzzleHash: Uint8Array; memoUrls: string[]; } +export interface NewServerCoin { serverCoin: ServerCoin; coinSpends: CoinSpend[]; } +export interface Output { puzzleHash: Uint8Array; amount: bigint; memos: Uint8Array[]; } +export interface SpendBundle { coinSpends: CoinSpend[]; aggregatedSignature: Uint8Array; } + +/** Initialise the module (installs the panic hook). Call once at startup. */ +export function init(): void; + +// --- key derivation / addresses --- +export function masterPublicKeyToWalletSyntheticKey(publicKey: Uint8Array): Uint8Array; +export function masterPublicKeyToFirstPuzzleHash(publicKey: Uint8Array): Uint8Array; +export function masterSecretKeyToWalletSyntheticSecretKey(secretKey: Uint8Array): Uint8Array; +export function secretKeyToPublicKey(secretKey: Uint8Array): Uint8Array; +export function syntheticKeyToPuzzleHash(syntheticKey: Uint8Array): Uint8Array; +export function puzzleHashToAddress(puzzleHash: Uint8Array, prefix: string): string; +export function addressToPuzzleHash(address: string): Uint8Array; + +// --- delegated puzzles / proofs / ids --- +export function adminDelegatedPuzzleFromKey(syntheticKey: Uint8Array): DelegatedPuzzle; +export function writerDelegatedPuzzleFromKey(syntheticKey: Uint8Array): DelegatedPuzzle; +export function newLineageProof(lineageProof: LineageProof): Proof; +export function newEveProof(eveProof: EveProof): Proof; +export function getCoinId(coin: Coin): Uint8Array; +export function morphLauncherId(launcherId: Uint8Array, offset: bigint): Uint8Array; +export function getMainnetGenesisChallenge(): Uint8Array; +export function getTestnet11GenesisChallenge(): Uint8Array; + +// --- DIGStore spend builders --- +export function mintStore(minterSyntheticKey: Uint8Array, selectedCoins: Coin[], rootHash: Uint8Array, label: string | undefined, description: string | undefined, bytes: bigint | undefined, sizeProof: Uint8Array | undefined, ownerPuzzleHash: Uint8Array, delegatedPuzzles: DelegatedPuzzle[], fee: bigint): SuccessResponse; +export function oracleSpend(spenderSyntheticKey: Uint8Array, selectedCoins: Coin[], store: DataStore, fee: bigint): SuccessResponse; +export function meltStore(store: DataStore, ownerPublicKey: Uint8Array): CoinSpend[]; +export function updateStoreMetadata(store: DataStore, newRootHash: Uint8Array, newLabel: string | undefined, newDescription: string | undefined, newBytes: bigint | undefined, newSizeProof: Uint8Array | undefined, ownerPublicKey: Uint8Array | undefined, adminPublicKey: Uint8Array | undefined, writerPublicKey: Uint8Array | undefined): SuccessResponse; +export function updateStoreOwnership(store: DataStore, newOwnerPuzzleHash: Uint8Array | undefined, newDelegatedPuzzles: DelegatedPuzzle[], ownerPublicKey: Uint8Array | undefined, adminPublicKey: Uint8Array | undefined): SuccessResponse; + +// --- signing / serialization / selection / server coins --- +export function signCoinSpends(coinSpends: CoinSpend[], privateKeys: Uint8Array[], forTestnet: boolean): Uint8Array; +export function signMessage(message: Uint8Array, privateKey: Uint8Array): Uint8Array; +export function verifySignedMessage(signature: Uint8Array, publicKey: Uint8Array, message: Uint8Array): boolean; +export function getCost(coinSpends: CoinSpend[]): bigint; +export function selectCoins(allCoins: Coin[], totalAmount: bigint): Coin[]; +export function spendBundleToHex(spendBundle: SpendBundle): string; +export function hexSpendBundleToCoinSpends(hex: string): CoinSpend[]; +export function sendXch(syntheticKey: Uint8Array, selectedCoins: Coin[], outputs: Output[], fee: bigint): CoinSpend[]; +export function addFee(spenderSyntheticKey: Uint8Array, selectedCoins: Coin[], assertCoinIds: Uint8Array[], fee: bigint): CoinSpend[]; +export function createServerCoin(syntheticKey: Uint8Array, selectedCoins: Coin[], hint: Uint8Array, uris: string[], amount: bigint, fee: bigint): NewServerCoin;