Skip to main content

Custom Data Contracts

Advanced guide for developers creating custom data contracts for specialized storage needs.

Overview​

Data contracts define the structure and validation rules for your data on Dash Platform. While EvoBin provides default contracts for file storage, you can create custom contracts for specialized applications.

When to Use Custom Contracts​

Consider custom data contracts when:

  • Specialized data structures - Storing data that doesn't fit the file/document model
  • Complex relationships - Data with complex relationships between documents
  • Advanced validation - Custom validation logic beyond basic schemas
  • Performance optimization - Optimized schemas for specific query patterns
  • Privacy requirements - Custom encryption or access control schemes

Creating a Custom Data Contract​

Basic Structure​

import { DashPlatformSDK } from 'dash-platform-sdk'

const sdk = new DashPlatformSDK({ network: 'testnet' })

// Define your schema
const mySchema = {
documentTypes: {
userProfile: {
type: 'object',
indices: [
{
name: 'userId',
properties: [{ '$ownerId': 'asc' }],
unique: true
}
],
properties: {
username: {
type: 'string',
maxLength: 50,
position: 0
},
bio: {
type: 'string',
maxLength: 500,
position: 1
},
avatar: {
type: 'string', // IPFS hash or reference
position: 2
},
preferences: {
type: 'object',
position: 3,
properties: {
theme: { type: 'string', position: 0 },
notifications: { type: 'boolean', position: 1 }
}
}
},
required: ['username'],
additionalProperties: false
}
}
}

// Create the data contract
async function createCustomContract(identityId: string) {
const identityNonce = await sdk.identities.getIdentityNonce(identityId)

const dataContract = await sdk.dataContracts.create(
identityId,
identityNonce,
mySchema
)

// Sign and broadcast
const stateTransition = await sdk.dataContracts.createStateTransition(
dataContract,
'create',
identityNonce
)

// You'll need to sign this with the extension
return stateTransition
}

Advanced Schema Features​

Composite Indexes​

const complexSchema = {
documentTypes: {
activity: {
type: 'object',
indices: [
{
name: 'user_activity',
properties: [
{ '$ownerId': 'asc' },
{ 'timestamp': 'desc' }
]
},
{
name: 'type_user',
properties: [
{ 'activityType': 'asc' },
{ '$ownerId': 'asc' }
]
}
],
properties: {
activityType: { type: 'string', position: 0 },
timestamp: { type: 'integer', position: 1 },
metadata: { type: 'object', position: 2 }
}
}
}
}

Token-Based Contracts​

const tokenSchema = {
documentTypes: {
premiumContent: {
type: 'object',
indices: [
{
name: 'contentOwner',
properties: [{ '$ownerId': 'asc' }]
}
],
properties: {
contentHash: { type: 'string', position: 0 },
price: { type: 'integer', position: 1 },
accessToken: { type: 'string', position: 2 }
}
}
}
}

const tokenConfig = {
tokenContractId: '6hVQW16jyvZyGSQk2YVty4ND6bgFXozizYWnPt753uW5',
tokenContractPosition: 0,
minimumTokenCost: BigInt(1000), // Minimum tokens required
maximumTokenCost: BigInt(10000), // Maximum tokens required
gasFeesPaidBy: 'ContractOwner' // Or 'DocumentOwner'
}

Deploying Custom Contracts​

Step 1: Prepare Your Identity​

Ensure you have sufficient credits and a registered identity:

async function prepareIdentity() {
const ex = window.dashPlatformExtension
if (!ex?.signer?.connect) {
throw new Error('Dash Platform Extension not available')
}

const { currentIdentity } = await ex.signer.connect()

// Check identity balance
const sdk = window.dashPlatformSDK
const balance = await sdk.identities.getIdentityBalance(currentIdentity)

if (balance < BigInt(1000)) {
throw new Error('Insufficient credits for contract deployment')
}

return currentIdentity
}

Step 2: Create and Deploy Contract​

async function deployCustomContract() {
try {
// 1. Prepare identity
const identityId = await prepareIdentity()

// 2. Create contract
const identityNonce = await sdk.identities.getIdentityNonce(identityId)
const contract = await createCustomContract(identityId)

// 3. Sign with extension
const signedContract = await window.dashPlatformExtension.signer.signAndBroadcast(contract)

// 4. Wait for confirmation
await sdk.stateTransitions.waitForStateTransitionResult(signedContract)

console.log('Contract deployed successfully:', signedContract.hash(true))
return signedContract

} catch (error) {
console.error('Contract deployment failed:', error)
throw error
}
}

Interacting with Custom Contracts​

Creating Documents​

async function createCustomDocument(contractId: string, documentType: string, data: any) {
const identityId = await prepareIdentity()
const sdk = window.dashPlatformSDK

// Get current nonce
const identityContractNonce = await sdk.identities.getIdentityContractNonce(
identityId,
contractId
)

// Create document
const document = await sdk.documents.create(
contractId,
documentType,
data,
identityId,
identityContractNonce + BigInt(1)
)

// Create and sign state transition
const stateTransition = await sdk.documents.createStateTransition(
document,
'create',
identityContractNonce + BigInt(1)
)

const signedTransition = await window.dashPlatformExtension.signer.signAndBroadcast(stateTransition)
await sdk.stateTransitions.waitForStateTransitionResult(signedTransition)

return document
}

Querying Documents​

async function queryCustomDocuments(contractId: string, documentType: string, query: any[] = []) {
const sdk = window.dashPlatformSDK

const documents = await sdk.documents.query(
contractId,
documentType,
query,
[['$createdAt', 'desc']], // Order by creation date
100 // Limit
)

return documents
}

// Example query for user profiles
const userProfiles = await queryCustomDocuments(
'your-contract-id-here',
'userProfile',
[['username', '==', 'alice']]
)

Best Practices​

1. Schema Design​

const bestPracticesSchema = {
documentTypes: {
optimizedType: {
type: 'object',
indices: [
{
name: 'primary_index',
properties: [
{ '$ownerId': 'asc' },
{ 'createdAt': 'desc' }
],
unique: true
}
],
properties: {
// Required fields first
createdAt: { type: 'integer', position: 1 },
// Frequently queried fields
status: { type: 'string', position: 2 },
category: { type: 'string', position: 3 },
// Large fields last
metadata: { type: 'object', position: 4 },
content: { type: 'string', position: 5 }
},
// Validation rules
required: ['id', 'createdAt'],
additionalProperties: false,
// Document constraints
maxItems: 10000, // Maximum documents per identity
minItems: 0
}
}
}

2. Gas Optimization​

// Tips for minimizing gas costs:
// 1. Use smallest possible data types
// 2. Avoid unnecessary indices
// 3. Batch operations when possible
// 4. Use efficient serialization

const optimizedSchema = {
documentTypes: {
efficientData: {
type: 'object',
indices: [
{
name: 'lookup',
properties: [{ 'key': 'asc' }],
unique: true
}
],
properties: {
key: {
type: 'string',
position: 0,
maxLength: 32 // Limit size
},
value: {
type: 'string',
position: 1,
maxLength: 256
},
timestamp: {
type: 'integer',
position: 2,
minimum: 0
}
}
}
}
}

3. Error Handling​

class CustomContractManager {
private sdk: any
private contractId?: string

constructor() {
this.sdk = window.dashPlatformSDK
}

async deployContract(identityId: string, schema: any): Promise<string> {
try {
const identityNonce = await this.sdk.identities.getIdentityNonce(identityId)

const contract = await this.sdk.dataContracts.create(
identityId,
identityNonce,
schema
)

const stateTransition = await this.sdk.dataContracts.createStateTransition(
contract,
'create',
identityNonce
)

const signed = await window.dashPlatformExtension.signer.signAndBroadcast(stateTransition)
await this.sdk.stateTransitions.waitForStateTransitionResult(signed)

this.contractId = contract.getId().toString()
return this.contractId

} catch (error) {
if (error.message.includes('insufficient balance')) {
throw new Error('Insufficient credits. Please top up your identity.')
} else if (error.message.includes('invalid schema')) {
throw new Error('Invalid schema format. Check your document definitions.')
} else {
throw new Error(`Contract deployment failed: ${error.message}`)
}
}
}

async createDocument(documentType: string, data: any): Promise<any> {
if (!this.contractId) {
throw new Error('Contract not deployed. Call deployContract first.')
}

const { currentIdentity } = await window.dashPlatformExtension.signer.connect()

try {
const nonce = await this.sdk.identities.getIdentityContractNonce(
currentIdentity,
this.contractId
)

const document = await this.sdk.documents.create(
this.contractId,
documentType,
data,
currentIdentity,
nonce + BigInt(1)
)

const stateTransition = await this.sdk.documents.createStateTransition(
document,
'create',
nonce + BigInt(1)
)

const signed = await window.dashPlatformExtension.signer.signAndBroadcast(stateTransition)
await this.sdk.stateTransitions.waitForStateTransitionResult(signed)

return document

} catch (error) {
// Handle specific contract errors
if (error.message.includes('Document already exists')) {
throw new Error('Document with this ID already exists')
} else if (error.message.includes('Invalid document')) {
throw new Error('Document data does not match schema')
}
throw error
}
}
}

Example: User Profiles Contract​

// Complete example: User profiles with social features
const userProfileContract = {
documentTypes: {
profile: {
type: 'object',
indices: [
{
name: 'username_index',
properties: [{ 'username': 'asc' }],
unique: true
},
{
name: 'user_profile',
properties: [{ '$ownerId': 'asc' }],
unique: true
}
],
properties: {
username: {
type: 'string',
maxLength: 30,
pattern: '^[a-zA-Z0-9_]{3,30}$',
position: 0
},
displayName: {
type: 'string',
maxLength: 50,
position: 1
},
bio: {
type: 'string',
maxLength: 500,
position: 2
},
avatar: {
type: 'string', // IPFS hash
maxLength: 64,
position: 3
},
socialLinks: {
type: 'object',
position: 4,
properties: {
twitter: { type: 'string', position: 0 },
github: { type: 'string', position: 1 },
website: { type: 'string', position: 2 }
}
},
preferences: {
type: 'object',
position: 5,
properties: {
theme: {
type: 'string',
position: 0,
enum: ['light', 'dark', 'auto']
},
emailNotifications: { type: 'boolean', position: 1 },
twoFactorEnabled: { type: 'boolean', position: 2 }
}
}
},
required: ['username'],
additionalProperties: false,
// Document limits
maxItems: 1 // One profile per identity
},
connection: {
type: 'object',
indices: [
{
name: 'connection_index',
properties: [
{ 'fromUserId': 'asc' },
{ 'toUserId': 'asc' }
],
unique: true
},
{
name: 'incoming_index',
properties: [{ 'toUserId': 'asc' }]
}
],
properties: {
fromUserId: { type: 'string', position: 0 },
toUserId: { type: 'string', position: 1 },
status: {
type: 'string',
position: 2,
enum: ['pending', 'accepted', 'blocked']
},
createdAt: { type: 'integer', position: 3 }
},
required: ['fromUserId', 'toUserId', 'status'],
additionalProperties: false
}
}
}

// Usage example
async function setupUserProfileSystem() {
const manager = new CustomContractManager()
const identityId = await prepareIdentity()

// Deploy contract
const contractId = await manager.deployContract(identityId, userProfileContract)
console.log('Profile contract deployed:', contractId)

// Create user profile
const profile = await manager.createDocument('profile', {
username: 'alice123',
displayName: 'Alice Johnson',
bio: 'Decentralization enthusiast and developer',
avatar: 'QmXoypiz...',
socialLinks: {
twitter: '@alice123',
github: 'alice123'
},
preferences: {
theme: 'dark',
emailNotifications: true,
twoFactorEnabled: false
}
})

console.log('Profile created:', profile.getId().toString())
}

Migration Strategies​

Versioning Data Contracts​

// Version 1 Schema
const v1Schema = {
documentTypes: {
user: {
type: 'object',
properties: {
name: { type: 'string', position: 0 },
email: { type: 'string', position: 1 }
}
}
}
}

// Version 2 Schema (adds new field)
const v2Schema = {
documentTypes: {
user: {
type: 'object',
properties: {
name: { type: 'string', position: 0 },
email: { type: 'string', position: 1 },
phone: { type: 'string', position: 2 } // New field
}
}
}
}

// Migration strategy:
// 1. Deploy new contract version
// 2. Create migration documents
// 3. Use both contracts during transition
// 4. Phase out old contract

Testing & Validation​

Local Testing​

import { DashPlatformSDK } from 'dash-platform-sdk'

// Test in development/testnet first
const testSDK = new DashPlatformSDK({
network: 'testnet',
dapiAddresses: ['https://testnet.dash.org']
})

// Validate schema before deployment
function validateSchema(schema: any): string[] {
const errors: string[] = []

// Check for required fields
if (!schema.documentTypes || Object.keys(schema.documentTypes).length === 0) {
errors.push('Schema must contain at least one document type')
}

// Validate each document type
for (const [typeName, typeDef] of Object.entries(schema.documentTypes)) {
if (!typeDef.properties) {
errors.push(`Document type ${typeName} must have properties`)
}

// Check position uniqueness
const positions = new Set()
for (const [propName, propDef] of Object.entries(typeDef.properties)) {
if (propDef.position !== undefined) {
if (positions.has(propDef.position)) {
errors.push(`Duplicate position ${propDef.position} in ${typeName}.${propName}`)
}
positions.add(propDef.position)
}
}
}

return errors
}

// Test document creation
async function testDocumentCreation(contractId: string, documentType: string, testData: any) {
try {
const document = await testSDK.documents.create(
contractId,
documentType,
testData,
'test-identity-id',
BigInt(1)
)

// Validate document structure
const isValid = document.validate()
return isValid

} catch (error) {
console.error('Document creation test failed:', error)
return false
}
}

Security Considerations​

Access Control​

// Implement role-based access control in your application
class AccessController {
private userRoles = new Map<string, string[]>()

async canCreateDocument(userId: string, documentType: string): Promise<boolean> {
const roles = this.userRoles.get(userId) || []

switch (documentType) {
case 'adminSettings':
return roles.includes('admin')
case 'userProfile':
return roles.includes('user') || roles.includes('admin')
case 'publicContent':
return true // Anyone can create
default:
return false
}
}

async canReadDocument(userId: string, document: any, documentType: string): Promise<boolean> {
const roles = this.userRoles.get(userId) || []

// Example: Only document owner or admin can read
if (document.$ownerId === userId || roles.includes('admin')) {
return true
}

// Public documents
if (documentType === 'publicContent' && document.isPublic === true) {
return true
}

return false
}
}

Data Validation​

// Always validate on client side before sending to blockchain
function validateUserInput(data: any, schema: any): ValidationResult {
const result: ValidationResult = {
valid: true,
errors: []
}

// Type checking
for (const [field, definition] of Object.entries(schema.properties)) {
const value = data[field]

if (definition.required && (value === undefined || value === null)) {
result.valid = false
result.errors.push(`${field} is required`)
continue
}

if (value !== undefined) {
// Type validation
if (definition.type === 'string' && typeof value !== 'string') {
result.valid = false
result.errors.push(`${field} must be a string`)
} else if (definition.type === 'integer' && !Number.isInteger(value)) {
result.valid = false
result.errors.push(`${field} must be an integer`)
}

// Length validation
if (definition.maxLength && value.length > definition.maxLength) {
result.valid = false
result.errors.push(`${field} exceeds maximum length of ${definition.maxLength}`)
}

// Pattern validation
if (definition.pattern && !new RegExp(definition.pattern).test(value)) {
result.valid = false
result.errors.push(`${field} does not match required pattern`)
}
}
}

return result
}

interface ValidationResult {
valid: boolean
errors: string[]
}

Resources​

Next Steps​