Guides
Dash CoreDash PlatformDash.orgDash User DocsLog In
These docs are for v0.20.0. Click to read the latest docs for v0.25-redirect.

Register a Data Contract

Overview

In this tutorial we will register a data contract.

Prerequisites

Code

Defining contract documents

As described in the data contract explanation, data contracts must include one or more developer-defined documents.

The most basic example below (tab 1) demonstrates a data contract containing a single document type (note) which has a single string property (message).

The second tab shows the same data contract with an index defined on the $ownerId field. This would allow querying for documents owned by a specific identity using a where clause.

The third tab shows a data contract using the JSON-Schema $ref feature that enables reuse of defined objects.

The fourth tab shows a data contract requiring the optional $createdAt and $updatedAt base fields. Using these fields enables retrieving timestamps that indicate when a document was created or modified.

πŸ‘

As of Dash Platform 0.16, binary data can be stored using byte arrays. This improves efficiency by removing the need to encode binary data (e.g. with base64).

The fifth tab shows a data contract using the custom JSON-Schema keyword (byteArray) that is used to indicate the provided data will be an array of bytes.

❗️

Dash Platform 0.20.0 breaking change

As of Dash Platform 0.20.0 it is necessary to include "type": "object" for each document. Additionally references have moved to $defs (previously definitions) following an update to AJV v7.

{
  "note": {
    "type": "object",
    "properties": {
      "message": {
        "type": "string"
      }
    },
    "additionalProperties": false
  }
}
{
  "note": {
    "type": "object",
    "indices": [
      {
        "properties": [{ "$ownerId": "asc" }], "unique": false },
    ],
    "properties": {
      "message": {
        "type": "string"
      }
    },
    "additionalProperties": false
  }
}

/*
An identity's documents are accessible via a query including a where clause like:
{
  where: [['$ownerId', '==', 'an identity id']],
}
*/
{
  customer: {
    type: "object",
    properties: {
      name: { type: "string" },
      billing_address: { $ref: "#/$defs/address" },
      shipping_address: { $ref: "#/$defs/address" }
    },
    additionalProperties: false
  },
}

/*
The contract document defined above is dependent on the following object 
being added to the contract via the contracts `.setDefinitions` method:

{
  address: {
    type: "object",
    properties: {
      street_address: { type: "string" },
      city:           { type: "string" },
      state:          { type: "string" }
    },
    required: ["street_address", "city", "state"],
    additionalProperties: false
  }
}
*/
{
  "note": {
    "type": "object",
    "properties": {
      "message": {
        "type": "string"
      }
    },
    "required": ["$createdAt", "$updatedAt"],
    "additionalProperties": false
  }
}

/*
If $createdAt and/or $updatedAt are added to the list of required properties 
for a document, all documents of that type will store a timestamp indicating
when the document was created or modified. 

This information will be returned when the document is retrieved.
*/
{
 "block": {
   "type": "object",
    "properties": {
      "hash": {
        "type": 'array',
        "byteArray": true,
        "maxItems": 64,
        "description": 'Store block hashes',
      },
    },
    "additionalProperties": false,
  },
}
 
/*
Setting `"byteArray": true` indicates that the provided data will be an 
array of bytes (e.g. a NodeJS Buffer).
*/

πŸ“˜

Please refer to the data contract reference page for more comprehensive details related to contracts and documents.

Registering the data contract

πŸ“˜

Wallet Operations

Currently, the JavaScript SDK does not cache wallet information, and therefore, it re-syncs the entire Core chain for some wallet operations (e.g. client.getWalletAccount()). This can result in wait times of 5+ minutes.

An upcoming release will add a persistence feature to cache wallet information during initial sync so that subsequent access is much faster. For now, the skipSynchronizationBeforeHeight option can be used to only sync the wallet starting at a certain block height.

The following examples demonstrate the details of creating contracts using the features described above:

const Dash = require('dash');

const clientOpts = {
  wallet: {
    mnemonic: 'a Dash wallet mnemonic with funds goes here',
    unsafeOptions: {
      skipSynchronizationBeforeHeight: 415000, // only sync from start of 2021
    },    
  },
};
const client = new Dash.Client(clientOpts);

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',
        },
      },
      additionalProperties: false,
    },
  };

  const contract = await platform.contracts.create(contractDocuments, identity);
  console.dir({ contract });

  // Make sure contract passes validation checks
  await platform.dpp.initialize();
  const validationResult = await platform.dpp.dataContract.validate(contract);

  if (validationResult.isValid()) {
    console.log('Validation passed, broadcasting contract..');
    // Sign and submit the data contract
    return platform.contracts.broadcast(contract, identity);
  }
  console.error(validationResult); // An array of detailed validation errors
  throw validationResult.errors[0];
};

registerContract()
  .then((d) => console.log('Contract registered:\n', d.toJSON()))
  .catch((e) => console.error('Something went wrong:\n', e))
  .finally(() => client.disconnect());
const Dash = require('dash');

const clientOpts = {
  wallet: {
    mnemonic: 'a Dash wallet mnemonic with funds goes here',
    unsafeOptions: {
      skipSynchronizationBeforeHeight: 415000, // only sync from start of 2021
    },
  },
};
const client = new Dash.Client(clientOpts);

const registerContract = async () => {
  const { platform } = client;
  const identity = await platform.identities.get('an identity ID goes here');

  const contractDocuments = {
    note: {
      type: 'object',
      indices: [
        { properties: [{ $ownerId: 'asc' }], unique: false },
      ],
      properties: {
        message: {
          type: 'string',
        },
      },
      additionalProperties: false,
    },
  };

  const contract = await platform.contracts.create(contractDocuments, identity);
  console.dir({ contract });

  // Make sure contract passes validation checks
  await platform.dpp.initialize();
  const validationResult = await platform.dpp.dataContract.validate(contract);

  if (validationResult.isValid()) {
    console.log('Validation passed, broadcasting contract..');
    // Sign and submit the data contract
    return platform.contracts.broadcast(contract, identity);
  }
  console.error(validationResult); // An array of detailed validation errors
  throw validationResult.errors[0];
};

registerContract()
  .then((d) => console.log('Contract registered:\n', d.toJSON()))
  .catch((e) => console.error('Something went wrong:\n', e))
  .finally(() => client.disconnect());
const Dash = require('dash');

const clientOpts = {
  wallet: {
    mnemonic: 'a Dash wallet mnemonic with funds goes here',
    unsafeOptions: {
      skipSynchronizationBeforeHeight: 415000, // only sync from start of 2021
    },
  },
};
const client = new Dash.Client(clientOpts);

const registerContract = async () => {
  const { platform } = client;
  const identity = await platform.identities.get('an identity ID goes here');

  // Define a reusable object
  const definitions = {
    address: {
      type: 'object',
      properties: {
        street_address: { type: 'string' },
        city: { type: 'string' },
        state: { type: 'string' },
      },
      required: ['street_address', 'city', 'state'],
      additionalProperties: false,
    },
  };

  // Create a document with properties using a definition via $ref
  const contractDocuments = {
    customer: {
      type: 'object',
      properties: {
        name: { type: 'string' },
        billing_address: { $ref: '#/$defs/address' },
        shipping_address: { $ref: '#/$defs/address' },
      },
      additionalProperties: false,
    },
  };
  
  const contract = await platform.contracts.create(contractDocuments, identity);

  // Add reusable definitions referred to by "$ref" to contract
  contract.setDefinitions(definitions);
  console.dir({ contract });

  // Make sure contract passes validation checks
  await platform.dpp.initialize();
  const validationResult = await platform.dpp.dataContract.validate(contract);

  if (validationResult.isValid()) {
    console.log('Validation passed, broadcasting contract..');
    // Sign and submit the data contract
    return platform.contracts.broadcast(contract, identity);
  }
  console.error(validationResult); // An array of detailed validation errors
  throw validationResult.errors[0];
};

registerContract()
  .then((d) => console.log('Contract registered:\n', d.toJSON()))
  .catch((e) => console.error('Something went wrong:\n', e))
  .finally(() => client.disconnect());
const Dash = require('dash');

const clientOpts = {
  wallet: {
    mnemonic: 'a Dash wallet mnemonic with funds goes here',
    unsafeOptions: {
      skipSynchronizationBeforeHeight: 415000, // only sync from start of 2021
    },
  },
};
const client = new Dash.Client(clientOpts);

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',
        },
      },
      required: ['$createdAt', '$updatedAt'],
      additionalProperties: false,
    },
  };

  const contract = await platform.contracts.create(contractDocuments, identity);
  console.dir({ contract });

  // Make sure contract passes validation checks
  await platform.dpp.initialize();
  const validationResult = await platform.dpp.dataContract.validate(contract);

  if (validationResult.isValid()) {
    console.log('Validation passed, broadcasting contract..');
    // Sign and submit the data contract
    return platform.contracts.broadcast(contract, identity);
  }
  console.error(validationResult); // An array of detailed validation errors
  throw validationResult.errors[0];
};

registerContract()
  .then((d) => console.log('Contract registered:\n', d.toJSON()))
  .catch((e) => console.error('Something went wrong:\n', e))
  .finally(() => client.disconnect());
const Dash = require('dash');

const clientOpts = {
  wallet: {
    mnemonic: 'a Dash wallet mnemonic with funds goes here',
    unsafeOptions: {
      skipSynchronizationBeforeHeight: 415000, // only sync from start of 2021
    },
  },
};
const client = new Dash.Client(clientOpts);

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',
        },
      },
      additionalProperties: false,
    },
  };

  const contract = await platform.contracts.create(contractDocuments, identity);
  console.dir({ contract }, { depth: 5 });

  // Make sure contract passes validation checks
  await platform.dpp.initialize();
  const validationResult = await platform.dpp.dataContract.validate(contract);

  if (validationResult.isValid()) {
    console.log('Validation passed, broadcasting contract..');
    // Sign and submit the data contract
    return platform.contracts.broadcast(contract, identity);
  }
  console.error(validationResult); // An array of detailed validation errors
  throw validationResult.errors[0];
};

registerContract()
  .then((d) => console.log('Contract registered:\n', d.toJSON()))
  .catch((e) => console.error('Something went wrong:\n', e))
  .finally(() => client.disconnect());

πŸ‘

Make a note of the returned data contract $id as it will be used 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.broadcast 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,