@claritydao/midnight-sdk
v1.1.3
Published
Production-ready TypeScript SDK for governance operations on the Midnight blockchain.
Downloads
420
Keywords
Readme
Agora DAO Midnight SDK
Production-ready TypeScript SDK for governance operations on the Midnight blockchain.
Overview
The Agora DAO Midnight SDK provides a type-safe, professional-grade interface for interacting with governance contracts on Midnight Network. It integrates with the Lace wallet extension and provides comprehensive APIs for DAO governance operations.
Features
- Type-Safe: Full TypeScript support with comprehensive type definitions
- Production-Ready: No hardcoded values, proper error handling, validation
- Browser-Native: Works directly in modern browsers with Lace wallet
- Observable State: Reactive patterns with RxJS for real-time updates
- Zero-Knowledge: Privacy-preserving governance with ZK proofs
- Well-Documented: Complete API reference and examples
Installation
pnpm add @claritydao/midnight-sdkQuick Start
1. Deploy a New Governor Contract
import {
GovernorAPI,
initializeProviders,
ConfigPresets,
} from "@claritydao/midnight-sdk";
import { randomBytes } from "@midnight-ntwrk/midnight-js-utils";
import pino from "pino";
const logger = pino({ level: "info" });
// Initialize providers (connects to Lace wallet)
const providers = await initializeProviders(logger);
// Create deployment configuration
const config = {
governor: {
governance: ConfigPresets.testing(), // or ConfigPresets.production()
executionDelay: BigInt(60),
gracePeriod: BigInt(300),
maxActiveProposals: BigInt(100),
proposalLifetime: BigInt(86400),
emergencyVetoEnabled: true,
emergencyVetoThreshold: BigInt(75000),
},
token: {
name: "My DAO Token",
symbol: "MDAO",
decimals: BigInt(18),
},
adminKey: randomBytes(32), // IMPORTANT: Use secure random bytes!
};
// Deploy contract
const api = await GovernorAPI.deploy(providers, config, logger);
console.log("Contract deployed at:", api.deployedContractAddress);2. Join an Existing Contract
import { GovernorAPI, initializeProviders } from "@claritydao/midnight-sdk";
import pino from "pino";
const logger = pino({ level: "info" });
const providers = await initializeProviders(logger);
// Join existing contract
const contractAddress = "existing-contract-address";
const api = await GovernorAPI.join(providers, contractAddress, logger);3. Create a Proposal
import { VoteChoice } from "@claritydao/midnight-sdk";
import type { ProposalAction } from "@claritydao/midnight-sdk";
// Define proposal actions (what happens if proposal passes)
const actions: ProposalAction[] = [
{
typ: 0, // Transfer type
token: tokenAddress,
amount: BigInt(10000),
to: recipientAddress,
configKey: new Uint8Array(32),
configValue: new Uint8Array(32),
},
];
// Create proposal (requires sufficient stake)
const { txData, proposalId } = await api.createProposal(
"Fund Community Initiative",
"Transfer 10,000 tokens to community wallet for Q1 initiatives",
creatorAddress,
actions
);
console.log("Proposal created with ID:", proposalId);4. Cast a Vote
import { VoteChoice } from "@claritydao/midnight-sdk";
// Vote on proposal (type-safe!)
await api.castVote(
proposalId,
voterAddress,
VoteChoice.For // VoteChoice.Against or VoteChoice.Abstain
);
console.log("Vote cast successfully");Configuration Presets
The SDK provides two configuration presets:
Testing Configuration
For development and testnet:
import { ConfigPresets } from "@claritydao/midnight-sdk";
const governanceConfig = ConfigPresets.testing();
// Returns:
// {
// votingPeriod: BigInt(300), // 5 minutes
// quorumThreshold: BigInt(100), // 100 tokens
// proposalThreshold: BigInt(50), // 50 tokens to create proposals
// proposalLifetime: BigInt(3600), // 1 hour
// executionDelay: BigInt(60), // 1 minute
// gracePeriod: BigInt(300), // 5 minutes
// minStakeAmount: BigInt(10), // 10 tokens minimum
// stakingPeriod: BigInt(60), // 1 minute lock
// }Production Configuration
For mainnet:
import { ConfigPresets } from "@claritydao/midnight-sdk";
const governanceConfig = ConfigPresets.production();
// Returns:
// {
// votingPeriod: BigInt(604800), // 7 days
// quorumThreshold: BigInt(100000), // 100K tokens
// proposalThreshold: BigInt(50000), // 50K tokens to create proposals
// proposalLifetime: BigInt(1209600), // 14 days
// executionDelay: BigInt(86400), // 24 hours
// gracePeriod: BigInt(259200), // 3 days
// minStakeAmount: BigInt(1000), // 1K tokens minimum
// stakingPeriod: BigInt(604800), // 7 days lock
// }API Reference
Types
DeploymentConfig
Complete deployment configuration:
interface DeploymentConfig {
governor: GovernorConfig;
token: TokenConfig;
adminKey: Uint8Array; // 32 bytes - use randomBytes(32)
}GovernorConfig
Governor contract parameters:
interface GovernorConfig {
governance: GovernanceConfig;
executionDelay: bigint;
gracePeriod: bigint;
maxActiveProposals: bigint;
proposalLifetime: bigint;
emergencyVetoEnabled: boolean;
emergencyVetoThreshold: bigint;
}GovernanceConfig
Core governance parameters:
interface GovernanceConfig {
votingPeriod: bigint; // How long voting lasts
quorumThreshold: bigint; // Minimum votes to pass
proposalThreshold: bigint; // Stake required to create proposals
proposalLifetime: bigint; // How long proposals remain valid
executionDelay: bigint; // Time-lock before execution
gracePeriod: bigint; // Window to execute after delay
minStakeAmount: bigint; // Minimum stake to vote
stakingPeriod: bigint; // Stake lock duration
}TokenConfig
Token parameters:
interface TokenConfig {
name: string; // Max 64 characters
symbol: string; // Max 16 characters
decimals: bigint; // Max 18
}ProposalAction
Actions to execute when proposal passes:
interface ProposalAction {
typ: number; // Action type (0 = transfer)
token: Uint8Array; // Token address (32 bytes)
amount: bigint; // Amount to transfer
to: Uint8Array; // Recipient address (32 bytes)
configKey: Uint8Array; // Config key (32 bytes)
configValue: Uint8Array; // Config value (32 bytes)
}VoteChoice
Type-safe vote choices:
enum VoteChoice {
For = 0, // Vote in favor
Against = 1, // Vote against
Abstain = 2, // Abstain from voting
}GovernorAPI
Static Methods
deploy()
static async deploy(
providers: GovernorProviders,
config: DeploymentConfig,
logger?: Logger
): Promise<GovernorAPI>Deploys a new governor contract with the specified configuration.
Parameters:
providers: Initialized providers (frominitializeProviders)config: Deployment configurationlogger(optional): Pino logger instance
Returns: GovernorAPI instance
Throws:
InvalidConfigurationError: Invalid configuration parametersDeploymentError: Contract deployment failedWalletNotConnectedError: Wallet not connected
Example:
const api = await GovernorAPI.deploy(providers, config, logger);join()
static async join(
providers: GovernorProviders,
contractAddress: ContractAddress,
logger?: Logger
): Promise<GovernorAPI>Joins an existing governor contract.
Parameters:
providers: Initialized providerscontractAddress: Address of deployed contractlogger(optional): Pino logger instance
Returns: GovernorAPI instance
Throws:
ContractNotFoundError: Contract not found at addressWalletNotConnectedError: Wallet not connected
Instance Methods
createProposal()
async createProposal(
title: string,
description: string,
creator: Uint8Array,
actions: ProposalAction[]
): Promise<{ txData: FinalizedTxData; proposalId: bigint }>Creates a new governance proposal.
Parameters:
title: Proposal title (max 256 chars)description: Proposal description (max 10,000 chars)creator: Creator address (32 bytes, must have sufficient stake)actions: Array of actions to execute if proposal passes
Returns: Object with transaction data and proposal ID
Throws:
InvalidParameterError: Invalid parametersInsufficientStakeError: Creator has insufficient stakeTransactionError: Transaction failed
Example:
const { proposalId } = await api.createProposal(
"Increase Treasury Allocation",
"Detailed description of the proposal...",
creatorAddress,
[
{
typ: 0,
token: tokenAddress,
amount: BigInt(10000),
to: recipientAddress,
configKey: new Uint8Array(32),
configValue: new Uint8Array(32),
},
]
);castVote()
async castVote(
proposalId: bigint,
voter: Uint8Array,
choice: VoteChoice
): Promise<FinalizedTxData>Casts a vote on an active proposal.
Parameters:
proposalId: Proposal ID to vote onvoter: Voter address (32 bytes, must have voting power)choice: Vote choice (VoteChoice.For, VoteChoice.Against, or VoteChoice.Abstain)
Returns: Transaction data
Throws:
InvalidParameterError: Invalid parametersInsufficientStakeError: Voter has insufficient voting powerProposalNotFoundError: Proposal not found or not activeTransactionError: Transaction failed
Example:
await api.castVote(proposalId, voterAddress, VoteChoice.For);registerStake()
async registerStake(
userId: Uint8Array,
amount: bigint
): Promise<FinalizedTxData>Registers a stake for governance participation.
Parameters:
userId: User address (32 bytes)amount: Amount to stake
Returns: Transaction data
finalizeProposal()
async finalizeProposal(proposalId: bigint): Promise<FinalizedTxData>Finalizes a proposal after voting period ends.
queueProposal()
async queueProposal(proposalId: bigint): Promise<FinalizedTxData>Queues a finalized proposal for execution.
executeProposal()
async executeProposal(
proposalId: bigint,
effectId: bigint
): Promise<FinalizedTxData>Executes a queued proposal after execution delay.
getProposalDetails()
async getProposalDetails(proposalId: bigint): Promise<ProposalDetails>Gets detailed information about a proposal.
getProposalVoteCount()
async getProposalVoteCount(proposalId: bigint): Promise<VoteCount>Gets vote counts for a proposal.
getStakeInfo()
async getStakeInfo(userId: Uint8Array): Promise<StakeInfo>Gets staking information for a user.
initializeProviders()
async function initializeProviders(logger: Logger): Promise<GovernorProviders>;Initializes all required providers for governance operations.
What it does:
- Connects to Lace wallet extension
- Requests user authorization
- Retrieves wallet state and service URIs
- Configures all providers (IndexedDB, HTTP, Indexer, Wallet, Proof server)
Parameters:
logger: Pino logger instance
Returns: Configured providers object
Throws:
- Error if Lace wallet not found
- Error if wallet connection rejected
- Error if incompatible wallet version
Example:
const providers = await initializeProviders(logger);GovernorManager
High-level API with observable state management.
Constructor
constructor(
deploymentConfig: DeploymentConfig,
logger: Logger
)Parameters:
deploymentConfig: Deployment configurationlogger: Pino logger instance
Example:
const manager = new GovernorManager(config, logger);Methods
resolve()
resolve(contractAddress?: ContractAddress): Observable<GovernorDeployment>Deploys a new contract or joins an existing one, with observable state.
Returns: Observable that emits deployment state changes
Deployment States:
{ status: 'in-progress' }
{ status: 'deployed', api: DeployedGovernorAPI }
{ status: 'failed', error: Error }Error Handling
Custom Error Classes
The SDK provides specific error classes for different failure scenarios:
import {
GovernorSDKError,
WalletNotConnectedError,
InsufficientStakeError,
InvalidParameterError,
ContractNotFoundError,
ProposalNotFoundError,
TransactionError,
DeploymentError,
InvalidConfigurationError,
NetworkNotConfiguredError,
} from "@claritydao/midnight-sdk";Error Handling Pattern
try {
await api.createProposal(title, description, creator, actions);
} catch (error) {
if (error instanceof InsufficientStakeError) {
console.error("Need more stake:", error.message);
} else if (error instanceof InvalidParameterError) {
console.error("Invalid input:", error.message);
} else if (error instanceof TransactionError) {
console.error("Transaction failed:", error.message);
} else {
console.error("Unexpected error:", error);
}
}Validation
The SDK includes comprehensive input validation:
import { validators } from "@claritydao/midnight-sdk";
// Validate proposal parameters
validators.validateProposal(title, description, creator);
// Validate vote choice
validators.validateVoteChoice(choice);
// Validate deployment config
validators.validateDeploymentConfig(config);
// Validate governance config
validators.validateGovernanceConfig(governanceConfig);Browser Integration
Critical: Frontend Setup Requirements
Before integrating this SDK into a browser application, you MUST configure your build tool correctly to handle WASM modules. Failure to do so will result in the error ocrt.maxField is not a function.
Required: Vite Configuration
IMPORTANT: Use Vite, not Next.js. Next.js 15's Turbopack does not support the webpack configurations required for this WASM pattern.
1. Install Required Vite Plugins
pnpm add -D vite vite-plugin-wasm vite-plugin-top-level-await @vitejs/plugin-react2. Create vite.config.ts
This exact configuration is required. It's based on Midnight Network's official example-bboard implementation:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";
export default defineConfig({
cacheDir: "./.vite",
build: {
target: "esnext",
minify: false,
rollupOptions: {
output: {
manualChunks: {
// CRITICAL: Separate chunk for WASM modules
wasm: ["@midnight-ntwrk/onchain-runtime"],
},
},
},
commonjsOptions: {
// CRITICAL: Transform CommonJS to ESM
transformMixedEsModules: true,
extensions: [".js", ".cjs"],
ignoreDynamicRequires: true,
},
},
plugins: [
react(),
wasm(),
topLevelAwait({
promiseExportName: "__tla",
promiseImportName: (i) => `__tla_${i}`,
}),
// CRITICAL: Custom resolver for WASM module handling
{
name: "wasm-module-resolver",
resolveId(source, importer) {
if (
source === "@midnight-ntwrk/onchain-runtime" &&
importer &&
importer.includes("@midnight-ntwrk/compact-runtime")
) {
return {
id: source,
external: false,
moduleSideEffects: true,
};
}
return null;
},
},
],
optimizeDeps: {
esbuildOptions: {
target: "esnext",
supported: { "top-level-await": true },
platform: "browser",
format: "esm",
loader: { ".wasm": "binary" },
},
// CRITICAL: Pre-bundle for CJS→ESM conversion
include: ["@midnight-ntwrk/compact-runtime"],
// CRITICAL: Exclude WASM from optimization
exclude: [
"@midnight-ntwrk/onchain-runtime",
"@midnight-ntwrk/onchain-runtime/midnight_onchain_runtime_wasm_bg.wasm",
"@midnight-ntwrk/onchain-runtime/midnight_onchain_runtime_wasm.js",
],
},
resolve: {
extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".wasm"],
mainFields: ["browser", "module", "main"],
},
});Why This Configuration is Required
The SDK depends on @midnight-ntwrk/compact-runtime, which:
- Uses CommonJS (
require()) - Imports
@midnight-ntwrk/onchain-runtime(WASM module with top-level await) - Executes
ocrt.maxField()at the top level
This creates a CJS/ESM/WASM incompatibility that requires the custom resolver plugin and specific optimization settings.
Why Next.js Doesn't Work
Next.js 15 uses Turbopack by default, which doesn't support the webpack configurations needed:
// These webpack configs don't work with Turbopack:
config.experiments = {
asyncWebAssembly: true,
topLevelAwait: true,
};Even with the --webpack flag, the CJS/ESM/WASM interaction is too complex for Next.js to handle correctly.
Recommendation: Use Vite for browser dApps (officially supported by Midnight Network).
React Example
import {
GovernorManager,
VoteChoice,
ConfigPresets,
} from "@claritydao/midnight-sdk";
import { randomBytes } from "@midnight-ntwrk/midnight-js-utils";
import { useEffect, useState } from "react";
import pino from "pino";
const logger = pino({ level: "info" });
const config = {
governor: {
governance: ConfigPresets.testing(),
executionDelay: BigInt(60),
gracePeriod: BigInt(300),
maxActiveProposals: BigInt(100),
proposalLifetime: BigInt(3600),
emergencyVetoEnabled: true,
emergencyVetoThreshold: BigInt(75000),
},
token: {
name: "Test DAO",
symbol: "TDAO",
decimals: BigInt(18),
},
adminKey: randomBytes(32), // CRITICAL: Use secure random bytes!
};
const manager = new GovernorManager(config, logger);
function GovernanceApp() {
const [deployment, setDeployment] = useState(null);
useEffect(() => {
const subscription = manager.resolve().subscribe(setDeployment);
return () => subscription.unsubscribe();
}, []);
if (!deployment || deployment.status === "in-progress") {
return <div>Connecting to wallet and deploying contract...</div>;
}
if (deployment.status === "failed") {
return <div>Error: {deployment.error.message}</div>;
}
const api = deployment.api;
const handleCreateProposal = async () => {
try {
const actions = [
{
typ: 0,
token: tokenAddress,
amount: BigInt(1000),
to: recipientAddress,
configKey: new Uint8Array(32),
configValue: new Uint8Array(32),
},
];
const { proposalId } = await api.createProposal(
"My Proposal",
"Description here",
creatorAddress,
actions
);
alert(`Proposal created: ${proposalId}`);
} catch (error) {
alert(`Error: ${error.message}`);
}
};
const handleVote = async (proposalId) => {
try {
await api.castVote(proposalId, voterAddress, VoteChoice.For);
alert("Vote cast successfully");
} catch (error) {
alert(`Error: ${error.message}`);
}
};
return (
<div>
<h1>Governor Contract</h1>
<p>Address: {api.deployedContractAddress}</p>
<button onClick={handleCreateProposal}>Create Proposal</button>
<button onClick={() => handleVote(1n)}>Vote For Proposal #1</button>
</div>
);
}Security Best Practices
1. Always Use Secure Random for Admin Keys
import { randomBytes } from "@midnight-ntwrk/midnight-js-utils";
// ✅ Good: Cryptographically secure random
const adminKey = randomBytes(32);
// ❌ Bad: Predictable key
const adminKey = new Uint8Array(32); // All zeros - INSECURE!2. Use Configuration Presets
// ✅ Good: Use tested presets
const config = ConfigPresets.production();
// ❌ Bad: Hardcoded magic numbers
const config = {
votingPeriod: BigInt(300), // What does 300 mean?
// ...
};3. Validate User Inputs
import { validators } from "@claritydao/midnight-sdk";
// ✅ Good: Validate before submission
validators.validateProposal(title, description, creator);
await api.createProposal(title, description, creator, actions);
// ❌ Bad: No validation
await api.createProposal(title, description, creator, actions);4. Handle Errors Gracefully
// ✅ Good: Specific error handling
try {
await api.castVote(proposalId, voter, choice);
} catch (error) {
if (error instanceof InsufficientStakeError) {
showMessage("You need more stake to vote");
} else {
showMessage("An error occurred");
}
}
// ❌ Bad: Silent failures
try {
await api.castVote(proposalId, voter, choice);
} catch (error) {
// Silent failure
}Troubleshooting
Critical: WASM Initialization Error
Problem: TypeError: ocrt.maxField is not a function
Root Cause: CJS/ESM/WASM module incompatibility. The SDK depends on @midnight-ntwrk/compact-runtime (CommonJS) which requires @midnight-ntwrk/onchain-runtime (WASM with top-level await).
Solutions:
Use Vite, not Next.js
# Install Vite and required plugins pnpm add -D vite vite-plugin-wasm vite-plugin-top-level-await @vitejs/plugin-reactConfigure vite.config.ts correctly - See "Browser Integration" section above for complete configuration
Key configuration requirements:
- Custom
wasm-module-resolverplugin transformMixedEsModules: truein commonjsOptions- Manual chunks for WASM modules
- Proper include/exclude in optimizeDeps
- Custom
Error Location:
File: node_modules/@midnight-ntwrk/compact-runtime/dist/runtime.js:90
Code: exports.MAX_FIELD = ocrt.maxField();Why Next.js Doesn't Work:
- Next.js 15 uses Turbopack which doesn't support required webpack configurations
- Even with
--webpackflag, the CJS/ESM/WASM interaction is too complex
Reference Implementations:
- Working example: Midnight Network's
example-bboardapplication - Configuration tested and verified in production
Admin Key Security Error
Problem: "Insecure admin key detected" or predictable deployment behavior
Root Cause: Using an all-zeros Uint8Array for admin key
Bad Example:
const adminKey = new Uint8Array(32); // All zeros - INSECURE!Correct Solution:
import { randomBytes } from "@midnight-ntwrk/midnight-js-utils";
const adminKey = randomBytes(32); // Cryptographically secureWhy It Matters:
- Admin keys control contract upgrades and governance
- Predictable keys can be exploited
- All production deployments MUST use secure random keys
Build Configuration Issues
Problem: "require is not defined" in browser
Root Cause: Contract module uses CommonJS but wasn't transformed to ESM
Solution:
// In vite.config.ts
commonjsOptions: {
transformMixedEsModules: true,
extensions: ['.js', '.cjs'],
}Problem: "Top-level await is not available"
Root Cause: WASM module with top-level await in wrong bundle chunk
Solution:
// In vite.config.ts
rollupOptions: {
output: {
manualChunks: {
wasm: ['@midnight-ntwrk/onchain-runtime'],
},
},
}Problem: Module optimization errors during build
Root Cause: Vite trying to optimize WASM and CJS modules together
Solution:
// In vite.config.ts
optimizeDeps: {
include: ['@midnight-ntwrk/compact-runtime'], // Pre-bundle for CJS→ESM
exclude: [
'@midnight-ntwrk/onchain-runtime', // Don't optimize WASM
'@claritydao/midnight-agora-contracts', // Don't optimize contract
],
}Wallet Connection Issues
Problem: "Could not find Midnight Lace wallet"
Solutions:
- Install Lace wallet extension from lace.io
- Refresh the browser page
- Check browser console for errors
- Ensure extension is enabled
Problem: "Incompatible version of Midnight Lace wallet"
Solutions:
- Update Lace extension to latest version
- SDK requires Lace API v1.x
- Check compatibility in browser console
Transaction Failures
Problem: InsufficientStakeError
Solutions:
- Register stake before creating proposals
- Ensure stake amount ≥ proposalThreshold
- Check staking period hasn't expired
Problem: TransactionError: "User rejected transaction"
Solutions:
- User must approve transaction in Lace wallet
- Check wallet has sufficient tDUST balance
- Verify transaction parameters are correct
Build Errors
Problem: TypeScript compilation errors
Solutions:
# Update dependencies
pnpm install
# Rebuild SDK
pnpm run buildProblem: Module not found errors
Solutions:
- Ensure all dependencies are installed
- Check
node_modulesexists - Verify import paths are correct
Runtime Errors
Problem: "Contract not found at address"
Solutions:
- Verify contract address is correct
- Check network connection
- Ensure contract is deployed on current network
Problem: "InvalidParameterError"
Solutions:
- Check parameter types and formats
- Validate inputs before submission
- Review API documentation for correct usage
Cache Issues
Problem: Changes not reflected after rebuild
Solutions:
# Clear Vite cache
rm -rf node_modules/.vite
# Clear browser cache or hard refresh
# Chrome/Edge: Ctrl+Shift+R (Windows) or Cmd+Shift+R (Mac)
# Firefox: Ctrl+F5 (Windows) or Cmd+Shift+R (Mac)Development
Building from Source
# Clone repository
git clone https://github.com/your-org/agora-dao-midnight-sdk
cd internalAgoraMidnightSDK
# Install dependencies
pnpm install
# Build contract first
cd contract
pnpm run build
# Build SDK
cd ../sdk
pnpm run buildProject Structure
sdk/
├── src/
│ ├── browser-providers.ts # Lace wallet integration
│ ├── governor-api.ts # Main API implementation
│ ├── manager.ts # Observable state manager
│ ├── types.ts # Type definitions
│ ├── errors.ts # Error classes
│ ├── validators.ts # Input validation
│ ├── common-types.ts # Shared types
│ ├── index.ts # Main exports
│ └── utils/ # Utility functions
├── package.json
├── tsconfig.json
└── README.mdPrerequisites
Required Software
Lace Wallet Extension
- Install from lace.io
- Create or import wallet
- Switch to Midnight TestNet
TestNet Tokens (tDUST)
- Request from Midnight Faucet
- Needed to pay for transactions
Modern Browser
- Chrome/Edge 89+
- Firefox 89+
- Safari 15+
Browser APIs Required
- IndexedDB (private state storage)
- Web Crypto API (cryptographic operations)
- Fetch API (HTTP requests)
- WebSocket (real-time updates)
Resources
License
Apache-2.0
Support
For issues and questions:
- GitHub Issues: Create an issue
- Documentation: Read the docs
