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 theWitnessContext
. - 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.
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:
- 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) - The name of the key under which the private state is stored in the
PrivateStateProvider
of the providers - The initial private state for the contract, whose type matches the state stored under the preceding key
- 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.