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: