Skip to main content
Version: Canary 🚧

bboard-dapp

Explore the TypeScript code for a working bulletin board DApp. The complete codebase is available for examination, including the contract interaction logic and private state management.

Prerequisites​

The contract must exist in the contract/src subdirectory. Compile the contract to generate the TypeScript API and related files in the managed subdirectory.

note

The Compact compiler generates the TypeScript API from your Compact contract code. This compilation step produces type-safe interfaces for contract interaction.

The TypeScript code spans three directories:

  • contract/src - Contract definitions and witness functions
  • api/src - Application abstractions
  • bboard-cli/src - Main application run loop

Key files for review:

  • contract/src/witnesses.ts - Private state and witness function definitions
  • api/src/index.ts - Application interface implementation

Define the private state​

The blockchain stores public contract state visible to all users. Private state remains local to each DApp instance and varies per user. Contracts declare types for private state access functions but don't define the functions or specify the private state structure. The generated contract API parameterizes certain components by the private state type.

Define an interface or type alias for the private state as a best practice.

The bulletin board's private state contains the user's secret key, which remains constant throughout the application lifecycle. The localSecretKey() witness retrieves this value.

The bulletin board contract declares the secret key as a byte array, corresponding to TypeScript's Uint8Array. Define BBoardPrivateState with a secretKey property:

// Generated by the Compact compiler, this type definition enables type-safe private state handling
export type BBoardPrivateState = {
secretKey: Uint8Array;
};

The following helper function creates BBoardPrivateState objects:

export function createBBoardPrivateState(secretKey: Uint8Array): BBoardPrivateState {
return { secretKey };
}

Initialize the private oracle​

Zero-knowledge proof systems use the term oracle for components that access private state. This terminology appears throughout the Midnight API and documentation.

The witnesses object requires a property or method for each declared witness function. The bboard.compact contract declares one witness function: localSecretKey.

Function structure​

The function signature contains:

  • Parameter: WitnessContext - Contains ledger state, private state, and contract address
  • Returns: Tuple containing:
    • New private oracle state
    • Value matching the witness function's declared return type

The WitnessContext<L, PS> type includes:

  • ledger: L
  • privateState: PS
  • contractAddress: string

This implementation uses:

  • Ledger type from the Compact compiler-generated API
  • BBoardPrivateState from Exercise 1

Implementation​

The localSecretKey function returns the unchanged private state (since it doesn't modify state) and the secret key value for contract hash generation and verification:

// This witness function integrates with the Compact compiler-generated contract API
export const witnesses = createBBoardWitnesses<BBoardPrivateState>({
localSecretKey: ({ privateState }) => {
return [privateState, privateState.secretKey];
},
});
important

Never mutate private state in place. Return new state values from witness functions.

caution

Avoid global variables for private state. Always use the value passed to witness functions.

Invoke the post circuit​

The api/src/index.ts file defines BBoardAPI, the application interface for deployed bulletin board contracts.

Post method implementation​

The post method submits new messages to the bulletin board. This asynchronous method belongs to the BBoardAPI class, initialized with a DeployedBBoardContract.

DeployedBBoardContract aliases FoundContract, Midnight.js's abstraction for deployed smart contracts. The application uses FoundContract rather than DeployedContract because it doesn't require private deployment metadata.

FoundContract exposes callTx, providing functions for each contract circuit. These functions create and submit transactions.

The bulletin board's post method invokes the circuit:

// The Compact compiler-generated API provides type-safe circuit invocation
await this.deployedContract.callTx.post([message]);

Posting to non-empty boards causes transaction failure and throws an exception. The current implementation propagates exceptions to the main run function, causing the DApp to exit. Consider adding try/catch blocks for improved error handling after testing.

Invoke the take-down circuit​

The takeDown function follows the post method in api/src/index.ts.

This method invokes the contract's takeDown circuit using the deployed contract's callTx property:

// The Compact compiler ensures type safety for circuit parameters
await this.deployedContract.callTx.takeDown([]);

Deploy a new bulletin board contract​

Contract circuit invocation and transaction submission require minimal code. Contract deployment requires more setup but remains concise.

Deployment function​

The deploy function in api/src/index.ts contains the deployContract call with all required arguments.

deployContract requires two arguments:

  • MidnightProviders object: Contains all necessary provider implementations
  • DeployContractOptions object: Contains deployment configuration parameters

For bulletin board contracts, DeployContractOptions requires:

  • privateStateKey: Key for storing private state in the PrivateStateProvider
  • contract: Contract object containing executable JavaScript (generated by the Compact compiler)
  • initialPrivateState: Initial private state matching the type stored under privateStateKey

Configuration​

Private state key​

privateStateKey: 'bboard-private-state'

Unique identifier for retrieving user private state from the configured provider. The system generates and stores new state under this key if none exists.

Contract instance​

contract: bboardContractInstance

Defined as:

// The Compact compiler generates the Contract class with witness function integration
export const bboardContractInstance = new Contract(witnesses);

Creates a Contract instance (generated by the Compact compiler) with the witness functions defined earlier.

Initial private state​

initialPrivateState: await getPrivateState(providers.privateStateProvider)

The getPrivateState method retrieves existing private state or generates new state using 32 random bytes, ensuring consistent and valid private state access.

Complete deployment call​

// The Compact compiler-generated Contract class enables type-safe deployment
const deployedContract = await deployContract(providers, {
privateStateKey: 'bboard-private-state',
contract: bboardContractInstance,
initialPrivateState: await getPrivateState(providers.privateStateProvider),
});

This minimal code deploys a complete contract to the Midnight blockchain.

Join existing contracts​

The join function (below deploy) contains similar code for joining existing contracts. Compare both implementations to understand the differences.

Reference documentation​

Explore the Midnight.js reference documentation for library functions:

Compile and run the DApp​

Navigate to the example-bboard directory (containing contract, api, and bboard-cli) and run:

npx turbo build
note

The build process uses the Compact compiler output from the managed directory to ensure type safety throughout the application.

Troubleshooting: ERR_UNSUPPORTED_DIR_IMPORT​

This Node.js error occurs due to environment caching after modifying .zshrc, .bashrc, or changing Node versions with NVM.

Resolution steps:

  1. Open a new terminal window (don't rely on source ~/.zshrc)
  2. Verify Node version: nvm use 18
  3. Clear cached modules:
    rm -rf node_modules/.cache

This environment-related issue affects certain ESM-style imports when the Node.js setup becomes stale.