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
12 changes: 11 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ NETWORK='testnet'
# Used by document and contract-update tutorials
DATA_CONTRACT_ID=''

# RECIPIENT_ID is an identity ID for credit transfer tutorials
# RECIPIENT_ID is an identity ID for the credit transfer tutorial
RECIPIENT_ID=''

# Token transfer tutorial variables
Expand All @@ -32,6 +32,16 @@ DISABLE_KEY_ID=''
# NAME_LABEL is an optional username label for name registration (without .dash)
NAME_LABEL=''

# Optional second funded identity used by document purchase and transfer tests
SECONDARY_PLATFORM_MNEMONIC='your mnemonic phrase goes here ...'

# DOCUMENT_RECIPIENT_ID is the new owner identity ID for document-transfer.mjs
DOCUMENT_RECIPIENT_ID=''
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# DOCUMENT_PRICE is the listing price in credits for document-set-price.mjs
# Set it to 0 to remove the document from sale
DOCUMENT_PRICE=100000000

# NFT Variables
# NFT_CONTRACT_ID comes from contract-register-nft.mjs output
NFT_CONTRACT_ID=''
Expand Down
55 changes: 55 additions & 0 deletions 2-Contracts-and-Documents/document-purchase.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { setupDashClient } from '../setupDashClient.mjs';

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

// Purchase works with any document type whose contract enables `tradeMode`.
// Here the purchased document is the `domain` behind a DPNS name.
const DPNS_CONTRACT_ID = 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec';
const NAME_LABEL = process.env.NAME_LABEL || 'alice';

try {
// Fetch immediately before purchase so the revision, owner, and price are
// current. Platform rejects the purchase if the listing changes meanwhile.
const documents = await sdk.documents.query({
dataContractId: DPNS_CONTRACT_ID,
documentTypeName: 'domain',
where: [
['normalizedParentDomainName', '==', 'dash'],
['normalizedLabel', '==', NAME_LABEL.toLowerCase()],
],
});
const document = [...documents.values()][0];

if (!document) {
throw new Error(`Name "${NAME_LABEL}.dash" was not found`);
}

// Buying your own listing is rejected by the platform
if (document.ownerId.toString() === identity.id.toString()) {
throw new Error(`"${NAME_LABEL}.dash" is already owned by ${identity.id}`);
}

// Read the native bigint directly. toJSON() cannot safely represent every
// possible unsigned 64-bit credit value.
const price = document.properties?.['$price'];
if (typeof price !== 'bigint' || price <= 0n) {
throw new Error(`Name "${NAME_LABEL}.dash" is not currently for sale`);
}

document.revision = BigInt(document.revision ?? 0) + 1n;

await sdk.documents.purchase({
document,
buyerId: identity.id,
price,
identityKey,
signer,
});

console.log(
`Document for "${NAME_LABEL}.dash" purchased for ${price} credits.`,
);
} catch (e) {
console.error('Something went wrong:\n', e.message);
}
52 changes: 52 additions & 0 deletions 2-Contracts-and-Documents/document-set-price.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { setupDashClient } from '../setupDashClient.mjs';

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

// Pricing works with any document type whose contract enables `tradeMode`.
// This tutorial lists the `domain` document behind a DPNS name.
const DPNS_CONTRACT_ID = 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec';
const NAME_LABEL = process.env.NAME_LABEL || 'alice';

// Price in credits. A price of 0 removes the document from sale.
const PRICE = BigInt(process.env.DOCUMENT_PRICE || 100_000_000);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

try {
const documents = await sdk.documents.query({
dataContractId: DPNS_CONTRACT_ID,
documentTypeName: 'domain',
where: [
['normalizedParentDomainName', '==', 'dash'],
['normalizedLabel', '==', NAME_LABEL.toLowerCase()],
],
});
const document = [...documents.values()][0];

if (!document) {
throw new Error(`Name "${NAME_LABEL}.dash" was not found`);
}

// Only the current owner can change a document's sale price
if (document.ownerId.toString() !== identity.id.toString()) {
throw new Error(
`"${NAME_LABEL}.dash" is owned by ${document.ownerId}, not ${identity.id}`,
);
}

document.revision = BigInt(document.revision ?? 0) + 1n;

await sdk.documents.setPrice({
document,
price: PRICE,
identityKey,
signer,
});

console.log(
PRICE === 0n
? `Document for "${NAME_LABEL}.dash" removed from sale.`
: `Document for "${NAME_LABEL}.dash" listed for ${PRICE} credits.`,
);
} catch (e) {
console.error('Something went wrong:\n', e.message);
}
55 changes: 55 additions & 0 deletions 2-Contracts-and-Documents/document-transfer.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { setupDashClient } from '../setupDashClient.mjs';

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

// Transfer works with any document type whose contract enables `transferable`.
// This tutorial uses a DPNS name because names are familiar transferable documents.
const DPNS_CONTRACT_ID = 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec';
const NAME_LABEL = process.env.NAME_LABEL || 'alice';
const DOCUMENT_RECIPIENT_ID =
process.env.DOCUMENT_RECIPIENT_ID || 'YOUR_DOCUMENT_RECIPIENT_ID';

try {
// Check configuration before spending a network round-trip on the query
if (DOCUMENT_RECIPIENT_ID === 'YOUR_DOCUMENT_RECIPIENT_ID') {
throw new Error('Set DOCUMENT_RECIPIENT_ID to the new owner identity ID');
}

const documents = await sdk.documents.query({
dataContractId: DPNS_CONTRACT_ID,
documentTypeName: 'domain',
where: [
['normalizedParentDomainName', '==', 'dash'],
['normalizedLabel', '==', NAME_LABEL.toLowerCase()],
],
});
const document = [...documents.values()][0];

if (!document) {
throw new Error(`Name "${NAME_LABEL}.dash" was not found`);
}

// Only the current owner can transfer a document
if (document.ownerId.toString() !== identity.id.toString()) {
throw new Error(
`"${NAME_LABEL}.dash" is owned by ${document.ownerId}, not ${identity.id}`,
);
}

document.revision = BigInt(document.revision ?? 0) + 1n;

// A successful transfer also clears any active sale price
await sdk.documents.transfer({
document,
recipientId: DOCUMENT_RECIPIENT_ID,
identityKey,
signer,
});

console.log(
`Document for "${NAME_LABEL}.dash" transferred to ${DOCUMENT_RECIPIENT_ID}.`,
);
} catch (e) {
console.error('Something went wrong:\n', e.message);
}
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,15 @@ Proceed with the [Identities and Names tutorials](./1-Identities-and-Names/) fir
[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
- 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.
- The document marketplace tutorials use the DPNS `domain` document behind `NAME_LABEL` to
demonstrate the general transfer, set-price, and purchase APIs. These APIs also work with other
document types whose contracts enable `transferable` or `tradeMode` as appropriate.
- 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.

Expand Down
102 changes: 102 additions & 0 deletions test/read-write.test.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import dotenv from 'dotenv';
import { runTutorial } from './run-tutorial.mjs';
import {
assertTutorialSuccess,
Expand All @@ -8,8 +9,11 @@ import {
extractKeyId,
} from './assertions.mjs';

dotenv.config();

// Accumulated state passed forward as env vars to dependent tutorials.
const state = {};
const secondaryMnemonic = process.env.SECONDARY_PLATFORM_MNEMONIC;

describe('Write tutorials (sequential)', { concurrency: 1 }, () => {
// -----------------------------------------------------------------------
Expand Down Expand Up @@ -55,6 +59,13 @@ describe('Write tutorials (sequential)', { concurrency: 1 }, () => {
expectedPatterns: ['Identity retrieved:'],
errorPatterns: ['Something went wrong'],
});

const id = extractId(result.stdout);
assert.ok(
id,
`Failed to extract identity ID from stdout:\n${result.stdout}`,
);
state.primaryIdentityId = id;
});

it('identity-topup', { timeout: 120_000 }, async () => {
Expand Down Expand Up @@ -144,6 +155,97 @@ describe('Write tutorials (sequential)', { concurrency: 1 }, () => {
expectedPatterns: ['Name registered:'],
errorPatterns: ['Something went wrong', 'already registered'],
});
state.nameLabel = label;
});

it('document-set-price (DPNS domain)', { timeout: 120_000 }, async (ctx) => {
if (!state.nameLabel) {
ctx.skip('No NAME_LABEL (name-register must pass first)');
return;
}

const result = await runTutorial(
'2-Contracts-and-Documents/document-set-price.mjs',
{
// Pin the price so a DOCUMENT_PRICE of 0 in the developer's .env
// cannot delist the name the purchase and transfer tests depend on.
env: { NAME_LABEL: state.nameLabel, DOCUMENT_PRICE: '100000000' },
timeoutMs: 120_000,
},
);
assertTutorialSuccess(result, {
name: 'document-set-price',
expectedPatterns: ['listed for [1-9][0-9]* credits'],
errorPatterns: ['Something went wrong'],
});

const listedPrice = extractFromOutput(
result.stdout,
/listed for ([1-9][0-9]*) credits/,
);
assert.ok(
listedPrice,
`Failed to extract listed document price from stdout:\n${result.stdout}`,
);
state.documentSalePrice = listedPrice;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

it('document-purchase (DPNS domain)', { timeout: 120_000 }, async (ctx) => {
if (!secondaryMnemonic) {
ctx.skip('SECONDARY_PLATFORM_MNEMONIC is not configured');
return;
}
if (!state.nameLabel || !state.documentSalePrice) {
ctx.skip('Name registration or set-price did not complete');
return;
}

const result = await runTutorial(
'2-Contracts-and-Documents/document-purchase.mjs',
{
env: {
PLATFORM_MNEMONIC: secondaryMnemonic,
NAME_LABEL: state.nameLabel,
},
timeoutMs: 120_000,
},
);
assertTutorialSuccess(result, {
name: 'document-purchase',
expectedPatterns: [`purchased for ${state.documentSalePrice} credits`],
errorPatterns: ['Something went wrong'],
});
state.secondaryOwnsName = true;
});

it('document-transfer (DPNS domain)', { timeout: 120_000 }, async (ctx) => {
if (!secondaryMnemonic) {
ctx.skip('SECONDARY_PLATFORM_MNEMONIC is not configured');
return;
}
if (!state.secondaryOwnsName || !state.primaryIdentityId) {
ctx.skip(
'Purchase did not complete or primary identity ID is unavailable',
);
return;
}

const result = await runTutorial(
'2-Contracts-and-Documents/document-transfer.mjs',
{
env: {
PLATFORM_MNEMONIC: secondaryMnemonic,
NAME_LABEL: state.nameLabel,
DOCUMENT_RECIPIENT_ID: state.primaryIdentityId,
},
timeoutMs: 120_000,
},
);
assertTutorialSuccess(result, {
name: 'document-transfer',
expectedPatterns: ['transferred to'],
errorPatterns: ['Something went wrong'],
});
});

// -----------------------------------------------------------------------
Expand Down