Skip to main content

Bulletin board API implementation

This tutorial shows how to implement the API package that provides a reusable abstraction layer for bulletin board contract interactions. This package can be shared between the CLI and browser-based UI.

Prerequisites

Before you begin, ensure that you have completed the bulletin board CLI setup and installed root dependencies.

Create the API directory

From the root, create the API structure:

mkdir -p api/src/utils
cd api

Configure the API package

Create api/package.json:

{
"name": "@midnight-ntwrk/bboard-api",
"version": "0.1.0",
"author": "IOG",
"license": "MIT",
"private": true,
"type": "module",
"module": "./dist/index.js",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "rm -rf dist && tsc",
"ci": "npm run typecheck && npm run lint && npm run build",
"lint": "eslint src",
"typecheck": "tsc -p tsconfig.json --noEmit"
}
}

Configure TypeScript

Create api/tsconfig.json:

{
"include": ["src/**/*.ts", "src/test/jest.setup.ts"],
"compilerOptions": {
"outDir": "dist",
"declaration": true,
"lib": ["ESNext", "DOM"],
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "node",
"allowJs": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": true,
"strict": true,
"isolatedModules": true,
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}

Implement utility functions

Create api/src/utils/index.ts:

api/src/utils/index.ts
/**
* Provides utility functions.
*
* @module
*/

/**
* Generates a buffer containing a series of randomly generated bytes.
*
* @param length The number of bytes to generate.
* @returns A `Uint8Array` representing `length` randomly generated bytes.
*/
export const randomBytes = (length: number): Uint8Array => {
const bytes = new Uint8Array(length);
crypto.getRandomValues(bytes);
return bytes;
};

This utility function generates cryptographically secure random bytes using the Web Crypto API. The bulletin board uses this to generate secret keys for users.

Define common types

Create api/src/common-types.ts:

api/src/common-types.ts
import { type MidnightProviders } from '@midnight-ntwrk/midnight-js-types';
import { type FoundContract } from '@midnight-ntwrk/midnight-js-contracts';
import type { State, BBoardPrivateState, Contract, Witnesses } from '../../contract/src/index';

export const bboardPrivateStateKey = 'bboardPrivateState';
export type PrivateStateId = typeof bboardPrivateStateKey;

export type PrivateStates = {
readonly bboardPrivateState: BBoardPrivateState;
};

export type BBoardContract = Contract<BBoardPrivateState, Witnesses<BBoardPrivateState>>;

export type BBoardCircuitKeys = Exclude<keyof BBoardContract['impureCircuits'], number | symbol>;

export type BBoardProviders = MidnightProviders<BBoardCircuitKeys, PrivateStateId, BBoardPrivateState>;

export type DeployedBBoardContract = FoundContract<BBoardContract>;

export type BBoardDerivedState = {
readonly state: State;
readonly sequence: bigint;
readonly message: string | undefined;
readonly isOwner: boolean;
};

These type definitions create aliases for complex generic types. The BBoardDerivedState combines public ledger state with computed ownership information. The isOwner field determines whether the current user posted the message by comparing the ledger's owner commitment with the user's secret key.

Implement the BBoardAPI class

The BBoardAPI class provides a high-level interface for interacting with the bulletin board smart contract. It handles contract deployment, state management, and transaction submission while exposing a reactive state observable for real-time updates.

Create api/src/index.ts and add the following sections.

Import required packages

api/src/index.ts
import * as BBoard from '../../contract/src/managed/bboard/contract/index.js';

import { type ContractAddress, convertFieldToBytes } from '@midnight-ntwrk/compact-runtime';
import { type Logger } from 'pino';
import {
type BBoardDerivedState,
type BBoardContract,
type BBoardProviders,
type DeployedBBoardContract,
bboardPrivateStateKey,
} from './common-types.js';
import { CompiledBBoardContractContract } from '../../contract/src/index';
import * as utils from './utils/index.js';
import { deployContract, findDeployedContract } from '@midnight-ntwrk/midnight-js-contracts';
import { combineLatest, map, tap, from, type Observable } from 'rxjs';
import { toHex } from '@midnight-ntwrk/midnight-js-utils';
import { BBoardPrivateState, createBBoardPrivateState } from '@midnight-ntwrk/bboard-contract';

Define the API interface

api/src/index.ts
export interface DeployedBBoardAPI {
readonly deployedContractAddress: ContractAddress;
readonly state$: Observable<BBoardDerivedState>;

post: (message: string) => Promise<void>;
takeDown: () => Promise<void>;
}

This interface exposes the contract address, a reactive state observable, and methods for posting and removing messages.

Implement the constructor and state observable

The BBoardAPI class constructor is private, ensuring instances are only created through the static deploy or join methods.

api/src/index.ts
export class BBoardAPI implements DeployedBBoardAPI {
private constructor(
public readonly deployedContract: DeployedBBoardContract,
providers: BBoardProviders,
private readonly logger?: Logger,
) {
this.deployedContractAddress = deployedContract.deployTxData.public.contractAddress;
this.state$ = combineLatest(
[
providers.publicDataProvider.contractStateObservable(this.deployedContractAddress, { type: 'latest' }).pipe(
map((contractState) => BBoard.ledger(contractState.data)),
tap((ledgerState) =>
logger?.trace({
ledgerStateChanged: {
ledgerState: {
...ledgerState,
state: ledgerState.state === BBoard.State.OCCUPIED ? 'occupied' : 'vacant',
owner: toHex(ledgerState.owner),
},
},
}),
),
),
from(providers.privateStateProvider.get(bboardPrivateStateKey) as Promise<BBoardPrivateState>),
],
(ledgerState, privateState) => {
const hashedSecretKey = BBoard.pureCircuits.publicKey(
privateState.secretKey,
convertFieldToBytes(32, ledgerState.sequence, 'api/src/index.ts'),
);

return {
state: ledgerState.state,
message: ledgerState.message.value,
sequence: ledgerState.sequence,
isOwner: toHex(ledgerState.owner) === toHex(hashedSecretKey),
};
},
);
}

readonly deployedContractAddress: ContractAddress;

readonly state$: Observable<BBoardDerivedState>;

The reactive state observable uses RxJS combineLatest to merge two data streams: public ledger state from the indexer and private state from local storage. It computes the isOwner flag by hashing the private secret key and comparing it with the on-chain owner field, which determines whether the user can call takeDown() to remove their message.

Implement the post method

api/src/index.ts
  async post(message: string): Promise<void> {
this.logger?.info(`postingMessage: ${message}`);

const txData = await this.deployedContract.callTx.post(message);

this.logger?.trace({
transactionAdded: {
circuit: 'post',
txHash: txData.public.txHash,
blockHeight: txData.public.blockHeight,
},
});
}

The post method invokes the smart contract's post circuit, waits for the zero-knowledge (ZK) proof to be generated and the transaction to be confirmed, then logs the transaction details.

Implement the takeDown method

api/src/index.ts
  async takeDown(): Promise<void> {
this.logger?.info('takingDownMessage');

const txData = await this.deployedContract.callTx.takeDown();

this.logger?.trace({
transactionAdded: {
circuit: 'takeDown',
txHash: txData.public.txHash,
blockHeight: txData.public.blockHeight,
},
});
}

The takeDown method invokes the smart contract's takeDown circuit, which requires proof of ownership. It can only succeed if the caller's hashed secret key matches the on-chain owner field.

Implement the deploy method

api/src/index.ts
  static async deploy(providers: BBoardProviders, logger?: Logger): Promise<BBoardAPI> {
logger?.info('deployContract');

const deployedBBoardContract = await deployContract(providers, {
compiledContract: CompiledBBoardContractContract,
privateStateId: bboardPrivateStateKey,
initialPrivateState: await BBoardAPI.getPrivateState(providers),
});

logger?.trace({
contractDeployed: {
finalizedDeployTxData: deployedBBoardContract.deployTxData.public,
},
});

return new BBoardAPI(deployedBBoardContract, providers, logger);
}

The deploy method creates a new bulletin board contract instance on the blockchain. Each deployed contract gets a unique on-chain address that other users can use to join and interact with the bulletin board.

Implement the join method

api/src/index.ts
  static async join(providers: BBoardProviders, contractAddress: ContractAddress, logger?: Logger): Promise<BBoardAPI> {
logger?.info({
joinContract: {
contractAddress,
},
});

const deployedBBoardContract = await findDeployedContract<BBoardContract>(providers, {
contractAddress,
compiledContract: CompiledBBoardContractContract,
privateStateId: bboardPrivateStateKey,
initialPrivateState: await BBoardAPI.getPrivateState(providers),
});

logger?.trace({
contractJoined: {
finalizedDeployTxData: deployedBBoardContract.deployTxData.public,
},
});

return new BBoardAPI(deployedBBoardContract, providers, logger);
}

The join method connects to an existing deployed bulletin board contract, allowing users to read the public state and, if they deployed the current message, remove it using their secret key.

Implement private state helper

api/src/index.ts
  private static async getPrivateState(providers: BBoardProviders): Promise<BBoardPrivateState> {
const existingPrivateState = await providers.privateStateProvider.get(bboardPrivateStateKey);
return existingPrivateState ?? createBBoardPrivateState(utils.randomBytes(32));
}
}

This helper retrieves existing private state or generates a new random 32-byte secret key if none exists. The private state persists across application restarts, allowing users to prove ownership of their posted messages in future sessions.

Export utilities and types

api/src/index.ts
export * as utils from './utils/index.js';

export * from './common-types.js';

These exports make the API's utility functions and TypeScript types available to the CLI application and other consumers of the API module.

Build the API package

Build the API package:

npm run build

This compiles the TypeScript to JavaScript and generates type definitions in the dist directory.

Next steps

Continue to the CLI implementation tutorial to build the command-line interface that uses this API.