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.
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 functionsapi/src- Application abstractionsbboard-cli/src- Main application run loop
Key files for review:
contract/src/witnesses.ts- Private state and witness function definitionsapi/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: LprivateState: PScontractAddress: string
This implementation uses:
Ledgertype from the Compact compiler-generated APIBBoardPrivateStatefrom 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];
},
});
Never mutate private state in place. Return new state values from witness functions.
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 thePrivateStateProvidercontract:Contractobject containing executable JavaScript (generated by the Compact compiler)initialPrivateState: Initial private state matching the type stored underprivateStateKey
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
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:
- Open a new terminal window (don't rely on
source ~/.zshrc) - Verify Node version:
nvm use 18 - Clear cached modules:
rm -rf node_modules/.cache
This environment-related issue affects certain ESM-style imports when the Node.js setup becomes stale.