Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ DATA_CONTRACT_ID=''
# RECIPIENT_ID is an identity ID for credit transfer tutorials
RECIPIENT_ID=''

# Token transfer tutorial variables
# TOKEN_CONTRACT_ID comes from token-register.mjs
TOKEN_CONTRACT_ID=''

# RECIPIENT_PLATFORM_ADDRESS is a bech32m platform address (tdash1...) for send-funds tutorial
RECIPIENT_PLATFORM_ADDRESS=''

Expand Down
39 changes: 39 additions & 0 deletions 3-Tokens/token-burn.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// See https://docs.dash.org/projects/platform/en/stable/docs/tutorials/tokens/burn-tokens.html
import { setupDashClient } from '../setupDashClient.mjs';

const { sdk, keyManager } = await setupDashClient();
const { identity, identityKey, signer } = await keyManager.getAuth();

// TOKEN_CONTRACT_ID comes from token-register.mjs.
const dataContractId = process.env.TOKEN_CONTRACT_ID;
const tokenPosition = 0;
const amount = 1n; // Token amounts are bigint values

try {
if (!dataContractId) {
throw new Error(
'Set TOKEN_CONTRACT_ID in .env from token-register.mjs output.',
);
}

const tokenId = await sdk.tokens.calculateId(dataContractId, tokenPosition);

await sdk.tokens.burn({
dataContractId,
tokenPosition,
amount,
identityId: identity.id.toString(),
identityKey,
signer,
});

const balances = await sdk.tokens.identityBalances(identity.id, [tokenId]);
const totalSupply = await sdk.tokens.totalSupply(tokenId);

console.log(`Burned ${amount} token`);
console.log('Token ID:', tokenId);
console.log(`Identity token balance: ${balances.get(tokenId) ?? 0n}`);
console.log('Total token supply:', totalSupply?.totalSupply ?? 0n);
} catch (e) {
console.error('Something went wrong:\n', e.message);
}
50 changes: 50 additions & 0 deletions 3-Tokens/token-info.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// See https://docs.dash.org/projects/platform/en/stable/docs/tutorials/tokens/retrieve-token-info.html
import { setupDashClient } from '../setupDashClient.mjs';

const { sdk, keyManager } = await setupDashClient();

// TOKEN_CONTRACT_ID comes from token-register.mjs.
const dataContractId = process.env.TOKEN_CONTRACT_ID;
const tokenPosition = 0;

// Default recipient (testnet). Replace or override via RECIPIENT_ID.
const recipientId =
process.env.RECIPIENT_ID || '7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC';

try {
if (!dataContractId) {
throw new Error(
'Set TOKEN_CONTRACT_ID in .env from token-register.mjs output.',
);
}

const tokenId = await sdk.tokens.calculateId(dataContractId, tokenPosition);
const contractInfo = await sdk.tokens.contractInfo(tokenId);
const totalSupply = await sdk.tokens.totalSupply(tokenId);
const statuses = await sdk.tokens.statuses([tokenId]);
const identityBalances = await sdk.tokens.identityBalances(
keyManager.identityId,
[tokenId],
);
const recipientBalances = await sdk.tokens.identityBalances(recipientId, [
tokenId,
]);

// A token only has a status record once one is published on-chain (e.g. via
// an emergency pause), so the Map is empty for a freshly registered token.
const status = statuses.get(tokenId);

console.log('Token ID:', tokenId);
console.log('Token contract info:\n', contractInfo?.toJSON());
console.log(
'Token status:',
status ? status.isPaused : '(no status published)',
);
console.log('Total token supply:', totalSupply?.totalSupply ?? 0n);
console.log(`Identity token balance: ${identityBalances.get(tokenId) ?? 0n}`);
console.log(
`Recipient token balance: ${recipientBalances.get(tokenId) ?? 0n}`,
);
} catch (e) {
console.error('Something went wrong:\n', e.message);
}
39 changes: 39 additions & 0 deletions 3-Tokens/token-mint.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// See https://docs.dash.org/projects/platform/en/stable/docs/tutorials/tokens/mint-tokens.html
import { setupDashClient } from '../setupDashClient.mjs';

const { sdk, keyManager } = await setupDashClient();
const { identity, identityKey, signer } = await keyManager.getAuth();

// TOKEN_CONTRACT_ID comes from token-register.mjs.
const dataContractId = process.env.TOKEN_CONTRACT_ID;
const tokenPosition = 0;
const amount = 10n; // Token amounts are bigint values

try {
if (!dataContractId) {
throw new Error(
'Set TOKEN_CONTRACT_ID in .env from token-register.mjs output.',
);
}

const tokenId = await sdk.tokens.calculateId(dataContractId, tokenPosition);

await sdk.tokens.mint({
dataContractId,
tokenPosition,
amount,
identityId: identity.id.toString(),
identityKey,
signer,
});

const balances = await sdk.tokens.identityBalances(identity.id, [tokenId]);
const totalSupply = await sdk.tokens.totalSupply(tokenId);

console.log(`Minted ${amount} tokens`);
console.log('Token ID:', tokenId);
console.log(`Identity token balance: ${balances.get(tokenId) ?? 0n}`);
console.log('Total token supply:', totalSupply?.totalSupply ?? 0n);
} catch (e) {
console.error('Something went wrong:\n', e.message);
}
137 changes: 137 additions & 0 deletions 3-Tokens/token-register.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// See https://docs.dash.org/projects/platform/en/stable/docs/tutorials/tokens/register-a-token-contract.html
import {
AuthorizedActionTakers,
ChangeControlRules,
DataContract,
TokenConfiguration,
TokenConfigurationConvention,
TokenConfigurationLocalization,
TokenDistributionRules,
TokenKeepsHistoryRules,
TokenMarketplaceRules,
TokenTradeMode,
} from '@dashevo/evo-sdk';
import { setupDashClient } from '../setupDashClient.mjs';

const { sdk, keyManager } = await setupDashClient();
const { identity, identityKey, signer } = await keyManager.getAuth();

const TOKEN_POSITION = 0;
const TOKEN_NAME = 'TutorialToken';
const TOKEN_PLURAL = 'TutorialTokens';
const TOKEN_BASE_SUPPLY = 100n; // Token amounts are bigint values
const TOKEN_MAX_SUPPLY = 1000n;

// This contract includes one small document type so learners can still use the
// standard document tutorials with the same contract if they want to.
const documentSchemas = {
note: {
type: 'object',
properties: {
message: {
type: 'string',
position: 0,
},
},
additionalProperties: false,
},
};

function createTutorialTokenConfiguration(ownerId) {
const contractOwner = AuthorizedActionTakers.ContractOwner();
const noOne = AuthorizedActionTakers.NoOne();

const ownerRules = new ChangeControlRules({
authorizedToMakeChange: contractOwner,
adminActionTakers: contractOwner,
isChangingAuthorizedActionTakersToNoOneAllowed: true,
isChangingAdminActionTakersToNoOneAllowed: true,
isSelfChangingAdminActionTakersAllowed: true,
});
const lockedRules = new ChangeControlRules({
authorizedToMakeChange: noOne,
adminActionTakers: noOne,
});

return new TokenConfiguration({
conventions: new TokenConfigurationConvention(
{
en: new TokenConfigurationLocalization(false, TOKEN_NAME, TOKEN_PLURAL),
},
0,
),
conventionsChangeRules: ownerRules,
baseSupply: TOKEN_BASE_SUPPLY,
maxSupply: TOKEN_MAX_SUPPLY,
keepsHistory: new TokenKeepsHistoryRules({
isKeepingBurningHistory: true,
isKeepingMintingHistory: true,
isKeepingTransferHistory: true,
}),
maxSupplyChangeRules: lockedRules,
distributionRules: new TokenDistributionRules({
newTokensDestinationIdentity: ownerId,
newTokensDestinationIdentityRules: ownerRules,
mintingAllowChoosingDestination: false,
mintingAllowChoosingDestinationRules: ownerRules,
perpetualDistributionRules: lockedRules,
changeDirectPurchasePricingRules: lockedRules,
}),
marketplaceRules: new TokenMarketplaceRules(
TokenTradeMode.NotTradeable(),
lockedRules,
),
// Minting and burning are enabled so the next tutorials can demonstrate
// the normal issuer-managed token lifecycle.
manualMintingRules: ownerRules,
manualBurningRules: ownerRules,
freezeRules: lockedRules,
unfreezeRules: lockedRules,
destroyFrozenFundsRules: lockedRules,
emergencyActionRules: lockedRules,
mainControlGroupCanBeModified: noOne,
description: 'Issuer-managed token for Platform token tutorials.',
});
}

try {
const identityNonce = await sdk.identities.nonce(identity.id.toString());

const dataContract = new DataContract({
ownerId: identity.id,
identityNonce: (identityNonce || 0n) + 1n,
schemas: documentSchemas,
tokens: {
[TOKEN_POSITION]: createTutorialTokenConfiguration(
identity.id.toString(),
),
},
fullValidation: true,
});

const publishedContract = await sdk.contracts.publish({
dataContract,
identityKey,
signer,
});

const contractId =
publishedContract.id?.toString() || publishedContract.toJSON?.()?.id;

if (!contractId) {
const publishResult = publishedContract.toJSON?.() ?? publishedContract;
throw new Error(
`Contract publish returned no id: ${JSON.stringify(publishResult)}`,
);
}

const tokenId = await sdk.tokens.calculateId(contractId, TOKEN_POSITION);

console.log('Token contract registered:\n', publishedContract.toJSON());
console.log('Token position:', TOKEN_POSITION);
console.log('Token ID:', tokenId);
console.log('Initial owner token balance:', TOKEN_BASE_SUPPLY.toString());
console.log('Maximum token supply:', TOKEN_MAX_SUPPLY.toString());
} catch (e) {
console.error('Something went wrong:\n', e.message);
}
60 changes: 60 additions & 0 deletions 3-Tokens/token-transfer.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// See https://docs.dash.org/projects/platform/en/stable/docs/tutorials/tokens/transfer-tokens-to-an-identity.html
import { setupDashClient } from '../setupDashClient.mjs';

const { sdk, keyManager } = await setupDashClient();
const { identity, identityKey, signer } = await keyManager.getTransfer();

// TOKEN_CONTRACT_ID comes from token-register.mjs.
const dataContractId = process.env.TOKEN_CONTRACT_ID;
const tokenPosition = 0;

// Default recipient (testnet). Replace or override via RECIPIENT_ID.
const recipientId =
process.env.RECIPIENT_ID || '7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC';
const amount = 1n;

try {
if (!dataContractId) {
throw new Error(
'Set TOKEN_CONTRACT_ID in .env from token-register.mjs output.',
);
}

const senderId = identity.id.toString();
if (recipientId === senderId) {
throw new Error('Cannot transfer tokens to yourself.');
}

const tokenId = await sdk.tokens.calculateId(dataContractId, tokenPosition);
const balancesBefore = await sdk.tokens.identityBalances(recipientId, [
tokenId,
]);

console.log(
`Recipient token balance before transfer: ${balancesBefore.get(tokenId) ?? 0n}`,
);

await sdk.tokens.transfer({
dataContractId,
tokenPosition,
amount,
senderId,
recipientId,
identityKey,
signer,
});

const balancesAfter = await sdk.tokens.identityBalances(recipientId, [
tokenId,
]);

console.log(
`Transferred ${amount} token${amount === 1n ? '' : 's'} from ${senderId} to ${recipientId}`,
);
console.log('Token ID:', tokenId);
console.log(
`Recipient token balance after transfer: ${balancesAfter.get(tokenId) ?? 0n}`,
);
} catch (e) {
console.error('Something went wrong:\n', e.message);
}
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ Copy `.env.example` to `.env`. Key variables:
| `NETWORK` | `testnet` (default) or `mainnet` |
| `DATA_CONTRACT_ID` | Output of `contract-register-minimal.mjs` |
| `DOCUMENT_ID` | Output of `document-submit.mjs` |
| `TOKEN_CONTRACT_ID` | Output of `token-register.mjs`; required by the other `3-Tokens/` tutorials |
| `RECIPIENT_ID` | Identity ID for credit transfers |
| `RECIPIENT_PLATFORM_ADDRESS` | `tdash1...` address for send-funds |

Expand All @@ -119,6 +120,7 @@ Read-only tests skip gracefully when `PLATFORM_MNEMONIC` is unset.
- **Root** — shared utilities (`setupDashClient.mjs`, `connect.mjs`, `create-wallet.mjs`, `view-wallet.mjs`, `send-funds.mjs`)
- **`1-Identities-and-Names/`** — identity registration, top-up, key management, DPNS name registration/lookup
- **`2-Contracts-and-Documents/`** — data contract variants (minimal, indexed, binary, timestamps, history, NFT), document CRUD, NFT operations
- **`3-Tokens/`** — token contract registration, info queries, minting, burning, and transfers
- **`test/`** — test runner, assertions, read-only and read-write test suites
- **`docs/`** — HTML/JS interactive tutorial runner (separate from Node tutorials)
- **`example-apps/`** — Standalone applications (Vite + React + TypeScript) that consume the tutorial SDK code. Each has its own `package.json`, tsconfig, and toolchain — the conventions in this file (Node16 modules, `airbnb-base`, etc.) describe the **root** tutorial code only and do not apply inside `example-apps/`. See each app's local `CLAUDE.md` for its conventions.
Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,19 @@ and identity are found, and proceed with [Next Steps](#next-steps).

### Next steps

Proceed with the [Identities and Names tutorials](./1-Identities-and-Names/) first and the
[Contracts and Documents tutorials](./2-Contracts-and-Documents/) next. They align with the
tutorials section on the [documentation
Proceed with the [Identities and Names tutorials](./1-Identities-and-Names/) first, the
[Contracts and Documents tutorials](./2-Contracts-and-Documents/) next, and the
[Tokens tutorials](./3-Tokens/) after that. They align with the tutorials section on the [documentation
site](https://docs.dash.org/projects/platform/en/stable/docs/tutorials/introduction.html).

The identity ID is automatically resolved from your mnemonic, so there is no need to set it
manually. After [registering a data
contract](./2-Contracts-and-Documents/contract-register-minimal.mjs), set `DATA_CONTRACT_ID` in your
`.env` file to the new contract ID for use in subsequent document tutorials.
For token tutorials, run
[`token-register.mjs`](./3-Tokens/token-register.mjs), then set
`TOKEN_CONTRACT_ID` in `.env` to the newly registered contract ID. The token tutorials then follow
the normal lifecycle: info, mint, transfer, and burn.

Some client configuration options are included as comments in
[`setupDashClient.mjs`](./setupDashClient.mjs) if more advanced configuration is required.
Expand Down
Loading