diff --git a/.gitignore b/.gitignore index 2f2a1c2b7..4ccc56e36 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ *.exe _build **/.DS_Store +.local/ .vscode diff --git a/conf.py b/conf.py index 93ea45729..c0648ca40 100644 --- a/conf.py +++ b/conf.py @@ -39,10 +39,12 @@ '.DS_Store', 'README.md', '.devcontainer', + 'local', 'scripts', 'img/dev/gifs/README.md', 'docs/other', - 'docs/ai-prompt.md' + 'docs/ai-prompt.md', + 'platform-src' ] # The master toctree document. diff --git a/docs/tutorials/connecting-to-testnet.md b/docs/tutorials/connecting-to-testnet.md index 33f91fe1c..4462ba753 100644 --- a/docs/tutorials/connecting-to-testnet.md +++ b/docs/tutorials/connecting-to-testnet.md @@ -8,7 +8,7 @@ The purpose of this tutorial is to walk through the steps necessary to access th ## Overview -Platform services are provided via a combination of HTTP and gRPC connections to DAPI, and some connections to an Insight API. Although one could interact with DAPI by connecting to these directly, or by using [DAPI-client](https://github.com/dashpay/platform/tree/master/packages/js-dapi-client), the easiest approach is to use the [JavaScript Dash SDK](https://github.com/dashpay/platform/tree/master/packages/js-dash-sdk). +Platform services are provided via a combination of HTTP and gRPC connections to DAPI. The easiest approach is to use the [Dash Evo SDK](https://www.npmjs.com/package/@dashevo/evo-sdk), which handles connection management automatically. ## Prerequisites @@ -18,94 +18,60 @@ Platform services are provided via a combination of HTTP and gRPC connections to ### 1. Install the Dash SDK -The JavaScript SDK package is available from npmjs.com and can be installed by running `npm install dash` from the command line: +The JavaScript SDK package is available from npmjs.com and can be installed by running `npm install @dashevo/evo-sdk` from the command line: ```shell -npm install dash +npm install @dashevo/evo-sdk ``` ### 2. Connect to Dash Platform -:::{tip} -The JavaScript Dash SDK connects to mainnet by default. To connect to other networks, -set the `network` option when instantiating the client as shown in the following example. -::: - -Create a file named `dashConnect.js` with the following contents. Then run it by typing `node dashConnect.js` from the command line: +Create a file named `dashConnect.mjs` with the following contents. Then run it by typing `node dashConnect.mjs` from the command line: -```javascript dashConnect.js -const Dash = require('dash'); +```{code-block} javascript +:caption: dashConnect.mjs -const client = new Dash.Client({ network: 'testnet' }); +import { EvoSDK } from '@dashevo/evo-sdk'; -async function connect() { - return await client.getDAPIClient().core.getBestBlockHash(); +try { + const sdk = EvoSDK.testnetTrusted(); + await sdk.connect(); + const status = await sdk.system.status(); + console.log('Connected. System status:\n', status.toJSON()); +} catch (e) { + console.error('Failed to fetch system status:', e.message); } - -connect() - .then((d) => console.log('Connected. Best block hash:\n', d)) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); ``` -Once this returns successfully, you're ready to begin developing! See the [Quickstart](../tutorials/introduction.md#quickstart) for recommended next steps. For details on all SDK options and methods, please refer to the [SDK documentation](../sdk-js/overview.md). - -## Connect to a Devnet +Once this returns successfully, you're ready to begin developing! See the [Quickstart](../tutorials/introduction.md#quickstart) for recommended next steps. For details on SDK methods, please refer to the [SDK documentation](https://dashpay.github.io/evo-sdk-website/docs.html). -The SDK also supports connecting to development networks (devnets). Since devnets can be created by anyone, the client library will be unaware of them unless connection information is provided using one of the options described below. +## Connect to a Local Devnet -### Connect via Seed +The SDK supports connecting to a local development network managed by [dashmate](https://github.com/dashpay/platform/tree/master/packages/dashmate). The `local` factory methods expect a dashmate-managed environment with a quorum sidecar running at `127.0.0.1:2444`. -Using a seed node is the preferred method in most cases. The client uses the provided seed node to a retrieve a list of available masternodes on the network so requests can be spread across the entire network. +```{code-block} javascript +:caption: localConnect.mjs -```javascript -const Dash = require('dash'); +import { EvoSDK } from '@dashevo/evo-sdk'; -const client = new Dash.Client({ - network: 'testnet', - seeds: [{ - host: 'seed-1.testnet.networks.dash.org:1443', - }], -}); - -async function connect() { - return await client.getDAPIClient().core.getBestBlockHash(); +try { + const sdk = EvoSDK.localTrusted(); + await sdk.connect(); + const status = await sdk.system.status(); + console.log('Connected. System status:\n', status.toJSON()); +} catch (e) { + console.error('Failed to fetch system status:', e.message); } - -connect() - .then((d) => console.log('Connected. Best block hash:\n', d)) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); ``` -### Connect via Address - -Custom addresses may be directly specified via `dapiAddresses` in cases where it is beneficial to know exactly what node(s) are being accessed (e.g. debugging, local development, etc.). - -```javascript -const Dash = require('dash'); - -const client = new Dash.Client({ - dapiAddresses: [ - '127.0.0.1:3000:3010', - '127.0.0.2:3000:3010', - ], -}); - -async function connect() { - return await client.getDAPIClient().core.getBestBlockHash(); -} - -connect() - .then((d) => console.log('Connected. Best block hash:\n', d)) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); -``` +:::{note} +The WASM-based SDK currently only supports connecting to known networks (testnet, mainnet, local) via the built-in factory methods. Connecting to remote devnets with custom addresses is not yet supported. +::: ## Connect Directly to DAPI (Optional) :::{attention} -Normally, the Dash SDK, dapi-client, or another library should be used to interact with DAPI. Connecting directly may be helpful for debugging in some cases, but generally is not required. +Normally, the Dash SDK or another library should be used to interact with DAPI. Connecting directly may be helpful for debugging in some cases, but generally is not required. ::: The example below demonstrates retrieving the hash of the best block hash directly from a DAPI node via command line and several languages: diff --git a/docs/tutorials/contracts-and-documents/delete-documents.md b/docs/tutorials/contracts-and-documents/delete-documents.md index 0e9b7e6f5..65ab30e05 100644 --- a/docs/tutorials/contracts-and-documents/delete-documents.md +++ b/docs/tutorials/contracts-and-documents/delete-documents.md @@ -4,57 +4,60 @@ # Delete documents -In this tutorial we will update delete data from Dash Platform. Data is stored in the form of [documents](../../explanations/platform-protocol-document.md) which are encapsulated in a [state transition](../../explanations/platform-protocol-state-transition.md) before being submitted to DAPI. +In this tutorial we will delete data from Dash Platform. Data is stored in the form of [documents](../../explanations/platform-protocol-document.md) which are encapsulated in a [state transition](../../explanations/platform-protocol-state-transition.md) before being submitted to DAPI. ## Prerequisites - [General prerequisites](../../tutorials/introduction.md#prerequisites) (Node.js / Dash SDK installed) -- A wallet mnemonic with some funds in it: [Tutorial: Create and Fund a Wallet](../../tutorials/create-and-fund-a-wallet.md) +- 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) -- Access to a previously created document (e.g., one created using the [Submit Documents tutorial](../../tutorials/contracts-and-documents/submit-documents.md)) +- A Dash Platform Identity: [Tutorial: Register an Identity](../../tutorials/identities-and-names/register-an-identity.md) +- (Optional) A Dash Platform Contract ID: [Tutorial: Register a Data Contract](../../tutorials/contracts-and-documents/register-a-data-contract.md) — a default testnet tutorial contract is provided +- An existing document (e.g., one created using the [Submit Documents tutorial](../../tutorials/contracts-and-documents/submit-documents.md)) ## Code -```javascript -const setupDashClient = require('../setupDashClient'); - -const client = setupDashClient(); - -const deleteNoteDocument = async () => { - const { platform } = client; - const identity = await platform.identities.get('an identity ID goes here'); - const documentId = 'an existing document ID goes here'; - - // Retrieve the existing document - const [document] = await client.platform.documents.get( - 'tutorialContract.note', - { where: [['$id', '==', documentId]] }, - ); - - // Sign and submit the document delete transition - await platform.documents.broadcast({ delete: [document] }, identity); - return document; -}; - -deleteNoteDocument() - .then((d) => console.log('Document deleted:\n', d.toJSON())) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); +```{code-block} javascript +:caption: deleteDocument.mjs + +import { setupDashClient } from '../setupDashClient.mjs'; + +const { sdk, keyManager } = await setupDashClient(); +const { identity, identityKey, signer } = await keyManager.getAuth(); + +// Default tutorial contract (testnet). Replace or override via DATA_CONTRACT_ID. +const DATA_CONTRACT_ID = + process.env.DATA_CONTRACT_ID ?? + 'FW3DHrQiG24VqzPY4ARenMgjEPpBNuEQTZckV8hbVCG4'; + +// Replace with your existing document ID +const DOCUMENT_ID = 'YOUR_DOCUMENT_ID'; + +try { + // Delete the document from the platform + await sdk.documents.delete({ + document: { + id: DOCUMENT_ID, + ownerId: identity.id, + dataContractId: DATA_CONTRACT_ID, + documentTypeName: 'note', + }, + identityKey, + signer, + }); + + console.log('Document deleted successfully'); +} catch (e) { + console.error('Something went wrong:\n', e.message); +} ``` -:::{tip} -The example above shows how access to contract documents via `.` syntax (e.g. `tutorialContract.note`) can be enabled by passing a contract identity to the constructor. Please refer to the [Dash SDK documentation](https://github.com/dashpay/platform/blob/master/packages/js-dash-sdk/docs/getting-started/multiple-apps.md) for details. -::: - ## What's happening -After we initialize the Client, we retrieve the document to be deleted via `platform.documents.get` using its `id`. - -Once the document has been retrieved, we must submit it to [DAPI](../../explanations/dapi.md). Documents are submitted in batches that may contain multiple documents to be created, replaced, or deleted. In this example, a single document is being deleted. +After we initialize the client, we get the auth key signer from the key manager. We then call `sdk.documents.delete()` with an object identifying the document to delete — its `id`, `ownerId`, `dataContractId`, and `documentTypeName` — along with the signing credentials. -The `platform.documents.broadcast` method takes the document batch (e.g. `{delete: [documents[0]]}`) and an identity parameter. Internally, it creates a [State Transition](../../explanations/platform-protocol-state-transition.md) containing the previously created document, signs the state transition, and submits the signed state transition to DAPI. +Internally, the method creates a [State Transition](../../explanations/platform-protocol-state-transition.md) containing the document deletion instruction, signs the state transition, and submits it to DAPI. Only the document's owner can delete it. :::{note} -:class: note -Since the SDK does not cache wallet information, lengthy re-syncs (5+ minutes) may be required for some Core chain wallet operations. See [Wallet Operations](../setup-sdk-client.md#wallet-operations) for options. +You do not need to retrieve the full document before deleting it. The `sdk.documents.delete()` method only requires the document's identifying fields (`id`, `ownerId`, `dataContractId`, `documentTypeName`). ::: diff --git a/docs/tutorials/contracts-and-documents/register-a-data-contract.md b/docs/tutorials/contracts-and-documents/register-a-data-contract.md index 1eb09a10d..a49b410b9 100644 --- a/docs/tutorials/contracts-and-documents/register-a-data-contract.md +++ b/docs/tutorials/contracts-and-documents/register-a-data-contract.md @@ -4,14 +4,18 @@ # Register a data contract -In this tutorial we will register a data contract. +The purpose of this tutorial is to walk through the steps necessary to register a [data contract](../../explanations/platform-protocol-data-contract.md) on Dash Platform. + +## Overview + +Data contracts define the schema (structure) of the data an application stores on Dash Platform. Contracts are registered on the platform and referenced by applications when creating or querying documents. Additional details are available in the [data contract explanation](../../explanations/platform-protocol-data-contract.md). ## Prerequisites -* [General prerequisites](../../tutorials/introduction.md#prerequisites) (Node.js / Dash SDK installed) -* A wallet mnemonic with some funds in it: [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) +- [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 @@ -33,7 +37,7 @@ data. The fifth tab shows a data contract configured to store contract history. This allows all contract revisions to be retrieved in the future as needed. -The sixth tab shows a data contract configured for creating NFTs. It allows documents to be deleted, transferred, or traded. It also limits document creation to the contract owner. See the [NFT explanation section](../../explanations/nft.md) for more details about NFTs on Dash Platform. **_Note: the JavaScript SDK supports NFT contract registration, but does not currently support trades or transfers._** +The sixth tab shows a data contract configured for creating NFTs. It allows documents to be deleted, transferred, or traded. It also limits document creation to the contract owner. See the [NFT explanation section](../../explanations/nft.md) for more details about NFTs on Dash Platform. :::{attention} Since Platform v0.25.16, each document property must assign `position` value to support [backwards compatibility](https://github.com/dashpay/platform/pull/1594) for contract updates. @@ -241,330 +245,374 @@ array of bytes (e.g. a NodeJS Buffer). ::: :::: -:::{note +:::{note} Please refer to the [data contract reference page](../../reference/data-contracts.md) for more comprehensive details related to contracts and documents. ::: ### Registering the data contract -The following examples demonstrate the details of creating contracts using the features [described above](#defining-contract-documents). Also, note that the sixth tab shows a data contract with contract history enabled to store each contract revision so it can be retrieved as needed for future reference: +The following examples demonstrate the details of creating contracts using the features [described above](#defining-contract-documents). Also, note that the fifth tab shows a data contract with contract history enabled to store each contract revision so it can be retrieved as needed for future reference: ::::{tab-set} :::{tab-item} 1. Minimal contract :sync: minimal ```javascript -const setupDashClient = require('../setupDashClient'); - -const client = setupDashClient(); - -const registerContract = async () => { - const { platform } = client; - const identity = await platform.identities.get('an identity ID goes here'); - - const contractDocuments = { - note: { - type: 'object', - properties: { - message: { - type: 'string', - "position": 0 - }, +import { DataContract } from '@dashevo/evo-sdk'; +import { setupDashClient } from '../setupDashClient.mjs'; + +const { sdk, keyManager } = await setupDashClient(); +const { identity, identityKey, signer } = await keyManager.getAuth(); + +// Define the document schemas for the contract +const documentSchemas = { + note: { + type: 'object', + properties: { + message: { + type: 'string', + position: 0, }, - additionalProperties: false, }, - }; - - const contract = await platform.contracts.create(contractDocuments, identity); - console.dir({ contract: contract.toJSON() }); - - // Sign and submit the data contract - await platform.contracts.publish(contract, identity); - return contract; + additionalProperties: false, + }, }; -registerContract() - .then((d) => console.log('Contract registered:\n', d.toJSON())) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); +try { + // Get the next identity nonce for contract creation + const identityNonce = await sdk.identities.nonce(identity.id.toString()); + + // Create the data contract + const dataContract = new DataContract({ + ownerId: identity.id, + identityNonce: (identityNonce || 0n) + 1n, + schemas: documentSchemas, + fullValidation: true, + }); + + // Publish the contract to the platform + const publishedContract = await sdk.contracts.publish({ + dataContract, + identityKey, + signer, + }); + + console.log('Contract registered:\n', publishedContract.toJSON()); +} catch (e) { + console.error('Something went wrong:\n', e.message); +} ``` ::: :::{tab-item} 2. Indexed :sync: indexed ```javascript -const setupDashClient = require('../setupDashClient'); - -const client = setupDashClient(); - -const registerContract = async () => { - const { platform } = client; - const identity = await platform.identities.get('an identity ID goes here'); - - const contractDocuments = { - note: { - type: 'object', - indices: [{ - name: 'ownerId', - properties: [{ $ownerId: 'asc' }], - unique: false, - }], - properties: { - message: { - type: 'string', - "position": 0 - }, +import { DataContract } from '@dashevo/evo-sdk'; +import { setupDashClient } from '../setupDashClient.mjs'; + +const { sdk, keyManager } = await setupDashClient(); +const { identity, identityKey, signer } = await keyManager.getAuth(); + +// Define the document schemas for the contract +const documentSchemas = { + note: { + type: 'object', + indices: [{ + name: 'ownerId', + properties: [{ $ownerId: 'asc' }], + unique: false, + }], + properties: { + message: { + type: 'string', + position: 0, }, - additionalProperties: false, }, - }; - - const contract = await platform.contracts.create(contractDocuments, identity); - console.dir({ contract: contract.toJSON() }); - - await platform.contracts.publish(contract, identity); - return contract; + additionalProperties: false, + }, }; -registerContract() - .then((d) => console.log('Contract registered:\n', d.toJSON())) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); +try { + // Get the next identity nonce for contract creation + const identityNonce = await sdk.identities.nonce(identity.id.toString()); + + // Create the data contract + const dataContract = new DataContract({ + ownerId: identity.id, + identityNonce: (identityNonce || 0n) + 1n, + schemas: documentSchemas, + fullValidation: true, + }); + + // Publish the contract to the platform + const publishedContract = await sdk.contracts.publish({ + dataContract, + identityKey, + signer, + }); + + console.log('Contract registered:\n', publishedContract.toJSON()); +} catch (e) { + console.error('Something went wrong:\n', e.message); +} ``` ::: :::{tab-item} 3. Timestamps :sync: timestamp ```javascript -const setupDashClient = require('../setupDashClient'); - -const client = setupDashClient(); - -const registerContract = async () => { - const { platform } = client; - const identity = await platform.identities.get('an identity ID goes here'); - - const contractDocuments = { - note: { - type: 'object', - properties: { - message: { - type: 'string', - "position": 0 - }, +import { DataContract } from '@dashevo/evo-sdk'; +import { setupDashClient } from '../setupDashClient.mjs'; + +const { sdk, keyManager } = await setupDashClient(); +const { identity, identityKey, signer } = await keyManager.getAuth(); + +// Define the document schemas for the contract +const documentSchemas = { + note: { + type: 'object', + properties: { + message: { + type: 'string', + position: 0, }, - required: ['$createdAt', '$updatedAt'], - additionalProperties: false, }, - }; - - const contract = await platform.contracts.create(contractDocuments, identity); - console.dir({ contract: contract.toJSON() }); - - await platform.contracts.publish(contract, identity); - return contract; + required: ['$createdAt', '$updatedAt'], + additionalProperties: false, + }, }; -registerContract() - .then((d) => console.log('Contract registered:\n', d.toJSON())) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); +try { + // Get the next identity nonce for contract creation + const identityNonce = await sdk.identities.nonce(identity.id.toString()); + + // Create the data contract + const dataContract = new DataContract({ + ownerId: identity.id, + identityNonce: (identityNonce || 0n) + 1n, + schemas: documentSchemas, + fullValidation: true, + }); + + // Publish the contract to the platform + const publishedContract = await sdk.contracts.publish({ + dataContract, + identityKey, + signer, + }); + + console.log('Contract registered:\n', publishedContract.toJSON()); +} catch (e) { + console.error('Something went wrong:\n', e.message); +} ``` ::: :::{tab-item} 4. Binary data :sync: binary ```javascript -const setupDashClient = require('../setupDashClient'); - -const client = setupDashClient(); - -const registerContract = async () => { - const { platform } = client; - const identity = await platform.identities.get('an identity ID goes here'); - - const contractDocuments = { - block: { - type: 'object', - properties: { - hash: { - type: 'array', - byteArray: true, - maxItems: 64, - description: 'Store block hashes', - "position": 0 - }, +import { DataContract } from '@dashevo/evo-sdk'; +import { setupDashClient } from '../setupDashClient.mjs'; + +const { sdk, keyManager } = await setupDashClient(); +const { identity, identityKey, signer } = await keyManager.getAuth(); + +// Define the document schemas for the contract +const documentSchemas = { + block: { + type: 'object', + properties: { + hash: { + type: 'array', + byteArray: true, + maxItems: 64, + description: 'Store block hashes', + position: 0, }, - additionalProperties: false, }, - }; - - const contract = await platform.contracts.create(contractDocuments, identity); - console.dir({ contract: contract.toJSON() }, { depth: 5 }); - - await platform.contracts.publish(contract, identity); - return contract; + additionalProperties: false, + }, }; -registerContract() - .then((d) => console.log('Contract registered:\n', d.toJSON())) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); +try { + // Get the next identity nonce for contract creation + const identityNonce = await sdk.identities.nonce(identity.id.toString()); + + // Create the data contract + const dataContract = new DataContract({ + ownerId: identity.id, + identityNonce: (identityNonce || 0n) + 1n, + schemas: documentSchemas, + fullValidation: true, + }); + + // Publish the contract to the platform + const publishedContract = await sdk.contracts.publish({ + dataContract, + identityKey, + signer, + }); + + console.log('Contract registered:\n', publishedContract.toJSON()); +} catch (e) { + console.error('Something went wrong:\n', e.message); +} ``` ::: :::{tab-item} 5. Contract with history :sync: history ```javascript -const setupDashClient = require('../setupDashClient'); - -const client = setupDashClient(); - -const registerContract = async () => { - const { platform } = client; - const identity = await platform.identities.get('an identity ID goes here'); - - const contractDocuments = { - note: { - type: 'object', - properties: { - message: { - type: 'string', - "position": 0 - }, +import { DataContract } from '@dashevo/evo-sdk'; +import { setupDashClient } from '../setupDashClient.mjs'; + +const { sdk, keyManager } = await setupDashClient(); +const { identity, identityKey, signer } = await keyManager.getAuth(); + +// Define the document schemas for the contract +const documentSchemas = { + note: { + type: 'object', + properties: { + message: { + type: 'string', + position: 0, }, - additionalProperties: false, }, - }; + additionalProperties: false, + }, +}; + +try { + // Get the next identity nonce for contract creation + const identityNonce = await sdk.identities.nonce(identity.id.toString()); - const contract = await platform.contracts.create(contractDocuments, identity); - contract.setConfig({ + // Create the data contract + const dataContract = new DataContract({ + ownerId: identity.id, + identityNonce: (identityNonce || 0n) + 1n, + schemas: documentSchemas, + fullValidation: true, + }); + + // Enable storing of contract history + dataContract.setConfig({ canBeDeleted: false, - readonly: false, // Make contract read-only - keepsHistory: true, // Enable storing of contract history + readonly: false, + keepsHistory: true, documentsKeepHistoryContractDefault: false, documentsMutableContractDefault: true, - }) - console.dir({ contract: contract.toJSON() }); - - // Sign and submit the data contract - await platform.contracts.publish(contract, identity); - return contract; -}; - -registerContract() - .then((d) => console.log('Contract registered:\n', d.toJSON())) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); + }); + + // Publish the contract to the platform + const publishedContract = await sdk.contracts.publish({ + dataContract, + identityKey, + signer, + }); + + console.log('Contract registered:\n', publishedContract.toJSON()); +} catch (e) { + console.error('Something went wrong:\n', e.message); +} ``` ::: :::{tab-item} 6. NFT Contract :sync: nft ```javascript -const setupDashClient = require('../setupDashClient'); - -const client = setupDashClient(); - -const registerContract = async () => { - const { platform } = client; - const identity = await platform.identities.get('an identity ID goes here'); - - const contractDocuments = { - card: { - type: "object", - documentsMutable: false, // Documents cannot be modified/replaced - canBeDeleted: true, // Documents can be deleted - transferable: 1, // Transfers enabled - tradeMode: 1, // Direct purchase trades enabled - creationRestrictionMode: 1, // Only the contract owner can mint - properties: { - name: { - type: "string", - description: "Name of the card", - minLength: 0, - maxLength: 63, - position: 0 - }, - description: { - type: "string", - description: "Description of the card", - minLength: 0, - maxLength: 256, - position: 1 - }, - attack: { - type: "integer", - description: "Attack power of the card", - position: 2 - }, - defense: { - type: "integer", - description: "Defense level of the card", - position: 3 - } +import { DataContract } from '@dashevo/evo-sdk'; +import { setupDashClient } from '../setupDashClient.mjs'; + +const { sdk, keyManager } = await setupDashClient(); +const { identity, identityKey, signer } = await keyManager.getAuth(); + +// Define the document schemas for the contract +const documentSchemas = { + card: { + type: 'object', + documentsMutable: false, // true = documents can be modified (replaced) + canBeDeleted: true, // true = documents can be deleted + transferable: 1, // 0 = transfers disabled; 1 = transfers enabled + tradeMode: 1, // 0 = no trading; 1 = direct purchases + creationRestrictionMode: 1, // 0 = anyone can mint; 1 = only contract owner can mint + properties: { + name: { + type: 'string', + description: 'Name of the card', + minLength: 0, + maxLength: 63, + position: 0, }, - indices: [ - { - name: "owner", - properties: [ - { - $ownerId: "asc" - } - ] - }, - { - name: "attack", - properties: [ - { - attack: "asc" - } - ] - }, - { - name: "defense", - properties: [ - { - defense: "asc" - } - ] - } - ], - required: [ - "name", - "attack", - "defense" - ], - additionalProperties: false - } - } - - const contract = await platform.contracts.create(contractDocuments, identity); - console.dir({ contract: contract.toJSON() }); - - // Sign and submit the data contract - await platform.contracts.publish(contract, identity); - return contract; + description: { + type: 'string', + description: 'Description of the card', + minLength: 0, + maxLength: 256, + position: 1, + }, + attack: { + type: 'integer', + description: 'Attack power of the card', + position: 2, + }, + defense: { + type: 'integer', + description: 'Defense level of the card', + position: 3, + }, + }, + indices: [ + { + name: 'owner', + properties: [{ $ownerId: 'asc' }], + }, + { + name: 'attack', + properties: [{ attack: 'asc' }], + }, + { + name: 'defense', + properties: [{ defense: 'asc' }], + }, + ], + required: ['name', 'attack', 'defense'], + additionalProperties: false, + }, }; -registerContract() - .then((d) => console.log('Contract registered:\n', d.toJSON())) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); +try { + // Get the next identity nonce for contract creation + const identityNonce = await sdk.identities.nonce(identity.id.toString()); + + // Create the data contract + const dataContract = new DataContract({ + ownerId: identity.id, + identityNonce: (identityNonce || 0n) + 1n, + schemas: documentSchemas, + fullValidation: true, + }); + + // Publish the contract to the platform + const publishedContract = await sdk.contracts.publish({ + dataContract, + identityKey, + signer, + }); + + console.log('Contract registered:\n', publishedContract.toJSON()); +} catch (e) { + console.error('Something went wrong:\n', e.message); +} ``` ::: :::: :::{attention} -Make a note of the returned data contract `id` as it will be used used in subsequent tutorials throughout the documentation. +Make a note of the returned data contract ID as it will be used in subsequent tutorials throughout the documentation. ::: ## What's Happening -After we initialize the Client, we create an object defining the documents this data contract requires (e.g. a `note` document in the example). The `platform.contracts.create` method takes two arguments: a contract definitions JSON-schema object and an identity. The contract definitions object consists of the document types being created (e.g. `note`). It defines the properties and any indices. - -Once the data contract has been created, we still need to submit it to DAPI. The `platform.contracts.publish` method takes a data contract and an identity parameter. Internally, it creates a State Transition containing the previously created contract, signs the state transition, and submits the signed state transition to DAPI. A response will only be returned if an error is encountered. +After we initialize the client, we get the auth key signer from the key manager. We then define the document schemas for our contract (e.g. a `note` document). -:::{note} -:class: note -Since the SDK does not cache wallet information, lengthy re-syncs (5+ minutes) may be required for some Core chain wallet operations. See [Wallet Operations](../setup-sdk-client.md#wallet-operations) for options. -::: +To create the contract, we first fetch the identity's current nonce and increment it. We then create a `DataContract` object with the owner identity, nonce, and document schemas. Finally, we call `sdk.contracts.publish()` with the contract and signing credentials to submit it to the network. diff --git a/docs/tutorials/contracts-and-documents/retrieve-a-data-contract.md b/docs/tutorials/contracts-and-documents/retrieve-a-data-contract.md index 4ca0f9918..0e513a2e9 100644 --- a/docs/tutorials/contracts-and-documents/retrieve-a-data-contract.md +++ b/docs/tutorials/contracts-and-documents/retrieve-a-data-contract.md @@ -14,44 +14,24 @@ In this tutorial we will retrieve the data contract created in the [Register a D ## Code -### Retrieving a data contract +```{code-block} javascript +:caption: retrieveContract.mjs -```javascript -const setupDashClient = require('../setupDashClient'); +import { setupDashClient } from '../setupDashClient.mjs'; -const client = setupDashClient(); +const { sdk } = await setupDashClient(); -const retrieveContract = async () => { - const contractId = '8cvMFwa2YbEsNNoc1PXfTacy2PVq2SzVnkZLeQSzjfi6'; - return client.platform.contracts.get(contractId); -}; +// Default tutorial contract (testnet). Replace or override via DATA_CONTRACT_ID. +const DATA_CONTRACT_ID = + process.env.DATA_CONTRACT_ID ?? + 'FW3DHrQiG24VqzPY4ARenMgjEPpBNuEQTZckV8hbVCG4'; -retrieveContract() - .then((d) => console.dir(d.toJSON(), { depth: 5 })) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); -``` - -### Updating the client app list - -:::{note} -In many cases it may be desirable to work with a newly retrieved data contract using the `.` syntax (e.g. `dpns.domain`). Data contracts that were created after the client was initialized or not included in the initial client options can be added via `client.getApps().set(...)`. -::: - -```javascript -const Dash = require('dash'); -const { PlatformProtocol: { Identifier } } = Dash; - -const myContractId = 'a contract ID'; -const client = new Dash.Client({ network: 'testnet' }); - -client.platform.contracts.get(myContractId) - .then((myContract) => { - client.getApps().set('myNewContract', { - contractId: Identifier.from(myContractId), - contract: myContract, - }); - }); +try { + const contract = await sdk.contracts.fetch(DATA_CONTRACT_ID); + console.log('Contract retrieved:\n', contract.toJSON()); +} catch (e) { + console.error('Something went wrong:\n', e.message); +} ``` ## Example Data Contract @@ -60,8 +40,8 @@ The following example response shows a retrieved contract: ```json { - "$format_version": "0", - "id": "8cvMFwa2YbEsNNoc1PXfTacy2PVq2SzVnkZLeQSzjfi6", + "$format_version": "1", + "id": "FW3DHrQiG24VqzPY4ARenMgjEPpBNuEQTZckV8hbVCG4", "config": { "$format_version": "0", "canBeDeleted": false, @@ -69,19 +49,30 @@ The following example response shows a retrieved contract: "keepsHistory": false, "documentsKeepHistoryContractDefault": false, "documentsMutableContractDefault": true, + "documentsCanBeDeletedContractDefault": true, "requiresIdentityEncryptionBoundedKey": null, "requiresIdentityDecryptionBoundedKey": null }, - "version": 1, - "ownerId": "AsdMKouqE5NB7CeQFi4wr5oj8vFUYTtdSvxFtAvtCbhh", + "version": 2, + "ownerId": "CtnBVhWjGhtPihUHKS132b9f9zSKMxRHDA6wSDtjRofy", "schemaDefs": null, "documentSchemas": { "note": { "type": "object", - "properties": { "message": { "type": "string" } }, + "properties": ["Object"], "additionalProperties": false } - } + }, + "createdAt": null, + "updatedAt": null, + "createdAtBlockHeight": null, + "updatedAtBlockHeight": null, + "createdAtEpoch": null, + "updatedAtEpoch": null, + "groups": {}, + "tokens": {}, + "keywords": [], + "description": null } ``` @@ -91,6 +82,4 @@ Please refer to the [data contract reference page](../../reference/data-contract ## What's Happening -After we initialize the Client, we request a contract. The `platform.contracts.get` method takes a single argument: a contract ID. After the contract is retrieved, it is displayed on the console. - -The second code example shows how the contract could be assigned a name to make it easily accessible without initializing an additional client. +After we initialize the client, we call `sdk.contracts.fetch()` with a contract ID. After the contract is retrieved, it is displayed on the console. diff --git a/docs/tutorials/contracts-and-documents/retrieve-data-contract-history.md b/docs/tutorials/contracts-and-documents/retrieve-data-contract-history.md index 466f0ebdb..817039d46 100644 --- a/docs/tutorials/contracts-and-documents/retrieve-data-contract-history.md +++ b/docs/tutorials/contracts-and-documents/retrieve-data-contract-history.md @@ -17,96 +17,72 @@ information. ## Code -### Retrieving data contract history - -```javascript -const setupDashClient = require('../setupDashClient'); - -const client = setupDashClient(); - -const retrieveContractHistory = async () => { - const contractId = '8cvMFwa2YbEsNNoc1PXfTacy2PVq2SzVnkZLeQSzjfi6' - return await client.platform.contracts.history(contractId, 0, 10, 0); -}; - -retrieveContractHistory() - .then((d) => { - Object.entries(d).forEach(([key, value]) => { - client.platform.dpp.dataContract - .createFromObject(value) - .then((contract) => console.dir(contract.toJSON(), { depth: 5 })); - }); - }) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); +```{code-block} javascript +:caption: retrieveContractHistory.mjs + +import { setupDashClient } from '../setupDashClient.mjs'; + +const { sdk } = await setupDashClient(); + +// Default tutorial contract with history (testnet). Replace or override via DATA_CONTRACT_ID. +const DATA_CONTRACT_ID = + process.env.DATA_CONTRACT_ID ?? + '5J4VPym1Bnc2Ap9bbo9wNw6fZLGsCzDM7ZScdzcggN1r'; + +try { + const history = await sdk.contracts.getHistory({ + dataContractId: DATA_CONTRACT_ID, + }); + + for (const [timestamp, contract] of history) { + console.log(`Version at ${timestamp}:`, contract.toJSON()); + } +} catch (e) { + console.error('Something went wrong:\n', e.message); +} ``` ## Example data contract history The following example response shows a retrieved contract history: -```json -[ - { - "$format_version": "0", - "id": "BWgzcW4XRhmYKzup1xY8fMi3ZHGG1Hf8fD9Rm3e3bopm", - "config": { - "$format_version": "0", - "canBeDeleted": false, - "readonly": false, - "keepsHistory": true, - "documentsKeepHistoryContractDefault": false, - "documentsMutableContractDefault": true, - "requiresIdentityEncryptionBoundedKey": null, - "requiresIdentityDecryptionBoundedKey": null - }, - "version": 1, - "ownerId": "DKFKmJ58ZTDddvviDJwDyCznDMxd9Y6bsJcBN5Xp8m5w", - "schemaDefs": null, - "documentSchemas": { - "note": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "additionalProperties": false - } - } +```text +Version at 1772722751435: { + '$format_version': '1', + id: '5J4VPym1Bnc2Ap9bbo9wNw6fZLGsCzDM7ZScdzcggN1r', + config: { + '$format_version': '1', + canBeDeleted: false, + readonly: false, + keepsHistory: true, + documentsKeepHistoryContractDefault: false, + documentsMutableContractDefault: true, + documentsCanBeDeletedContractDefault: true, + requiresIdentityEncryptionBoundedKey: null, + requiresIdentityDecryptionBoundedKey: null, + sizedIntegerTypes: true }, - { - "$format_version": "0", - "id": "BWgzcW4XRhmYKzup1xY8fMi3ZHGG1Hf8fD9Rm3e3bopm", - "config": { - "$format_version": "0", - "canBeDeleted": false, - "readonly": false, - "keepsHistory": true, - "documentsKeepHistoryContractDefault": false, - "documentsMutableContractDefault": true, - "requiresIdentityEncryptionBoundedKey": null, - "requiresIdentityDecryptionBoundedKey": null - }, - "version": 2, - "ownerId": "DKFKmJ58ZTDddvviDJwDyCznDMxd9Y6bsJcBN5Xp8m5w", - "schemaDefs": null, - "documentSchemas": { - "note": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "author": { - "type": "string" - } - }, - "additionalProperties": false - } + version: 1, + ownerId: 'FKZZFDTfGdSWUmL2g7H9e46pMJMPQp9DHQcvjrsS6884', + schemaDefs: null, + documentSchemas: { + note: { + type: 'object', + properties: [Object], + additionalProperties: false } - } -] + }, + createdAt: 1772722751435, + updatedAt: null, + createdAtBlockHeight: 273561, + updatedAtBlockHeight: null, + createdAtEpoch: 14269, + updatedAtEpoch: null, + groups: {}, + tokens: {}, + keywords: [], + description: null +} ``` :::{note} @@ -115,7 +91,9 @@ Please refer to the [data contract reference page](../../reference/data-contract ## What's Happening -After we initialize the Client, we request a contract's history. The `platform.contracts.history` -method takes four arguments: a contract ID, timestamp to start at, number of revisions to retrieve, -and a number to offset the start of the records. After the contract history is retrieved, it is -displayed on the console. +After we initialize the client, we request a contract's history. The contract ID defaults to a +testnet tutorial contract but can be overridden via the `DATA_CONTRACT_ID` environment variable. The +`sdk.contracts.getHistory` method takes an object with a `dataContractId` property. It returns a +`Map` where each key is a timestamp (`BigInt`) and each value is the contract at that revision. +After the contract history is retrieved, we iterate over the entries and display each revision on the +console. diff --git a/docs/tutorials/contracts-and-documents/retrieve-documents.md b/docs/tutorials/contracts-and-documents/retrieve-documents.md index d766ba000..50a66ee23 100644 --- a/docs/tutorials/contracts-and-documents/retrieve-documents.md +++ b/docs/tutorials/contracts-and-documents/retrieve-documents.md @@ -9,138 +9,76 @@ In this tutorial we will retrieve some of the current data from a data contract. ## Prerequisites - [General prerequisites](../../tutorials/introduction.md#prerequisites) (Node.js / Dash SDK installed) -- A configured client: [Setup SDK Client](../setup-sdk-client.md)- A Dash Platform Contract ID: [Tutorial: Register a Data Contract](../../tutorials/contracts-and-documents/register-a-data-contract.md) +- A configured client: [Setup SDK Client](../setup-sdk-client.md) +- (Optional) A Dash Platform Contract ID: [Tutorial: Register a Data Contract](../../tutorials/contracts-and-documents/register-a-data-contract.md) — a default testnet tutorial contract is provided ## Code -```javascript -const setupDashClient = require('../setupDashClient'); +```{code-block} javascript +:caption: getDocuments.mjs -const client = setupDashClient(); +import { setupDashClient } from '../setupDashClient.mjs'; -const getDocuments = async () => { - return client.platform.documents.get('tutorialContract.note', { - limit: 2, // Only retrieve 2 document +const { sdk } = await setupDashClient(); + +// Default tutorial contract (testnet). Replace or override via DATA_CONTRACT_ID. +const DATA_CONTRACT_ID = + process.env.DATA_CONTRACT_ID ?? + 'FW3DHrQiG24VqzPY4ARenMgjEPpBNuEQTZckV8hbVCG4'; + +try { + const results = await sdk.documents.query({ + dataContractId: DATA_CONTRACT_ID, + documentTypeName: 'note', + limit: 2, }); -}; - -getDocuments() - .then((d) => { - for (const n of d) { - console.log('Document:\n', n.toJSON()); - } - }) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); -``` -:::{tip} -The example above shows how access to contract documents via `.` syntax (e.g. `tutorialContract.note`) can be enabled by passing a contract identity to the constructor. Please refer to the [Dash SDK documentation](https://github.com/dashpay/platform/blob/master/packages/js-dash-sdk/docs/getting-started/multiple-apps.md) for details. -::: + for (const [id, doc] of results) { + console.log('Document:', id.toString(), doc.toJSON()); + } +} catch (e) { + console.error('Something went wrong:\n', e.message); +} +``` ### Queries -The example code uses a very basic query to return only one result. More extensive querying capabilities are covered in the [query syntax reference](../../reference/query-syntax.md). +The example code uses a very basic query to return only two results. More extensive querying capabilities are covered in the [query syntax reference](../../reference/query-syntax.md). ## Example Document -The following examples show the structure of a `note` document (from the data contract registered in the tutorial) returned from the SDK when retrieved with various methods. - -The values returned by `.toJSON()` include the base document properties (prefixed with `$`) present in all documents along with the data contract defined properties. +The following examples show the structure of a `note` document returned from the SDK. The values returned by `.toJSON()` include the base document properties (prefixed with `$`) present in all documents along with the data contract defined properties (the `message` propoerty in the example document). :::{note} Note: When using `.toJSON()`, binary data is displayed as a base64 string (since JSON is a text-based format). ::: -The values returned by `.getData()` (and also shown in the console.dir() `data` property) represent _only_ the properties defined in the `note` document described by the [tutorial data contract](../../tutorials/contracts-and-documents/register-a-data-contract.md#code). - -::::{tab-set} -:::{tab-item} .toJSON() ```json -{ - "$protocolVersion": 0, - "$id": "6LpCQhkXYV2vqkv1UWByew4xQ6BaxxnGkhfMZsN3SV9u", - "$type": "note", - "$dataContractId": "3iaEhdyAVbmSjd59CT6SCrqPjfAfMdPTc8ksydgqSaWE", - "$ownerId": "CEPMcuBgAWeaCXiP2gJJaStANRHW6b158UPvL1C8zw2W", + { + "$id": "5CL7qwbGPi4P6jqhat5pQxzSZ9PPxvNkDU8tU9yXYyzt", + "$ownerId": "FKZZFDTfGdSWUmL2g7H9e46pMJMPQp9DHQcvjrsS6884", "$revision": 1, - "message": "Tutorial CI Test @ Fri, 23 Jul 2021 13:12:13 GMT" -} -``` -::: - -:::{tab-item} .getData() -```json -{ - "Tutorial CI Test @ Fri, 23 Jul 2021 13:12:13 GMT" -} -``` -::: - -:::{tab-item} .data.message -```text -Tutorial CI Test @ Fri, 23 Jul 2021 13:12:13 GMT -``` -::: - -:::{tab-item} console.dir(document) -```text -Document { - dataContract: DataContract { - protocolVersion: 0, - id: Identifier(32) [Uint8Array] [ - 40, 93, 196, 112, 38, 188, 51, 122, - 149, 59, 21, 39, 147, 119, 87, 53, - 236, 60, 97, 42, 31, 82, 135, 120, - 68, 188, 55, 153, 226, 198, 181, 139 - ], - ownerId: Identifier(32) [Uint8Array] [ - 166, 222, 98, 87, 193, 19, 82, 37, - 50, 118, 210, 64, 103, 122, 28, 155, - 168, 21, 198, 134, 142, 151, 153, 136, - 46, 64, 223, 74, 215, 153, 158, 167 - ], - schema: 'https://schema.dash.org/dpp-0-4-0/meta/data-contract', - documents: { note: [Object] }, - '$defs': undefined, - binaryProperties: { note: {} }, - metadata: Metadata { blockHeight: 526, coreChainLockedHeight: 542795 } - }, - entropy: undefined, - protocolVersion: 0, - id: Identifier(32) [Uint8Array] [ - 79, 93, 213, 226, 76, 79, 205, 191, - 165, 190, 68, 28, 8, 83, 61, 226, - 222, 248, 48, 235, 147, 110, 181, 229, - 7, 66, 65, 230, 100, 194, 192, 156 - ], - type: 'note', - dataContractId: Identifier(32) [Uint8Array] [ - 40, 93, 196, 112, 38, 188, 51, 122, - 149, 59, 21, 39, 147, 119, 87, 53, - 236, 60, 97, 42, 31, 82, 135, 120, - 68, 188, 55, 153, 226, 198, 181, 139 - ], - ownerId: Identifier(32) [Uint8Array] [ - 166, 222, 98, 87, 193, 19, 82, 37, - 50, 118, 210, 64, 103, 122, 28, 155, - 168, 21, 198, 134, 142, 151, 153, 136, - 46, 64, 223, 74, 215, 153, 158, 167 - ], - revision: 1, - data: { message: 'Tutorial CI Test @ Fri, 23 Jul 2021 13:12:13 GMT' }, - metadata: Metadata { blockHeight: 526, coreChainLockedHeight: 542795 } + "$createdAt": null, + "$updatedAt": null, + "$transferredAt": null, + "$createdAtBlockHeight": null, + "$updatedAtBlockHeight": null, + "$transferredAtBlockHeight": null, + "$createdAtCoreBlockHeight": null, + "$updatedAtCoreBlockHeight": null, + "$transferredAtCoreBlockHeight": null, + "$creatorId": null, + "$dataContractId": "FW3DHrQiG24VqzPY4ARenMgjEPpBNuEQTZckV8hbVCG4", + "$type": "note", + "$entropy": "jsO295ymKBeMAiAwrSsaDX7qjYD/9i+Q8g9MIDx/xik=" + "message": "Tutorial Test @ Wed, 04 Mar 2026 22:37:48 GMT", } ``` -::: -:::: ## What's happening -After we initialize the Client, we request some documents. The `client.platform.documents.get` method takes two arguments: a record locator and a query object. The records locator consists of an app name (e.g. `tutorialContract`) and the top-level document type requested, (e.g. `note`). +After we initialize the Client, we request documents using `sdk.documents.query()`. The method takes an object with the `dataContractId`, `documentTypeName`, and optional query parameters like `where`, `orderBy`, `limit`, `startAt`, and `startAfter`. -:::{note} -Access to the DPNS contract is built into the Dash SDK. DPNS documents may be accessed via the `dpns` app name (e.g. `dpns.domain`). -::: +Results are returned as a `Map` where each key is a document ID and each value is the document object. We iterate over the entries using `for (const [id, doc] of results)`. -If you need more than the first 100 documents, you'll have to make additional requests with `startAt` incremented by 100 each time. In the future, the Dash SDK may return documents with paging information to make this easier and reveal how many documents are returned in total. +If you need more than the default number of documents, use the `startAt` or `startAfter` parameters for pagination. diff --git a/docs/tutorials/contracts-and-documents/submit-documents.md b/docs/tutorials/contracts-and-documents/submit-documents.md index ed8da51e5..092590d82 100644 --- a/docs/tutorials/contracts-and-documents/submit-documents.md +++ b/docs/tutorials/contracts-and-documents/submit-documents.md @@ -9,62 +9,53 @@ In this tutorial we will submit some data to an application on Dash Platform. Da ## Prerequisites - [General prerequisites](../../tutorials/introduction.md#prerequisites) (Node.js / Dash SDK installed) -- A wallet mnemonic with some funds in it: [Tutorial: Create and Fund a Wallet](../../tutorials/create-and-fund-a-wallet.md) +- 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) -- A Dash Platform Contract ID: [Tutorial: Register a Data Contract](../../tutorials/contracts-and-documents/register-a-data-contract.md) +- (Optional) A Dash Platform Contract ID: [Tutorial: Register a Data Contract](../../tutorials/contracts-and-documents/register-a-data-contract.md) — a default testnet tutorial contract is provided ## Code -```javascript -const setupDashClient = require('../setupDashClient'); - -const client = setupDashClient(); - -const submitNoteDocument = async () => { - const { platform } = client; - const identity = await platform.identities.get('an identity ID goes here'); - - const docProperties = { - message: `Tutorial Test @ ${new Date().toUTCString()}`, - }; - - // Create the note document - const noteDocument = await platform.documents.create( - 'tutorialContract.note', - identity, - docProperties, - ); - - const documentBatch = { - create: [noteDocument], // Document(s) to create - replace: [], // Document(s) to update - delete: [], // Document(s) to delete - }; - // Sign and submit the document(s) - await platform.documents.broadcast(documentBatch, identity); - return noteDocument; -}; - -submitNoteDocument() - .then((d) => console.log(d.toJSON())) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); +```{code-block} javascript +:caption: submitDocument.mjs + +import { Document } from '@dashevo/evo-sdk'; +import { setupDashClient } from '../setupDashClient.mjs'; + +const { sdk, keyManager } = await setupDashClient(); +const { identity, identityKey, signer } = await keyManager.getAuth(); + +// Default tutorial contract (testnet). Replace or override via DATA_CONTRACT_ID. +const DATA_CONTRACT_ID = + process.env.DATA_CONTRACT_ID ?? + 'FW3DHrQiG24VqzPY4ARenMgjEPpBNuEQTZckV8hbVCG4'; + +try { + // Create a new document + const document = new Document({ + properties: { + message: `Tutorial Test @ ${new Date().toUTCString()}`, + }, + documentTypeName: 'note', + dataContractId: DATA_CONTRACT_ID, + ownerId: identity.id, + }); + + // Submit the document to the platform + await sdk.documents.create({ + document, + identityKey, + signer, + }); + + console.log('Document submitted:\n', document.toJSON()); +} catch (e) { + console.error('Something went wrong:\n', e.message); +} ``` -:::{tip} -The example above shows how access to contract documents via `.` syntax (e.g. `tutorialContract.note`) can be enabled by passing a contract identity to the constructor. Please refer to the [Dash SDK documentation](https://github.com/dashpay/platform/blob/master/packages/js-dash-sdk/docs/getting-started/multiple-apps.md) for details. -::: - ## What's happening -After we initialize the Client, we create a document that matches the structure defined by the data contract of the application being referenced (e.g. a `note` document for the contract registered in the [data contract tutorial](../../tutorials/contracts-and-documents/register-a-data-contract.md#code)). The `platform.documents.create` method takes three arguments: a document locator, an identity, and the document data. The document locator consists of an application name (e.g. `tutorialContract`) and the document type being created (e.g. `note`). The document data should contain values for each of the properties defined for it in the data contract (e.g. `message` for the tutorial contract's note). - -Once the document has been created, we still need to submit it to [DAPI](../../explanations/dapi.md). Documents are submitted in batches that may contain multiple documents to be created, replaced, or deleted. In this example, a single document is being created. The `documentBatch` object defines the action to be completed for the document (the empty action arrays - `replace` and `delete` in this example - may be excluded and are shown for reference only here). - -The `platform.documents.broadcast` method then takes the document batch and an identity parameter. Internally, it creates a [State Transition](../../explanations/platform-protocol-state-transition.md) containing the previously created document, signs the state transition, and submits the signed state transition to DAPI. +After we initialize the Client via `setupDashClient()`, we get the auth key signer from the key manager. We then create a `Document` object with the properties defined by the data contract (e.g. a `message` for the `note` document type), along with the contract ID and document type name. -:::{note} -:class: note -Since the SDK does not cache wallet information, lengthy re-syncs (5+ minutes) may be required for some Core chain wallet operations. See [Wallet Operations](../setup-sdk-client.md#wallet-operations) for options. -::: +The `sdk.documents.create()` method takes the document and signing credentials. Internally, it creates a [State Transition](../../explanations/platform-protocol-state-transition.md) containing the document, signs the state transition, and submits it to DAPI. diff --git a/docs/tutorials/contracts-and-documents/update-a-data-contract.md b/docs/tutorials/contracts-and-documents/update-a-data-contract.md index b3ba8fb1a..8fa424d9e 100644 --- a/docs/tutorials/contracts-and-documents/update-a-data-contract.md +++ b/docs/tutorials/contracts-and-documents/update-a-data-contract.md @@ -4,7 +4,7 @@ # Update a data contract -Since Dash Platform v0.22, it is possible to update existing data contracts in certain backwards-compatible ways. This includes: +It is possible to update existing data contracts in certain backwards-compatible ways. This includes: * Adding new documents * Adding new optional properties to existing documents @@ -15,7 +15,7 @@ In this tutorial we will update an existing data contract. ## Prerequisites * [General prerequisites](../../tutorials/introduction.md#prerequisites) (Node.js / Dash SDK installed) -* A wallet mnemonic with some funds in it: [Tutorial: Create and Fund a Wallet](../../tutorials/create-and-fund-a-wallet.md) +* 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) * A Dash Platform Contract ID: [Tutorial: Register a Data Contract](../../tutorials/contracts-and-documents/register-a-data-contract.md) @@ -27,68 +27,125 @@ The following examples demonstrate updating an existing contract to add a new pr ::::{tab-set} :::{tab-item} Minimal contract ```javascript -const setupDashClient = require('../setupDashClient'); +import { setupDashClient } from '../setupDashClient.mjs'; -const client = setupDashClient(); +const { sdk, keyManager } = await setupDashClient(); +const { identityKey, signer } = await keyManager.getAuth(); -const updateContract = async () => { - const { platform } = client; - const identity = await platform.identities.get('an identity ID goes here'); +// Edit these values for your environment +// Your contract ID from the Register a Data Contract tutorial +const DATA_CONTRACT_ID = + process.env.DATA_CONTRACT_ID ?? + 'YOUR_DATA_CONTRACT_ID'; +const DOCUMENT_TYPE = 'note'; - const existingDataContract = await platform.contracts.get('a contract ID goes here'); - const documentSchema = existingDataContract.getDocumentSchema('note'); +if (!DATA_CONTRACT_ID || DATA_CONTRACT_ID === 'YOUR_DATA_CONTRACT_ID') { + throw new Error('Set DATA_CONTRACT_ID (env var or in code) to your contract ID from the Register a Data Contract tutorial'); +} - documentSchema.properties.author = { +try { + const existingContract = await sdk.contracts.fetch(DATA_CONTRACT_ID); + + // Increment the contract version + existingContract.version += 1; + + // Clone schemas, then add a new "author" property to the DOCUMENT_TYPE schema + const updatedSchemas = structuredClone(existingContract.schemas); + updatedSchemas[DOCUMENT_TYPE].properties.author = { type: 'string', position: 1, }; - existingDataContract.setDocumentSchema('note', documentSchema); + // Apply the updated schemas (enable full validation) + existingContract.setSchemas(updatedSchemas, undefined, true, undefined); - // Sign and submit the data contract - await platform.contracts.update(existingDataContract, identity); - return existingDataContract; -}; + // Submit the update + await sdk.contracts.update({ + dataContract: existingContract, + identityKey, + signer, + }); -updateContract() - .then((d) => console.log('Contract updated:\n', d.toJSON())) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); + console.log('Contract updated:\n', existingContract.toJSON()); +} catch (e) { + console.error('Something went wrong:\n', e.message); +} ``` ::: :::{tab-item} Contract with history + +Note: This code includes a workaround for a known bug ([dashpay/platform#3165](https://github.com/dashpay/platform/issues/3165)) where `sdk.contracts.fetch()` returns undefined for contracts with `keepsHistory: true`. Instead, it uses `sdk.contracts.getHistory()` to retrieve the contract and takes the latest revision. It also calls `setConfig()` to enable history tracking on the contract before submitting the update. + ```javascript -const setupDashClient = require('../setupDashClient'); +import { setupDashClient } from '../setupDashClient.mjs'; + +const { sdk, keyManager } = await setupDashClient(); +const { identityKey, signer } = await keyManager.getAuth(); + +// Edit these values for your environment +// Your contract ID from the Register a Data Contract tutorial +const DATA_CONTRACT_ID = + process.env.DATA_CONTRACT_ID ?? + 'YOUR_DATA_CONTRACT_ID'; +const DOCUMENT_TYPE = 'note'; + +if (!DATA_CONTRACT_ID || DATA_CONTRACT_ID === 'YOUR_DATA_CONTRACT_ID') { + throw new Error('Set DATA_CONTRACT_ID (env var or in code) to your contract ID from the Register a Data Contract tutorial'); +} + +try { + // Workaround: sdk.contracts.fetch() returns undefined for contracts with + // keepsHistory: true due to a proof verification bug (dashpay/platform#3165). + // Use getHistory() and take the latest version instead. + // Note: for contracts with many revisions, history results may be paginated + // and the last entry here may not be the true latest version. + const history = await sdk.contracts.getHistory({ + dataContractId: DATA_CONTRACT_ID, + }); -const client = setupDashClient(); + let existingContract; + for (const [, contract] of history) { + existingContract = contract; + } -const updateContract = async () => { - const { platform } = client; - const identity = await platform.identities.get('an identity ID goes here'); + if (!existingContract) { + throw new Error(`Contract ${DATA_CONTRACT_ID} not found`); + } - const existingDataContract = await platform.contracts.get('a contract ID goes here'); - const documentSchema = existingDataContract.getDocumentSchema('note'); + // Increment the contract version + existingContract.version += 1; - documentSchema.properties.author = { + // Clone schemas, then add a new "author" property to the DOCUMENT_TYPE schema + const updatedSchemas = structuredClone(existingContract.schemas); + updatedSchemas[DOCUMENT_TYPE].properties.author = { type: 'string', position: 1, }; - existingDataContract.setDocumentSchema('note', documentSchema); - existingDataContract.setConfig({ - keepsHistory: true, // Enable storing of contract history + // Apply the updated schemas (enable full validation) + existingContract.setSchemas(updatedSchemas, undefined, true, undefined); + + // Enable storing of contract history + existingContract.setConfig({ + canBeDeleted: false, + readonly: false, + keepsHistory: true, + documentsKeepHistoryContractDefault: false, + documentsMutableContractDefault: true, }); - // Sign and submit the data contract - await platform.contracts.update(existingDataContract, identity); - return existingDataContract; -}; + // Submit the update + await sdk.contracts.update({ + dataContract: existingContract, + identityKey, + signer, + }); -updateContract() - .then((d) => console.log('Contract updated:\n', d.toJSON())) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); + console.log('Contract updated:\n', existingContract.toJSON()); +} catch (e) { + console.error('Something went wrong:\n', e.message); +} ``` ::: :::: @@ -99,11 +156,6 @@ Please refer to the [data contract reference page](../../reference/data-contract ## What's Happening -After we initialize the Client, we retrieve an existing contract owned by our identity. We then get the contract's document schema and modify a document (adding an `author` property to the `note` document in the example). The `setDocumentSchema` method takes two arguments: the name of the document schema to be updated and the object containing the updated document types. - -Once the data contract has been updated, we still need to submit it to DAPI. The `platform.contracts.update` method takes a data contract and an identity parameter. Internally, it creates a State Transition containing the updated contract, signs the state transition, and submits the signed state transition to DAPI. A response will only be returned if an error is encountered. +After we initialize the client, we get the auth key signer from the key manager. We then fetch the existing contract and modify the contract's document schema (adding an `author` property to the `note` document schema). -:::{note} -:class: note -Since the SDK does not cache wallet information, lengthy re-syncs (5+ minutes) may be required for some Core chain wallet operations. See [Wallet Operations](../setup-sdk-client.md#wallet-operations) for options. -::: +We clone the existing schemas, add the new property, then apply them with `setSchemas()`. After incrementing the contract version, we call `sdk.contracts.update()` with the modified contract and signing credentials to submit the update to the network. diff --git a/docs/tutorials/contracts-and-documents/update-documents.md b/docs/tutorials/contracts-and-documents/update-documents.md index 80faa3bbb..6d39c5451 100644 --- a/docs/tutorials/contracts-and-documents/update-documents.md +++ b/docs/tutorials/contracts-and-documents/update-documents.md @@ -9,53 +9,74 @@ In this tutorial we will update existing data on Dash Platform. Data is stored i ## Prerequisites - [General prerequisites](../../tutorials/introduction.md#prerequisites) (Node.js / Dash SDK installed) -- A wallet mnemonic with some funds in it: [Tutorial: Create and Fund a Wallet](../../tutorials/create-and-fund-a-wallet.md) +- 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) -- Access to a previously created document (e.g., one created using the [Submit Documents tutorial](../../tutorials/contracts-and-documents/submit-documents.md)) +- A Dash Platform Identity: [Tutorial: Register an Identity](../../tutorials/identities-and-names/register-an-identity.md) +- (Optional) A Dash Platform Contract ID: [Tutorial: Register a Data Contract](../../tutorials/contracts-and-documents/register-a-data-contract.md) — a default testnet tutorial contract is provided +- An existing document (e.g., one created using the [Submit Documents tutorial](../../tutorials/contracts-and-documents/submit-documents.md)) ## Code -```javascript -const setupDashClient = require('../setupDashClient'); - -const client = setupDashClient(); - -const updateNoteDocument = async () => { - const { platform } = client; - const identity = await platform.identities.get('an identity ID goes here'); - const documentId = 'an existing document ID goes here'; - - // Retrieve the existing document - const [document] = await client.platform.documents.get( - 'tutorialContract.note', - { where: [['$id', '==', documentId]] }, - ); - - // Update document - document.set('message', `Updated document @ ${new Date().toUTCString()}`); - - // Sign and submit the document replace transition - await platform.documents.broadcast({ replace: [document] }, identity); - return document; -}; - -updateNoteDocument() - .then((d) => console.log('Document updated:\n', d.toJSON())) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); +```{code-block} javascript +:caption: updateDocument.mjs + +import { Document } from '@dashevo/evo-sdk'; +import { setupDashClient } from '../setupDashClient.mjs'; + +const { sdk, keyManager } = await setupDashClient(); +const { identity, identityKey, signer } = await keyManager.getAuth(); + +// Default tutorial contract (testnet). Replace or override via DATA_CONTRACT_ID. +const DATA_CONTRACT_ID = + process.env.DATA_CONTRACT_ID ?? + 'FW3DHrQiG24VqzPY4ARenMgjEPpBNuEQTZckV8hbVCG4'; + +// Replace with your existing document ID from the Submit Documents tutorial +const DOCUMENT_ID = 'YOUR_DOCUMENT_ID'; + +try { + // Fetch the existing document to get current revision + const docs = await sdk.documents.query({ + dataContractId: DATA_CONTRACT_ID, + documentTypeName: 'note', + where: [['$id', '==', DOCUMENT_ID]], + }); + const existingDoc = [...docs.values()][0]; + if (!existingDoc) { + throw new Error(`Document ${DOCUMENT_ID} not found`); + } + + // Create the replacement document with incremented revision + const document = new Document({ + properties: { + message: `Updated Tutorial Test @ ${new Date().toUTCString()}`, + }, + documentTypeName: 'note', + dataContractId: DATA_CONTRACT_ID, + ownerId: identity.id, + revision: existingDoc.revision + 1n, + id: DOCUMENT_ID, + }); + + // Submit the replacement to the platform + await sdk.documents.replace({ + document, + identityKey, + signer, + }); + + console.log('Document updated:\n', document.toJSON()); +} catch (e) { + console.error('Something went wrong:\n', e.message); +} ``` -:::{tip} -The example above shows how access to contract documents via `.` syntax (e.g. `tutorialContract.note`) can be enabled by passing a contract identity to the constructor. Please refer to the [Dash SDK documentation](https://github.com/dashpay/platform/blob/master/packages/js-dash-sdk/docs/getting-started/multiple-apps.md) for details. -::: - ## What's happening -After we initialize the Client, we retrieve the document to be updated via `platform.documents.get` using its `id`. Once the document has been retrieved, we must submit it to [DAPI](../../explanations/dapi.md) with the desired data updates. Documents are submitted in batches that may contain multiple documents to be created, replaced, or deleted. In this example, a single document is being updated. +After we initialize the client, we get the auth key signer from the key manager. We first query for the existing document by its `$id` to retrieve the current `revision` number. We then create a new `Document` object with the updated properties, the same `id`, and the revision incremented by one (as a `BigInt`). -The `platform.documents.broadcast` method then takes the document batch (e.g. `{replace: [noteDocument]}`) and an identity parameter. Internally, it creates a [State Transition](../../explanations/platform-protocol-state-transition.md) containing the previously created document, signs the state transition, and submits the signed state transition to DAPI. +The `sdk.documents.replace()` method takes the document and signing credentials. Internally, it creates a [State Transition](../../explanations/platform-protocol-state-transition.md) containing the replacement document, signs the state transition, and submits it to DAPI. :::{note} -:class: note -Since the SDK does not cache wallet information, lengthy re-syncs (5+ minutes) may be required for some Core chain wallet operations. See [Wallet Operations](../setup-sdk-client.md#wallet-operations) for options. +The SDK requires constructing a complete replacement `Document` with all fields — including the document `id` and incremented `revision`. The example above queries the existing document first to determine the current revision. ::: diff --git a/docs/tutorials/create-and-fund-a-wallet.md b/docs/tutorials/create-and-fund-a-wallet.md index 7392b86d3..231ed5433 100644 --- a/docs/tutorials/create-and-fund-a-wallet.md +++ b/docs/tutorials/create-and-fund-a-wallet.md @@ -2,10 +2,9 @@ .. tutorials-create-wallet: ``` - # Create and fund a wallet -In order to make changes on Dash Platform, you need a wallet with a balance. This tutorial explains how to generate a new wallet, retrieve an address from it, and transfer test funds to the address from a faucet. +In order to make create an identity on Dash Platform, you need a platform address with a balance. This tutorial explains how to generate a new wallet, derive a platform address from it, and transfer test funds to the address. ## Prerequisites @@ -13,45 +12,44 @@ In order to make changes on Dash Platform, you need a wallet with a balance. Thi # Code -```javascript -const Dash = require('dash'); +```{code-block} javascript +:caption: generateWallet.mjs + +import { wallet, PlatformAddressSigner, PrivateKey } from '@dashevo/evo-sdk'; + +const network = 'testnet'; -const clientOpts = { - network: 'testnet', - wallet: { - mnemonic: null, // this indicates that we want a new wallet to be generated - // if you want to get a new address for an existing wallet - // replace 'null' with an existing wallet mnemonic - offlineMode: true, // this indicates we don't want to sync the chain - // it can only be used when the mnemonic is set to 'null' - }, -}; +try { + const mnemonic = await wallet.generateMnemonic(); + const pathInfo = network === 'testnet' + ? await wallet.derivationPathBip44Testnet(0, 0, 0) + : await wallet.derivationPathBip44Mainnet(0, 0, 0); -const client = new Dash.Client(clientOpts); + // Derive the first BIP44 key to get a platform address + const keyInfo = await wallet.deriveKeyFromSeedWithPath({ + mnemonic, + path: pathInfo.path, + network, + }); -const createWallet = async () => { - const account = await client.getWalletAccount(); + // Get the platform address (bech32m) from the private key + const privateKey = PrivateKey.fromWIF(keyInfo.toObject().privateKeyWif); + const signer = new PlatformAddressSigner(); + const address = signer.addKey(privateKey).toBech32m(network); - const mnemonic = client.wallet.exportWallet(); - const address = account.getUnusedAddress(); + // ⚠️ Never log mnemonics in real applications console.log('Mnemonic:', mnemonic); - console.log('Unused address:', address.address); -}; - -createWallet() - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); - -// Handle wallet async errors -client.on('error', (error, context) => { - console.error(`Client error: ${error.name}`); - console.error(context); -}); + console.log('Platform address:', address); + console.log('Fund address using:', `https://bridge.thepasta.org/?address=${address}`); +} catch (e) { + console.error('Something went wrong:', e.message); +} ``` ```text -Mnemonic: thrive wolf habit timber birth service crystal patient tiny depart tower focus -Unused address: yXF7LsyajRvJGX96vPHBmo9Dwy9zEvzkbh +Mnemonic: toilet kingdom uncover super company economy jump fence car later exact multiply +Platform address: tdash1kpk3fhjfj884gz6zmjj42m9udmvp2mg5rsdx8zhr +Fund address using: https://bridge.thepasta.org/?address=tdash1kpk3fhjfj884gz6zmjj42m9udmvp2mg5rsdx8zhr ``` :::{attention} @@ -60,8 +58,11 @@ Please save your mnemonic for the next step and for re-use in subsequent tutoria # What's Happening -Once we connect, we output the newly generated mnemonic from `client.wallet.exportWallet()` and an unused address from the wallet from `account.getUnusedAddress()`. +We use the SDK's `wallet` utilities to generate a BIP39 mnemonic phrase, then derive a platform +address from it using the BIP44 derivation path. Platform addresses are bech32m-encoded addresses +(prefixed with `tdash1` on testnet) that hold credits directly on Dash Platform. # Next Step -Using the [faucet](https://faucet.testnet.networks.dash.org/), send test funds to the "unused address" from the console output. You will need to wait until the funds are confirmed to use them. The [block explorer](https://insight.testnet.networks.dash.org/insight/) can be used to check confirmations. +Using the [Core -> Platform bridge](https://bridge.thepasta.org/), send test funds to the platform +address from the console output. diff --git a/docs/tutorials/identities-and-names/register-a-name-for-an-identity.md b/docs/tutorials/identities-and-names/register-a-name-for-an-identity.md index a0ec48943..70b1b824c 100644 --- a/docs/tutorials/identities-and-names/register-a-name-for-an-identity.md +++ b/docs/tutorials/identities-and-names/register-a-name-for-an-identity.md @@ -13,7 +13,7 @@ Dash Platform names make cryptographic identities easy to remember and communica ## Prerequisites - [General prerequisites](../../tutorials/introduction.md#prerequisites) (Node.js / Dash SDK installed) -- A wallet mnemonic with some funds in it: [Tutorial: Create and Fund a Wallet](../../tutorials/create-and-fund-a-wallet.md) +- 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) - A name you want to register: [Name restrictions](../../explanations/dpns.md#implementation) @@ -21,38 +21,39 @@ Dash Platform names make cryptographic identities easy to remember and communica ## Code :::{tip} -The name must be the full domain name including the parent domain (i.e. `myname.dash` rather than `myname`). Currently, only the `dash` top-level domain may be used. +Pass only the label (e.g., `myname`), not the full domain name. The `.dash` suffix is handled by the SDK. Currently, only the `dash` top-level domain may be used. ::: -```javascript -const setupDashClient = require('../setupDashClient'); +```{code-block} javascript +:caption: registerName.mjs -const client = setupDashClient(); +import { setupDashClient } from '../setupDashClient.mjs'; -const registerName = async () => { - const { platform } = client; +const { sdk, keyManager } = await setupDashClient(); +const { identity, identityKey, signer } = await keyManager.getAuth(); - const identity = await platform.identities.get('an identity ID goes here'); - const nameRegistration = await platform.names.register( - '.dash', - { identity: identity.getId() }, - identity, - ); - - return nameRegistration; -}; +// ⚠️ Change this to a unique name to register +const NAME_LABEL = 'alice'; -registerName() - .then((d) => console.log('Name registered:\n', d.toJSON())) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); +try { + // Register a DPNS name for the identity + const result = await sdk.dpns.registerName({ + label: NAME_LABEL, + identity, + identityKey, + signer, + }); + + console.log('Name registered:\n', result.toJSON()); +} catch (e) { + if (e.message?.includes('duplicate unique properties')) { + console.error(`Name "${NAME_LABEL}.dash" is already registered. Try a different name.`); + } else { + console.error('Something went wrong:\n', e.message); + } +} ``` ## What's Happening -After initializing the Client, we fetch the Identity we'll be associating with a name. This is an asynchronous method so we use _await_ to pause until the request is complete. Next, we call `platform.names.register` and pass in the name we want to register, the type of identity record to create, and the identity we just fetched. We wait for the result, and output it to the console. - -:::{note} -:class: note -Since the SDK does not cache wallet information, lengthy re-syncs (5+ minutes) may be required for some Core chain wallet operations. See [Wallet Operations](../setup-sdk-client.md#wallet-operations) for options. -::: +After initializing the client, we get the auth key signer using `keyManager.getAuth()`. We then call `sdk.dpns.registerName()` with the label (name without the `.dash` suffix), the identity, and the signing credentials. The SDK submits a DPNS domain document to the network. We wait for the result and output it to the console. diff --git a/docs/tutorials/identities-and-names/register-an-identity.md b/docs/tutorials/identities-and-names/register-an-identity.md index 5214e4029..b338efbe0 100644 --- a/docs/tutorials/identities-and-names/register-an-identity.md +++ b/docs/tutorials/identities-and-names/register-an-identity.md @@ -13,37 +13,60 @@ Identities serve as the basis for interactions with Dash Platform. They consist ## Prerequisites - [General prerequisites](../../tutorials/introduction.md#prerequisites) (Node.js / Dash SDK installed) -- A wallet mnemonic with some funds in it: [How to Create and Fund a Wallet](../../tutorials/create-and-fund-a-wallet.md) +- A platform address with a balance: [How to Create and Fund a Wallet](../../tutorials/create-and-fund-a-wallet.md) - A configured client: [Setup SDK Client](../setup-sdk-client.md) ## Code -:::{note} -:class: note -Since the SDK does not cache wallet information, lengthy re-syncs (5+ minutes) may be required for some Core chain wallet operations. See [Wallet Operations](../setup-sdk-client.md#wallet-operations) for options. -::: - -```javascript -const setupDashClient = require('../setupDashClient'); - -const client = setupDashClient(); - -const createIdentity = async () => { - return client.platform.identities.register(); -}; - -createIdentity() - .then((d) => console.log('Identity:\n', d.toJSON())) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); +```{code-block} javascript +:caption: registerIdentity.mjs + +import { randomBytes } from 'node:crypto'; +import { Identity, Identifier } from '@dashevo/evo-sdk'; +import { setupDashClient } from '../setupDashClient.mjs'; + +const { sdk, keyManager, addressKeyManager } = await setupDashClient({ requireIdentity: false }); + +try { + // Build the identity shell with 5 standard public keys + const identity = new Identity(new Identifier(randomBytes(32))); + keyManager.getKeysInCreation().forEach((key) => { + identity.addPublicKey(key.toIdentityPublicKey()); + }); + + // Create the identity on-chain, funded from the platform address + const result = await sdk.addresses.createIdentity({ + identity, + inputs: [{ + address: addressKeyManager.primaryAddress.bech32m, + amount: 5000000n, // Credits to fund the new identity + }], + identitySigner: keyManager.getFullSigner(), + addressSigner: addressKeyManager.getSigner(), + }); + + console.log('Identity registered!\nIdentity ID:', result.identity.id.toString()); +} catch (e) { + // Known SDK bug: proof verification fails but the identity was created + // Issue: https://github.com/dashpay/platform/issues/3095 + // Extract the real identity ID from the error message + const match = e.message?.match(/proof returned identity (\w+) but/); + if (match) { + console.log('Identity registered!\nIdentity ID:', match[1]); + } else { + console.error('Something went wrong:\n', e.message); + } +} ``` -The Identity will be output to the console. The Identity will need to have one confirmation before it is accessible via `client.platform.identity.get`. - :::{attention} -Make a note of the returned identity `id` as it will be used used in subsequent tutorials throughout the documentation. +Make a note of the returned identity ID as it will be used in subsequent tutorials throughout the documentation. ::: ## What's Happening -After connecting to the Client, we call `platform.identities.register`. This will generate a keypair and submit an _Identity Create State Transaction_. After the Identity is registered, we output it to the console. +We call `setupDashClient({ requireIdentity: false })` since we're creating a new identity rather than loading an existing one. This connects to the network and derives keys from the mnemonic, returning `sdk`, `keyManager`, and `addressKeyManager`. + +1. **Identity shell**: We create a temporary `Identity` object with a random identifier and attach all 5 standard public keys from the key manager. These keys serve different purposes (master, authentication, transfer, and encryption). + +2. **On-chain creation**: We call `sdk.addresses.createIdentity()` which requires two signers -- the `identitySigner` (proves ownership of the identity keys) and the `addressSigner` (authorizes the credit transfer from the platform address). This submits an _Identity Create State Transition_ to the network. diff --git a/docs/tutorials/identities-and-names/retrieve-a-name.md b/docs/tutorials/identities-and-names/retrieve-a-name.md index 05bfc23c8..7e624302e 100644 --- a/docs/tutorials/identities-and-names/retrieve-a-name.md +++ b/docs/tutorials/identities-and-names/retrieve-a-name.md @@ -14,93 +14,117 @@ In this tutorial we will retrieve the name created in the [Register a Name for a ## Code ::::{tab-set} -:::{tab-item} JavaScript - Resolve by Name -```javascript -const setupDashClient = require('../setupDashClient'); +:::{tab-item} Resolve by Name -const client = setupDashClient(); +```{code-block} javascript +:caption: resolve-by-name.mjs -const retrieveName = async () => { - // Retrieve by full name (e.g., myname.dash) - return client.platform.names.resolve('.dash'); -}; +import { setupDashClient } from '../setupDashClient.mjs'; -retrieveName() - .then((d) => console.log('Name retrieved:\n', d.getData())) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); +const { sdk } = await setupDashClient(); + +const NAME = 'quantumexplorer.dash'; + +try { + // Resolve by full name (e.g., myname.dash) + const result = await sdk.dpns.resolveName(NAME); + console.log(`Identity ID for "${NAME}": ${result}`); +} catch (e) { + console.error('Something went wrong:\n', e.message); +} +``` + +**Example Response** + +```text +Identity ID for "quantumexplorer.dash": BNnn19SAJZuvsUu787dMzPDXASwuCrm4yQ864tEpQFvo ``` + ::: -:::{tab-item} JavaScript - Revolve by Record -```javascript -const setupDashClient = require('../setupDashClient'); +:::{tab-item} Get Identity Names + +```{code-block} javascript +:caption: get-identity-names.mjs -const client = setupDashClient(); +import { setupDashClient } from '../setupDashClient.mjs'; -const retrieveNameByRecord = async () => { - // Retrieve by a name's identity ID - return client.platform.names.resolveByRecord( - 'identity', - '', - ); -}; +const { sdk, keyManager } = await setupDashClient(); -retrieveNameByRecord() - .then((d) => console.log('Name retrieved:\n', d[0].getData())) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); +// Identity ID from the identity create tutorial +let IDENTITY_ID = 'GgZekwh38XcWQTyWWWvmw6CEYFnLU7yiZFPWZEjqKHit'; + +// Uncomment the line below to use the identity created in the earlier tutorial +// IDENTITY_ID = keyManager.identityId; + +try { + // Retrieve usernames registered to an identity + const usernames = await sdk.dpns.usernames({ identityId: IDENTITY_ID }); + console.log(`Name(s) retrieved for ${IDENTITY_ID}:\n`, usernames); +} catch (e) { + console.error('Something went wrong:\n', e.message); +} ``` -::: -:::{tab-item} JavaScript - Search for Name -```javascript -const setupDashClient = require('../setupDashClient'); - -const client = setupDashClient(); - -const retrieveNameBySearch = async () => { - // Search for names (e.g. `user*`) - return client.platform.names.search('user', 'dash'); -}; - -retrieveNameBySearch() - .then((d) => { - for (const name of d) { - console.log('Name retrieved:\n', name.getData()); - } - }) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); +**Example Response** + +```text +Name(s) retrieved for GgZekwh38XcWQTyWWWvmw6CEYFnLU7yiZFPWZEjqKHit: + [ 'Tutorial-Test-000000-backup.dash', 'Tutorial-Test-000000.dash' ] ``` + ::: -:::: -## Example Name - -The following example response shows a retrieved name: - -```json -{ - "label": "Tutorial-Test-Jettie-94475", - "normalizedLabel": "tut0r1a1-test-jett1e-94475", - "normalizedParentDomainName": "dash", - "parentDomainName": "dash", - "records": { - "identity": "woTQprzGS4bLqqbAhY2heG8QfD58Doo2UhDbiVVrLKG" - }, - "subdomainRules": { - "allowSubdomains": false +:::{tab-item} Search for Name + +```{code-block} javascript +:caption: search-by-name.mjs + +import { setupDashClient } from '../setupDashClient.mjs'; + +const { sdk } = await setupDashClient(); + +const DPNS_CONTRACT_ID = 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec'; +const PREFIX = 'Tutorial-Test-00'; + +try { + // Convert prefix to homograph-safe form for normalized search + const normalizedPrefix = await sdk.dpns.convertToHomographSafe(PREFIX); + + // Search the DPNS contract for matching names + const results = await sdk.documents.query({ + dataContractId: DPNS_CONTRACT_ID, + documentTypeName: 'domain', + where: [ + ['normalizedParentDomainName', '==', 'dash'], + ['normalizedLabel', 'startsWith', normalizedPrefix], + ], + orderBy: [['normalizedLabel', 'asc']], + }); + + for (const [id, doc] of results) { + const { label, parentDomainName } = doc.toJSON(); + console.log(`${label}.${parentDomainName} (ID: ${id.toString()})`); } +} catch (e) { + console.error('Something went wrong:\n', e.message); } ``` +**Example Response** + +```text +Tutorial-Test-000000.dash (ID: E8m6NCCnpschx4WRfk1uLMHqttqMJKPwYt8fWaVSJPrL) +Tutorial-Test-000000-backup.dash (ID: 98bruK9TdJki5xP8BYpmNXqdH9ZHzBD9phwDRzhaJsWF) +``` + +::: +:::: + ## What's Happening After we initialize the Client, we request a name. The [code examples](#code) demonstrate the three ways to request a name: -1. Resolve by name. The `platform.names.resolve` method takes a single argument: a fully-qualified name (e.g., `user-9999.dash`). -2. Resolve by record. The `platform.names.resolveByRecord` method takes two arguments: the record type (e.g., `identity`) and the record value to resolve. -3. Search. The `platform.names.search` method takes two arguments: the leading characters of the name to search for and the domain to search (e.g., `dash` for names in the `*.dash` domain). The search will return names that begin the with string provided in the first parameter. - -After the name is retrieved, it is displayed on the console. +1. **Resolve by name.** The `sdk.dpns.resolveName` method takes a single argument: a fully-qualified name (e.g., `quantumexplorer.dash`). It returns the identity ID that the name resolves to. +2. **Get identity names.** The `sdk.dpns.usernames` method takes an object with an `identityId` property to find all names registered to that identity. It returns an array of fully-qualified names. +3. **Search.** To search for names by prefix, we query the DPNS contract's `domain` documents directly using `sdk.documents.query()`. The prefix is first converted to a homograph-safe form via `sdk.dpns.convertToHomographSafe()`. Results are returned as a `Map` of document IDs to document objects. We call `.toJSON()` on each document to extract the `label` and `parentDomainName`. diff --git a/docs/tutorials/identities-and-names/retrieve-an-accounts-identities.md b/docs/tutorials/identities-and-names/retrieve-an-accounts-identities.md index 4ebe8f128..761edca55 100644 --- a/docs/tutorials/identities-and-names/retrieve-an-accounts-identities.md +++ b/docs/tutorials/identities-and-names/retrieve-an-accounts-identities.md @@ -4,6 +4,10 @@ # Retrieve an account's identities +:::{attention} +This tutorial has not been migrated to use the latest Dash SDK yet and is out-of-date. +::: + In this tutorial we will retrieve the list of identities associated with a specified mnemonic-based account. Since multiple identities may be created using the same mnemonic, it is helpful to have a way to quickly retrieve all these identities (e.g. if importing the mnemonic into a new device). ## Prerequisites diff --git a/docs/tutorials/identities-and-names/retrieve-an-identity.md b/docs/tutorials/identities-and-names/retrieve-an-identity.md index bc7693438..538c4522f 100644 --- a/docs/tutorials/identities-and-names/retrieve-an-identity.md +++ b/docs/tutorials/identities-and-names/retrieve-an-identity.md @@ -14,19 +14,28 @@ In this tutorial we will retrieve the identity created in the [Register an Ident ## Code -```javascript -const setupDashClient = require('../setupDashClient'); +```{code-block} javascript +:caption: retrieveIdentity.mjs -const client = setupDashClient(); +import { setupDashClient } from '../setupDashClient.mjs'; -const retrieveIdentity = async () => { - return client.platform.identities.get('an identity ID goes here'); -}; +const { sdk, keyManager } = await setupDashClient(); -retrieveIdentity() - .then((d) => console.log('Identity retrieved:\n', d.toJSON())) - .catch((e) => console.error('Something went wrong:\n', e)) - .finally(() => client.disconnect()); +// Identity ID from the identity create tutorial +const IDENTITY_ID = keyManager.identityId; + +if (!IDENTITY_ID) { + throw new Error( + 'No identity found. Run the "Register an Identity" tutorial first or provide an identity ID.', + ); +} + +try { + const identity = await sdk.identities.fetch(IDENTITY_ID); + console.log('Identity retrieved:\n', identity.toJSON()); +} catch (e) { + console.error('Something went wrong:\n', e.message); +} ``` ## Example Identity @@ -35,20 +44,70 @@ The following example response shows a retrieved identity: ```json { - "protocolVersion":0, - "id":"6Jz8pFZFhssKSTacgQmZP14zGZNnFYZFKSbx4WVAJFy3", - "publicKeys":[ - { - "id":0, - "type":0, - "data":"A4zZl0EaRBB6IlDbyR80YUM2l02qqNUCoIizkQxubtxi" - } - ], - "balance":10997588, - "revision":0 + "$version": "0", + "id": "FKZZFDTfGdSWUmL2g7H9e46pMJMPQp9DHQcvjrsS6884", + "publicKeys": [ + { + "$version": "0", + "id": 0, + "purpose": 0, + "securityLevel": 0, + "contractBounds": null, + "type": 0, + "readOnly": false, + "data": "AlBCEk8Ic+6wNW6ifZvZEhAwowcwNsvnINdrM0g8v/E3", + "disabledAt": null + }, + { + "$version": "0", + "id": 1, + "purpose": 0, + "securityLevel": 2, + "contractBounds": null, + "type": 0, + "readOnly": false, + "data": "A8KQezkA1nv0K3KwL2rwfco3fKEevnnIMWQ6U6q18Oad", + "disabledAt": null + }, + { + "$version": "0", + "id": 2, + "purpose": 0, + "securityLevel": 1, + "contractBounds": null, + "type": 0, + "readOnly": false, + "data": "Axo18YRoOaN9QXpZBgpt7JOs+KkVSdwsa5qtigDJU9fR", + "disabledAt": null + }, + { + "$version": "0", + "id": 3, + "purpose": 3, + "securityLevel": 1, + "contractBounds": null, + "type": 0, + "readOnly": false, + "data": "A1XZSbd9slDQnDS+Wt2S2lHXOtoTrPEFSHFquHTryktX", + "disabledAt": null + }, + { + "$version": "0", + "id": 4, + "purpose": 1, + "securityLevel": 3, + "contractBounds": null, + "type": 0, + "readOnly": false, + "data": "As04TZMCOTYWZnAe2IA1qbdf47005uMDs1YEg+s9t6Rt", + "disabledAt": null + } + ], + "balance": 5000000, + "revision": 0 } ``` ## What's Happening -After we initialize the Client, we request an identity. The `platform.identities.get` method takes a single argument: an identity ID. After the identity is retrieved, it is displayed on the console. +After we initialize the client, we request an identity. The `sdk.identities.fetch` method takes a single argument: an identity ID. After the identity is retrieved, it is displayed on the console. diff --git a/docs/tutorials/identities-and-names/topup-an-identity-balance.md b/docs/tutorials/identities-and-names/topup-an-identity-balance.md index 039dce6a2..969b84c6b 100644 --- a/docs/tutorials/identities-and-names/topup-an-identity-balance.md +++ b/docs/tutorials/identities-and-names/topup-an-identity-balance.md @@ -4,6 +4,10 @@ # Topup an identity's balance +:::{attention} +This tutorial has not been migrated to use the latest Dash SDK yet and is out-of-date. +::: + The purpose of this tutorial is to walk through the steps necessary to add credits to an identity's balance. ## Overview diff --git a/docs/tutorials/identities-and-names/transfer-credits-to-an-identity.md b/docs/tutorials/identities-and-names/transfer-credits-to-an-identity.md index 49784b35b..96320e074 100644 --- a/docs/tutorials/identities-and-names/transfer-credits-to-an-identity.md +++ b/docs/tutorials/identities-and-names/transfer-credits-to-an-identity.md @@ -4,6 +4,10 @@ # Transfer to an Identity +:::{attention} +This tutorial has not been migrated to use the latest Dash SDK yet and is out-of-date. +::: + The purpose of this tutorial is to walk through the steps necessary to transfer credits to an identity. Additional details regarding credits can be found in the [credits description](../../explanations/identity.md#credits). diff --git a/docs/tutorials/identities-and-names/update-an-identity.md b/docs/tutorials/identities-and-names/update-an-identity.md index 22437915e..0cd037906 100644 --- a/docs/tutorials/identities-and-names/update-an-identity.md +++ b/docs/tutorials/identities-and-names/update-an-identity.md @@ -4,7 +4,10 @@ # Update an identity -Since Dash Platform v0.23, it is possible to update identities to add new keys or disable existing ones. Platform retains disabled keys so that any existing data they signed can still be verified while preventing them from signing new data. +:::{attention} +This tutorial has not been migrated to use the latest Dash SDK yet and is out-of-date. +::: + ## Prerequisites 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 0758666b2..fd88deeca 100644 --- a/docs/tutorials/identities-and-names/withdraw-an-identity-balance.md +++ b/docs/tutorials/identities-and-names/withdraw-an-identity-balance.md @@ -3,7 +3,7 @@ ``` :::{attention} -Mainnet withdrawals will not be available until the activation of Dash Platform v1.4 on mainnet in late October or early November. They are already available on testnet. +This tutorial has not been migrated to use the latest Dash SDK yet and is out-of-date. ::: # Withdraw an Identity's balance diff --git a/docs/tutorials/introduction.md b/docs/tutorials/introduction.md index 454681467..00496969e 100644 --- a/docs/tutorials/introduction.md +++ b/docs/tutorials/introduction.md @@ -6,12 +6,6 @@ The tutorials in this section walk through the steps necessary to begin building on Dash Platform using the Dash JavaScript SDK. As all communication happens via the masternode hosted decentralized API (DAPI), you can begin using Dash Platform immediately without running a local blockchain node. -:::{warning} -Only the JavaScript SDK provides easy access to Dash Platform without requiring a full node; -however, it **_does not support Dash Platform's proofs_**. Therefore, it is less secure than the -[Rust SDK](../sdk-rs/overview.md), which requests proofs for all queried data. -::: - Building on Dash Platform requires first registering an Identity and then registering a Data Contract describing the schema of data to be stored. Once that is done, data can be stored and updated by submitting Documents that comply with the Data Contract. ## Prerequisites @@ -20,7 +14,7 @@ The tutorials in this section are written in JavaScript and use [Node.js](https: - [Node.js](https://nodejs.org/en/) (v20+) - Familiarity with JavaScript asynchronous functions using [async/await](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await) -- The Dash JavaScript SDK (see [Connecting to a Network](../tutorials/connecting-to-testnet.md#1-install-the-dash-sdk)) +- 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)) ## Quickstart @@ -31,7 +25,7 @@ You can clone a repository containing the code for all tutorials from { - // Ensure that numeric values from environment variables are properly converted to numbers - if (clientOptions.wallet?.unsafeOptions?.skipSynchronizationBeforeHeight) { - clientOptions.wallet.unsafeOptions.skipSynchronizationBeforeHeight = - parseInt( - clientOptions.wallet.unsafeOptions.skipSynchronizationBeforeHeight, - 10, +class IdentityKeyManager { + constructor(sdk, identityId, keys, identityIndex) { + this.sdk = sdk; + this.id = identityId; + this.keys = keys; // { master, auth, authHigh, transfer, encryption } + this.identityIndex = identityIndex ?? 0; + } + + get identityId() { + return this.id; + } + + /** + * Create an IdentityKeyManager from a BIP39 mnemonic. + * Derives all standard identity keys using DIP-9 paths. + * + * @param {object} opts + * @param {object} opts.sdk - Connected EvoSDK instance + * @param {string} [opts.identityId] - Identity ID. If omitted, auto-resolved + * from the mnemonic by looking up the master key's public key hash on-chain. + * @param {string} opts.mnemonic - BIP39 mnemonic + * @param {string} [opts.network='testnet'] - 'testnet' or 'mainnet' + * @param {number} [opts.identityIndex=0] - Which identity derived from this mnemonic + */ + static async create({ + sdk, + identityId, + mnemonic, + network = 'testnet', + identityIndex = 0, + }) { + const derive = async (keyIndex) => + wallet.deriveKeyFromSeedWithPath({ + mnemonic, + path: await dip13KeyPath(network, identityIndex, keyIndex), + network, + }); + + const [masterKey, authHighKey, authKey, transferKey, encryptionKey] = + await Promise.all([ + derive(0), // MASTER + derive(1), // HIGH auth + derive(2), // CRITICAL auth + derive(3), // TRANSFER + derive(4), // ENCRYPTION MEDIUM + ]); + + let resolvedId = identityId; + if (!resolvedId) { + const privateKey = PrivateKey.fromWIF(masterKey.toObject().privateKeyWif); + const pubKeyHash = privateKey.getPublicKeyHash(); + const identity = await sdk.identities.byPublicKeyHash(pubKeyHash); + if (!identity) { + throw new Error( + 'No identity found for the given mnemonic (key 0 public key hash)', + ); + } + resolvedId = identity.id.toString(); + } + + return new IdentityKeyManager( + sdk, + resolvedId, + { + master: { keyId: 0, privateKeyWif: masterKey.toObject().privateKeyWif }, + authHigh: { + keyId: 1, + privateKeyWif: authHighKey.toObject().privateKeyWif, + }, + auth: { keyId: 2, privateKeyWif: authKey.toObject().privateKeyWif }, + transfer: { + keyId: 3, + privateKeyWif: transferKey.toObject().privateKeyWif, + }, + encryption: { + keyId: 4, + privateKeyWif: encryptionKey.toObject().privateKeyWif, + }, + }, + identityIndex, + ); + } + + /** + * Find the first unused DIP-9 identity index for a mnemonic. + * Scans indices starting at 0 until no on-chain identity is found. + * + * @param {object} sdk - Connected EvoSDK instance + * @param {string} mnemonic - BIP39 mnemonic + * @param {string} [network='testnet'] - 'testnet' or 'mainnet' + * @returns {Promise} The first unused identity index + */ + static async findNextIndex(sdk, mnemonic, network = 'testnet') { + /* eslint-disable no-await-in-loop */ + for (let i = 0; ; i += 1) { + const path = await dip13KeyPath(network, i, 0); + const key = await wallet.deriveKeyFromSeedWithPath({ + mnemonic, + path, + network, + }); + const privateKey = PrivateKey.fromWIF(key.toObject().privateKeyWif); + const existing = await sdk.identities.byPublicKeyHash( + privateKey.getPublicKeyHash(), ); + if (!existing) return i; + } + /* eslint-enable no-await-in-loop */ } - return new Dash.Client(clientOptions); -}; -module.exports = setupDashClient; -``` + /** + * Create an IdentityKeyManager for a new (not yet registered) identity. + * Derives keys and stores public key data needed for identity creation. + * If identityIndex is omitted, auto-selects the next unused index. + * + * @param {object} opts + * @param {object} opts.sdk - Connected EvoSDK instance + * @param {string} opts.mnemonic - BIP39 mnemonic + * @param {string} [opts.network='testnet'] - 'testnet' or 'mainnet' + * @param {number} [opts.identityIndex] - Identity index (auto-scanned if omitted) + * @returns {Promise} + */ + static async createForNewIdentity({ + sdk, + mnemonic, + network = 'testnet', + identityIndex, + }) { + const idx = + identityIndex ?? + (await IdentityKeyManager.findNextIndex(sdk, mnemonic, network)); + const derive = async (keyIndex) => + wallet.deriveKeyFromSeedWithPath({ + mnemonic, + path: await dip13KeyPath(network, idx, keyIndex), + network, + }); -## Wallet Operations + const derivedKeys = await Promise.all( + KEY_SPECS.map((spec) => derive(spec.keyId)), + ); -Since the SDK does not cache wallet information, lengthy re-syncs (5+ minutes) may be required for some Core chain wallet operations (e.g. `client.getWalletAccount()`). A future release will add caching so that access is much faster after the initial sync. + const keys = { + master: { + keyId: 0, + privateKeyWif: derivedKeys[0].toObject().privateKeyWif, + publicKey: derivedKeys[0].toObject().publicKey, + }, + authHigh: { + keyId: 1, + privateKeyWif: derivedKeys[1].toObject().privateKeyWif, + publicKey: derivedKeys[1].toObject().publicKey, + }, + auth: { + keyId: 2, + privateKeyWif: derivedKeys[2].toObject().privateKeyWif, + publicKey: derivedKeys[2].toObject().publicKey, + }, + transfer: { + keyId: 3, + privateKeyWif: derivedKeys[3].toObject().privateKeyWif, + publicKey: derivedKeys[3].toObject().publicKey, + }, + encryption: { + keyId: 4, + privateKeyWif: derivedKeys[4].toObject().privateKeyWif, + publicKey: derivedKeys[4].toObject().publicKey, + }, + }; -For now, the `skipSynchronizationBeforeHeight` option can be used to sync the wallet starting at a -certain block height. Set it to a height just below your wallet's first transaction height to -minimize the sync time. + return new IdentityKeyManager(sdk, null, keys, idx); + } + + /** + * Build IdentityPublicKeyInCreation objects for all 5 standard keys. + * Only works when public key data is available (via createForNewIdentity). + * + * @returns {IdentityPublicKeyInCreation[]} + */ + getKeysInCreation() { + return KEY_SPECS.map((spec) => { + const key = Object.values(this.keys).find((k) => k.keyId === spec.keyId); + if (!key?.publicKey) { + throw new Error( + `Public key data not available for key ${spec.keyId}. Use createForNewIdentity().`, + ); + } + const pubKeyData = Uint8Array.from(Buffer.from(key.publicKey, 'hex')); + return new IdentityPublicKeyInCreation({ + keyId: spec.keyId, + purpose: spec.purpose, + securityLevel: spec.securityLevel, + keyType: KeyType.ECDSA_SECP256K1, + data: pubKeyData, + }); + }); + } + + /** + * Build an IdentitySigner loaded with all 5 key WIFs. + * Useful for identity creation where all keys must sign. + * + * @returns {IdentitySigner} + */ + getFullSigner() { + const signer = new IdentitySigner(); + Object.values(this.keys).forEach((key) => { + signer.addKeyFromWif(key.privateKeyWif); + }); + return signer; + } + + /** + * Fetch identity and build { identity, identityKey, signer } for a given key. + * @param {string} keyName - One of: master, auth, authHigh, transfer, encryption + * @returns {{ identity, identityKey, signer }} + */ + async getSigner(keyName) { + if (!this.id) { + throw new Error( + 'Identity ID is not set. Use IdentityKeyManager.create() for an existing identity, ' + + 'or create/register the identity first and then set the ID.', + ); + } + const key = this.keys[keyName]; + if (!key) { + throw new Error( + `Unknown key "${keyName}". Use: ${Object.keys(this.keys).join(', ')}`, + ); + } + const identity = await this.sdk.identities.fetch(this.id); + const identityKey = identity.getPublicKeyById(key.keyId); + const signer = new IdentitySigner(); + signer.addKeyFromWif(key.privateKeyWif); + return { identity, identityKey, signer }; + } + + /** CRITICAL auth (key 2) — contracts, documents, names. */ + async getAuth() { + return this.getSigner('auth'); + } + + /** HIGH auth (key 1) — documents, names. */ + async getAuthHigh() { + return this.getSigner('authHigh'); + } + + /** TRANSFER — credit transfers, withdrawals. */ + async getTransfer() { + return this.getSigner('transfer'); + } + + /** ENCRYPTION MEDIUM — encrypted messaging/data. */ + async getEncryption() { + return this.getSigner('encryption'); + } + + /** + * MASTER — identity updates (add/disable keys). + * @param {string[]} [additionalKeyWifs] - WIFs for new keys being added + */ + async getMaster(additionalKeyWifs) { + const result = await this.getSigner('master'); + if (additionalKeyWifs) { + additionalKeyWifs.forEach((wif) => result.signer.addKeyFromWif(wif)); + } + return result; + } +} + +// --------------------------------------------------------------------------- +// AddressKeyManager +// --------------------------------------------------------------------------- + +/** + * Manages platform address keys and signing for address operations. + * + * Parallel to IdentityKeyManager but for platform address operations. + * Derives BIP44 keys from a mnemonic and provides ready-to-use + * PlatformAddressSigner instances. + * + * Platform addresses are bech32m-encoded L2 addresses (tdash1... on testnet) + * that hold credits directly, independent of identities. + */ +class AddressKeyManager { + constructor(sdk, addresses, network) { + this.sdk = sdk; + this.addresses = addresses; // [{ address, bech32m, privateKeyWif, path }] + this.network = network; + } + + /** The first derived address (index 0). */ + get primaryAddress() { + return this.addresses[0]; + } + + /** + * Create an AddressKeyManager from a BIP39 mnemonic. + * Derives platform address keys using BIP44 paths. + * + * @param {object} opts + * @param {object} opts.sdk - Connected EvoSDK instance + * @param {string} opts.mnemonic - BIP39 mnemonic + * @param {string} [opts.network='testnet'] - 'testnet' or 'mainnet' + * @param {number} [opts.count=1] - Number of addresses to derive + */ + static async create({ sdk, mnemonic, network = 'testnet', count = 1 }) { + const addresses = []; + + /* eslint-disable no-await-in-loop */ + for (let i = 0; i < count; i += 1) { + const pathInfo = + network === 'testnet' + ? await wallet.derivationPathBip44Testnet(0, 0, i) + : await wallet.derivationPathBip44Mainnet(0, 0, i); + const { path } = pathInfo; + const keyInfo = await wallet.deriveKeyFromSeedWithPath({ + mnemonic, + path, + network, + }); + const obj = keyInfo.toObject(); + const privateKey = PrivateKey.fromWIF(obj.privateKeyWif); + const signer = new PlatformAddressSigner(); + const platformAddress = signer.addKey(privateKey); + + addresses.push({ + address: platformAddress, + bech32m: platformAddress.toBech32m(network), + privateKeyWif: obj.privateKeyWif, + path, + }); + } + /* eslint-enable no-await-in-loop */ + + return new AddressKeyManager(sdk, addresses, network); + } + + /** + * Create a PlatformAddressSigner with the primary key loaded. + * @returns {PlatformAddressSigner} + */ + getSigner() { + const signer = new PlatformAddressSigner(); + const privateKey = PrivateKey.fromWIF(this.primaryAddress.privateKeyWif); + signer.addKey(privateKey); + return signer; + } + + /** + * Create a PlatformAddressSigner with all derived keys loaded. + * @returns {PlatformAddressSigner} + */ + getFullSigner() { + const signer = new PlatformAddressSigner(); + this.addresses.forEach((addr) => { + const privateKey = PrivateKey.fromWIF(addr.privateKeyWif); + signer.addKey(privateKey); + }); + return signer; + } + + /** + * Fetch current balance and nonce for the primary address. + * @returns {Promise} + */ + async getInfo() { + return this.sdk.addresses.get(this.primaryAddress.bech32m); + } + + /** + * Fetch current balance and nonce for an address by index. + * @param {number} index - Address index + * @returns {Promise} + */ + async getInfoAt(index) { + const entry = this.addresses[index]; + if (!entry) { + throw new Error( + `No derived address at index ${index} (count=${this.addresses.length})`, + ); + } + return this.sdk.addresses.get(entry.bech32m); + } +} + +// --------------------------------------------------------------------------- +// setupDashClient — convenience wrapper +// --------------------------------------------------------------------------- + +export async function setupDashClient({ + requireIdentity = true, + identityIndex = 0, +} = {}) { + const { network, mnemonic } = clientConfig; + const sdk = await createClient(network); + + let keyManager; + let addressKeyManager; + + if (mnemonic) { + addressKeyManager = await AddressKeyManager.create({ + sdk, + mnemonic, + network, + }); + + if (requireIdentity) { + keyManager = await IdentityKeyManager.create({ + sdk, + mnemonic, + network, + identityIndex, + }); + } else { + keyManager = await IdentityKeyManager.createForNewIdentity({ + sdk, + mnemonic, + network, + identityIndex, + }); + } + } + + return { sdk, keyManager, addressKeyManager }; +} + +export { IdentityKeyManager, AddressKeyManager, clientConfig }; +``` ## What's Happening -In this module, we return an SDK client configured with the options necessary for typical use. The -module is then imported in the following tutorials to streamline them and avoid repeating client -initialization details. +The `setupDashClient.mjs` module is a single-file setup that subsequent tutorials import. It handles: + +- **`clientConfig`** — your network and mnemonic, defined once. Set values directly or use a `.env` + file with `NETWORK` and `PLATFORM_MNEMONIC`. + +- **`setupDashClient()`** — the main entry point for most tutorials. Connects to the network and + creates key managers from `clientConfig`, returning `{ sdk, keyManager, addressKeyManager }`. + +- **`createClient()`** — creates a connected SDK instance for a given network (testnet, mainnet, or + local). + +- **`IdentityKeyManager`** — derives 5 standard identity keys from your mnemonic using DIP-9 key + paths. Each key serves a specific purpose (authentication, transfers, encryption). Call methods + like `getAuth()` to get `{ identity, identityKey, signer }` — everything the SDK needs to sign + and submit a transaction. + +- **`AddressKeyManager`** — derives platform address keys from your mnemonic using BIP44 paths. + These addresses hold credits on the L2 platform layer and are used for identity creation, top-ups, + and credit transfers between addresses.