Skip to main content

Bulletin board DApp

What remains to be done, in order to have a working bulletin board DApp, is to complete the TypeScript code. For the sake of simplicity, the initial version of the DApp will have have text-based menu prompts (making it look like something out of the early 1980s). The creation of a web GUI is left for a later part of the developer tutorial.

To get you started, most of the code is already written for you, including the interactive menu prompts. However, most of the parts that manipulate the contract and the private state are missing. Filling in those missing parts is your work for this part of the tutorial.

At this point, you should already have the contract in its contract/src subdirectory. Following the instructions on the preceding pages, you have compiled the contract, producing the contract's TypeScript API and other material in the managed subdirectory.

Most of the TypeScript code you need is already written for you, in the contract/src, api/src, and bboard-cli/src directories, but two files are incomplete:

  • contract/src/witnesses.ts
  • api/src/index.ts

Open these in your favorite TypeScript editor and get ready to fill in the missing pieces. (The discussion below sometimes refers to code in the file without displaying it on this page, so you must follow along in the code in your own editor to understand what's being said.)

Exercise 1: define the private state

Down at line 18 of witnesses.ts, you will find a comment identifying the place where you need to fill in some code for exercise 1. Look at its context in your editor.

The public state of the contract is stored in the blockchain and can be seen by anyone, but the private state is entirely local to the DApp and may be completely different for each user. A contract only declares the types of the functions for accessing and changing the private state; the contract does not define the functions, nor does it say anything about the type or structure of the private state itself. Thus, some parts of the generated contract API are parameterized by the type of the private state.

A good practice is to define an interface or type alias for the private state.

Now ask yourself: what should be held in the user's private state for the bulletin board? Hint: In this example, the private state does not evolve. It is simply a value that can be fetched through the local_secret_key() witness that the contract declared.

If you answered, 'the secret key,' you were right. The bulletin board contract declared the type of the secret key data to be a byte array, which corresponds to the TypeScript type Uint8Array, so fill in the missing code, defining the the BBoardPrivateState type to have a secretKey property of type Uint8Array, like this:

export type BBoardPrivateState = {
readonly secretKey: Uint8Array;
};

The next code in witnesses.ts defines a helpful function to create objects of type BBoardPrivateState, given a secret key. Fill in that code, too, so that it matches the type you just defined. When you are done, it should look like this:

export const createBBoardPrivateState = (secretKey: Uint8Array) => ({
secretKey,
});

Exercise 2: initialize the private oracle

In the research literature about zero-knowledge proofs, the part of the system that is consulted to access private state is called an oracle, so you will sometimes find that term appearing in the Midnight API and documentation. The next exercise is to create an object to represent the bulletin board DApp's private oracle: its set of witness functions.

Below the code you edited in exercise 1, you will find the definition of the witnesses object. The object must have a property (or method) for each of the contract's declared witness functions. Recall that bboard.compact declared only one witness function: local_secret_key. The type and outer structure of the function is already written for you. Exercise 2 is to fill in the missing return values.

Look carefully at the function's type:

  • It takes a single argument, which is a WitnessContext. If the contract had declared additional parameters for the witness function, they would appear here as additional parameters, after the WitnessContext.
  • It returns two values: a new overall state for the private oracle and a value corresponding to the declared return type of the witness function in the Compact code.

The WitnessContext type is parameterized by the ledger type L and private state type PS, so that it has three fields:

  • ledger: T
  • privateState: PS
  • contractAddress: string

You can see that the WitnessContext type in this file is instantiated with the Ledger type imported from the API that the Compact compiler generated for the bulletin board contract, plus the private state type BBoardPrivateState that you defined in exercise 1. This means that the privateState field in the WitnessContext will be of type BBoardPrivateState. This is the only field needed from the WitnessContext, so the definition uses TypeScript's parameter destructuring notation to name only the privateState from the context and ignore the ledger and contractAddress.

Now fill in the two values that local_secret_key should return. First, what is the new private state? Hint: local_secret_key does not change the private state.

The correct answer is that the new private state is the same as the old private state: simply the value privateState.

Now what about the 'interesting' return value, the one declared in the contract? The purpose of this function is to get the user's secret key, so that the contract can use it to generate and verify a public hash. For the second return value, extract the secret key from the private state. (You defined the contents of the private state in exercise 1.)

Putting these together, your solution should look like this:

export const witnesses = {
local_secret_key: ({ privateState }: WitnessContext<Ledger, BBoardPrivateState>): [BBoardPrivateState, Uint8Array] => [
privateState,
privateState.secretKey,
],
};

The bulletin board never needs to change the private state, but more complex contracts will need to update the private state. The way to do that is not to mutate the state in place, but to return a new state value from a witness function.

caution

Do not use a global variable to hold or access the private state; always use the value passed to the witness function.

The file witnesses.ts is complete. Look now at the file containing the main logic of the bulletin board DApp: api/src/index.ts.

In the remaining exercises, you will call the post and take_down circuits you defined in the bulletin board contract and then write the code to deploy the contract to the Midnight network.

Exercise 3: invoke the post circuit

Find the comment that identifies the missing code for exercise 3, at about line 122. It's in a post function. You can see that the declaration of the return values is already in place. The task for exercise 3 is write the code that creates and submits a post transaction on the contract.

There is only a small amount of code to write, but it requires a few steps to explain it. First, notice that post function is actually a method in the BBoardAPI class. The class's constructor takes a DeployedBBoardContract as its first argument, implicitly making the value available as a deployedContract field in the object. The type DeployedBBoardContract is defined in the adjacent file common-types.ts as a simple instantiation of DeployedContract. (Look at it now to see how it is defined.) That saves you from having to type the full verbose instantiation every time you need to refer to it.

A deployed contract has a property named contractCircuitsInterface. Through a bit of TypeScript magic, the ContractCircuitsInterface for your contract has a function for each circuit you defined. Thus, you can call the post circuit like this:

this.deployedContract.contractCircuitsInterface.post(message)

This call does not submit the transaction to the network. It merely constructs the transaction corresponding to the call, so that it is ready to be submitted. Every call to a circuit function returns a value of type UnsubmittedCallTx. More precisely, it returns a Promise for such an unsubmitted call transaction.

To submit an unsubmitted call transaction, you simply call its submit method, with no arguments.

Putting all that together, the code to post a new message to the bulletin board looks like this:

const { txHash, blockHeight } =
await this.deployedContract.contractCircuitsInterface
.post(message)
.then((tx) => tx.submit());

This code uses the then operator to sequence asynchronous calls. When the promise returned by submit is resolved, it means the transaction has been finalized by the Midnight network. In other words, the submit method does all the work of preparing and delivering a transaction to a Midnight node for you, plus waiting for the network to accept and finalize the transaction.

What if the user tries to post a message to a non-empty bulletin board? The transaction will fail, and the code will throw an exception. The current bulletin board DApp lets the exception propagate out to the main run function in bboard-cli/src/index.ts, so that the DApp exits. Once you have completed the DApp and tested it successfully, you could come back here and add an appropriate try / catch around the call to report the transaction failure in a more helpful way.

Exercise 4: invoke the take-down circuit

In the takeDown function, just after the post function, you'll find the comment identifying the destination for exercise 4's code. (After you have completed exercise 3, the marker for exercise 4 should be near line 148.)

You might not have figured out the answer to exercise 3 on your own, but after completing it, you can probably do exercise 4 without much help. Try writing the code yourself before looking at the answer below. (The take_down circuit is defined to return the old message, but this program doesn't need it, so you can ignore it and simply report that the transaction was submitted.)

Here is a solution:

const { txHash, blockHeight } =
await this.deployedContract.contractCircuitsInterface
.take_down()
.then((tx) => tx.submit());

Exercise 5: deploy a new bulletin board contract

You have seen that the code to invoke one of your contract's circuits and submit the transaction to the Midnight network is quite simple. The code to deploy a new contract is a little more complicated, but still short.

Find the comment that identifies the missing code for exercise 5, at about line 174 after completing the preceding exercises. It's in a deploy function. You can see that the call to deployContract is started for you. The task for exercise 5 is to fill in the arguments correctly.

The deployContract function requires at least four arguments. They are:

  1. A MidnightProviders object containing implementations of all the necessary providers (refer back to the discussion of providers in part 2 of the tutorial if necessary)
  2. The name of the key under which the private state is stored in the PrivateStateProvider of the providers
  3. The initial private state for the contract, whose type matches the state stored under the preceding key
  4. The Contract object to be deployed.

Additional arguments can be provided to initialize the contract state.

You can fill in the first argument to deployContract easily, because the deploy function already has a providers parameter.

    providers,

To understand the second argument, look in the file common-types.ts, where a type PrivateStates has been defined, with a bboardPrivateState property to hold a private state object of the sort that you defined in exercise 1. Notice, later in common-types.ts, that the type DeployedBBoardContract is defined by instantiating DeployedContract with appropriate type parameters. The second argument to deployContract must match the second parameter of DeployedContract; that is, bboardPrivateState.

    'bboardPrivateState',

The third argument to deployContract is the initial private state for the contract. You defined a function to construct the private state in the second half of exercise 1, when you filled in the body of createBBoardPrivateState. Call that function to produce the third argument now. But what is the initial secret key you should pass to createBBoardPrivateState? Simply generate one randomly. There is a randomBytes function already defined for you in api/src/utils/index.ts. Call it with the number of bytes you need: 32.

    createBBoardPrivateState(utils.randomBytes(32)),

Finally, as the fourth argument, create a contract using the helper createBBoardContract, defined near the top of the file you are editing. It needs to know the public key under which the user's tDUST tokens can be found. The WalletProvider in the providers has this information, stored as the property coinPublicKey. Thus, the value you need is providers.walletProvider.coinPublicKey.

    createBBoardContract(providers.walletProvider.coinPublicKey),

Putting all that together, the call to deployContract should look like this when you are done:

  const deployedBBoardContract = await deployContract(
providers,
'bboardPrivateState',
createBBoardPrivateState(utils.randomBytes(32)),
createBBoardContract(providers.walletProvider.coinPublicKey),
);

That's remarkably little code to deploy an entirely new contract to the Midnight blockchain.

The code for joining an existing contract is similar to the code for deploying a new one. You might want to look now at the code in the join function (just below the definition of deploy) and compare it to to the code in deploy.

You can explore the documentation for Midnight library functions, such as deployContract and findDeployedContract, in the Midnight reference documentation. For your convenience, here are links to the documentation for those two functions:

Compiling and running the DApp

If you have completed all the exercises, then your bulletin board DApp is ready to be compiled. Go back to the bboard-tutorial directory (the one containing contract, api, and bboard-cli) and run:

npx turbo build

If the project does not build successfully, you must have made a mistake along the way. Go back and check your work, or ask for help.

Also, adjacent to the bboard-tutorial directory in the examples package, there is a bboard directory with working answers for every exercise. If you think there is a mistake in this documentation, check the contents of bboard, because that code is tested regularly to be sure it works with the current Midnight devnet.

Once the project is built, go on to the next page to learn how to test your code without touching the live Midnight network.