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 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. As with the
counter application, bboard-cli/src/index.ts
contains the main run loop of the application
while api/src/index.ts
contains convenient abstractions for implementing the application. But, two
files in the project 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 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 an alias of FoundContract
.
FoundContract
is the base abstraction Midnight.js provides for creating
and submitting transactions for smart contracts that have already been
deployed to the blockchain. It is called FoundContract
because it works
with contracts that have been "found" on-chain. In other words, FoundContract
is indifferent to who deployed the contract for which it submits transactions.
There is a subtype of FoundContract
called DeployedContract
which offers
the same transaction builders but works with contracts that were deployed specifically by
your application. Consequentially, DeployedContract
contains private information
related to the contract deployment that FoundContract
does not contain.
Since this application doesn't make use of said private deployment information,
DeployedBBoardContract
is defined in terms of the more general FoundContract
type.
A FoundContract
has a callTx
property of type CircuitCallTxInterface
.
Through a bit of TypeScript magic, the CircuitCallTxInterface
type contains, for each
circuit defined in your contract, a function that will create and submit a call transaction for that
circuit. Putting all that together, the code to post a new message to the
bulletin board looks like this:
const txData = await this.deployedContract.callTx.post(message)
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 txData = await this.deployedContract.callTx.take_down();
Exercise 5: deploy a new bulletin board contract
You have seen that the code to invoke one of your contract's circuits and submit a corresponding 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 172 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 two 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) - A
DeployContractOptions
object containing configuration parameters for the deployment.
For the bulletin board contract, DeployContractOptions
requires only three properties:
privateStateKey
- The name of the key at which the private state is stored in thePrivateStateProvider
given in the firstproviders
argument.contract
- TheContract
object containing the executable JavaScript of the contract being deployed.initialPrivateState
- The initial private state for the contract, whose type matches the state stored under theprivateStateKey
.
You can fill in the first argument to deployContract
easily because
the deploy
function already has a providers
parameter.
providers,
The second argument to deployContract
is the configuration object.
{
contract: bboardContractInstance,
privateStateKey: 'bboardPrivateState',
initialPrivateState: createBBoardPrivateState(utils.randomBytes(32))
}
To understand the deployment configuration, let's look at its entries one by one. The first entry,
contract: bboardContractInstance,
is defined in terms of a constant bboardContractInstance
,
const bboardContractInstance: BBoardContract = new Contract(witnesses);
which is just an instance of Contract
(which is generated by compactc
) constructed
with the witnesses you defined at the beginning of the tutorial.
The second entry
privateStateKey: 'bboardPrivateState',
indicates that the FoundContract
returned from deployContract
should
store its private state at key bboardPrivateState
in the private state provider given in the providers
argument.
Furthermore, because providers.privateStateProvider
is of type PrivateStateProvider<PrivateStates>
, the private
state of the contract must be an object of type BBoardPrivateState
.
The third entry
initialPrivateState: createBBoardPrivateState(utils.randomBytes(32)),
is defined using the function createBBoardPrivateState
you wrote in the second half of exercise 1 to construct the private
state for the bulletin board contract. The initial secret key is created from the randomBytes
function provided for you in api/src/utils/index.ts
.
It is called with the number of bytes you need: 32.
Putting all that together, the call to deployContract
should look
like this when you are done:
const deployedBBoardContract = await deployContract(providers, {
contract: bboardContractInstance,
privateStateKey: "bboardPrivateState",
initialPrivateState: createBBoardPrivateState(utils.randomBytes(32))
});
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 the code in deploy
.
You can explore the documentation for Midnight library functions,
such as deployContract
and findDeployedContract
, in the Midnight.js
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 TestNet.
Once the project is built, go on to the next page to learn how to test your code without touching the live Midnight network.