Skip to main content

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.