From a07df375e9789340f051e0b4124509cb010b55cb Mon Sep 17 00:00:00 2001 From: thephez Date: Thu, 21 May 2026 12:40:46 -0400 Subject: [PATCH 1/6] docs: v3.1 minor cleanup (Proofs page, address encoding, constants) (#148) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(protocol-ref): update for v3.1 PlatformAddress encoding and remove feature-flags contract PR #3059 renamed the PlatformAddress HRPs (evo/tevo → dash/tdash) and introduced two distinct byte encodings (user-facing bech32m vs. internal GroveDB storage). PR #3522 removed the feature-flags system contract. Refresh source-line anchors against current rs-dpp line numbers. Co-Authored-By: Claude Opus 4.7 (1M context) * docs(explanations): add Proofs explanation page Cover the two-layer proof model (GroveDB Merkle proofs + Tenderdash consensus signatures), what can be proven, the v3.1 aggregate proof primitives (count/sum/average), and asset lock proofs. Wire it into the explanations toctree. Co-Authored-By: Claude Opus 4.7 (1M context) * docs(protocol-ref): document max_asset_lock_transaction_inputs constant Introduced in protocol v3 (#3491) to prevent stuck funds; v1 and v2 had no effective limit. Co-Authored-By: Claude Opus 4.7 (1M context) * chore: sync sidebar * docs: clarify hrp --------- Co-authored-by: Claude Opus 4.7 (1M context) --- _templates/sidebar-main.html | 25 +++ .../platform-protocol-data-trigger.md | 1 - docs/explanations/proofs.md | 168 ++++++++++++++++++ docs/index.md | 1 + docs/protocol-ref/address-system.md | 19 +- docs/protocol-ref/protocol-constants.md | 9 +- .../connect-to-a-network-dash-masternode.md | 6 - 7 files changed, 207 insertions(+), 22 deletions(-) create mode 100644 docs/explanations/proofs.md diff --git a/_templates/sidebar-main.html b/_templates/sidebar-main.html index 8a3287e4c..56fa79b72 100644 --- a/_templates/sidebar-main.html +++ b/_templates/sidebar-main.html @@ -180,6 +180,26 @@ DashMint Lab — NFT marketplace +
  • + + Dashnote + +
  • +
  • + + DashMint Lite + +
  • +
  • + + Dashnote Lite + +
  • +
  • + + DashProof Lite + +
  • @@ -330,6 +350,11 @@ Non-Fungible Tokens (NFTs) +
  • + + Proofs + +
  • Query Capabilities diff --git a/docs/explanations/platform-protocol-data-trigger.md b/docs/explanations/platform-protocol-data-trigger.md index cbb02be66..b3ac7d9b2 100644 --- a/docs/explanations/platform-protocol-data-trigger.md +++ b/docs/explanations/platform-protocol-data-trigger.md @@ -41,6 +41,5 @@ In addition to DPNS, DPP ships data triggers for a small set of other system con | DashPay | `contactRequest` | [`CREATE`](https://github.com/dashpay/platform/tree/master/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay) | Enforces DashPay-specific rules on outgoing contact requests | | ---- | ---- | ---- | ---- | | Withdrawals | `withdrawal` | [`CREATE`/`REPLACE`/`DELETE`](https://github.com/dashpay/platform/tree/master/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals) | Enforces withdrawal status transitions and prevents direct external mutation of withdrawal documents | -| Feature flags | (various) | [Protocol-version updates](https://github.com/dashpay/platform/tree/master/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/feature_flags) | Restricts feature flag changes to the authorized feature-flag identity | When document state transitions are received, DPP checks if there is a trigger associated with the document type and action. If a trigger is found, DPP executes the trigger logic. Successful execution of the trigger logic is necessary for the document to be accepted and applied to the [platform state](../explanations/drive-platform-state.md). diff --git a/docs/explanations/proofs.md b/docs/explanations/proofs.md new file mode 100644 index 000000000..531cdf524 --- /dev/null +++ b/docs/explanations/proofs.md @@ -0,0 +1,168 @@ +```{eval-rst} +.. _explanations-proofs: +``` + +# Proofs + +## Overview + +Proofs are a fundamental security feature of Dash Platform that enable trustless verification of data. When retrieving information from the network, clients can request cryptographic proofs that allow them to verify the data's authenticity without trusting the individual node that provided it. + +This is particularly important in a decentralized system where any single node could potentially return incorrect or malicious data. With proofs, clients can mathematically verify that: + +- The data exists (or doesn't exist) in the platform state +- The state was agreed upon by the network's validator quorum +- No tampering occurred between the node and the client + +Proofs enable light clients and mobile applications to interact securely with Dash Platform without running a full node or blindly trusting remote servers. + +## How Proofs Work + +Dash Platform uses a two-layer proof architecture that combines Merkle proofs from the storage layer with consensus signatures from the validator network. + +### GroveDB Merkle Proofs + +The first layer of verification uses [GroveDB](https://github.com/dashpay/grovedb), Dash Platform's authenticated data structure. GroveDB organizes all platform data in a tree structure where each piece of data contributes to a cryptographic hash that rolls up to a single root hash. + +When a client requests data with a proof, GroveDB returns: + +- The requested data (or proof of its absence) +- A Merkle path showing how the data connects to the root hash + +This allows clients to independently calculate what the root hash should be and verify it matches. Any modification to the data would produce a different root hash, making tampering detectable. + +### Tenderdash Consensus Signatures + +The second layer connects the GroveDB root hash to the network's consensus. Dash Platform uses [Tenderdash](../explanations/platform-consensus.md), a Byzantine fault-tolerant consensus protocol, where validator quorums sign each block. + +The proof includes: + +- A BLS threshold signature from the validator quorum +- The quorum hash identifying which validators signed +- Block metadata (height, round, timestamp) + +Clients verify that the root hash from the GroveDB proof matches what the quorum signed. Since producing a valid BLS threshold signature requires participation from more than two-thirds of the quorum members, this proves the network agreed on this exact state. + +:::{tip} +BLS threshold signatures are particularly efficient because regardless of how many validators participated, the final signature is always the same compact size. This keeps proofs small even as the validator set scales. +::: + +### Verification Flow + +The complete verification process follows these steps: + +1. Client sends a request to [DAPI](../explanations/dapi.md) with `prove: true` +2. DAPI retrieves the data and generates a proof from [Drive](../explanations/drive.md) +3. Client receives the response containing data, GroveDB proof, and consensus signature +4. Client verifies the GroveDB proof to extract the root hash +5. Client verifies the BLS signature against the root hash using the quorum's public key +6. If both verifications pass, the data is cryptographically confirmed + +## What Can Be Proven + +Dash Platform supports proofs for all core data types: + +**Identities** + +- Identity existence and full details +- Identity balance and revision +- Public keys associated with an identity +- Identity nonces (for replay protection) + +**Data Contracts** + +- Contract existence and contents +- Contract history (for contracts that track changes) + +**Documents** + +- Document existence within a contract +- Document queries with multiple results +- Proof of document absence (data doesn't exist) +- Aggregate values over a document set (count, sum, average) — see [Aggregate Proofs](#aggregate-proofs) below + +**Tokens** + +- Token balances for identities +- Token total supply +- Token status and configuration + +**System State** + +- Current epoch information +- Protocol version and upgrade status +- Contested resource voting state + +## Aggregate Proofs + +Beyond proving the existence and contents of individual documents, Dash Platform can produce verifiable answers to aggregate queries — questions about a *set* of documents, answered with one or more aggregate values instead of a list. This avoids streaming and verifying every matching document just to learn how many there are or what they sum to. + +Three aggregate primitives are supported: + +- **Count** — number of documents matching the query. +- **Sum** — sum of an integer field across matching documents. +- **Average** — average of an integer field across matching documents. + +Some aggregate queries can return either one total or grouped totals, depending on the query shape. + +Aggregate queries use the same two-layer verification as any other proof (GroveDB Merkle proof plus Tenderdash consensus signature), so the result carries the same trust model as other proven Platform responses. + +For the exact request and response shapes, see the [DAPI Platform endpoints reference](../reference/dapi-endpoints-platform-endpoints.md). + +## Requesting and Verifying Proofs + +### DAPI Integration + +The Decentralized API (DAPI) provides the interface for requesting proofs. When making queries, clients can set the `prove` parameter to receive cryptographic proofs alongside the data. + +Without proofs, clients must trust that the DAPI node is returning accurate data. With proofs enabled, clients can verify responses independently, treating DAPI nodes as untrusted data carriers rather than trusted authorities. + +:::{note} +The Dash Platform SDKs handle proof verification automatically when proofs are requested. Developers using the SDK don't need to implement verification logic manually. +::: + +### What Verification Confirms + +When a proof verifies successfully, the client has cryptographic assurance that: + +1. **Data integrity**: The data matches exactly what is stored in platform state +2. **Consensus agreement**: A valid validator quorum signed this state at a specific block height +3. **Temporal accuracy**: The proof is tied to a specific block height and timestamp +4. **Completeness**: For queries, all matching results are included (nothing omitted) + +Proof verification also detects proof-of-absence, confirming when requested data genuinely doesn't exist rather than being withheld by a malicious node. + +## Asset Lock Proofs + +Asset lock proofs are a special category used when creating or funding [identities](../explanations/identity.md). They prove that Dash has been locked on the core blockchain (layer 1) to establish credits on Dash Platform (layer 2). + +### Instant Asset Lock Proof + +Uses Dash's InstantSend feature to prove funds are locked immediately: + +- Contains the InstantSend lock proving transaction finality +- Includes the asset lock special transaction +- Enables immediate identity creation without waiting for block confirmations + +This is the preferred method as it allows near-instant identity creation. + +### Chain Asset Lock Proof + +Uses ChainLocks to prove funds are locked at a specific core blockchain height: + +- References the asset lock transaction by its outpoint +- Specifies the core chain height where the transaction was chain-locked +- Provides finality guarantee through Dash's ChainLock mechanism + +This method is used when InstantSend confirmation is not available. + +:::{attention} +Asset lock proofs are verified by the network during identity creation and topup state transitions. The locked funds cannot be spent on the core chain once used to create platform credits. +::: + +## Related Topics + +- [Platform Consensus](../explanations/platform-consensus.md) - How Tenderdash and validator quorums work +- [Identity](../explanations/identity.md) - Identity creation using asset lock proofs +- [DAPI](../explanations/dapi.md) - The API layer for requesting proofs +- [Drive](../explanations/drive.md) - The storage layer that generates proofs diff --git a/docs/index.md b/docs/index.md index 7c9f81501..e89b33b43 100644 --- a/docs/index.md +++ b/docs/index.md @@ -134,6 +134,7 @@ explanations/dashpay explanations/fees explanations/tokens explanations/nft +explanations/proofs explanations/query ``` diff --git a/docs/protocol-ref/address-system.md b/docs/protocol-ref/address-system.md index 55e97c2ae..cd078e609 100644 --- a/docs/protocol-ref/address-system.md +++ b/docs/protocol-ref/address-system.md @@ -27,19 +27,16 @@ There are six address-based state transition types: ### Platform Address -Platform addresses are derived from standard Bitcoin/Dash address formats and encoded using bech32m per [DIP-0018](https://github.com/dashpay/dips/blob/master/dip-0018.md). +Platform addresses are derived from standard Bitcoin/Dash address formats and encoded as bech32m strings per [DIP-0018](https://github.com/dashpay/dips/blob/master/dip-0018.md). The human-readable part (HRP) is `dash` on mainnet and `tdash` on testnet, devnet, and regtest. The 21-byte payload is `type_byte || Hash160(compressed_pubkey)`, where `Hash160 = RIPEMD160(SHA256(x))`. The checksum is bech32m ([BIP-350](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki)). -| Variant | Type Byte | Size | Description | -| ------- | ---- | -------- | ---------------------------------------------------- | -| `P2PKH` | 0xb0 | 21 bytes | Pay-to-Public-Key-Hash (1 type byte + 20 hash bytes) | -| `P2SH` | 0x80 | 21 bytes | Pay-to-Script-Hash (1 type byte + 20 hash bytes) | +| Variant | Type Byte | Description | +| ------- | --------- | ----------- | +| `P2PKH` | 0xb0 | Pay-to-Public-Key-Hash | +| `P2SH` | 0x80 | Pay-to-Script-Hash | -**Encoding:** - -- **Mainnet HRP:** `dash` -- **Testnet HRP:** `tdash` (also used for Devnet and Regtest) - -**Derivation:** Standard Bitcoin derivation using `Hash160(compressed_pubkey)` where Hash160 = RIPEMD160(SHA256(x)). +:::{note} +A `PlatformAddress` has two distinct byte encodings depending on context. The type bytes above (`0xb0` / `0x80`) apply to the user-facing bech32m encoding — what appears in address strings like `dash1k...`. Internal GroveDB storage keys use bincode variant indices `0x00` / `0x01` instead. Decoding one through the other's code path will fail. +::: See the [Platform address implementation in rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/address_funds/platform_address.rs). diff --git a/docs/protocol-ref/protocol-constants.md b/docs/protocol-ref/protocol-constants.md index 4d0b94995..c06962160 100644 --- a/docs/protocol-ref/protocol-constants.md +++ b/docs/protocol-ref/protocol-constants.md @@ -177,6 +177,7 @@ Fees related to contested document voting. | Min top-up balance | 50,000 duffs | 0.0005 Dash minimum | [rs-platform-version](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v1.rs#L21) | | Min address funding balance | 50,000 duffs | 0.0005 Dash minimum | [rs-platform-version](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v1.rs#L22) | | Min identity funding amount | 200,000 credits | Minimum for address-based creation | [rs-platform-version](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v1.rs#L41) | +| Max asset-lock transaction inputs | 100 | Maximum Core inputs in an asset-lock transaction used to fund an identity or top-up (introduced in protocol v3 to prevent stuck funds; v1/v2 had no effective limit) | [rs-platform-version](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v3.rs#L25) | ## Document & Data Contract Model @@ -253,10 +254,10 @@ These limits apply to token perpetual distribution function parameters. | Constant | Value | Description | Source | |----------|-------|-------------|--------| | Address hash size | 20 bytes | Size of address hash | [rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/address_funds/platform_address.rs#L22) | -| Platform HRP (mainnet) | "dash" | Human-readable prefix | [rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/address_funds/platform_address.rs#L89) | -| Platform HRP (testnet) | "tdash" | Testnet human-readable prefix | [rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/address_funds/platform_address.rs#L91) | -| P2PKH address type | 0xb0 (176) | Pay-to-public-key-hash encoding type byte | [rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/address_funds/platform_address.rs#L95) | -| P2SH address type | 0x80 (128) | Pay-to-script-hash encoding type byte | [rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/address_funds/platform_address.rs#L97) | +| Platform HRP (mainnet) | "dash" | Human-readable prefix | [rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/address_funds/platform_address.rs#L184) | +| Platform HRP (non-mainnet) | "tdash" | Human-readable prefix used for testnet, devnet, and regtest | [rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/address_funds/platform_address.rs#L186) | +| P2PKH address type (bech32m) | 0xb0 (176) | Pay-to-public-key-hash bech32m encoding type byte | [rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/address_funds/platform_address.rs#L190) | +| P2SH address type (bech32m) | 0x80 (128) | Pay-to-script-hash bech32m encoding type byte | [rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/address_funds/platform_address.rs#L192) | ### Transaction Limits diff --git a/docs/tutorials/node-setup/connect-to-a-network-dash-masternode.md b/docs/tutorials/node-setup/connect-to-a-network-dash-masternode.md index 3d2f03800..8d007fe21 100644 --- a/docs/tutorials/node-setup/connect-to-a-network-dash-masternode.md +++ b/docs/tutorials/node-setup/connect-to-a-network-dash-masternode.md @@ -44,12 +44,6 @@ Example (partial) output of the setup wizard showing important information: › Dashpay contract ID: EAv8ePXREdJ719ntcRiKuEYxv9XooMwL1mJmPHMGuW9r ✔ Obtain Dashpay contract commit block height › Dashpay contract block height: 15 - ✔ Register Feature Flags identity - › Feature Flags identity: 8BsvV4RCbW7srWj81kgjJCykRBF2rzyigys8XkBchY96 - ✔ Register Feature Flags contract - › Feature Flags contract ID: JDrDAGVqTWsM9k7KGBsSjcyC11Vd2UdPxPoPf4NzyyrP - ✔ Obtain Feature Flags contract commit block height - › Feature Flags contract block height: 20 ``` From 35c27c7c84bd83b4d7d8efb007fcf64555fb5955 Mon Sep 17 00:00:00 2001 From: thephez Date: Tue, 26 May 2026 14:03:10 -0400 Subject: [PATCH 2/6] docs(reference): document v3.1 getDocuments v0/v1 and aggregate queries (#149) * docs(reference): document v3.1 getDocuments v0/v1 surface and aggregate queries Rewrite the getDocuments entry to cover the v0 legacy CBOR surface and the v1 typed SQL-shaped surface with Fetch / Count / Sum / Average modes. Add doctype-level aggregate query flags (documentsCountable, rangeCountable, documentsSummable, rangeSummable, documentsAverageable, rangeAverageable) to the data-contract-document reference, an aggregate-queries section to query-syntax, and a v3.1 annotation on the dapi-endpoints overview row. Co-Authored-By: Claude Opus 4.7 (1M context) * docs: add DAPI endpoint reference convention and release checklist Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- CLAUDE.md | 12 + RELEASE.md | 14 + conf.py | 1 + docs/protocol-ref/data-contract-document.md | 27 + docs/protocol-ref/data-contract.md | 2 +- .../dapi-endpoints-platform-endpoints.md | 462 ++++++++++++------ docs/reference/dapi-endpoints.md | 2 +- docs/reference/query-syntax.md | 24 + 8 files changed, 406 insertions(+), 138 deletions(-) create mode 100644 RELEASE.md diff --git a/CLAUDE.md b/CLAUDE.md index 01c6b2abe..8cc4a1371 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,6 +74,18 @@ When updating documentation values that include GitHub source links: - Update the line anchor (`#L`) to match the correct line **in the branch the link points to** - When available, use the local platform repository checkout to verify line numbers against the correct branch +## DAPI endpoint reference + +The DAPI endpoint reference is split between an overview page (`docs/reference/dapi-endpoints.md`) and per-section detail pages (`docs/reference/dapi-endpoints-*.md`). The authoritative list of endpoints lives in the platform proto at `https://github.com/dashpay/platform/tree//packages/dapi-grpc/protos` — check the proto when adding or modifying entries. + +When you add or materially update an entry on a detail page, also update the matching row on `dapi-endpoints.md`: + +- Keep the description in sync between the two pages. +- Prefix the overview row's description with `**Added in Dash Platform vX.Y.Z**` (new endpoints) or `**Updated in Dash Platform vX.Y.Z**` (modified endpoints), followed by `
    ` and the description. Use **bold** for the current release's annotations; older releases use *italics*. +- For a whole new endpoint group, wrap the new section in a `:::{versionadded} X.Y.Z` admonition above its table — see Security Groups, Tokens, Address System, and Shielded Transactions for the pattern. + +For the full per-release endpoint review process (proto diff, example refresh, demoting annotations to italics), see [RELEASE.md](RELEASE.md). + ## File Patterns - Documentation files: `docs/**/*.md` diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 000000000..c87e3594d --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,14 @@ +# Release Checklist + +Per-release tasks for the docs. For editing conventions, see [CLAUDE.md](CLAUDE.md). + +## DAPI endpoint review + +1. Diff the platform proto between the previous release tag and the current release branch — that's the source of truth for what changed. Proto source: `https://github.com/dashpay/platform/tree//packages/dapi-grpc/protos`. +2. For each affected endpoint, update both the detail page (`docs/reference/dapi-endpoints-*.md`) and the matching row on the overview page (`docs/reference/dapi-endpoints.md`), with the bold version annotation on the overview row. +3. Demote the previous release's bold annotations on the overview page to italics. +4. Re-run example requests against testnet and refresh response examples if necessary. Testnet state may have been wiped, so even unchanged endpoints may have stale data. + +## Update "Previous version" links + +Several pages (including the DAPI endpoints pages) link to the previous version of the docs. These links are not updated automatically. Search the site for "previous version" and update each link to point to the appropriate version. diff --git a/conf.py b/conf.py index f4814c927..8945ee5ff 100644 --- a/conf.py +++ b/conf.py @@ -41,6 +41,7 @@ '.DS_Store', 'README.md', 'CLAUDE.md', + 'RELEASE.md', '.devcontainer', '.codex', '.local', diff --git a/docs/protocol-ref/data-contract-document.md b/docs/protocol-ref/data-contract-document.md index ce360232e..22d85fbcc 100644 --- a/docs/protocol-ref/data-contract-document.md +++ b/docs/protocol-ref/data-contract-document.md @@ -328,6 +328,33 @@ The following example (from the [DPNS contract's `domain` document](https://gith } ``` +## Aggregate Query Flags + +:::{versionadded} 3.1.0 +::: + +Document types can opt into aggregate query support (count / sum / average) by setting flags at the document-type level. These flags control the underlying storage layout — once set on a published contract they cannot be changed by a contract update. + +There are two axes: + +* **Doctype-wide** (`documents*`) — applies the aggregate over the entire document type. Set at the document type root, alongside other doctype options like `documentsKeepHistory`. +* **Per-index range** (`range*`) — extends the corresponding aggregate to range queries on indexed properties. Set on the index's property entry. Requires the matching base flag. + +| Flag | Type | Purpose | Required for | +| - | - | - | - | +| `documentsCountable` | Boolean | Doctype-wide counts (empty `where` or `==`/`IN` clauses on indexed fields). | `SELECT COUNT(*)` without a range clause. | +| `rangeCountable` | Boolean | Per-index counts over a range. Requires `documentsCountable`. | `SELECT COUNT(*)` with a range clause or `GROUP BY `. | +| `documentsSummable` | String | Doctype-wide sums of the named integer property. | `SELECT SUM()`. | +| `rangeSummable` | Boolean | Per-index sums over a range. Requires `documentsSummable`. | `SELECT SUM()` with a range clause. | +| `documentsAverageable` | String | Syntactic sugar for `documentsCountable: true` + `documentsSummable: ""`. | `SELECT AVG()`. | +| `rangeAverageable` | Boolean | Syntactic sugar for `rangeCountable: true` + `rangeSummable: true`. Requires `documentsAverageable`. | `SELECT AVG()` with a range clause. | + +The averageable flags desugar to the underlying count + sum flags during contract parsing — same on-disk layout — so authors who think in terms of averages get a single flag and downstream code paths (insert, query, estimation) stay unchanged. If both `documentsAverageable` and `documentsSummable` are set, they must name the same property. + +These flags are validated against the v1 document meta-schema and are rejected when applied to pre-v12 contracts. The full v1 meta-schema, including these flags, is defined [in rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/schema/meta_schemas/document/v1/document-meta.json). + +See the [`getDocuments` reference](../reference/dapi-endpoints-platform-endpoints.md#getdocuments) for the request/response shapes that consume these flags. + ## Keyword Constraints There are a variety of keyword constraints currently defined for performance and security reasons. The diff --git a/docs/protocol-ref/data-contract.md b/docs/protocol-ref/data-contract.md index 252be8ec7..c2c351576 100644 --- a/docs/protocol-ref/data-contract.md +++ b/docs/protocol-ref/data-contract.md @@ -740,7 +740,7 @@ property must be incremented if the contract is updated. ### Data Contract documents -See the [data contract documents](./data-contract-document.md) page for details. +See the [data contract documents](./data-contract-document.md) page for details, including the [aggregate query flags](./data-contract-document.md#aggregate-query-flags) that opt document types into count/sum/average queries. ### Data Contract config diff --git a/docs/reference/dapi-endpoints-platform-endpoints.md b/docs/reference/dapi-endpoints-platform-endpoints.md index 47ca03209..9ec4003da 100644 --- a/docs/reference/dapi-endpoints-platform-endpoints.md +++ b/docs/reference/dapi-endpoints-platform-endpoints.md @@ -984,32 +984,89 @@ grpcurl -proto protos/platform/v0/platform.proto \ ### getDocuments -**Returns**: [Document](../explanations/platform-protocol-document.md) information for the requested document(s) -**Parameters**: +:::{versionchanged} 3.1.0 +Adds a typed v1 request surface (`WhereClause` / `OrderClause` / `Select`) and four aggregate modes — `DOCUMENTS`, `COUNT`, `SUM`, `AVG`. The legacy v0 CBOR surface is still supported. +::: -:::{note} -The `where`, `order_by`, `limit`, `start_at`, and `start_after` parameters must comply with the limits defined on the [Query Syntax](../reference/query-syntax.md) page. -::: - -| Name | Type | Required | Description | -| ----------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------ | -| `data_contract_id` | Bytes | Yes | A data contract `id` | -| `document_type` | String | Yes | A document type defined by the data contract (e.g. `preorder` or `domain` for the DPNS contract) | -| `where` \* | Bytes | No | Where clause to filter the results | -| `order_by` \* | Bytes | No | Sort records by the field(s) provided | -| `limit` | Integer | No | Maximum number of results to return | -| ---------- | | | | -| _One_ of the following: | | | | -| `start_at` | Integer | No | Return records beginning with the index provided | -| `start_after` | Integer | No | Return records beginning after the index provided | -| ---------- | | | | -| `prove` | Boolean | No | Set to `true` to receive a proof that contains the requested document(s). The data requested will be encoded as part of the proof in the response. | +**Returns**: [Document](../explanations/platform-protocol-document.md) information for the requested document(s), or an aggregate count/sum/average over the matched document set. -**Example Request and Response** +The request envelope is `oneof version { v0; v1; }`. Pick a version per call: + +- **v1** (default for new code, v3.1+) — typed request fields and aggregate `select` modes. +- **v0** (legacy) — CBOR-encoded `where` / `order_by` byte strings. Fetch only. + +**Common request fields** (apply to every variant below) + +| Name | Type | Required | Description | +| ---- | ---- | -------- | ----------- | +| `data_contract_id` | Bytes | Yes | A data contract `id`. | +| `document_type` | String | Yes | A document type defined by the data contract. | +| `where_clauses` (v1) / `where` (v0) | Typed (v1) or CBOR bytes (v0) | No | Filter clauses. See [Query Syntax](../reference/query-syntax.md). | +| `order_by` | Typed (v1) or CBOR bytes (v0) | No | Sort order. See [Query Syntax](../reference/query-syntax.md). | +| `prove` | Boolean | No | Return a proof instead of data. See [Platform proofs](../reference/platform-proofs.md). | + +For v1, see also the [doctype-level aggregate flags](../protocol-ref/data-contract-document.md#aggregate-query-flags), which control whether a document type supports the `COUNT` / `SUM` / `AVG` modes below. + +#### Fetch documents + +Returns matched documents. + +**Mode-specific request fields** + +| Name | Type | Required | Description | +| ---- | ---- | -------- | ----------- | +| `limit` | Integer | No | Maximum number of documents to return. | +| `start_at` _or_ `start_after` | Bytes | No | Cursor — start at / after this document ID. | + +For v1, `selects` may be omitted (defaults to `[Select{ function: DOCUMENTS }]`) or set explicitly. + +**Example Request** ::::{tab-set} +:::{tab-item} v1 (gRPCurl) +:sync: v1-grpcurl + +```shell +# `data_contract_id` must be represented in base64 +grpcurl -proto protos/platform/v0/platform.proto \ + -d '{ + "v1": { + "data_contract_id": "5mjGWa9mruHnLBht3ntbfgodcSoJxA1XIfYiv1PFMVU=", + "document_type": "domain", + "where_clauses": [ + { "field": "normalizedParentDomainName", "operator": "EQUAL", "value": { "text": "dash" } } + ], + "limit": 1 + } + }' \ + seed-1.testnet.networks.dash.org:1443 \ + org.dash.platform.dapi.v0.Platform/getDocuments +``` + +::: + +:::{tab-item} v0 (gRPCurl) +:sync: v0-grpcurl + +```shell +# `data_contract_id` must be represented in base64 +grpcurl -proto protos/platform/v0/platform.proto \ + -d '{ + "v0": { + "data_contract_id": "5mjGWa9mruHnLBht3ntbfgodcSoJxA1XIfYiv1PFMVU=", + "document_type": "domain", + "limit": 1 + } + }' \ + seed-1.testnet.networks.dash.org:1443 \ + org.dash.platform.dapi.v0.Platform/getDocuments +``` + +::: + :::{tab-item} JavaScript (dapi-client) :sync: js-dapi-client + ```javascript const DAPIClient = require('@dashevo/dapi-client'); const { @@ -1029,11 +1086,8 @@ client.platform.getDataContract(contractId).then((contractResponse) => { dpp.dataContract .createFromBuffer(contractResponse.getDataContract()) .then((contract) => { - // Get document(s) client.platform - .getDocuments(contractId, type, { - limit, - }) + .getDocuments(contractId, type, { limit }) .then((response) => { for (const document of response.documents) { const doc = dpp.document.createExtendedDocumentFromDocumentBuffer( @@ -1047,155 +1101,291 @@ client.platform.getDataContract(contractId).then((contractResponse) => { }); }); ``` + ::: +:::: -:::{tab-item} JavaScript (dapi-grpc) -:sync: js-dapi-grpc -```javascript -const { - v0: { PlatformPromiseClient, GetDataContractRequest, GetDocumentsRequest }, -} = require('@dashevo/dapi-grpc'); -const { default: loadDpp, DashPlatformProtocol, Identifier } = require('@dashevo/wasm-dpp'); +**Example Response** -loadDpp(); -const dpp = new DashPlatformProtocol(null); -const platformPromiseClient = new PlatformPromiseClient( - 'https://seed-1.testnet.networks.dash.org:1443', -); +::::{tab-set} +:::{tab-item} v1 (gRPCurl) +:sync: v1-grpcurl -const contractId = Identifier.from('GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec'); -const contractIdBuffer = Buffer.from(contractId); -const getDataContractRequest = new GetDataContractRequest(); -getDataContractRequest.setId(contractIdBuffer); +```json +{ + "v1": { + "data": { + "documents": { + "documents": [ + "AAZ1S7dbhY4VJrSCvjs2Z1DIwa9Qt9MAyjbJdh7gPu6oDsGC/h1Ayf+ZzXp2zLWDF4XB2qMLWZ0brsAKo0r/0sYBAAcAAAGRivixugAAAZGK+LG6AAABkYr4sboAF2F1ZzI1LTEyMzQ1Njc4OTAxMjM0NTY3F2F1ZzI1LTEyMzQ1Njc4OTAxMjM0NTY3AQRkYXNoBGRhc2gAIQEOwYL+HUDJ/5nNenbMtYMXhcHaowtZnRuuwAqjSv/SxgEA" + ] + } + }, + "metadata": { + "height": "5991", + "coreChainLockedHeight": 1097384, + "epoch": 1170, + "timeMs": "1725567845055", + "protocolVersion": 1, + "chainId": "dash-testnet-51" + } + } +} +``` -platformPromiseClient - .getDataContract(getDataContractRequest) - .then((contractResponse) => { - dpp.dataContract.createFromBuffer(contractResponse.getDataContract()).then((contract) => { - // Get documents - const getDocumentsRequest = new GetDocumentsRequest(); - const type = 'domain'; - const limit = 10; - - getDocumentsRequest.setDataContractId(contractIdBuffer); - getDocumentsRequest.setDocumentType(type); - // getDocumentsRequest.setWhere(whereSerialized); - // getDocumentsRequest.setOrderBy(orderBySerialized); - getDocumentsRequest.setLimit(limit); - // getDocumentsRequest.setStartAfter(startAfter); - // getDocumentsRequest.setStartAt(startAt); - - platformPromiseClient.getDocuments(getDocumentsRequest).then((response) => { - for (const document of response.getDocuments().getDocumentsList()) { - const documentBuffer = Buffer.from(document); - const doc = dpp.document.createExtendedDocumentFromDocumentBuffer( - documentBuffer, - type, - contract, - ); - console.log(doc.toJSON()); - } - }); - }); - }) - .catch((e) => console.error(e)); +::: + +:::{tab-item} v0 (gRPCurl) +:sync: v0-grpcurl + +```json +{ + "v0": { + "documents": { + "documents": [ + "AAZ1S7dbhY4VJrSCvjs2Z1DIwa9Qt9MAyjbJdh7gPu6oDsGC/h1Ayf+ZzXp2zLWDF4XB2qMLWZ0brsAKo0r/0sYBAAcAAAGRivixugAAAZGK+LG6AAABkYr4sboAF2F1ZzI1LTEyMzQ1Njc4OTAxMjM0NTY3F2F1ZzI1LTEyMzQ1Njc4OTAxMjM0NTY3AQRkYXNoBGRhc2gAIQEOwYL+HUDJ/5nNenbMtYMXhcHaowtZnRuuwAqjSv/SxgEA" + ] + }, + "metadata": { + "height": "5991", + "coreChainLockedHeight": 1097384, + "epoch": 1170, + "timeMs": "1725567845055", + "protocolVersion": 1, + "chainId": "dash-testnet-51" + } + } +} ``` + ::: -:::{tab-item} Request (gRPCurl) +:::{tab-item} JavaScript (decoded document) +:sync: js-dapi-client + +```json +{ + "$id": "Do3YtBPJG72zG4tCbN5VE8djJ6rLpvx7yvtMWEy89HC", + "$ownerId": "4pk6ZhgDtxn9yN2bbB6kfsYLRmUBH7PKUq275cjyzepT", + "label": "Chronic", + "normalizedLabel": "chr0n1c", + "normalizedParentDomainName": "dash", + "parentDomainName": "dash", + "records": { + "dashUniqueIdentityId": "OM4WaCQNLedQ0rpbl1UMTZhEbnVeMfL4941ZD08iyFw=" + }, + "subdomainRules": { "allowSubdomains": false }, + "$revision": 1, + "$type": "domain" +} +``` + +::: +:::: + +#### Count documents + +:::{versionadded} 3.1.0 +::: + +Returns one aggregate count, or per-group counts when `group_by` is set. Requires the doctype to set `documentsCountable: true` (and `rangeCountable: true` for range-grouped queries). See [aggregate query flags](../protocol-ref/data-contract-document.md#aggregate-query-flags). + +**Mode-specific request fields** + +| Name | Type | Required | Description | +| ---- | ---- | -------- | ----------- | +| `selects` | `[Select{ function: COUNT }]` | Yes | Projection. | +| `group_by` | Repeated string | No | `[]`, `[in_field]`, `[range_field]`, or `[in_field, range_field]`. | + +`limit` is rejected for `group_by=[]` and `group_by=[in_field]` (the result is bounded by construction). `start_at` / `start_after` are not valid in this mode — paginate by narrowing the where clause. + +**Response shape** + +- `group_by = []` → `counts.aggregate_count` (single integer). +- `group_by = [...]` → `counts.entries[]` of `{ in_key?, key, count }`. + +`IN` values that match no documents are omitted from `counts.entries` rather than returned with `count: 0`. Diff your request's `IN` array against the returned `key` values to detect "queried but absent." + +**Example Request** + +::::{tab-set} +:::{tab-item} gRPCurl :sync: grpcurl + ```shell -# Request documents -# `id` must be represented in base64 +# TODO: Replace with a real example once a contract using +# `documentsCountable` is published on testnet. The contract id, +# document type, and field name below are illustrative only. grpcurl -proto protos/platform/v0/platform.proto \ -d '{ - "v0": { - "data_contract_id":"5mjGWa9mruHnLBht3ntbfgodcSoJxA1XIfYiv1PFMVU=", - "document_type":"domain", - "limit":1 + "v1": { + "data_contract_id": "", + "document_type": "shipments", + "selects": [{ "function": "COUNT" }], + "where_clauses": [ + { "field": "status", "operator": "EQUAL", "value": { "text": "delivered" } } + ] } }' \ seed-1.testnet.networks.dash.org:1443 \ org.dash.platform.dapi.v0.Platform/getDocuments ``` + ::: :::: -::::{tab-set} -:::{tab-item} Response (JavaScript) -:sync: js-dapi-client +**Example Response** + ```json { - "$id":"Do3YtBPJG72zG4tCbN5VE8djJ6rLpvx7yvtMWEy89HC", - "$ownerId":"4pk6ZhgDtxn9yN2bbB6kfsYLRmUBH7PKUq275cjyzepT", - "label":"Chronic", - "normalizedLabel":"chr0n1c", - "normalizedParentDomainName":"dash", - "parentDomainName":"dash", - "preorderSalt":"1P9N5qv1Ww2xkv6/XXpsvymyGYychRsLXMhCqvW79Jo=", - "records":{ - "dashUniqueIdentityId":"OM4WaCQNLedQ0rpbl1UMTZhEbnVeMfL4941ZD08iyFw=" - }, - "subdomainRules":{ - "allowSubdomains":false - }, - "$revision":1, - "$createdAt":null, - "$updatedAt":null, - "$dataContract":{ - "$format_version":"0", - "id":"GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", - "config":{ - "$format_version":"0", - "canBeDeleted":false, - "readonly":false, - "keepsHistory":false, - "documentsKeepHistoryContractDefault":false, - "documentsMutableContractDefault":true, - "requiresIdentityEncryptionBoundedKey":null, - "requiresIdentityDecryptionBoundedKey":null + "v1": { + "data": { + "counts": { + "aggregateCount": "1234" + } }, - "version":1, - "ownerId":"EuzJmuZdBSJs2eTrxHEp6QqJztbp6FKDNGMeb4W2Ds7h", - "schemaDefs":null, - "documentSchemas":{ - "domain":[ - "Object" - ], - "preorder":[ - "Object" - ] - } - }, - "$type":"domain" + "metadata": { "height": "5991", "coreChainLockedHeight": 1097384 } + } } ``` + +Count values use `[jstype = JS_STRING]` on the proto, so JavaScript clients receive strings to avoid precision loss above `Number.MAX_SAFE_INTEGER`. + +#### Sum documents + +:::{versionadded} 3.1.0 ::: -:::{tab-item} Response (gRPCurl) +Returns the sum of an integer field across matched documents, or per-group sums when `group_by` is set. Requires the doctype to set `documentsSummable: ""` (and `rangeSummable: true` for range-grouped queries). See [aggregate query flags](../protocol-ref/data-contract-document.md#aggregate-query-flags). + +**Mode-specific request fields** + +| Name | Type | Required | Description | +| ---- | ---- | -------- | ----------- | +| `selects` | `[Select{ function: SUM, field: "" }]` | Yes | `field` must name the summable property. | +| `group_by` | Repeated string | No | Same shape rules as Count above. | + +`start_at` / `start_after` are not valid. + +**Response shape** + +- `group_by = []` → `sums.aggregate_sum` (signed integer). +- `group_by = [...]` → `sums.entries[]` of `{ in_key?, key, sum }`. + +**Example Request** + +::::{tab-set} +:::{tab-item} gRPCurl :sync: grpcurl + +```shell +# TODO: Replace with a real example once a contract using +# `documentsSummable` is published on testnet. The contract id, +# document type, and field name below are illustrative only. +grpcurl -proto protos/platform/v0/platform.proto \ + -d '{ + "v1": { + "data_contract_id": "", + "document_type": "inventory", + "selects": [{ "function": "SUM", "field": "quantity" }], + "where_clauses": [ + { "field": "warehouse", "operator": "EQUAL", "value": { "text": "north" } } + ] + } + }' \ + seed-1.testnet.networks.dash.org:1443 \ + org.dash.platform.dapi.v0.Platform/getDocuments +``` + +::: +:::: + +**Example Response** + ```json { - "v0": { - "documents": { - "documents": [ - "AAZ1S7dbhY4VJrSCvjs2Z1DIwa9Qt9MAyjbJdh7gPu6oDsGC/h1Ayf+ZzXp2zLWDF4XB2qMLWZ0brsAKo0r/0sYBAAcAAAGRivixugAAAZGK+LG6AAABkYr4sboAF2F1ZzI1LTEyMzQ1Njc4OTAxMjM0NTY3F2F1ZzI1LTEyMzQ1Njc4OTAxMjM0NTY3AQRkYXNoBGRhc2gAIQEOwYL+HUDJ/5nNenbMtYMXhcHaowtZnRuuwAqjSv/SxgEA" - ] + "v1": { + "data": { + "sums": { + "aggregateSum": "42000" + } }, - "metadata": { - "height": "5991", - "coreChainLockedHeight": 1097384, - "epoch": 1170, - "timeMs": "1725567845055", - "protocolVersion": 1, - "chainId": "dash-testnet-51" - } + "metadata": { "height": "5991", "coreChainLockedHeight": 1097384 } } } ``` + +#### Average documents + +:::{versionadded} 3.1.0 +::: + +Returns a `(count, sum)` pair the client divides to compute the average, or per-group `(count, sum)` pairs when `group_by` is set. Requires the doctype to set `documentsAverageable: ""` (and `rangeAverageable: true` for range-grouped queries). See [aggregate query flags](../protocol-ref/data-contract-document.md#aggregate-query-flags). + +Why `(count, sum)` instead of a single `average`? Returning the pair preserves full precision and lets the client pick how to represent the result (integer division, floating-point, decimal). + +**Mode-specific request fields** + +| Name | Type | Required | Description | +| ---- | ---- | -------- | ----------- | +| `selects` | `[Select{ function: AVG, field: "" }]` | Yes | `field` must name the averageable property. | +| `group_by` | Repeated string | No | Same shape rules as Count above. | + +`start_at` / `start_after` are not valid. + +**Response shape** + +- `group_by = []` → `averages.aggregate_average` of `{ count, sum }`. +- `group_by = [...]` → `averages.entries[]` of `{ in_key?, key, count, sum }`. + +**Example Request** + +::::{tab-set} +:::{tab-item} gRPCurl +:sync: grpcurl + +```shell +# TODO: Replace with a real example once a contract using +# `documentsAverageable` is published on testnet. The contract id, +# document type, and field name below are illustrative only. +grpcurl -proto protos/platform/v0/platform.proto \ + -d '{ + "v1": { + "data_contract_id": "", + "document_type": "ratings", + "selects": [{ "function": "AVG", "field": "score" }], + "where_clauses": [ + { "field": "productId", "operator": "EQUAL", "value": { "text": "abc123" } } + ] + } + }' \ + seed-1.testnet.networks.dash.org:1443 \ + org.dash.platform.dapi.v0.Platform/getDocuments +``` + ::: :::: +**Example Response** + +```json +{ + "v1": { + "data": { + "averages": { + "aggregateAverage": { + "count": "50", + "sum": "215" + } + } + }, + "metadata": { "height": "5991", "coreChainLockedHeight": 1097384 } + } +} +``` + +Client computes `avg = 215 / 50 = 4.3`. + ## Identity Endpoints ### getIdentity diff --git a/docs/reference/dapi-endpoints.md b/docs/reference/dapi-endpoints.md index 0e996f47d..9ea2f26d2 100644 --- a/docs/reference/dapi-endpoints.md +++ b/docs/reference/dapi-endpoints.md @@ -34,7 +34,7 @@ without introducing issues for endpoint consumers. | [`getDataContract`](../reference/dapi-endpoints-platform-endpoints.md#getdatacontract) | Returns the requested data contract | | [`getDataContracts`](../reference/dapi-endpoints-platform-endpoints.md#getdatacontracts) | Returns the requested data contracts | | [`getDataContractHistory`](../reference/dapi-endpoints-platform-endpoints.md#getdatacontracthistory) | Returns the requested data contract history | -| [`getDocuments`](../reference/dapi-endpoints-platform-endpoints.md#getdocuments) | Returns the requested document(s) | +| [`getDocuments`](../reference/dapi-endpoints-platform-endpoints.md#getdocuments) | **Updated in Dash Platform v3.1.0**
    Returns the requested document(s), or an aggregate count/sum/average over the matched document set. | ### Identities diff --git a/docs/reference/query-syntax.md b/docs/reference/query-syntax.md index 266dcc899..4bef41722 100644 --- a/docs/reference/query-syntax.md +++ b/docs/reference/query-syntax.md @@ -148,6 +148,30 @@ The query modifiers described here determine how query results will be sorted an For indices composed of multiple fields ([example from the DPNS data contract](https://github.com/dashpay/platform/blob/master/packages/dpns-contract/schema/v1/dpns-contract-documents.json)), the sort order in an `orderBy` must either match the order defined in the data contract OR be the inverse order. ::: +## Aggregate Queries + +:::{versionadded} 3.1.0 +::: + +The [getDocuments](../reference/dapi-endpoints-platform-endpoints.md#getdocuments) v1 surface adds an aggregate-query mode. The same `where` / `orderBy` clauses described above still apply; an additional `select` projection (and optional `groupBy`) determines whether the request returns documents or aggregate values over the matched set. + +| `select` | Returns | +| ---------------- | ------- | +| `DOCUMENTS` | Matched documents (same as v0). | +| `COUNT(*)` | Number of documents matching the query. | +| `SUM()` | Sum of `` across matching documents. | +| `AVG()` | `(count, sum)` pair the client divides to compute the average. | + +`groupBy` is optional. With an empty `groupBy`, the response carries a single aggregate value; with a `groupBy` of one or two fields, the response carries one entry per group. + +Aggregate queries impose extra schema requirements on the document type — `COUNT` needs `documentsCountable`, `SUM` needs `documentsSummable`, `AVG` needs `documentsAverageable` (or both base flags). Range-grouped aggregates additionally need the `range*` variants. See the [doctype-level aggregate flags](../protocol-ref/data-contract-document.md#aggregate-query-flags) for the schema annotations and the [`getDocuments` reference](../reference/dapi-endpoints-platform-endpoints.md#getdocuments) for the full `select` × `groupBy` shape table. + +`SUM` / `AVG` integer values are returned as JS strings so JavaScript clients don't lose precision on values larger than `Number.MAX_SAFE_INTEGER`. + +:::{note} +`HAVING`, `OFFSET`, `COUNT()`, `MIN`, `MAX`, and multi-projection `SELECT` are present on the wire but currently return `Unsupported`. Callers can encode them in builders ahead of server support landing, but evaluation rejects them today. +::: + ## Example query The following query combines both a where clause and query modifiers. From 1a8793b9900d8dad692c4f33aa34e2ece41fe8a4 Mon Sep 17 00:00:00 2001 From: thephez Date: Wed, 27 May 2026 10:36:10 -0400 Subject: [PATCH 3/6] docs(protocol-ref): add shielded pool concept and wire format (#150) * docs(explanations): add shielded pool concept page Introduces the Orchard-based shielded pool, its core concepts (notes, nullifiers, anchors, encrypted notes), the 5 shielded transition flows, and the 16-action-per-transition limit. Wired into the Explanations toctree. Co-Authored-By: Claude Opus 4.7 (1M context) * docs(protocol-ref): add shielded pool wire format and signing New protocol-ref/shielded-pool.md documents the five shielded state transition types (Shield, Shielded Transfer, Unshield, Shield from Asset Lock, Shielded Withdrawal) along with the shared Orchard bundle primitives, the per-action serialized form, and the Orchard / address witness / asset-lock signature layers that authorize them. Wires the discriminator rows 15-19 in state-transition.md to the new page, adds a Signing Shielded Transitions subsection, and links from the signing-methods table. Fixes the stale max_shielded_transition_actions constant (was 100; correct value is 16) and adds the max_asset_lock_transaction_inputs limit to protocol-constants.md. Co-Authored-By: Claude Opus 4.7 (1M context) * docs: document toctree requirement and sync sidebar for shielded pool Adds an "Adding a new doc page" section to CLAUDE.md explaining that new pages must be wired into a Sphinx toctree in docs/index.md and that scripts/sync_sidebar.py needs to run afterwards. Syncs the sidebar so the new explanations/shielded-pool.md and protocol-ref/shielded-pool.md pages appear in the rendered nav. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- CLAUDE.md | 8 + _templates/sidebar-main.html | 10 ++ docs/explanations/shielded-pool.md | 73 ++++++++ docs/index.md | 2 + docs/protocol-ref/protocol-constants.md | 3 +- docs/protocol-ref/shielded-pool.md | 213 ++++++++++++++++++++++++ docs/protocol-ref/state-transition.md | 21 ++- 7 files changed, 323 insertions(+), 7 deletions(-) create mode 100644 docs/explanations/shielded-pool.md create mode 100644 docs/protocol-ref/shielded-pool.md diff --git a/CLAUDE.md b/CLAUDE.md index 8cc4a1371..9760866d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,6 +74,14 @@ When updating documentation values that include GitHub source links: - Update the line anchor (`#L`) to match the correct line **in the branch the link points to** - When available, use the local platform repository checkout to verify line numbers against the correct branch +## Adding a new doc page + +A new page is only visible in the rendered site if it appears in a Sphinx toctree. Sphinx will emit a `document isn't included in any toctree` warning at build time for any orphaned page, and the page won't show up in the sidebar. + +Top-level toctrees live in [docs/index.md](docs/index.md), grouped by section caption (`Tutorials`, `Explanations`, `Reference`, `Platform Protocol Reference`, `Resources`). When you add a new page under `docs/
    /`, add its path (without the `.md` extension) to the matching toctree in `docs/index.md`. After editing the toctree, run `python scripts/sync_sidebar.py` so the custom sidebar template picks up the new entry. + +Tutorials and the TUI section use nested `index.md` files with their own toctrees — check the parent `index.md` of the directory you're adding to. + ## DAPI endpoint reference The DAPI endpoint reference is split between an overview page (`docs/reference/dapi-endpoints.md`) and per-section detail pages (`docs/reference/dapi-endpoints-*.md`). The authoritative list of endpoints lives in the platform proto at `https://github.com/dashpay/platform/tree//packages/dapi-grpc/protos` — check the proto when adding or modifying entries. diff --git a/_templates/sidebar-main.html b/_templates/sidebar-main.html index 56fa79b72..c973bc7b4 100644 --- a/_templates/sidebar-main.html +++ b/_templates/sidebar-main.html @@ -335,6 +335,11 @@ DashPay
  • +
  • + + Shielded Pool + +
  • Fees @@ -484,6 +489,11 @@ Platform Address System
  • +
  • + + Shielded Pool + +
  • Protocol Constants diff --git a/docs/explanations/shielded-pool.md b/docs/explanations/shielded-pool.md new file mode 100644 index 000000000..489ec95dc --- /dev/null +++ b/docs/explanations/shielded-pool.md @@ -0,0 +1,73 @@ +```{eval-rst} +.. _explanations-shielded-pool: +``` + +# Shielded Pool + +## Overview + +The shielded pool is an optional privacy layer on Dash Platform that lets users hold and move credits without revealing balances, sender, or recipient on-chain. Funds move *into* the pool through a shield transition, move *within* the pool privately, and exit through an unshield or shielded withdrawal. While funds remain inside the pool, only their owner can see them. + +The pool uses the [Orchard](https://zips.z.cash/protocol/protocol.pdf) shielded protocol — the same zk-SNARK-based design used by Zcash for its current shielded pool. Transactions inside the pool prove their own validity without disclosing the amounts or parties involved. + +## When to use the shielded pool + +Shielded transitions cost more than transparent ones — they carry a zero-knowledge proof and produce permanent on-chain artifacts (note commitments, nullifiers, and encrypted note ciphertexts). Use the pool when you need confidentiality for a specific payment, transfer, or balance. Use transparent transitions for everyday activity where privacy is not a requirement. + +The pool is well-suited to: + +- Payments where the amount or counterparty should not be public. +- Holding balances privately before unshielding to spend transparently. +- Moving credits between identities or addresses you control without linking them. + +## Core concepts + +### Notes, commitments, and the note tree + +Each unit of value in the pool is held as a **note** — an off-chain record describing an owner, an amount, and a unique randomness value. When a note is created, the platform records only its **commitment** (a hash of the note) into an append-only Merkle tree called the **note commitment tree**. The note itself is never published; only its commitment is, and the commitment reveals nothing about the note's contents. + +The root of the note commitment tree is called an **anchor**. Anchors serve as snapshots that shielded transitions reference to prove "the note I am spending was added to the tree by some earlier transition." Spenders prove membership against an anchor without revealing *which* note they are spending. + +### Nullifiers + +When a note is spent, the spender publishes a unique **nullifier** derived from the note. The platform tracks all nullifiers ever published; spending the same note twice would produce the same nullifier and be rejected as a double-spend. + +Nullifiers are unlinkable to their notes' commitments. An observer can see that *some* note was spent but cannot tell which one. This is how the pool prevents double-spends while preserving privacy. + +### Encrypted notes + +When a note is created for a recipient, the platform stores an **encrypted note payload** alongside the commitment. The recipient scans new encrypted notes, attempts trial decryption with their viewing key, and learns about notes addressed to them. Other observers see only opaque ciphertext. + +### Actions and the action-count limit + +A shielded transition is composed of one or more **actions**. Each action structurally pairs one spend (consuming a prior note) with one output (creating a new note), bundled together so observers cannot tell which spend funded which output. A single shielded transition is limited to **16 actions** to keep transitions within the platform's 20 KB state-transition size budget. + +## Transition types + +Five state transition types interact with the shielded pool. The wire-level structure of each — including field-by-field tables and source links — is documented in the [Shielded Pool protocol reference](../protocol-ref/shielded-pool.md). + +### Shield + +Moves credits *into* the pool from one or more [Platform addresses](../protocol-ref/address-system.md#platform-address) the sender controls. The total contributed across address inputs must cover the value being shielded plus the transition fee. Excess credits remain in the source addresses. + +### Shield from asset lock + +Moves credits *into* the pool directly from a Dash Core (L1) asset-lock transaction. This avoids first funding a Platform address and lets users enter the pool in a single Platform transition tied to an L1 lock proof. + +### Shielded transfer + +Moves credits *within* the pool — between notes — without any transparent surface. To an outside observer, only the actions, anchor, proof, and binding signature are visible; the sender, recipient, and amount remain private. + +### Unshield + +Moves credits *out of* the pool to a [Platform address](../protocol-ref/address-system.md#platform-address) the sender designates. The unshielded amount becomes spendable through normal address-based transitions. + +### Shielded withdrawal + +Moves credits *out of* the pool back to Dash Core (L1) via the platform's withdrawal mechanism. Like an unshield, it reveals an amount and an L1 destination, but the funds leave Platform entirely rather than landing in a Platform address. + +## What the pool does not provide + +- **Anonymity sets**: The privacy guarantee depends on how many other notes exist in the pool. A pool with a single user offers limited cover; privacy improves as more users participate. +- **L1 transaction privacy**: Funds entering or leaving the pool traverse transparent transitions or L1 transactions on either side. Only activity *inside* the pool is shielded. +- **Hiding the act of using the pool**: Observers can see that a transition is a shield, unshield, or transfer — they just cannot see who or how much is involved on the shielded side. diff --git a/docs/index.md b/docs/index.md index e89b33b43..e42d5fdf5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -131,6 +131,7 @@ explanations/dpns explanations/drive explanations/platform-consensus explanations/dashpay +explanations/shielded-pool explanations/fees explanations/tokens explanations/nft @@ -164,6 +165,7 @@ protocol-ref/document protocol-ref/token protocol-ref/data-trigger protocol-ref/address-system +protocol-ref/shielded-pool protocol-ref/protocol-constants protocol-ref/errors ``` diff --git a/docs/protocol-ref/protocol-constants.md b/docs/protocol-ref/protocol-constants.md index c06962160..a5a620808 100644 --- a/docs/protocol-ref/protocol-constants.md +++ b/docs/protocol-ref/protocol-constants.md @@ -21,7 +21,7 @@ Maximum sizes and limits for various platform components. | Max withdrawal amount | 50,000,000,000,000 credits | 500 Dash maximum per withdrawal | [rs-platform-version](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-platform-version/src/version/system_limits/v1.rs#L10) | | Max contract group size | 256 | Maximum members per group | [rs-platform-version](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-platform-version/src/version/system_limits/v1.rs#L11) | | Max token redemption cycles | 128 | Maximum redemption cycles | [rs-platform-version](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-platform-version/src/version/system_limits/v1.rs#L12) | -| Max shielded transition actions | 100 | Maximum shielded transitions per batch | [rs-platform-version](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-platform-version/src/version/system_limits/v1.rs#L13) | +| Max shielded transition actions | 16 | Maximum [actions](shielded-pool.md#actions) per shielded transition | [rs-platform-version](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-platform-version/src/version/system_limits/v1.rs#L27) | | Max CBOR encoded length | 16,384 bytes (16 KiB) | Maximum CBOR encoding size | [rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/util/cbor_serializer.rs#L8) | | Contract deserialization limit | 15,000 | Maximum contract deserialization | [rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/data_contract/serialized_version/mod.rs#L38) | @@ -268,6 +268,7 @@ These limits apply to token perpetual distribution function parameters. | Max fee strategies | 4 | Maximum fee strategy steps | [rs-platform-version](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v3.rs#L45) | | Max address inputs | 16 | Maximum input addresses per address-based transition | [rs-platform-version](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v3.rs#L43) | | Max address outputs | 128 | Maximum output addresses per address-based transition | [rs-platform-version](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v3.rs#L44) | +| Max asset lock transaction inputs | 100 | Maximum L1 transaction inputs in an asset lock proof | [rs-platform-version](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v3.rs#L25) | ## Epoch and Time Constants diff --git a/docs/protocol-ref/shielded-pool.md b/docs/protocol-ref/shielded-pool.md new file mode 100644 index 000000000..6279b9de5 --- /dev/null +++ b/docs/protocol-ref/shielded-pool.md @@ -0,0 +1,213 @@ +```{eval-rst} +.. _protocol-ref-shielded-pool: +``` + +# Shielded Pool + +:::{attention} +Shielded state transitions were [enabled in Protocol Version 12](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs#L4). They use the [Orchard](https://zips.z.cash/protocol/protocol.pdf) shielded protocol to move credits into, within, and out of a pool that hides amounts, senders, and recipients. + +For the conceptual overview of how the pool works and when to use it, see [Shielded Pool](../explanations/shielded-pool.md). +::: + +## Overview + +The shielded pool is implemented through five state transition types that share a common Orchard bundle structure: + +| Type | Name | Description | +| --- | --- | --- | +| 15 | [Shield](#shield) | Move credits from Platform addresses into the shielded pool | +| 16 | [Shielded Transfer](#shielded-transfer) | Move credits within the pool (no transparent surface) | +| 17 | [Unshield](#unshield) | Move credits from the pool to a Platform address | +| 18 | [Shield from Asset Lock](#shield-from-asset-lock) | Move credits from an L1 asset lock directly into the pool | +| 19 | [Shielded Withdrawal](#shielded-withdrawal) | Move credits from the pool back to Dash Core (L1) | + +All five transitions share a common Orchard bundle (anchor, actions, proof, binding signature). Transitions that touch the transparent side (Shield, Unshield, Shield from Asset Lock, Shielded Withdrawal) layer the transparent fields on top of that bundle. Shielded Transfer has no transparent surface beyond the bundle itself. + +## Common Components + +### Orchard Bundle + +Every shielded transition includes an Orchard bundle proving that a set of note spends and outputs is internally consistent. The bundle consists of: + +| Field | Type | Size | Description | +| --- | --- | --- | --- | +| actions | array | Varies | Orchard [actions](#actions) (spend-output pairs). Limited to [`max_shielded_transition_actions`](protocol-constants.md) per transition. | +| anchor | array of bytes | 32 bytes | Sinsemilla root of the note commitment tree at bundle creation time. Must match an [anchor](#anchors) the platform has previously recorded | +| proof | array of bytes | Varies | Halo 2 zero-knowledge proof that the actions are valid | +| bindingSignature | array of bytes | 64 bytes | RedPallas signature binding the bundle's actions to its net value balance | + +See the [Orchard bundle primitives in rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/shielded/mod.rs). + +### Actions + +Each Orchard action structurally contains one spend and one output. The spend consumes a previously created note (revealing its nullifier), while the output creates a new note (publishing its commitment). Although paired in the same struct, observers cannot link which prior note was spent or what value the new note holds — the zero-knowledge proof ensures privacy. + +Each action publishes: + +| Field | Type | Size | Description | +| --- | --- | --- | --- | +| nullifier | array of bytes | 32 bytes | Unique tag derived from the spent note. Used to prevent double-spending | +| rk | array of bytes | 32 bytes | Randomized verification key for the action's spend authorization signature | +| cmx | array of bytes | 32 bytes | Extracted note commitment for the new note | +| encryptedNote | array of bytes | 216 bytes | Encrypted note payload — 32-byte ephemeral public key + 104-byte note ciphertext + 80-byte out-of-band ciphertext | +| cvNet | array of bytes | 32 bytes | Net value commitment (Pedersen commitment to the action's value contribution) | +| spendAuthSig | array of bytes | 64 bytes | Per-action spend authorization signature — see [Shielded Transition Signing](#shielded-transition-signing) | + +Permanent storage cost per action is [312 bytes](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/shielded/mod.rs#L13-L16) (280 bytes in the note commitment tree + 32 bytes in the nullifier tree). + +See the [serialized action implementation in rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/shielded/mod.rs). + +### Anchors + +An **anchor** is the Sinsemilla root of the note commitment tree at the time the bundle was constructed. Each shielded transition specifies the anchor it was built against; the platform validates that the anchor was previously published. Clients fetch anchors using [`getShieldedAnchors`](../reference/dapi-endpoints-platform-endpoints.md#getshieldedanchors) or [`getMostRecentShieldedAnchor`](../reference/dapi-endpoints-platform-endpoints.md#getmostrecentshieldedanchor). + +### Platform Sighash + +Transitions with transparent fields (Unshield, Shielded Withdrawal, etc.) bind those fields to the Orchard signatures via a platform sighash computed as: + +``` +SHA-256(SIGHASH_DOMAIN || bundle_commitment || extra_data) +``` + +This prevents replay attacks where an attacker substitutes transparent fields while reusing a valid Orchard bundle. See the [platform sighash implementation in rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/shielded/mod.rs#L20-L40). + +## Shielded State Transition Details + +### Shield + +Move credits from one or more [Platform addresses](address-system.md#platform-address) into the shielded pool. The total contributed across address inputs must cover the value being shielded plus the transition fee; excess credits remain in the source addresses. + +| Field | Type | Size | Description | +| --- | --- | --- | --- | +| inputs | map | Varies | Map of source [Platform addresses](address-system.md#platform-address) to (`AddressNonce`, max contribution in credits) pairs | +| actions | array | Varies | Orchard [actions](#actions) (output-only — Shield creates new notes without consuming prior ones) | +| amount | unsigned integer | 64 bits | Credits entering the shielded pool | +| anchor | array of bytes | 32 bytes | [Anchor](#anchors) | +| proof | array of bytes | Varies | Halo 2 proof | +| bindingSignature | array of bytes | 64 bytes | RedPallas binding signature | +| feeStrategy | array | Varies | [Fee deduction strategy](address-system.md#fee-strategy) for address inputs | +| userFeeIncrease | unsigned integer | 16 bits | Extra fee to prioritize processing if the mempool is full | +| inputWitnesses | array | Varies | [Address witnesses](address-system.md#address-witness) for each input | + +:::{note} +Maximum actions per transition: [`max_shielded_transition_actions`](protocol-constants.md). Address witness signatures are excluded from the signable bytes used by the platform sighash. +::: + +See the [implementation in rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_transition/). + +### Shielded Transfer + +Move credits within the pool between notes. There is no transparent surface — to an outside observer, only the Orchard bundle is visible. + +| Field | Type | Size | Description | +| --- | --- | --- | --- | +| actions | array | Varies | Orchard [actions](#actions) | +| valueBalance | unsigned integer | 64 bits | Net value balance — the fee amount extracted from the shielded pool for this transition | +| anchor | array of bytes | 32 bytes | [Anchor](#anchors) | +| proof | array of bytes | Varies | Halo 2 proof | +| bindingSignature | array of bytes | 64 bytes | RedPallas binding signature | + +:::{note} +Maximum actions per transition: [`max_shielded_transition_actions`](protocol-constants.md). +::: + +See the [implementation in rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_transfer_transition/). + +### Unshield + +Move credits from the pool to a [Platform address](address-system.md#platform-address) the sender designates. The unshielded amount becomes spendable through normal address-based transitions. + +| Field | Type | Size | Description | +| --- | --- | --- | --- | +| outputAddress | object | Varies | Destination [Platform address](address-system.md#platform-address) | +| actions | array | Varies | Orchard [actions](#actions) (spends consume shielded notes) | +| unshieldingAmount | unsigned integer | 64 bits | Total credits leaving the pool (recipient amount + fee) | +| anchor | array of bytes | 32 bytes | [Anchor](#anchors) | +| proof | array of bytes | Varies | Halo 2 proof | +| bindingSignature | array of bytes | 64 bytes | RedPallas binding signature | + +:::{note} +The `outputAddress` is bound to the Orchard bundle through the [platform sighash](#platform-sighash) to prevent substitution attacks. Maximum actions per transition: [`max_shielded_transition_actions`](protocol-constants.md). +::: + +See the [implementation in rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/state_transition/state_transitions/shielded/unshield_transition/). + +### Shield from Asset Lock + +Move credits from a Dash Core (L1) asset-lock transaction directly into the shielded pool, without first funding a Platform address. + +| Field | Type | Size | Description | +| --- | --- | --- | --- | +| assetLockProof | object | Varies | [Asset lock proof](identity.md#asset-lock) (InstantSend or ChainLock) authorizing the funds | +| actions | array | Varies | Orchard [actions](#actions) | +| valueBalance | unsigned integer | 64 bits | Credits entering the shielded pool from the asset lock | +| anchor | array of bytes | 32 bytes | [Anchor](#anchors) | +| proof | array of bytes | Varies | Halo 2 proof | +| bindingSignature | array of bytes | 64 bytes | RedPallas binding signature | +| signature | array of bytes | 65 bytes | ECDSA signature over the signable bytes proving control of the asset-locked output | + +:::{note} +`valueBalance` must be greater than zero and at most `i64::MAX`. The ECDSA signature is excluded from the signable bytes used by the platform sighash. Maximum actions per transition: [`max_shielded_transition_actions`](protocol-constants.md). +::: + +See the [implementation in rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/). + +### Shielded Withdrawal + +Move credits from the pool back to Dash Core (L1). The funds leave Platform entirely rather than landing in a Platform address. + +| Field | Type | Size | Description | +| --- | --- | --- | --- | +| actions | array | Varies | Orchard [actions](#actions) (spends + change outputs) | +| unshieldingAmount | unsigned integer | 64 bits | Total credits leaving the pool (recipient amount + fee) | +| anchor | array of bytes | 32 bytes | [Anchor](#anchors) | +| proof | array of bytes | Varies | Halo 2 proof | +| bindingSignature | array of bytes | 64 bytes | RedPallas binding signature | +| coreFeePerByte | unsigned integer | 32 bits | Core transaction fee rate for the L1 withdrawal transaction | +| pooling | unsigned integer | 8 bits | Withdrawal pooling strategy (see [Identity Credit Withdrawal](identity.md#identity-credit-withdrawal)) | +| outputScript | array of bytes | Varies | Core script of the L1 address receiving the withdrawn funds | + +:::{note} +Transparent fields (`coreFeePerByte`, `pooling`, `outputScript`) are bound to the Orchard bundle through the [platform sighash](#platform-sighash). Maximum actions per transition: [`max_shielded_transition_actions`](protocol-constants.md). +::: + +See the [implementation in rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_withdrawal_transition/). + +## Shielded Transition Signing + +Shielded transitions are not signed by an identity public key. The 65-byte `signature` and the `signaturePublicKeyId` fields listed in the [common fields](state-transition.md#common-fields) for identity-signed transitions do not appear on Unshield, Shielded Transfer, or Shielded Withdrawal. Authorization is instead carried by cryptographic primitives attached to the Orchard bundle and, where applicable, to the transparent side of the transition. + +### Orchard bundle signatures + +Every shielded transition includes: + +- **Per-action spend authorization signatures** (`spendAuthSig` on each [action](#actions)). Each is a 64-byte RedPallas signature, produced by the holder of the spent note over the randomized verification key `rk`. The proof binds `rk` to the original spending key, so verifying the signature against `rk` proves the spender is authorized. +- **Binding signature** (`bindingSignature` on the transition). A 64-byte RedPallas signature over the sum of the action value commitments, proving that the actions' net value balance matches the transition's declared value balance. + +### Platform sighash + +Transitions that include transparent fields (Shield, Unshield, Shield from Asset Lock, Shielded Withdrawal) bind those fields to the Orchard bundle through the [platform sighash](#platform-sighash). Any modification to the transparent fields invalidates the Orchard signatures, preventing replay attacks that substitute transparent fields while reusing a valid bundle. + +### Transparent signatures (Shield, Shield from Asset Lock) + +Two shielded transitions also carry transparent signatures over the transparent side of the transition: + +- **Shield** includes an array of [address witnesses](address-system.md#address-witness) (`inputWitnesses`) — one per address input. Each witness proves control of its corresponding Platform address. Address witness signatures are excluded from the bytes that feed the platform sighash (they sign the platform sighash output, not vice-versa). +- **Shield from Asset Lock** includes a 65-byte ECDSA `signature` proving control of the L1 asset-locked output, in the same form used by [Identity Create](identity.md#identity-create). The signature is excluded from the bytes that feed the platform sighash. + +Shielded Transfer, Unshield, and Shielded Withdrawal have no transparent signatures; the Orchard bundle signatures plus the platform sighash provide full authorization. + +## Querying shielded state + +DAPI exposes a set of read-only endpoints for clients that need to fetch anchors, scan encrypted notes, verify nullifier status, or sync incremental nullifier updates. See the [DAPI Platform endpoints reference](../reference/dapi-endpoints-platform-endpoints.md) for request and response shapes: + +- [`getShieldedPoolState`](../reference/dapi-endpoints-platform-endpoints.md#getshieldedpoolstate) +- [`getShieldedAnchors`](../reference/dapi-endpoints-platform-endpoints.md#getshieldedanchors) +- [`getMostRecentShieldedAnchor`](../reference/dapi-endpoints-platform-endpoints.md#getmostrecentshieldedanchor) +- [`getShieldedEncryptedNotes`](../reference/dapi-endpoints-platform-endpoints.md#getshieldedencryptednotes) +- [`getShieldedNullifiers`](../reference/dapi-endpoints-platform-endpoints.md#getshieldednullifiers) +- [`getNullifiersTrunkState`](../reference/dapi-endpoints-platform-endpoints.md#getnullifierstrunkstate) +- [`getNullifiersBranchState`](../reference/dapi-endpoints-platform-endpoints.md#getnullifiersbranchstate) +- [`getRecentNullifierChanges`](../reference/dapi-endpoints-platform-endpoints.md#getrecentnullifierchanges) +- [`getRecentCompactedNullifierChanges`](../reference/dapi-endpoints-platform-endpoints.md#getrecentcompactednullifierchanges) diff --git a/docs/protocol-ref/state-transition.md b/docs/protocol-ref/state-transition.md index 9f310528d..e55d1c76e 100644 --- a/docs/protocol-ref/state-transition.md +++ b/docs/protocol-ref/state-transition.md @@ -26,7 +26,7 @@ The list of common fields used by multiple state transitions is defined in [rs-d | Field | Type | Size | Description | | --------------- | -------------- | ---- | ----------- | | $version | unsigned integer | 16 bits | The state transition format version (FeatureVersion). Currently `0` for most transitions, `1` for Batch. This is not the global platform protocol version, which is negotiated separately. | -| type | unsigned integer | 8 bits | State transition type (defined in [rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/state_transition/state_transition_types.rs#L21)):
    `0` - [data contract create](../protocol-ref/data-contract.md#data-contract-create)
    `1` - [batch](#batch)
    `2` - [identity create](../protocol-ref/identity.md#identity-create)
    `3` - [identity topup](identity.md#identity-topup)
    `4` - [data contract update](data-contract.md#data-contract-update)
    `5` - [identity update](identity.md#identity-update)
    `6` - [identity credit withdrawal](identity.md#identity-credit-withdrawal)
    `7` - [identity credit transfer](identity.md#identity-credit-transfer)
    `8` - [masternode vote](#masternode-vote)
    `9` - [identity credit transfer to addresses](address-system.md#identity-credit-transfer-to-addresses)
    `10` - [identity create from addresses](address-system.md#identity-create-from-addresses)
    `11` - [identity topup from addresses](address-system.md#identity-topup-from-addresses)
    `12` - [address funds transfer](address-system.md#address-funds-transfer)
    `13` - [address funding from asset lock](address-system.md#address-funding-from-asset-lock)
    `14` - [address credit withdrawal](address-system.md#address-credit-withdrawal)
    `15` - shield
    `16` - shielded transfer
    `17` - unshield
    `18` - shield from asset lock
    `19` - shielded withdrawal | +| type | unsigned integer | 8 bits | State transition type (defined in [rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/state_transition/state_transition_types.rs#L21)):
    `0` - [data contract create](../protocol-ref/data-contract.md#data-contract-create)
    `1` - [batch](#batch)
    `2` - [identity create](../protocol-ref/identity.md#identity-create)
    `3` - [identity topup](identity.md#identity-topup)
    `4` - [data contract update](data-contract.md#data-contract-update)
    `5` - [identity update](identity.md#identity-update)
    `6` - [identity credit withdrawal](identity.md#identity-credit-withdrawal)
    `7` - [identity credit transfer](identity.md#identity-credit-transfer)
    `8` - [masternode vote](#masternode-vote)
    `9` - [identity credit transfer to addresses](address-system.md#identity-credit-transfer-to-addresses)
    `10` - [identity create from addresses](address-system.md#identity-create-from-addresses)
    `11` - [identity topup from addresses](address-system.md#identity-topup-from-addresses)
    `12` - [address funds transfer](address-system.md#address-funds-transfer)
    `13` - [address funding from asset lock](address-system.md#address-funding-from-asset-lock)
    `14` - [address credit withdrawal](address-system.md#address-credit-withdrawal)
    `15` - [shield](shielded-pool.md#shield)
    `16` - [shielded transfer](shielded-pool.md#shielded-transfer)
    `17` - [unshield](shielded-pool.md#unshield)
    `18` - [shield from asset lock](shielded-pool.md#shield-from-asset-lock)
    `19` - [shielded withdrawal](shielded-pool.md#shielded-withdrawal) | | userFeeIncrease | unsigned integer | 16 bits | Extra fee to prioritize processing if the mempool is full. Typically set to zero. | | signature | array of bytes | 65 bytes |Signature of state transition data | @@ -142,14 +142,13 @@ transition type: | Signing Method | State Transitions | | -------------- | ----------------- | | [Identity](#signing-with-identity) | Batch, Contract create, Contract update, Identity update, Identity credit transfer, Identity credit transfer to addresses, Identity credit withdrawal, Masternode vote | -| [Asset lock](#signing-with-asset-lock) | Identity create, Identity topup, Address funding from asset lock* | -| [Address witness](#signing-with-address-witness) | Identity create from addresses, Identity topup from addresses, Address funds transfer, Address credit withdrawal, Address funding from asset lock* | +| [Asset lock](#signing-with-asset-lock) | Identity create, Identity topup, Address funding from asset lock\*, Shield from asset lock\*\* | +| [Address witness](#signing-with-address-witness) | Identity create from addresses, Identity topup from addresses, Address funds transfer, Address credit withdrawal, Address funding from asset lock\*, Shield\*\* | +| [Shielded (Orchard)](shielded-pool.md#shielded-transition-signing) | Shield\*\*, Shielded transfer, Unshield, Shield from asset lock\*\*, Shielded withdrawal | \* Address funding from asset lock requires both an asset lock signature and address witnesses (`input_witnesses`). -:::{note} -Shield-related state transitions (types 15-19: Shield, ShieldedTransfer, Unshield, ShieldFromAssetLock, ShieldedWithdrawal) are defined in the protocol but their signing methods are not yet documented here. -::: +\*\* Shielded transitions are always authorized by Orchard bundle signatures (per-action `spendAuthSig` plus the transition-level `bindingSignature`). Shield additionally carries address witnesses for its transparent address inputs; Shield from asset lock additionally carries an asset-lock ECDSA signature. :::{note} Address-based state transitions (types 9-14) were introduced in Protocol Version 11. For detailed information on these transitions, see [Address-Based State Transitions](address-system.md). @@ -218,6 +217,12 @@ Public keys can be added to an identity by the identity create or identity updat - Use the private key that derived the public key to sign the hash. - Store the result in the public key's `signature` field. +### Signing Shielded Transitions + +Shielded transitions are not signed by an identity public key or an address private key at the transition level — they do not include `signature` or `signaturePublicKeyId` fields. Authorization is carried instead by Orchard primitives attached to each action and to the bundle as a whole. Shield additionally carries [address witnesses](#signing-with-address-witness) over its address inputs, and Shield from asset lock additionally carries an [asset-lock ECDSA signature](#signing-with-asset-lock). Both `input_witnesses` (on Shield) and `signature` (on Shield from asset lock) are omitted from the bytes that feed the platform sighash. + +See [Shielded Transition Signing](shielded-pool.md#shielded-transition-signing) for the full signing model. + ### Non-signable Fields This table shows the fields that must be excluded when creating state transition signatures. All transitions exclude the signature field. Some transitions contain other fields that must be excluded also. Click the state transition name to see the rs-dpp implementation for additional context. @@ -233,3 +238,7 @@ This table shows the fields that must be excluded when creating state transition | [Identity credit transfer](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/v0/mod.rs#L49-L52) | Exclude | Exclude | N/A | N/A | | [Identity credit withdrawal](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v1/mod.rs#L44-L47) | Exclude | Exclude | N/A | N/A | | [Masternode vote](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/state_transition/state_transitions/identity/masternode_vote_transition/v0/mod.rs#L49-L52) | Exclude | Exclude | N/A | N/A | + +:::{note} +The table above does not cover shielded transitions, which do not carry transition-level `signature` or `signaturePublicKeyId` fields. See [Signing Shielded Transitions](#signing-shielded-transitions). +::: From d4807267f8cb9c905cc3a8b4b65e7b51113af08a Mon Sep 17 00:00:00 2001 From: thephez Date: Thu, 28 May 2026 10:34:33 -0400 Subject: [PATCH 4/6] docs: v3.1 release cleanup (state-transition dedup, deprecated DAPI removal, build-warning fixes) (#151) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(protocol-ref): dedup state transition page and drop deprecated DAPI entries Replace the cramped one-cell type discriminator with a proper catalog table, remove the 7 type-0–8 detail sections that duplicate the canonical identity and data-contract pages, and drop the long-deprecated getIdentities and getIdentitiesByPublicKeyHashes endpoints (removed from the platform proto in v1.0.0). Co-Authored-By: Claude Opus 4.7 (1M context) * docs: minor formatting update * docs: fix broken cross-references to clean up build warnings Co-Authored-By: Claude Opus 4.7 (1M context) * docs: silence Pygments warnings on placeholder JSON blocks Convert affected fences to code-block directives with :force: so JSON highlighting is preserved without lexer errors on schema-style placeholders and JS-literal query examples. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- docs/protocol-ref/data-contract-document.md | 12 ++- docs/protocol-ref/state-transition.md | 97 +++++-------------- .../dapi-endpoints-platform-endpoints.md | 33 ------- docs/reference/dapi-endpoints.md | 4 +- docs/reference/data-contracts.md | 22 +++-- docs/reference/query-syntax.md | 45 ++++++--- docs/tutorials/example-apps/dashmint-lab.md | 2 +- 7 files changed, 79 insertions(+), 136 deletions(-) diff --git a/docs/protocol-ref/data-contract-document.md b/docs/protocol-ref/data-contract-document.md index 22d85fbcc..6aca6f486 100644 --- a/docs/protocol-ref/data-contract-document.md +++ b/docs/protocol-ref/data-contract-document.md @@ -144,7 +144,9 @@ The `indices` array consists of one or more objects that each contain: * An optional `nullSearchable` element that indicates whether the index allows searching for NULL values. If nullSearchable is false (default: true) and all properties of the index are null then no reference is added. * An optional `contested` element that determines if duplicate values are allowed for the document -```json +:::{code-block} json +:force: + "indices": [ { "name": "", @@ -171,7 +173,7 @@ The `indices` array consists of one or more objects that each contain: ], } ] -``` +::: ### Contested Indices @@ -380,7 +382,9 @@ schema](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/schema This example syntax shows the structure of a documents object that defines two documents, an index, and a required field. -```json +:::{code-block} json +:force: + { "": { "type": "object", @@ -425,7 +429,7 @@ This example syntax shows the structure of a documents object that defines two d "additionalProperties": false }, } -``` +::: ## Document Schema diff --git a/docs/protocol-ref/state-transition.md b/docs/protocol-ref/state-transition.md index e55d1c76e..e52209e7e 100644 --- a/docs/protocol-ref/state-transition.md +++ b/docs/protocol-ref/state-transition.md @@ -26,7 +26,7 @@ The list of common fields used by multiple state transitions is defined in [rs-d | Field | Type | Size | Description | | --------------- | -------------- | ---- | ----------- | | $version | unsigned integer | 16 bits | The state transition format version (FeatureVersion). Currently `0` for most transitions, `1` for Batch. This is not the global platform protocol version, which is negotiated separately. | -| type | unsigned integer | 8 bits | State transition type (defined in [rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/state_transition/state_transition_types.rs#L21)):
    `0` - [data contract create](../protocol-ref/data-contract.md#data-contract-create)
    `1` - [batch](#batch)
    `2` - [identity create](../protocol-ref/identity.md#identity-create)
    `3` - [identity topup](identity.md#identity-topup)
    `4` - [data contract update](data-contract.md#data-contract-update)
    `5` - [identity update](identity.md#identity-update)
    `6` - [identity credit withdrawal](identity.md#identity-credit-withdrawal)
    `7` - [identity credit transfer](identity.md#identity-credit-transfer)
    `8` - [masternode vote](#masternode-vote)
    `9` - [identity credit transfer to addresses](address-system.md#identity-credit-transfer-to-addresses)
    `10` - [identity create from addresses](address-system.md#identity-create-from-addresses)
    `11` - [identity topup from addresses](address-system.md#identity-topup-from-addresses)
    `12` - [address funds transfer](address-system.md#address-funds-transfer)
    `13` - [address funding from asset lock](address-system.md#address-funding-from-asset-lock)
    `14` - [address credit withdrawal](address-system.md#address-credit-withdrawal)
    `15` - [shield](shielded-pool.md#shield)
    `16` - [shielded transfer](shielded-pool.md#shielded-transfer)
    `17` - [unshield](shielded-pool.md#unshield)
    `18` - [shield from asset lock](shielded-pool.md#shield-from-asset-lock)
    `19` - [shielded withdrawal](shielded-pool.md#shielded-withdrawal) | +| type | unsigned integer | 8 bits | State transition type discriminator (defined in [rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/state_transition/state_transition_types.rs#L21)). See [State Transition Types](#state-transition-types) for the full list. | | userFeeIncrease | unsigned integer | 16 bits | Extra fee to prioritize processing if the mempool is full. Typically set to zero. | | signature | array of bytes | 65 bytes |Signature of state transition data | @@ -42,85 +42,40 @@ Additionally, all state transitions except the identity create and topup state t ## State Transition Types -Dash Platform Protocol defines the [state transition types](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/state_transition/state_transition_types.rs#L21-L43) that perform identity, contract, document, and token operations. See the subsections below for details on each state transition type. +Dash Platform Protocol defines the following [state transition types](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/state_transition/state_transition_types.rs#L21-L43). Most are documented in detail on the protocol reference page for the feature they operate on. Batch and Masternode Vote do not have a dedicated feature page; their formats are documented inline below. + +| Type | Name | Documented in | +| --- | --- | --- | +| 0 | Data Contract Create | [Data Contract Create](data-contract.md#data-contract-create) | +| 1 | Batch | [Batch](#batch) (below) | +| 2 | Identity Create | [Identity Create](identity.md#identity-create) | +| 3 | Identity TopUp | [Identity TopUp](identity.md#identity-topup) | +| 4 | Data Contract Update | [Data Contract Update](data-contract.md#data-contract-update) | +| 5 | Identity Update | [Identity Update](identity.md#identity-update) | +| 6 | Identity Credit Withdrawal | [Identity Credit Withdrawal](identity.md#identity-credit-withdrawal) | +| 7 | Identity Credit Transfer | [Identity Credit Transfer](identity.md#identity-credit-transfer) | +| 8 | Masternode Vote | [Masternode Vote](#masternode-vote) (below) | +| 9 | Identity Credit Transfer to Addresses | [Identity Credit Transfer to Addresses](address-system.md#identity-credit-transfer-to-addresses) | +| 10 | Identity Create from Addresses | [Identity Create from Addresses](address-system.md#identity-create-from-addresses) | +| 11 | Identity TopUp from Addresses | [Identity TopUp from Addresses](address-system.md#identity-top-up-from-addresses) | +| 12 | Address Funds Transfer | [Address Funds Transfer](address-system.md#address-funds-transfer) | +| 13 | Address Funding from Asset Lock | [Address Funding from Asset Lock](address-system.md#address-funding-from-asset-lock) | +| 14 | Address Credit Withdrawal | [Address Credit Withdrawal](address-system.md#address-credit-withdrawal) | +| 15 | Shield | [Shield](shielded-pool.md#shield) | +| 16 | Shielded Transfer | [Shielded Transfer](shielded-pool.md#shielded-transfer) | +| 17 | Unshield | [Unshield](shielded-pool.md#unshield) | +| 18 | Shield from Asset Lock | [Shield from Asset Lock](shielded-pool.md#shield-from-asset-lock) | +| 19 | Shielded Withdrawal | [Shielded Withdrawal](shielded-pool.md#shielded-withdrawal) | ### Batch | Field | Type | Size | Description | | ----------- | -------------- | ---- | ----------- | -| ownerId | array of bytes | 32 bytes | [Identity](../protocol-ref/identity.md) submitting the document(s) | +| ownerId | array of bytes | 32 bytes | [Identity](../protocol-ref/identity.md) submitting the document(s) or token action(s) | | transitions | array of transition objects | Varies | A batch of [document](../protocol-ref/document.md#document-overview) or token actions (currently limited to [1 object per batch](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-platform-version/src/version/system_limits/v1.rs#L7)) | More detailed information about the `transitions` array can be found in the [document section](../protocol-ref/document.md). See the implementation in [rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/mod.rs#L31-L39). -### Data Contract Create - -| Field | Type | Size | Description | -| --------------- | -------------- | ---- | ----------- | -| dataContract | [data contract object](../protocol-ref/data-contract.md#data-contract-object) | Varies | Object containing valid [data contract](../protocol-ref/data-contract.md) details | -| identityNonce | unsigned integer | 64 bits | Identity nonce for this transition to prevent replay attacks | - -More detailed information about the `dataContract` object can be found in the [data contract section](../protocol-ref/data-contract.md). - -### Data Contract Update - -| Field | Type | Size | Description | -| --------------- | -------------- | ---- | ----------- | -| dataContract | [data contract object](../protocol-ref/data-contract.md#data-contract-object) | Varies | Object containing valid [data contract](../protocol-ref/data-contract.md) details | -| identityContractNonce | unsigned integer | 64 bits | Identity contract nonce for replay protection | - -More detailed information about the `dataContract` object can be found in the [data contract section](../protocol-ref/data-contract.md). - -### Identity Create - -| Field | Type | Size | Description | -| --------------- | -------------- | ---- | ----------- | -| assetLockProof | array of bytes | 36 bytes | Lock [outpoint](https://docs.dash.org/en/stable/docs/core/resources/glossary.html#outpoint) from the layer 1 locking transaction (36 bytes) | -| publicKeys | array of keys | Varies | [Public key(s)](../protocol-ref/identity.md#identity-publickeys) associated with the identity (maximum number of keys: `6`) | - -More detailed information about the `publicKeys` object can be found in the [identity section](../protocol-ref/identity.md). - -### Identity TopUp - -| Field | Type | Size | Description | -| --------------- | -------------- | ---- | ----------- | -| assetLockProof | array of bytes | 36 bytes | Lock [outpoint](https://docs.dash.org/en/stable/docs/core/resources/glossary.html#outpoint) from the layer 1 locking transaction (36 bytes) | -| identityId | array of bytes | 32 bytes | An [Identity ID](../protocol-ref/identity.md#identity-id) for the identity receiving the topup (can be any identity) (32 bytes) | - -### Identity Update - -| Field | Type | Size | Description | -| --------------- | -------------- | ---- | ----------- | -| identityId | array of bytes | 32 bytes | The [Identity ID](../protocol-ref/identity.md#identity-id) for the identity being updated | -| revision | unsigned integer | 64 bits | Identity update revision. Used for optimistic concurrency control. Incremented by one with each new update so that the update will fail if the underlying data is modified between reading and writing. | -| nonce | unsigned integer | 64 bits | Identity nonce for this transition to prevent replay attacks | -| addPublicKeys | array of public keys | Varies | (Optional) Array of up to 6 new public keys to add to the identity. Required if adding keys. | -| disablePublicKeys | array of integers | Varies | (Optional) Array of up to 10 existing identity public key ID(s) to disable for the identity. Required if disabling keys. | - -### Identity Credit Transfer - -| Field | Type | Size | Description | -| --------------- | -------------- | ---- | ----------- | -| identityId | array of bytes | 32 bytes | An [Identity ID](../protocol-ref/identity.md#identity-id) for the identity sending the credits | -| recipientId | array of bytes | 32 bytes | An [Identity ID](../protocol-ref/identity.md#identity-id) for the identity receiving the credits | -| amount | unsigned integer | 64 bits | Number of credits being transferred | -| nonce | unsigned integer | 64 bits | Identity nonce for this transition to prevent replay attacks | - -See the implementation in [rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/v0/mod.rs#L42-L53). - -### Identity Credit Withdrawal - -| Field | Type | Size | Description | -| --------------- | -------------- | ---- | ----------- | -| identityId | array of bytes | 32 bytes | An [Identity ID](../protocol-ref/identity.md#identity-id) for the identity sending the credits | -| amount | unsigned integer | 64 bits | Number of credits being transferred | -| coreFeePerByte | unsigned integer | 32 bits | | -| pooling | unsigned integer | 8 bits | 0 = Never, 1 = If Available, 2 = Standard | -| outputScript | script | Varies | If None, the withdrawal is sent to the address set by Core | -| nonce | unsigned integer | 64 bits | Identity nonce for this transition to prevent replay attacks | - -See the implementation in [rs-dpp](https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v1/mod.rs#L35-L48). - ### Masternode Vote | Field | Type | Size | Description | diff --git a/docs/reference/dapi-endpoints-platform-endpoints.md b/docs/reference/dapi-endpoints-platform-endpoints.md index 9ec4003da..d0a985791 100644 --- a/docs/reference/dapi-endpoints-platform-endpoints.md +++ b/docs/reference/dapi-endpoints-platform-endpoints.md @@ -4627,39 +4627,6 @@ Returns compacted nullifier additions from a specified block height. Compacted c | `start_block_height` | String (uint64) | Yes | Block height to start from (as a string due to uint64 size) | | `prove` | Boolean | No | Set to `true` to receive a proof that contains the requested changes | -## Deprecated Endpoints - -The following endpoints were recently deprecated. See the [previous version of documentation](https://docs.dash.org/projects/platform/en/2.0.0/docs/reference/dapi-endpoints-platform-endpoints.html) for additional information on these endpoints. - -### getIdentities - -:::{attention} -Deprecated in Dash Platform v1.0.0 -::: - -**Returns**: [Identity](../explanations/identity.md) information for the requested identities - -**Parameters**: - -| Name | Type | Required | Description | -| ------- | ------- | -------- | ------------ | -| `ids` | Array | Yes | An array of identity IDs -| `prove` | Boolean | No | Set to `true` to receive a proof that contains the requested identity - -### getIdentitiesByPublicKeyHashes - -:::{attention} -Deprecated in Dash Platform v1.0.0 -::: - -**Returns**: An array of [identities](../explanations/identity.md) associated with the provided public key hashes -**Parameters**: - -| Name | Type | Required | Description | -| ------------------- | ------- | -------- | ----------------------------------------------------------------------- | -| `public_key_hashes` | Bytes | Yes | Public key hashes (sha256-ripemd160) of identity public keys | -| `prove` | Boolean | No | Set to `true` to receive a proof that contains the requested identities | - ## Code Reference Implementation details related to the information on this page can be found in: diff --git a/docs/reference/dapi-endpoints.md b/docs/reference/dapi-endpoints.md index 9ea2f26d2..5e1e87c41 100644 --- a/docs/reference/dapi-endpoints.md +++ b/docs/reference/dapi-endpoints.md @@ -43,7 +43,7 @@ without introducing issues for endpoint consumers. | [`getIdentity`](../reference/dapi-endpoints-platform-endpoints.md#getidentity) | Returns the requested identity | | [`getIdentityBalance`](../reference/dapi-endpoints-platform-endpoints.md#getidentitybalance) | Returns the requested identity's balance | | [`getIdentityBalanceAndRevision`](../reference/dapi-endpoints-platform-endpoints.md#getidentitybalanceandrevision) | Returns the requested identity's balance and revision | -| [`getIdentityByNonUniquePublicKeyHash`](../reference/dapi-endpoints-platform-endpoints.md#getidentitybynonuniquepublickeyhash) | **Added in Dash Platform v2.0.0**
    Returns one or more identities associated with a public key hash, including for non-unique masternode keys. | +| [`getIdentityByNonUniquePublicKeyHash`](../reference/dapi-endpoints-platform-endpoints.md#getidentitybynonuniquepublickeyhash) | *Added in Dash Platform v2.0.0*
    Returns one or more identities associated with a public key hash, including for non-unique masternode keys. | | [`getIdentityByPublicKeyHash`](../reference/dapi-endpoints-platform-endpoints.md#getidentitybypublickeyhash) | Returns the identity associated with the provided public key hash | | [`getIdentityContractNonce`](../reference/dapi-endpoints-platform-endpoints.md#getidentitycontractnonce) | Returns the identity contract nonce | | [`getIdentityKeys`](../reference/dapi-endpoints-platform-endpoints.md#getidentitykeys) | Returns the requested identity keys | @@ -80,7 +80,7 @@ Security groups provide a way to distribute token configuration and update autho | [`getEvonodesProposedEpochBlocksByIds`](../reference/dapi-endpoints-platform-endpoints.md#getevonodesproposedepochblocksbyids) | *Added in Dash Platform v1.3.0*
    Retrieves the number of blocks proposed by the specified evonodes in a certain epoch, based on their IDs | | [`getEvonodesProposedEpochBlocksByRange`](../reference/dapi-endpoints-platform-endpoints.md#getevonodesproposedepochblocksbyrange) | *Added in Dash Platform v1.3.0*
    Retrieves the number of blocks proposed by evonodes for a specified epoch | | [`getEpochsInfo`](../reference/dapi-endpoints-platform-endpoints.md#getepochsinfo) | Returns information about the requested epoch(s) | -| [`getFinalizedEpochInfos`](../reference/dapi-endpoints-platform-endpoints.md#getfinalizedepochinfos) | **Added in Dash Platform v2.0.0**
    Retrieves finalized epoch information within a specified index range | +| [`getFinalizedEpochInfos`](../reference/dapi-endpoints-platform-endpoints.md#getfinalizedepochinfos) | *Added in Dash Platform v2.0.0*
    Retrieves finalized epoch information within a specified index range | | [`getPathElements`](../reference/dapi-endpoints-platform-endpoints.md#getpathelements) | *Added in Dash Platform v1.0.0*
    Returns elements for a specified path in the Platform | | [`getPrefundedSpecializedBalance`](../reference/dapi-endpoints-platform-endpoints.md#getprefundedspecializedbalance) | *Added in Dash Platform v1.0.0*
    Returns the pre-funded specialized balance for a specific identity | | [`getProtocolVersionUpgradeState`](../reference/dapi-endpoints-platform-endpoints.md#getprotocolversionupgradestate) | Returns the number of votes cast for each protocol version | diff --git a/docs/reference/data-contracts.md b/docs/reference/data-contracts.md index a96c6af26..5f38d1c62 100644 --- a/docs/reference/data-contracts.md +++ b/docs/reference/data-contracts.md @@ -249,14 +249,16 @@ The `indices` array consists of one or more objects that each contain: * An optional `nullSearchable` element that indicates whether the index allows searching for NULL values. If nullSearchable is false (default: true) and all properties of the index are null then no reference is added. * An optional `contested` element that determines if duplicate values are allowed for the document -```json -"indices": [ +:::{code-block} json +:force: + +"indices": [ { "name": "", "properties": [ { "": "" }, { "": "" } - ], + ], "unique": true|false, "nullSearchable": true|false, "contested": { @@ -273,10 +275,10 @@ The `indices` array consists of one or more objects that each contain: "name": "", "properties": [ { "": "" }, - ], - } + ], + } ] -``` +::: #### Contested indices @@ -343,10 +345,12 @@ The following example (excerpt from the DPNS contract's `preorder` document) cre This example syntax shows the structure of a document object including all optional properties. -:::{dropdown} Document schema +::::{dropdown} Document schema :open: -```json +:::{code-block} json +:force: + { "": { "documentsKeepHistory": true|false, @@ -411,8 +415,8 @@ This example syntax shows the structure of a document object including all optio "additionalProperties": false }, } -``` ::: +:::: ## General Constraints diff --git a/docs/reference/query-syntax.md b/docs/reference/query-syntax.md index 4bef41722..c2859d12b 100644 --- a/docs/reference/query-syntax.md +++ b/docs/reference/query-syntax.md @@ -12,14 +12,17 @@ Generally queries will consist of a `where` clause plus optional [modifiers](#qu The Where clause is an optional array of conditions. If omitted or empty, all documents of the queried type are returned (subject to `limit`). For some operators, `value` will be an array. All fields referenced in a query's where clause must be defined in the same index. This includes system timestamp fields (e.g., `$createdAt`, `$updatedAt`, `$transferredAt`, and their block-height variants such as `$createdAtBlockHeight` and `$createdAtCoreBlockHeight`). See the following general syntax example: -```json Syntax +:::{code-block} json +:force: +:caption: Syntax + { where: [ [, , ], [, , [, ]] - ] + ] } -``` +::: ### Fields @@ -78,19 +81,23 @@ Valid fields consist of the indices defined for the document being queried. For ### Operator Examples -::::{tab-set} -:::{tab-item} Range -```json +:::::{tab-set} +::::{tab-item} Range +:::{code-block} json +:force: + { where: [ ["nameHash", "<", "56116861626961756e6176657a382e64617368"], ], } -``` ::: +:::: + +::::{tab-item} Between +:::{code-block} json +:force: -:::{tab-item} Between -```json { where: [ ["normalizedParentDomainName", "==", "dash"], @@ -101,11 +108,14 @@ Valid fields consist of the indices defined for the document being queried. For ["normalizedLabel", "asc"], ] } -``` ::: +:::: + +::::{tab-item} in +:::{code-block} json +:force: +:caption: in -:::{tab-item} in -```json in { where: [ ["normalizedParentDomainName", "==", "dash"], @@ -113,11 +123,14 @@ Valid fields consist of the indices defined for the document being queried. For ["normalizedLabel", "in", ["alice", "bob"]], ] } -``` ::: +:::: + +::::{tab-item} startsWith +:::{code-block} json +:force: +:caption: startsWith -:::{tab-item} startsWith -```json startsWith { where: [ ["normalizedParentDomainName", "==", "dash"], @@ -128,9 +141,9 @@ Valid fields consist of the indices defined for the document being queried. For ["normalizedLabel", "asc"], ] } -``` ::: :::: +::::: ## Query Modifiers diff --git a/docs/tutorials/example-apps/dashmint-lab.md b/docs/tutorials/example-apps/dashmint-lab.md index 226ffd1ea..60950caf8 100644 --- a/docs/tutorials/example-apps/dashmint-lab.md +++ b/docs/tutorials/example-apps/dashmint-lab.md @@ -660,7 +660,7 @@ export async function burnCard({ ### What makes this an NFT contract -The card data contract defines one document type (`card`) with four fields and three indices. Three top-level flags turn it into an NFT contract: `transferable: 1` lets owners send cards to other identities, `tradeMode: 1` enables the built-in price/purchase flow, and `creationRestrictionMode: 1` controls who can mint. See the [NFT explanation](../../explanations/nft.md#explanations-dash-nfts) for what each flag does, and the [NFT tab in Register a Data Contract](../contracts-and-documents/register-a-data-contract.md) for the schema in JSON form. +The card data contract defines one document type (`card`) with four fields and three indices. Three top-level flags turn it into an NFT contract: `transferable: 1` lets owners send cards to other identities, `tradeMode: 1` enables the built-in price/purchase flow, and `creationRestrictionMode: 1` controls who can mint. See the {ref}`NFT explanation ` for what each flag does, and the [NFT tab in Register a Data Contract](../contracts-and-documents/register-a-data-contract.md) for the schema in JSON form. ### How the app registers or reuses the contract From bb51758c966bce3aed1db5e58981514447cc9502 Mon Sep 17 00:00:00 2001 From: thephez Date: Wed, 17 Jun 2026 11:55:21 -0400 Subject: [PATCH 5/6] docs(tutorials): sync to Evo SDK 4.0.0-rc.2 and document token-burn scarcity (#152) * docs(tutorials): sync with tutorial repo -Bump to Evo SDK v4.0-rc.2 (3.1 was renamed 4.0) and Node 22. -Add the DashMint token-cost flow: a fixed-supply token configuration burned on card create, with creationRestrictionMode opened to anyone who can pay the token cost. - Add note update revision checking, raise the withdrawal amount to the protocol minimum, and drop the obsolete note message maxLength. * docs(tutorials): document DashMint token-burn scarcity model Add a token flow section and Transfer DashMint tokens walkthrough to DashMint Lab, register both blocks in the sync map, and align the intro, TL;DR, and contract-schema prose with token-gated minting. For Dashnote, restructure the update-note steps, note the expectedRevision guard, fix the message maxLength description, and bump the Node prerequisite to 22. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- _static/dashmint-lite.html | 16 +- _static/dashnote-lite.html | 2 +- _static/dashproof-lite.html | 2 +- docs/tutorials/connecting-to-testnet.md | 2 +- docs/tutorials/example-apps/dashmint-lab.md | 302 ++++++++++++++++-- docs/tutorials/example-apps/dashnote.md | 34 +- .../withdraw-an-identity-balance.md | 2 +- docs/tutorials/introduction.md | 2 +- docs/tutorials/setup-sdk-client.md | 11 +- scripts/tutorial-sync/tutorial-code-map.yml | 12 + 10 files changed, 348 insertions(+), 37 deletions(-) diff --git a/_static/dashmint-lite.html b/_static/dashmint-lite.html index 41de9f965..bb7836ab1 100644 --- a/_static/dashmint-lite.html +++ b/_static/dashmint-lite.html @@ -119,18 +119,24 @@

    Browse cards

    // package and serves it as a browser-native ES module. Pinned to the same // version the React app at ../package.json depends on so both UIs behave // identically against the same testnet contract. - import { EvoSDK } from 'https://esm.sh/@dashevo/evo-sdk@3.1.0-dev.1'; + import { EvoSDK } from 'https://esm.sh/@dashevo/evo-sdk@4.0.0-rc.2'; - // The "card" data contract is already published on testnet by the React app. - // Anyone querying with the same contract id hits the same documents. - const CONTRACT_ID = '4eJR4pgV9mQdyoodfTTwFUp3SYBRJbUrJ5X1ViN2zBhY'; + // The token-enabled "card" data contract is already published on testnet by + // the React app. Anyone querying with the same contract id hits the same + // documents. + const CONTRACT_ID = '5hK6SMfN4m2vU1t9qhvngUUQjsXeMNwr8MZdFeGBH8Aa'; const DOC_TYPE = 'card'; // Connect to testnet. testnetTrusted() uses the SDK's bundled list of trusted // nodes — no node URL or config needed. connect() does the gRPC handshake // + initial sync. No identity or signing is required for read-only queries. + // + // Workaround: pin the platform protocol version for evo-sdk dev.6 so the + // SDK doesn't ask testnet for a newer protocol it can't decode. Mirrors + // PLATFORM_VERSION_OVERRIDE in setupDashClient-core.mjs. Remove once a + // fixed SDK release lands. async function connectSdk() { - const sdk = EvoSDK.testnetTrusted(); + const sdk = EvoSDK.testnetTrusted({ version: 11 }); await sdk.connect(); return sdk; } diff --git a/_static/dashnote-lite.html b/_static/dashnote-lite.html index 4227bc038..3a3f29beb 100644 --- a/_static/dashnote-lite.html +++ b/_static/dashnote-lite.html @@ -129,7 +129,7 @@

    Get note by ID

    // package and serves it as a browser-native ES module. Pinned to the same // version the React app at ../package.json depends on so both UIs behave // identically against the same testnet contract. - import { EvoSDK } from 'https://esm.sh/@dashevo/evo-sdk@3.1.0-dev.1'; + import { EvoSDK } from 'https://esm.sh/@dashevo/evo-sdk@4.0.0-rc.2'; // The "note" data contract is already published on testnet by the React app. // Anyone querying with the same contract id hits the same documents. diff --git a/_static/dashproof-lite.html b/_static/dashproof-lite.html index a9e10f558..1631115df 100644 --- a/_static/dashproof-lite.html +++ b/_static/dashproof-lite.html @@ -120,7 +120,7 @@

    History by chainId

    // package and serves it as a browser-native ES module. Pinned to the same // version the React app at ../package.json depends on so both UIs behave // identically against the same testnet contract. - import { EvoSDK } from 'https://esm.sh/@dashevo/evo-sdk@3.1.0-dev.1'; + import { EvoSDK } from 'https://esm.sh/@dashevo/evo-sdk@4.0.0-rc.2'; // The "anchor" data contract is already published on testnet by the React app. // Anyone querying with the same contract id hits the same documents. diff --git a/docs/tutorials/connecting-to-testnet.md b/docs/tutorials/connecting-to-testnet.md index 91e6ecade..96cc8d9ee 100644 --- a/docs/tutorials/connecting-to-testnet.md +++ b/docs/tutorials/connecting-to-testnet.md @@ -12,7 +12,7 @@ Platform services are provided via a combination of HTTP and gRPC connections to ## Prerequisites -- An installation of [NodeJS v20 or higher](https://nodejs.org/en/download/) +- An installation of [NodeJS v22 or higher](https://nodejs.org/en/download/) ## Connect via Dash SDK diff --git a/docs/tutorials/example-apps/dashmint-lab.md b/docs/tutorials/example-apps/dashmint-lab.md index 60950caf8..5245391bb 100644 --- a/docs/tutorials/example-apps/dashmint-lab.md +++ b/docs/tutorials/example-apps/dashmint-lab.md @@ -4,13 +4,13 @@ # DashMint Lab — NFT marketplace -[DashMint Lab](https://dashpay.github.io/platform-tutorials/dashmint-lab/) is a React + TypeScript + Vite single-page app that exercises every Dash Platform NFT operation: mint, transfer, price, purchase, burn, and query. This walkthrough shows how those SDK calls are organized inside a real UI. +[DashMint Lab](https://dashpay.github.io/platform-tutorials/dashmint-lab/) is a React + TypeScript + Vite single-page app that exercises every Dash Platform NFT operation: mint, transfer, price, purchase, burn, and query. Minting is gated by a fixed-supply DashMint token, so the walkthrough also shows how token balances, token transfers, and token-paid document creation fit into a real UI. ![DashMint Lab - Collection](./img/dashmint-collection.png) ## What this app does -The app lets users log in with a BIP-39 mnemonic, mint "card" NFTs with random attack/defense stats, browse cards across the network, set sale prices, purchase cards from other identities, transfer cards as gifts, and burn cards they no longer want. Read-only browsing works without any credentials. +The app lets users log in with a BIP-39 mnemonic, mint "card" NFTs with random attack/defense stats by burning DashMint tokens, browse cards across the network, set sale prices, purchase cards from other identities, transfer cards or DashMint tokens as gifts, and burn cards they no longer want. Read-only browsing works without any credentials. For background on Dash Platform NFT features such as transfer, trade, delete, and creation restrictions, see the [NFT explanation](../../explanations/nft.md). @@ -21,12 +21,13 @@ Every Platform SDK call lives in its own file under `src/dash/`. The React UI is ## TL;DR - Each NFT operation lives in its own `src/dash/*.ts` file. -- The easiest entry points are `src/dash/queries.ts`, `src/dash/mintCard.ts`, and `src/dash/transferCard.ts`. +- The easiest entry points are `src/dash/queries.ts`, `src/dash/contract.ts`, `src/dash/dashMintToken.ts`, and `src/dash/mintCard.ts`. +- Minting costs 1 DashMint token. The contract burns that token through `card.tokenCost.create`, so the fixed token supply caps the card supply. - Most mutations share one helper: `src/dash/withAuthedCard.ts`. - The UI mostly passes form input into those functions and renders the results. - `client.ts` and `keyManager.ts` are thin re-exports of `setupDashClient-core.mjs`. -If you just want the mental model: read the architecture table, then `withAuthedCard.ts`, then whichever operation you care about. +If you just want the mental model: read the architecture table, then `contract.ts`, `dashMintToken.ts`, `mintCard.ts`, and whichever operation you care about. ## Prerequisites @@ -34,7 +35,8 @@ If you just want the mental model: read the architecture table, then `withAuthed - A configured client: [Setup SDK Client](../setup-sdk-client.md) — DashMint re-uses `setupDashClient-core.mjs` - A registered identity: [Register an Identity](../identities-and-names/register-an-identity.md) - Familiarity with data contracts: [Register a Data Contract](../contracts-and-documents/register-a-data-contract.md) — particularly the NFT tab -- Node >= 20 and a funded testnet identity (BIP-39 mnemonic + identity index) +- Node >= 22 and a funded testnet identity (BIP-39 mnemonic + identity index) +- DashMint tokens on the active contract if you want to mint on an existing contract. Registering a fresh contract starts it with 100 DashMint tokens owned by the registering identity. - (Optional) A second funded identity to test cross-profile transfer and purchase ## Clone and run @@ -46,7 +48,7 @@ npm install npm run dev ``` -The dev server runs on `http://localhost:5173`. Open it in a browser, click **Login**, paste your testnet mnemonic, and start minting. The app ships with a default contract ID so browse-only mode works on a fresh install. +The dev server runs on `http://localhost:5173`. Open it in a browser, click **Login**, and paste your testnet mnemonic. The app ships with a default contract ID so browse-only mode works on a fresh install. To mint, use an identity that already has DashMint tokens for the active contract, transfer tokens from another identity, or register a fresh contract from the login modal. Production build: `npm run build && npm run preview`. @@ -60,15 +62,18 @@ Every Platform SDK call lives in its own file under `src/dash/`: | Derive identity keys | `src/dash/keyManager.ts` | `wallet.deriveKeyFromSeedWithPath` | | Deploy card contract | `src/dash/contract.ts` | `sdk.contracts.publish` | | Query cards | `src/dash/queries.ts` | `sdk.documents.query` | -| Mint a card | `src/dash/mintCard.ts` | `sdk.documents.create` | +| Token balance / supply | `src/dash/dashMintToken.ts` | `sdk.tokens.calculateId`, `sdk.tokens.identityBalances`, `sdk.tokens.totalSupply` | +| Mint a card | `src/dash/mintCard.ts` | `sdk.documents.create` + `tokenPaymentInfo` | +| Transfer DashMint tokens | `src/dash/transferDashMintTokens.ts` | `sdk.tokens.transfer` | | Transfer a card | `src/dash/transferCard.ts` | `sdk.documents.transfer` | | Set / remove price | `src/dash/setPrice.ts` | `sdk.documents.setPrice` | | Purchase a card | `src/dash/purchaseCard.ts` | `sdk.documents.purchase` | | Burn (delete) a card | `src/dash/burnCard.ts` | `sdk.documents.delete` | -Two supporting files glue the operations together: +A few supporting files glue the operations together: - `src/dash/withAuthedCard.ts` — shared mutation prelude used by transfer, setPrice, purchase, and burn. Fetches the document, bumps its revision, and resolves the auth signer. +- `src/dash/dashMintToken.ts` — fixed-supply DashMint token constants, `tokenPaymentInfo`, token balance lookup, and minted-count calculation. - `src/dash/logger.ts` — shared `Logger` type so every operation can stream progress to the UI activity log. `client.ts` and `keyManager.ts` are just re-exports: @@ -80,6 +85,78 @@ export { IdentityKeyManager } from '../../../../setupDashClient-core.mjs'; That means the connection and key-derivation behavior are the same as in the Node tutorials. Read [Setup SDK Client](../setup-sdk-client.md) for the full client setup details. +## DashMint token flow + +DashMint Lab uses a token to make minting scarce without adding a separate minting service. The data contract defines token position `0` as a fixed-supply DashMint token with a supply of 100. The `card` document type charges 1 token on create and burns it, so every successful card mint reduces the remaining mint capacity. + +The token helper file centralizes the constants, the `tokenPaymentInfo` passed to `sdk.documents.create`, and the read helpers used by the Mint and Tokens tabs. + +```{code-block} typescript +:caption: dashMintToken.ts +:name: dashmint-token.ts + +/** + * DashMint token constants and helpers. + * + * The data contract defines token position 0 as a fixed-supply DashMint token. + * Creating a `card` document burns one token via `card.tokenCost.create`. + * UI code uses this file to build tokenPaymentInfo and display the signed-in + * identity's remaining DashMint token balance. + */ +import type { DashSdk } from "./types"; + +export const DASHMINT_TOKEN_POSITION = 0; +export const DASHMINT_TOKEN_COST = 1n; +export const DASHMINT_TOKEN_SUPPLY = 100n; +export const DASHMINT_TOKEN_NAME = "DashMint"; +export const DASHMINT_TOKEN_PLURAL = "DashMint"; + +// Agreement passed to sdk.documents.create() to satisfy the contract's +// one-token burn requirement for card creation. +export const DASHMINT_TOKEN_PAYMENT_INFO = { + tokenContractPosition: DASHMINT_TOKEN_POSITION, + maximumTokenCost: DASHMINT_TOKEN_COST, + gasFeesPaidBy: "documentOwner" as const, +}; + +export async function fetchDashMintTokenBalance({ + sdk, + contractId, + identityId, +}: { + sdk: DashSdk; + contractId: string; + identityId: string; +}): Promise { + const tokenId = await sdk.tokens.calculateId( + contractId, + DASHMINT_TOKEN_POSITION, + ); + const balances = await sdk.tokens.identityBalances(identityId, [tokenId]); + return balances.get(tokenId) ?? 0n; +} + +// Every mint burns exactly one DashMint token (manual burns/mints are locked +// in the contract), so cards minted = SUPPLY - current circulating supply. +export async function fetchCardsMintedCount({ + sdk, + contractId, +}: { + sdk: DashSdk; + contractId: string; +}): Promise { + const tokenId = await sdk.tokens.calculateId( + contractId, + DASHMINT_TOKEN_POSITION, + ); + const supply = await sdk.tokens.totalSupply(tokenId); + const remaining = supply?.totalSupply ?? DASHMINT_TOKEN_SUPPLY; + return DASHMINT_TOKEN_SUPPLY - remaining; +} +``` + +The Mint tab uses `fetchDashMintTokenBalance()` to disable minting when the signed-in identity has no DashMint tokens. It uses `fetchCardsMintedCount()` to show the supply meter, hide the mint form when all 100 cards have been minted, and disable the Starter Pack button when fewer than three mints remain. + ## Shared mutation pattern Every mutation on an existing card — transfer, set price, purchase, burn — runs the same four steps: @@ -233,6 +310,10 @@ export interface Card { $price?: number | bigint; } +function hasSalePrice(card: Card): boolean { + return card.$price != null && card.$price !== 0 && card.$price !== 0n; +} + function toCard(id: string | null, raw: DashCardQueryDocument): Card { const j: Record = typeof raw?.toJSON === "function" ? raw.toJSON() : raw; @@ -311,7 +392,7 @@ export async function listMarketplaceCards({ documentTypeName: "card", limit, }); - const cards = normalizeCards(results).filter((c) => c.$price); + const cards = normalizeCards(results).filter(hasSalePrice); log?.(`Found ${cards.length} card(s) for sale.`); return cards; } @@ -323,12 +404,14 @@ Each operation file is intentionally small. The app-level pattern is: validate i ### Mint a card -Minting is the simplest write operation: build a `Document` with the card properties and owner, then call `sdk.documents.create`. No existing document to fetch, no revision to bump. +Minting builds a `Document` with the card properties and owner, then calls `sdk.documents.create`. There is no existing document to fetch and no revision to bump. The `card` document type has `tokenCost.create` configured, so the call also passes `tokenPaymentInfo` to burn one DashMint token. That token burn is what enforces the fixed card supply. + +The UI exposes this in two ways: a single-card form that burns 1 token, and a Starter Pack button that calls `mintCard()` repeatedly for multiple predefined cards. Both paths use the same SDK helper, so each card still costs 1 DashMint token and each successful create burns one token. ```{code-block} typescript :caption: mintCard.ts :name: dashmint-mintCard.ts -:emphasize-lines: 51,56-63 +:emphasize-lines: 56-58,72-77 /** * Mint a new card (create a document against the card data contract). @@ -336,11 +419,18 @@ Minting is the simplest write operation: build a `Document` with the card proper * Attack and defense are rolled client-side (1-10 each). Name is required, * description is optional. * - * SDK method: sdk.documents.create({ document, identityKey, signer }) + * Scarcity comes from the contract, not this function: the `card` document + * type has `tokenCost.create` configured to burn 1 token at position 0. + * Passing `tokenPaymentInfo` below is the caller's agreement to spend that + * DashMint token, so each successful document create consumes one fixed-supply + * token and reduces the remaining mint capacity. + * + * SDK method: sdk.documents.create({ document, identityKey, signer, tokenPaymentInfo }) */ import { Document } from "@dashevo/evo-sdk"; import type { Logger } from "./logger"; +import { DASHMINT_TOKEN_PAYMENT_INFO } from "./dashMintToken"; import type { DashKeyManager, DashSdk } from "./types"; export interface MintCardInput { @@ -378,7 +468,9 @@ export async function mintCard({ const defense = card.defense ?? rollStat(); const description = card.description?.trim(); - log?.(`Minting "${name}" (ATK ${attack} / DEF ${defense})…`); + log?.( + `Burning 1 DashMint token to mint "${name}" (ATK ${attack} / DEF ${defense})…`, + ); const { identity, identityKey, signer } = await keyManager.getAuth(); @@ -392,7 +484,12 @@ export async function mintCard({ ownerId: identity.id, }); - await sdk.documents.create({ document: doc, identityKey, signer }); + await sdk.documents.create({ + document: doc, + identityKey, + signer, + tokenPaymentInfo: DASHMINT_TOKEN_PAYMENT_INFO, + }); log?.(`Card "${name}" minted!`, "success"); } ``` @@ -656,11 +753,89 @@ export async function burnCard({ } ``` +### Transfer DashMint tokens + +DashMint tokens are ordinary Platform tokens on the active app contract. The Tokens tab lets a signed-in identity send whole DashMint token amounts to another identity, which is useful for testing scarcity across multiple profiles. Unlike card document operations, explicit token sends use the transfer key returned by `keyManager.getTransfer()`. + +```{code-block} typescript +:caption: transferDashMintTokens.ts +:name: dashmint-transfer-dashmint-tokens.ts +:emphasize-lines: 44-64 + +/** + * Transfer DashMint tokens from the signed-in identity to another identity. + * + * DashMint lives at token position 0 on the active app contract. Token + * single-transfer transitions can be signed by a critical auth or transfer + * purpose key; this app keeps explicit token sends on the transfer key. + * + * SDK method: sdk.tokens.transfer({ dataContractId, tokenPosition, amount, senderId, recipientId, identityKey, signer }) + */ +import { DASHMINT_TOKEN_NAME, DASHMINT_TOKEN_POSITION } from "./dashMintToken"; +import type { Logger } from "./logger"; +import type { DashKeyManager, DashSdk } from "./types"; + +export interface TransferDashMintTokensInput { + sdk: DashSdk; + keyManager: DashKeyManager; + contractId: string; + recipientId: string; + amount: bigint; + log?: Logger; +} + +export async function transferDashMintTokens({ + sdk, + keyManager, + contractId, + recipientId, + amount, + log, +}: TransferDashMintTokensInput): Promise { + const trimmedRecipientId = recipientId.trim(); + if (!trimmedRecipientId) { + throw new Error("Recipient identity ID is required."); + } + if (amount <= 0n) { + throw new Error("Amount must be greater than 0."); + } + + const knownSenderId = keyManager.identityId?.toString(); + if (knownSenderId && trimmedRecipientId === knownSenderId) { + throw new Error("Cannot transfer tokens to yourself."); + } + + const { identity, identityKey, signer } = await keyManager.getTransfer(); + const senderId = identity.id.toString(); + if (trimmedRecipientId === senderId) { + throw new Error("Cannot transfer tokens to yourself."); + } + + log?.( + `Transferring ${amount.toString()} ${DASHMINT_TOKEN_NAME} token${ + amount === 1n ? "" : "s" + }...`, + ); + + await sdk.tokens.transfer({ + dataContractId: contractId, + tokenPosition: DASHMINT_TOKEN_POSITION, + amount, + senderId, + recipientId: trimmedRecipientId, + identityKey, + signer, + }); + + log?.(`${DASHMINT_TOKEN_NAME} tokens transferred.`, "success"); +} +``` + ## Contract schema ### What makes this an NFT contract -The card data contract defines one document type (`card`) with four fields and three indices. Three top-level flags turn it into an NFT contract: `transferable: 1` lets owners send cards to other identities, `tradeMode: 1` enables the built-in price/purchase flow, and `creationRestrictionMode: 1` controls who can mint. See the {ref}`NFT explanation ` for what each flag does, and the [NFT tab in Register a Data Contract](../contracts-and-documents/register-a-data-contract.md) for the schema in JSON form. +The card data contract defines one document type (`card`) with four fields and three indices. Three top-level settings control its NFT behavior: `transferable: 1` lets owners send cards to other identities, `tradeMode: 1` enables the built-in price/purchase flow, and `creationRestrictionMode: 0` leaves creation open. Scarcity comes from `tokenCost.create`, so any identity can mint when it can pay and burn 1 DashMint token. See the {ref}`NFT explanation ` for what each flag does, and the [NFT tab in Register a Data Contract](../contracts-and-documents/register-a-data-contract.md) for the schema in JSON form. ### How the app registers or reuses the contract @@ -681,7 +856,10 @@ The card data contract defines one document type (`card`) with four fields and t * The three flags at the top of the schema are what make this an NFT: * transferable: 1 — documents can be sent to another identity (0 to disable) * tradeMode: 1 — documents can be priced and purchased (0 to disable) - * creationRestrictionMode: 1 — (1 - only the contract owner can mint; 0 - anyone can mint) + * creationRestrictionMode: 0 — anyone can create when they can pay tokenCost.create + * + * tokenCost.create burns 1 DashMint token, turning the fixed token + * supply into the maximum number of cards that can ever be minted. * * Storage helpers (loadStoredContractId, saveContractId, …) and the owner * lookup live in contractStorage.ts so they can be imported without @@ -689,10 +867,27 @@ The card data contract defines one document type (`card`) with four fields and t * * SDK methods: new DataContract({ ... }), sdk.contracts.publish(...) */ -import { DataContract } from "@dashevo/evo-sdk"; +import { + AuthorizedActionTakers, + ChangeControlRules, + DataContract, + TokenConfiguration, + TokenConfigurationConvention, + TokenConfigurationLocalization, + TokenDistributionRules, + TokenKeepsHistoryRules, + TokenMarketplaceRules, + TokenTradeMode, +} from "@dashevo/evo-sdk"; import { loadStoredContractId, saveContractId } from "./contractStorage"; import type { Logger } from "./logger"; +import { + DASHMINT_TOKEN_NAME, + DASHMINT_TOKEN_PLURAL, + DASHMINT_TOKEN_POSITION, + DASHMINT_TOKEN_SUPPLY, +} from "./dashMintToken"; import type { DashKeyManager, DashSdk } from "./types"; export { @@ -710,7 +905,15 @@ export const CARD_SCHEMAS = { canBeDeleted: true, transferable: 1, tradeMode: 1, - creationRestrictionMode: 1, + creationRestrictionMode: 0, + tokenCost: { + create: { + tokenPosition: DASHMINT_TOKEN_POSITION, + amount: 1, + effect: 1, + gasFeesPaidBy: 0, + }, + }, properties: { name: { type: "string", @@ -747,6 +950,64 @@ export const CARD_SCHEMAS = { }, } as const; +export function createDashMintTokenConfiguration(ownerId: string) { + const contractOwner = AuthorizedActionTakers.ContractOwner(); + const noOne = AuthorizedActionTakers.NoOne(); + + const ownerRules = new ChangeControlRules({ + authorizedToMakeChange: contractOwner, + adminActionTakers: contractOwner, + isChangingAuthorizedActionTakersToNoOneAllowed: true, + isChangingAdminActionTakersToNoOneAllowed: true, + isSelfChangingAdminActionTakersAllowed: true, + }); + const lockedRules = new ChangeControlRules({ + authorizedToMakeChange: noOne, + adminActionTakers: noOne, + }); + + return new TokenConfiguration({ + conventions: new TokenConfigurationConvention( + { + en: new TokenConfigurationLocalization( + false, + DASHMINT_TOKEN_NAME, + DASHMINT_TOKEN_PLURAL, + ), + }, + 0, + ), + conventionsChangeRules: ownerRules, + baseSupply: DASHMINT_TOKEN_SUPPLY, + maxSupply: DASHMINT_TOKEN_SUPPLY, + keepsHistory: new TokenKeepsHistoryRules({ + isKeepingBurningHistory: true, + isKeepingTransferHistory: true, + }), + maxSupplyChangeRules: lockedRules, + distributionRules: new TokenDistributionRules({ + newTokensDestinationIdentity: ownerId, + newTokensDestinationIdentityRules: ownerRules, + mintingAllowChoosingDestination: false, + mintingAllowChoosingDestinationRules: ownerRules, + perpetualDistributionRules: lockedRules, + changeDirectPurchasePricingRules: lockedRules, + }), + marketplaceRules: new TokenMarketplaceRules( + TokenTradeMode.NotTradeable(), + lockedRules, + ), + manualMintingRules: lockedRules, + manualBurningRules: lockedRules, + freezeRules: lockedRules, + unfreezeRules: lockedRules, + destroyFrozenFundsRules: lockedRules, + emergencyActionRules: lockedRules, + mainControlGroupCanBeModified: noOne, + description: "Fixed-supply DashMint token burned to mint demo cards.", + }); +} + /** * Register a fresh NFT card data contract on Platform and persist its ID. * @@ -768,6 +1029,11 @@ export async function registerContract({ ownerId: identity.id, identityNonce: (identityNonce || 0n) + 1n, schemas: CARD_SCHEMAS, + tokens: { + [DASHMINT_TOKEN_POSITION]: createDashMintTokenConfiguration( + identity.id.toString(), + ), + }, fullValidation: true, }); diff --git a/docs/tutorials/example-apps/dashnote.md b/docs/tutorials/example-apps/dashnote.md index 9a63bca05..52e22a58a 100644 --- a/docs/tutorials/example-apps/dashnote.md +++ b/docs/tutorials/example-apps/dashnote.md @@ -35,7 +35,7 @@ If you just want the mental model: read the architecture table, then `createNote - A configured client: [Setup SDK Client](../setup-sdk-client.md) — Dashnote re-uses `setupDashClient-core.mjs` - A registered identity: [Register an Identity](../identities-and-names/register-an-identity.md) - Familiarity with data contracts: [Register a Data Contract](../contracts-and-documents/register-a-data-contract.md) -- Node >= 20 and a funded testnet identity (BIP-39 mnemonic + identity index) for write operations +- Node >= 22 and a funded testnet identity (BIP-39 mnemonic + identity index) for write operations - Read-only browse works without any credentials against the bundled default contract ## Clone and run @@ -255,6 +255,7 @@ Each operation file is intentionally small. The app-level pattern is: validate i * SDK method: sdk.documents.create({ document, identityKey, signer }) */ import type { Logger } from "../lib/logger"; +import { PLATFORM_VERSION_OVERRIDE } from "../../../../platformVersion.mjs"; import { loadSdkModule } from "./sdkModule"; import type { DashKeyManager, DashSdk } from "./types"; @@ -297,7 +298,7 @@ export async function createNote({ const json = typeof document.toJSON === "function" - ? (document.toJSON() as Record) + ? (document.toJSON(PLATFORM_VERSION_OVERRIDE) as Record) : {}; const noteId = String(json.$id ?? json.id ?? ""); if (!noteId) { @@ -310,7 +311,13 @@ export async function createNote({ ### Update a note -`updateNote.ts` is the canonical fetch-then-bump-revision write. It calls `sdk.documents.get` to read the on-chain revision, increments it by one, builds a new `Document` with the same id and ownerId, and submits via `sdk.documents.replace`. Replays without bumping the revision are rejected by the state transition. +`updateNote.ts` is the canonical fetch-then-bump-revision write: + +- Call `sdk.documents.get` to read the current on-chain revision. +- Increment it by one and build a new `Document` with the same id and ownerId. +- Submit via `sdk.documents.replace`. Replays without bumping the revision are rejected by the state transition. + +The optional `expectedRevision` parameter guards against a concurrent edit: if the on-chain revision no longer matches what the caller last loaded, the update is refused with a "reload and try again" error instead of silently overwriting the newer version. ```{code-block} typescript :caption: updateNote.ts @@ -320,6 +327,10 @@ export async function createNote({ * Update an existing note. Fetches the current document to bump its revision, * then submits a replace state transition. * + * Pass `expectedRevision` to refuse the update if the network's revision + * doesn't match — i.e. the note was changed on the network after the local + * copy was loaded. + * * SDK methods: * sdk.documents.get(contractId, documentTypeName, documentId) * sdk.documents.replace({ document, identityKey, signer }) @@ -335,6 +346,7 @@ export interface UpdateNoteParams { noteId: string; title?: string; message: string; + expectedRevision?: number; log?: Logger; } @@ -345,6 +357,7 @@ export async function updateNote({ noteId, title, message, + expectedRevision, log, }: UpdateNoteParams): Promise { log?.(`Saving note ${noteId}…`); @@ -354,8 +367,18 @@ export async function updateNote({ throw new Error(`Note ${noteId} not found.`); } + const currentRevision = BigInt(existingDoc.revision ?? 0); + if ( + expectedRevision !== undefined && + currentRevision !== BigInt(expectedRevision) + ) { + throw new Error( + `Note changed on network (you had revision ${expectedRevision}, network is at ${currentRevision}). Reload your notes and try again.`, + ); + } + const { Document } = await loadSdkModule(); - const revision = BigInt(existingDoc.revision ?? 0) + 1n; + const revision = currentRevision + 1n; const trimmedTitle = title?.trim(); const document = new Document({ properties: { @@ -431,7 +454,7 @@ export async function deleteNote({ The note contract is intentionally minimal: one document type, two user-editable fields, two indices to support the recent-notes list. Key choices worth calling out: - `documentsMutable: true` and `canBeDeleted: true` — notes are editable and deletable. -- `maxLength: 120` for `title` and `maxLength: 10000` for `message` are **UTF-8 byte budgets**, not character counts. The editor's progress bar reflects bytes; emoji and non-ASCII sequences consume more of the budget than ASCII. +- `maxLength: 120` for `title` caps the title; `message` carries no `maxLength` and is instead bounded by Platform's per-field byte limit. The editor's progress bar tracks the `message` byte count against that limit — emoji and non-ASCII sequences consume more of the budget than ASCII. - `byOwnerUpdated` (`$ownerId`, `$updatedAt`) is the index the recent-notes list paginates on; `byOwnerCreated` is its created-time sibling. `registerContract` builds the `DataContract`, calls `setConfig()` to lock in those choices, then publishes via `sdk.contracts.publish`. `ensureContract` is the lazy wrapper used by the login flow: re-use a saved contract ID if one is present, otherwise register a fresh one. @@ -464,7 +487,6 @@ export const NOTE_SCHEMAS = { }, message: { type: "string", - maxLength: 10000, position: 1, }, }, diff --git a/docs/tutorials/identities-and-names/withdraw-an-identity-balance.md b/docs/tutorials/identities-and-names/withdraw-an-identity-balance.md index ef4311b47..e74319590 100644 --- a/docs/tutorials/identities-and-names/withdraw-an-identity-balance.md +++ b/docs/tutorials/identities-and-names/withdraw-an-identity-balance.md @@ -32,7 +32,7 @@ console.log('Identity balance before withdrawal:', identity.balance); // Default: testnet faucet address. Replace or override via WITHDRAWAL_ADDRESS. const toAddress = process.env.WITHDRAWAL_ADDRESS || 'yXWJGWuD4VBRMp9n2MtXQbGpgSeWyTRHme'; -const amount = 190000n; // Credits to withdraw +const amount = 1000000n; // Credits to withdraw (protocol minimum) const amountDash = Number(amount) / (1000 * 100000000); console.log(`Withdrawing ${amount} credits (${amountDash} DASH)`); diff --git a/docs/tutorials/introduction.md b/docs/tutorials/introduction.md index bf56233c1..9b2756950 100644 --- a/docs/tutorials/introduction.md +++ b/docs/tutorials/introduction.md @@ -12,7 +12,7 @@ Building on Dash Platform requires first registering an Identity and then regist The tutorials in this section are written in JavaScript and use [Node.js](https://nodejs.org/en/about/). The following prerequisites are necessary to complete the tutorials: -- [Node.js](https://nodejs.org/en/) (v20+) +- [Node.js](https://nodejs.org/en/) (v22+) - Familiarity with JavaScript asynchronous functions using [async/await](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await) - The [Dash JavaScript SDK](https://www.npmjs.com/package/@dashevo/evo-sdk) (see [Connecting to a Network](../tutorials/connecting-to-testnet.md#1-install-the-dash-sdk)) diff --git a/docs/tutorials/setup-sdk-client.md b/docs/tutorials/setup-sdk-client.md index 1ba48510e..00e8f16d1 100644 --- a/docs/tutorials/setup-sdk-client.md +++ b/docs/tutorials/setup-sdk-client.md @@ -67,6 +67,7 @@ import { SecurityLevel, wallet, } from '@dashevo/evo-sdk'; +import { PLATFORM_VERSION_OVERRIDE } from './platformVersion.mjs'; /** @typedef {import('@dashevo/evo-sdk').Identity} Identity */ /** @typedef {import('@dashevo/evo-sdk').IdentityPublicKey} IdentityPublicKey */ @@ -144,6 +145,8 @@ export async function dip13KeyPath(network, identityIndex, keyIndex) { // SDK client helpers // --------------------------------------------------------------------------- +export { PLATFORM_VERSION_OVERRIDE }; + /** * Create and connect an EvoSDK client for the selected network. * @@ -152,9 +155,11 @@ export async function dip13KeyPath(network, identityIndex, keyIndex) { */ export async function createClient(network = 'testnet') { const factories = /** @type {Record EvoSDK>} */ ({ - testnet: () => EvoSDK.testnetTrusted(), - mainnet: () => EvoSDK.mainnetTrusted(), - local: () => EvoSDK.localTrusted(), + testnet: () => + EvoSDK.testnetTrusted({ version: PLATFORM_VERSION_OVERRIDE }), + mainnet: () => + EvoSDK.mainnetTrusted({ version: PLATFORM_VERSION_OVERRIDE }), + local: () => EvoSDK.localTrusted({ version: PLATFORM_VERSION_OVERRIDE }), }); const factory = factories[network]; diff --git a/scripts/tutorial-sync/tutorial-code-map.yml b/scripts/tutorial-sync/tutorial-code-map.yml index 56e9aba3e..c1296b9af 100644 --- a/scripts/tutorial-sync/tutorial-code-map.yml +++ b/scripts/tutorial-sync/tutorial-code-map.yml @@ -199,6 +199,12 @@ mappings: caption: contract.ts language: typescript + - source: example-apps/dashmint-lab/src/dash/dashMintToken.ts + doc: example-apps/dashmint-lab.md + block_id: + caption: dashMintToken.ts + language: typescript + - source: example-apps/dashmint-lab/src/dash/withAuthedCard.ts doc: example-apps/dashmint-lab.md block_id: @@ -217,6 +223,12 @@ mappings: caption: transferCard.ts language: typescript + - source: example-apps/dashmint-lab/src/dash/transferDashMintTokens.ts + doc: example-apps/dashmint-lab.md + block_id: + caption: transferDashMintTokens.ts + language: typescript + - source: example-apps/dashmint-lab/src/dash/setPrice.ts doc: example-apps/dashmint-lab.md block_id: From 07d9895fd47c6801984ddb17fe92780917f14721 Mon Sep 17 00:00:00 2001 From: thephez Date: Tue, 23 Jun 2026 12:07:15 -0400 Subject: [PATCH 6/6] docs(tutorials): add token lifecycle tutorials (#154) * docs(tutorials): add token lifecycle tutorials Add a Tokens tutorial section covering the issuer-managed token lifecycle on Evo SDK 4.0.0-rc.2: register a token contract, retrieve token info, mint, burn, and transfer tokens. Wire the new pages into the tutorials toctree and sidebar, and add their source mappings to the tutorial code-sync map so the embedded examples stay in sync with platform-tutorials. Co-Authored-By: Claude Opus 4.8 (1M context) * docs(tutorials): link token tutorials to DashMint Lab example app Add 'See this in an example app' tips on the register, retrieve-info, and transfer token tutorials pointing to the matching DashMint Lab sections, matching the cross-link pattern used by the contract and document tutorials. Co-Authored-By: Claude Opus 4.8 (1M context) * docs(tutorials): clarify token tutorial recipient and contract ID guidance Note that the recipient defaults to a demo testnet identity and explain when to set RECIPIENT_ID to a second identity you control. On the register page, clarify that TOKEN_CONTRACT_ID is the published contract ID, not the token ID printed at the end. --------- Co-authored-by: Claude Opus 4.8 (1M context) --- _templates/sidebar-main.html | 40 ++++ docs/index.md | 1 + docs/tutorials/tokens.md | 22 ++ docs/tutorials/tokens/burn-tokens.md | 68 ++++++ docs/tutorials/tokens/mint-tokens.md | 68 ++++++ .../tokens/register-a-token-contract.md | 203 ++++++++++++++++++ docs/tutorials/tokens/retrieve-token-info.md | 88 ++++++++ .../tokens/transfer-tokens-to-an-identity.md | 96 +++++++++ scripts/tutorial-sync/tutorial-code-map.yml | 27 +++ 9 files changed, 613 insertions(+) create mode 100644 docs/tutorials/tokens.md create mode 100644 docs/tutorials/tokens/burn-tokens.md create mode 100644 docs/tutorials/tokens/mint-tokens.md create mode 100644 docs/tutorials/tokens/register-a-token-contract.md create mode 100644 docs/tutorials/tokens/retrieve-token-info.md create mode 100644 docs/tutorials/tokens/transfer-tokens-to-an-identity.md diff --git a/_templates/sidebar-main.html b/_templates/sidebar-main.html index c973bc7b4..71eb257e9 100644 --- a/_templates/sidebar-main.html +++ b/_templates/sidebar-main.html @@ -163,6 +163,46 @@
  • +
  • + + Tokens + +
    + + + + + + + +
    +
  • Example apps diff --git a/docs/index.md b/docs/index.md index e42d5fdf5..875e97d6a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -111,6 +111,7 @@ tutorials/create-and-fund-a-wallet tutorials/setup-sdk-client tutorials/identities-and-names tutorials/contracts-and-documents +tutorials/tokens tutorials/example-apps tutorials/send-funds tutorials/setup-a-node diff --git a/docs/tutorials/tokens.md b/docs/tutorials/tokens.md new file mode 100644 index 000000000..eea02af93 --- /dev/null +++ b/docs/tutorials/tokens.md @@ -0,0 +1,22 @@ +```{eval-rst} +.. tutorials-tokens: +``` + +# Tokens + +The following tutorials cover registering, querying, and managing tokens on Dash Platform, including minting, burning, and transferring them between identities. + +```{toctree} +:maxdepth: 2 +:titlesonly: + +tokens/register-a-token-contract +tokens/retrieve-token-info +tokens/mint-tokens +tokens/burn-tokens +tokens/transfer-tokens-to-an-identity +``` + +:::{tip} +You can clone a repository containing the code for all tutorials from GitHub or download it as a [zip file](https://github.com/dashpay/platform-readme-tutorials/archive/refs/heads/main.zip). +::: diff --git a/docs/tutorials/tokens/burn-tokens.md b/docs/tutorials/tokens/burn-tokens.md new file mode 100644 index 000000000..3cb662e68 --- /dev/null +++ b/docs/tutorials/tokens/burn-tokens.md @@ -0,0 +1,68 @@ +```{eval-rst} +.. tutorials-burn-tokens: +``` + +# Burn tokens + +The purpose of this tutorial is to walk through the steps necessary to burn (permanently destroy) [tokens](../../explanations/tokens.md), reducing the total supply. + +## Overview + +Burning permanently removes tokens from circulation, decreasing the token's total supply. Burning is only possible when the token contract authorizes it, which the [Register a token contract](register-a-token-contract.md) tutorial sets up. Additional details are available in the [tokens explanation](../../explanations/tokens.md) and the [token protocol reference](../../protocol-ref/token.md). + +## Prerequisites + +- [General prerequisites](../../tutorials/introduction.md#prerequisites) (Node.js / Dash SDK installed) +- A configured client: [Setup SDK Client](../setup-sdk-client.md) +- A registered token contract with a balance to burn: [Tutorial: Register a token contract](register-a-token-contract.md). Set the resulting contract ID as the `TOKEN_CONTRACT_ID` environment variable. + +## Code + +```{code-block} javascript +:caption: token-burn.mjs + +import { setupDashClient } from '../setupDashClient.mjs'; + +const { sdk, keyManager } = await setupDashClient(); +const { identity, identityKey, signer } = await keyManager.getAuth(); + +// TOKEN_CONTRACT_ID comes from token-register.mjs. +const dataContractId = process.env.TOKEN_CONTRACT_ID; +const tokenPosition = 0; +const amount = 1n; // Token amounts are bigint values + +try { + if (!dataContractId) { + throw new Error( + 'Set TOKEN_CONTRACT_ID in .env from token-register.mjs output.', + ); + } + + const tokenId = await sdk.tokens.calculateId(dataContractId, tokenPosition); + + await sdk.tokens.burn({ + dataContractId, + tokenPosition, + amount, + identityId: identity.id.toString(), + identityKey, + signer, + }); + + const balances = await sdk.tokens.identityBalances(identity.id, [tokenId]); + const totalSupply = await sdk.tokens.totalSupply(tokenId); + + console.log(`Burned ${amount} token`); + console.log('Token ID:', tokenId); + console.log(`Identity token balance: ${balances.get(tokenId) ?? 0n}`); + console.log('Total token supply:', totalSupply?.totalSupply ?? 0n); +} catch (e) { + console.error('Something went wrong:\n', e.message); +} +``` + +## What's Happening + +After connecting to the client, we get the auth key signer with `keyManager.getAuth()`. We then call `sdk.tokens.burn()` with the contract ID, token position, amount, and signing credentials to destroy 1 token from our balance. Token amounts are `bigint` values, which is why `1n` is written with the `n` suffix. + +After burning, we read back the identity's balance with `sdk.tokens.identityBalances()` and the new total supply with `sdk.tokens.totalSupply()` to confirm both have decreased. diff --git a/docs/tutorials/tokens/mint-tokens.md b/docs/tutorials/tokens/mint-tokens.md new file mode 100644 index 000000000..5924ea3c5 --- /dev/null +++ b/docs/tutorials/tokens/mint-tokens.md @@ -0,0 +1,68 @@ +```{eval-rst} +.. tutorials-mint-tokens: +``` + +# Mint tokens + +The purpose of this tutorial is to walk through the steps necessary to mint (issue) new [tokens](../../explanations/tokens.md), increasing the total supply. + +## Overview + +Minting issues new tokens to the contract owner, increasing the token's total supply up to its maximum. Minting is only possible when the token contract authorizes it, which the [Register a token contract](register-a-token-contract.md) tutorial sets up. Additional details are available in the [tokens explanation](../../explanations/tokens.md) and the [token protocol reference](../../protocol-ref/token.md). + +## Prerequisites + +- [General prerequisites](../../tutorials/introduction.md#prerequisites) (Node.js / Dash SDK installed) +- A configured client: [Setup SDK Client](../setup-sdk-client.md) +- A registered token contract: [Tutorial: Register a token contract](register-a-token-contract.md). Set the resulting contract ID as the `TOKEN_CONTRACT_ID` environment variable. + +## Code + +```{code-block} javascript +:caption: token-mint.mjs + +import { setupDashClient } from '../setupDashClient.mjs'; + +const { sdk, keyManager } = await setupDashClient(); +const { identity, identityKey, signer } = await keyManager.getAuth(); + +// TOKEN_CONTRACT_ID comes from token-register.mjs. +const dataContractId = process.env.TOKEN_CONTRACT_ID; +const tokenPosition = 0; +const amount = 10n; // Token amounts are bigint values + +try { + if (!dataContractId) { + throw new Error( + 'Set TOKEN_CONTRACT_ID in .env from token-register.mjs output.', + ); + } + + const tokenId = await sdk.tokens.calculateId(dataContractId, tokenPosition); + + await sdk.tokens.mint({ + dataContractId, + tokenPosition, + amount, + identityId: identity.id.toString(), + identityKey, + signer, + }); + + const balances = await sdk.tokens.identityBalances(identity.id, [tokenId]); + const totalSupply = await sdk.tokens.totalSupply(tokenId); + + console.log(`Minted ${amount} tokens`); + console.log('Token ID:', tokenId); + console.log(`Identity token balance: ${balances.get(tokenId) ?? 0n}`); + console.log('Total token supply:', totalSupply?.totalSupply ?? 0n); +} catch (e) { + console.error('Something went wrong:\n', e.message); +} +``` + +## What's Happening + +After connecting to the client, we get the auth key signer with `keyManager.getAuth()`. We then call `sdk.tokens.mint()` with the contract ID, token position, amount, and signing credentials to issue 10 new tokens to our identity. Token amounts are `bigint` values, which is why `10n` is written with the `n` suffix. + +After minting, we read back the identity's balance with `sdk.tokens.identityBalances()` and the new total supply with `sdk.tokens.totalSupply()` to confirm both have increased. diff --git a/docs/tutorials/tokens/register-a-token-contract.md b/docs/tutorials/tokens/register-a-token-contract.md new file mode 100644 index 000000000..b2ad8cc07 --- /dev/null +++ b/docs/tutorials/tokens/register-a-token-contract.md @@ -0,0 +1,203 @@ +```{eval-rst} +.. tutorials-register-token-contract: +``` + +# Register a token contract + +The purpose of this tutorial is to walk through the steps necessary to register a [token](../../explanations/tokens.md) on Dash Platform. + +## Overview + +Tokens on Dash Platform are defined inside a [data contract](../../explanations/platform-protocol-data-contract.md). A single contract can carry one or more tokens alongside its document types, and each token is identified by its position within the contract. Registering the contract creates the token, sets its supply limits, and establishes the rules that control who may mint, burn, transfer, or otherwise manage it. + +This tutorial registers an issuer-managed token: the contract owner controls minting and burning, and newly minted tokens always go to the owner identity. The token is not configured with advanced options. That keeps this tutorial focused on the normal token lifecycle used by the remaining token tutorials. Additional details are available in the [tokens explanation](../../explanations/tokens.md) and the [token protocol reference](../../protocol-ref/token.md). + +## Prerequisites + +- [General prerequisites](../../tutorials/introduction.md#prerequisites) (Node.js / Dash SDK installed) +- A platform address with a balance: [Tutorial: Create and Fund a Wallet](../../tutorials/create-and-fund-a-wallet.md) +- A configured client: [Setup SDK Client](../setup-sdk-client.md) +- A Dash Platform Identity: [Tutorial: Register an Identity](../../tutorials/identities-and-names/register-an-identity.md) + +## Code + +```{code-block} javascript +:caption: token-register.mjs + +import { + AuthorizedActionTakers, + ChangeControlRules, + DataContract, + TokenConfiguration, + TokenConfigurationConvention, + TokenConfigurationLocalization, + TokenDistributionRules, + TokenKeepsHistoryRules, + TokenMarketplaceRules, + TokenTradeMode, +} from '@dashevo/evo-sdk'; +import { setupDashClient } from '../setupDashClient.mjs'; + +const { sdk, keyManager } = await setupDashClient(); +const { identity, identityKey, signer } = await keyManager.getAuth(); + +const TOKEN_POSITION = 0; +const TOKEN_NAME = 'TutorialToken'; +const TOKEN_PLURAL = 'TutorialTokens'; +const TOKEN_BASE_SUPPLY = 100n; // Token amounts are bigint values +const TOKEN_MAX_SUPPLY = 1000n; + +// This contract includes one small document type so learners can still use the +// standard document tutorials with the same contract if they want to. +const documentSchemas = { + note: { + type: 'object', + properties: { + message: { + type: 'string', + position: 0, + }, + }, + additionalProperties: false, + }, +}; + +function createTutorialTokenConfiguration(ownerId) { + const contractOwner = AuthorizedActionTakers.ContractOwner(); + const noOne = AuthorizedActionTakers.NoOne(); + + const ownerRules = new ChangeControlRules({ + authorizedToMakeChange: contractOwner, + adminActionTakers: contractOwner, + isChangingAuthorizedActionTakersToNoOneAllowed: true, + isChangingAdminActionTakersToNoOneAllowed: true, + isSelfChangingAdminActionTakersAllowed: true, + }); + const lockedRules = new ChangeControlRules({ + authorizedToMakeChange: noOne, + adminActionTakers: noOne, + }); + + return new TokenConfiguration({ + conventions: new TokenConfigurationConvention( + { + en: new TokenConfigurationLocalization(false, TOKEN_NAME, TOKEN_PLURAL), + }, + 0, + ), + conventionsChangeRules: ownerRules, + baseSupply: TOKEN_BASE_SUPPLY, + maxSupply: TOKEN_MAX_SUPPLY, + keepsHistory: new TokenKeepsHistoryRules({ + isKeepingBurningHistory: true, + isKeepingMintingHistory: true, + isKeepingTransferHistory: true, + }), + maxSupplyChangeRules: lockedRules, + distributionRules: new TokenDistributionRules({ + newTokensDestinationIdentity: ownerId, + newTokensDestinationIdentityRules: ownerRules, + mintingAllowChoosingDestination: false, + mintingAllowChoosingDestinationRules: ownerRules, + perpetualDistributionRules: lockedRules, + changeDirectPurchasePricingRules: lockedRules, + }), + marketplaceRules: new TokenMarketplaceRules( + TokenTradeMode.NotTradeable(), + lockedRules, + ), + // Minting and burning are enabled so the next tutorials can demonstrate + // the normal issuer-managed token lifecycle. + manualMintingRules: ownerRules, + manualBurningRules: ownerRules, + freezeRules: lockedRules, + unfreezeRules: lockedRules, + destroyFrozenFundsRules: lockedRules, + emergencyActionRules: lockedRules, + mainControlGroupCanBeModified: noOne, + description: 'Issuer-managed token for Platform token tutorials.', + }); +} + +try { + const identityNonce = await sdk.identities.nonce(identity.id.toString()); + + const dataContract = new DataContract({ + ownerId: identity.id, + identityNonce: (identityNonce || 0n) + 1n, + schemas: documentSchemas, + tokens: { + [TOKEN_POSITION]: createTutorialTokenConfiguration( + identity.id.toString(), + ), + }, + fullValidation: true, + }); + + const publishedContract = await sdk.contracts.publish({ + dataContract, + identityKey, + signer, + }); + + const contractId = + publishedContract.id?.toString() || publishedContract.toJSON?.()?.id; + + if (!contractId) { + const publishResult = publishedContract.toJSON?.() ?? publishedContract; + throw new Error( + `Contract publish returned no id: ${JSON.stringify(publishResult)}`, + ); + } + + const tokenId = await sdk.tokens.calculateId(contractId, TOKEN_POSITION); + + console.log('Token contract registered:\n', publishedContract.toJSON()); + console.log('Token position:', TOKEN_POSITION); + console.log('Token ID:', tokenId); + console.log('Initial owner token balance:', TOKEN_BASE_SUPPLY.toString()); + console.log('Maximum token supply:', TOKEN_MAX_SUPPLY.toString()); +} catch (e) { + console.error('Something went wrong:\n', e.message); +} +``` + +:::{attention} +Make a note of the returned contract ID. The remaining token tutorials read it from the `TOKEN_CONTRACT_ID` environment variable, so set it before running them. + +Use the contract ID from the published contract output, not the token ID printed at the end: + +```text +TOKEN_CONTRACT_ID= +``` +::: + +## What's Happening + +After initializing the client, we get the auth key signer from the key manager. We then register a data contract that carries a token at position 0. The token starts with a base supply of 100 (minted to the owner) and a maximum supply of 1,000. Token amounts are always `bigint` values, which is why they are written with the `n` suffix (for example, `100n`). + +Minting and burning are enabled and restricted to the contract owner, so the [mint](mint-tokens.md) and [burn](burn-tokens.md) tutorials work out of the box. The token also keeps a full history of mints, burns, and transfers. + +To register the contract, we fetch the identity's current nonce and increment it, build a `DataContract` with the document schemas and the token configuration, and call `sdk.contracts.publish()`. Finally, we derive the token ID from the contract ID and token position with `sdk.tokens.calculateId()`. + +:::{dropdown} Token configuration details +The token's behaviour is defined by a `TokenConfiguration`. Each group of rules is a `ChangeControlRules` object that says who may perform an action and who may change that permission. The tutorial uses two presets: + +- `ownerRules` — the contract owner is authorized (`AuthorizedActionTakers.ContractOwner()`). +- `lockedRules` — no one is authorized (`AuthorizedActionTakers.NoOne()`), permanently disabling the action. + +The main groups: + +- **Supply** — `baseSupply` is minted to the owner at registration; `maxSupply` caps the total. `maxSupplyChangeRules` is locked, so the cap cannot be changed later. +- **Minting and burning** — `manualMintingRules` and `manualBurningRules` use `ownerRules`, letting the owner issue and destroy tokens. +- **History** — `keepsHistory` records minting, burning, and transfer events so they can be queried later. +- **Distribution** — `distributionRules` sends newly minted tokens to the owner (`newTokensDestinationIdentity`) and prevents minting from choosing a different destination (`mintingAllowChoosingDestination: false`). Perpetual distribution is locked, so there is no automated recurring distribution. +- **Marketplace and pricing** — `marketplaceRules` uses `TokenTradeMode.NotTradeable()`, so the token cannot be listed for direct purchase, and the trade mode itself is locked. The permission to set a direct-purchase price (`changeDirectPurchasePricingRules`, defined with the distribution rules) is also locked. +- **Freeze and emergency actions** — `freezeRules`, `unfreezeRules`, `destroyFrozenFundsRules`, and `emergencyActionRules` are locked in this example. + +See the [tokens explanation](../../explanations/tokens.md) and the [token protocol reference](../../protocol-ref/token.md) for the full set of configuration fields. +::: + +:::{tip} +See this in an example app: [DashMint Lab — Contract schema](../example-apps/dashmint-lab.md#contract-schema) defines a token alongside its NFT documents, and [DashMint Lab — DashMint token flow](../example-apps/dashmint-lab.md#dashmint-token-flow) shows how the token is used. +::: diff --git a/docs/tutorials/tokens/retrieve-token-info.md b/docs/tutorials/tokens/retrieve-token-info.md new file mode 100644 index 000000000..38b3af04e --- /dev/null +++ b/docs/tutorials/tokens/retrieve-token-info.md @@ -0,0 +1,88 @@ +```{eval-rst} +.. tutorials-retrieve-token-info: +``` + +# Retrieve token info + +The purpose of this tutorial is to walk through the steps necessary to retrieve information about a [token](../../explanations/tokens.md), including its contract details, total supply, status, and identity balances. + +## Overview + +Once a token contract is registered, its metadata and balances can be queried without submitting a state transition. This tutorial retrieves the token's contract info, total supply, status, and the token balances held by two identities. Additional details are available in the [tokens explanation](../../explanations/tokens.md) and the [token protocol reference](../../protocol-ref/token.md). + +## Prerequisites + +- [General prerequisites](../../tutorials/introduction.md#prerequisites) (Node.js / Dash SDK installed) +- A configured client: [Setup SDK Client](../setup-sdk-client.md) +- A registered token contract: [Tutorial: Register a token contract](register-a-token-contract.md). Set the resulting contract ID as the `TOKEN_CONTRACT_ID` environment variable. + +## Code + +```{code-block} javascript +:caption: token-info.mjs + +import { setupDashClient } from '../setupDashClient.mjs'; + +const { sdk, keyManager } = await setupDashClient(); + +// TOKEN_CONTRACT_ID comes from token-register.mjs. +const dataContractId = process.env.TOKEN_CONTRACT_ID; +const tokenPosition = 0; + +// Default recipient (testnet). Replace or override via RECIPIENT_ID. +const recipientId = + process.env.RECIPIENT_ID || '7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC'; + +try { + if (!dataContractId) { + throw new Error( + 'Set TOKEN_CONTRACT_ID in .env from token-register.mjs output.', + ); + } + + const tokenId = await sdk.tokens.calculateId(dataContractId, tokenPosition); + const contractInfo = await sdk.tokens.contractInfo(tokenId); + const totalSupply = await sdk.tokens.totalSupply(tokenId); + const statuses = await sdk.tokens.statuses([tokenId]); + const identityBalances = await sdk.tokens.identityBalances( + keyManager.identityId, + [tokenId], + ); + const recipientBalances = await sdk.tokens.identityBalances(recipientId, [ + tokenId, + ]); + + // A token only has a status record once one is published on-chain (e.g. via + // an emergency pause), so the Map is empty for a freshly registered token. + const status = statuses.get(tokenId); + + console.log('Token ID:', tokenId); + console.log('Token contract info:\n', contractInfo?.toJSON()); + console.log( + 'Token status:', + status ? status.isPaused : '(no status published)', + ); + console.log('Total token supply:', totalSupply?.totalSupply ?? 0n); + console.log(`Identity token balance: ${identityBalances.get(tokenId) ?? 0n}`); + console.log( + `Recipient token balance: ${recipientBalances.get(tokenId) ?? 0n}`, + ); +} catch (e) { + console.error('Something went wrong:\n', e.message); +} +``` + +## What's Happening + +After connecting to the client, we derive the token ID from the contract ID and token position with `sdk.tokens.calculateId()`. We then query several pieces of information: + +- `sdk.tokens.contractInfo()` returns the token's contract metadata. +- `sdk.tokens.totalSupply()` returns the number of tokens currently in circulation. +- `sdk.tokens.statuses()` returns a Map of token statuses. A status record only exists once one is published on-chain (for example, after an emergency pause), so the Map is empty for a freshly registered token. We fall back to `(no status published)` in that case. +- `sdk.tokens.identityBalances()` returns each identity's token balance, keyed by token ID. + +The recipient defaults to a demo testnet identity so the script can run without extra setup. Set `RECIPIENT_ID` in the .env file to your own second identity when you want the recipient balance check to reflect an identity you control. + +:::{tip} +See this in an example app: [DashMint Lab — DashMint token flow](../example-apps/dashmint-lab.md#dashmint-token-flow) reads the signed-in identity's token balance to display remaining mint capacity. +::: diff --git a/docs/tutorials/tokens/transfer-tokens-to-an-identity.md b/docs/tutorials/tokens/transfer-tokens-to-an-identity.md new file mode 100644 index 000000000..3d3d0c7bd --- /dev/null +++ b/docs/tutorials/tokens/transfer-tokens-to-an-identity.md @@ -0,0 +1,96 @@ +```{eval-rst} +.. tutorials-transfer-tokens-to-identity: +``` + +# Transfer tokens to an identity + +The purpose of this tutorial is to walk through the steps necessary to transfer [tokens](../../explanations/tokens.md) from one identity to another. + +## Overview + +Transferring moves tokens from the sender's balance to a recipient identity. The total supply is unchanged; only the balances of the two identities are affected. Additional details are available in the [tokens explanation](../../explanations/tokens.md) and the [token protocol reference](../../protocol-ref/token.md). + +## Prerequisites + +- [General prerequisites](../../tutorials/introduction.md#prerequisites) (Node.js / Dash SDK installed) +- A configured client: [Setup SDK Client](../setup-sdk-client.md) +- A registered token contract with a balance to transfer: [Tutorial: Register a token contract](register-a-token-contract.md). Set the resulting contract ID as the `TOKEN_CONTRACT_ID` environment variable. +- A second Dash Platform Identity to receive the tokens: [Tutorial: Register an Identity](../../tutorials/identities-and-names/register-an-identity.md) + +## Code + +```{code-block} javascript +:caption: token-transfer.mjs + +import { setupDashClient } from '../setupDashClient.mjs'; + +const { sdk, keyManager } = await setupDashClient(); +const { identity, identityKey, signer } = await keyManager.getTransfer(); + +// TOKEN_CONTRACT_ID comes from token-register.mjs. +const dataContractId = process.env.TOKEN_CONTRACT_ID; +const tokenPosition = 0; + +// Default recipient (testnet). Replace or override via RECIPIENT_ID. +const recipientId = + process.env.RECIPIENT_ID || '7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC'; +const amount = 1n; + +try { + if (!dataContractId) { + throw new Error( + 'Set TOKEN_CONTRACT_ID in .env from token-register.mjs output.', + ); + } + + const senderId = identity.id.toString(); + if (recipientId === senderId) { + throw new Error('Cannot transfer tokens to yourself.'); + } + + const tokenId = await sdk.tokens.calculateId(dataContractId, tokenPosition); + const balancesBefore = await sdk.tokens.identityBalances(recipientId, [ + tokenId, + ]); + + console.log( + `Recipient token balance before transfer: ${balancesBefore.get(tokenId) ?? 0n}`, + ); + + await sdk.tokens.transfer({ + dataContractId, + tokenPosition, + amount, + senderId, + recipientId, + identityKey, + signer, + }); + + const balancesAfter = await sdk.tokens.identityBalances(recipientId, [ + tokenId, + ]); + + console.log( + `Transferred ${amount} token${amount === 1n ? '' : 's'} from ${senderId} to ${recipientId}`, + ); + console.log('Token ID:', tokenId); + console.log( + `Recipient token balance after transfer: ${balancesAfter.get(tokenId) ?? 0n}`, + ); +} catch (e) { + console.error('Something went wrong:\n', e.message); +} +``` + +## What's Happening + +After connecting to the client, we get the transfer key signer with `keyManager.getTransfer()`. Token transfers are authorized with the identity's transfer key rather than its auth key. + +We derive the token ID with `sdk.tokens.calculateId()` and read the recipient's balance before the transfer so the change is visible. A guard rejects transfers where the recipient matches the sender, since an identity cannot transfer tokens to itself. + +We then call `sdk.tokens.transfer()` with the contract ID, token position, amount, sender ID, recipient ID, and signing credentials to move 1 token. After the transfer, we read the recipient's balance again with `sdk.tokens.identityBalances()` to confirm it increased. The recipient defaults to a demo testnet identity, but set `RECIPIENT_ID` in the .env file to your own second identity when you want to verify a transfer to an identity you control. + +:::{tip} +See this in an example app: [DashMint Lab — Transfer DashMint tokens](../example-apps/dashmint-lab.md#transfer-dashmint-tokens). +::: diff --git a/scripts/tutorial-sync/tutorial-code-map.yml b/scripts/tutorial-sync/tutorial-code-map.yml index c1296b9af..f8954a871 100644 --- a/scripts/tutorial-sync/tutorial-code-map.yml +++ b/scripts/tutorial-sync/tutorial-code-map.yml @@ -188,6 +188,33 @@ mappings: block_id: caption: document-delete.mjs + # -- Tokens -- + + - source: 3-Tokens/token-register.mjs + doc: tokens/register-a-token-contract.md + block_id: + caption: token-register.mjs + + - source: 3-Tokens/token-info.mjs + doc: tokens/retrieve-token-info.md + block_id: + caption: token-info.mjs + + - source: 3-Tokens/token-mint.mjs + doc: tokens/mint-tokens.md + block_id: + caption: token-mint.mjs + + - source: 3-Tokens/token-burn.mjs + doc: tokens/burn-tokens.md + block_id: + caption: token-burn.mjs + + - source: 3-Tokens/token-transfer.mjs + doc: tokens/transfer-tokens-to-an-identity.md + block_id: + caption: token-transfer.mjs + # -- Example apps -- # DashMint Lab — React + TypeScript NFT app. Every SDK operation lives in