The counter DApp
Project structure
You might find it useful to split DApps into several sub-projects. This is especially true for larger, more complex DApps with web-based user interfaces, where it is desirable to test the core logic independently from the user experience. Even for a DApp as simple as the counter, however, you will see that the code is split into two sub-projects:
contract- This sub-project contains the contract itself, plus supporting code to define the implementation of the local private state and code to test the contract's logic.counter-cli- This sub-project defines the command-line interface of the DApp. It depends on the outputs of thecontractsub-project.
You will find project definition and configuration files in each
sub-project's top level directory, while the source code is in its
src subdirectory.
The contract sub-project
The preceding page already gave you a tour of the contract itself. Because the contract is so simple, there is very little additional code to be written.
Witness implementation
Look in the file contract/src/witnesses.ts. The counter contract defines no private state and therefore
requires no witness functions to be defined. In the TypeScript
representation of contracts, however, every contract is parameterized
over its private state, so there remains a need to define some
private state object for the contract. In this case, we use the type
Record<string, never> to represent the type of an empty object.
The definitions in counter's witnesses.ts therefore provide the minimal (empty)
implementations of witnesses possible.
Loading...
Part 3 of the Midnight tutorial introduces an example that follows the
same structure, but with more interesting contents in its
witnesses.ts.
Generated source
The file contract/src/index.ts merely re-exports the definitions
from the other files in the contract sub-project. Which definitions?
The empty definitions of the witnesses, of course, but also the
definitions generated by the Compact compiler.
If you are following the steps of this tutorial in order, then you
have already built the counter project. That means you have already
run the Compact compiler on counter.compact.
Look in the managed/counter subdirectory of the contract
sub-project. In it, you see four directories:
contract- contains files defining the TypeScript/JavaScript API for the contractzkir- contains the intermediate representations for the circuits defined in the contractkeys- contains the prover and verifier keys for each circuit defined in the contractcompiler- contains metadata about the contract, including the names of exported circuits and their argument and return types in JSON format
The counter contract defines only one circuit, increment, so there is
a single pair of prover and verifier key for increment in keys. The zkir
directory contains two files, increment.bzkir and increment.zkir. Respectively,
these files are the binary and JSON representations of the ZKIR of the increment
circuit.
Most of this generated content is not useful to examine, but you might
want to look at managed/counter/contract/index.d.ts. You will see
that it defines various types and functions that correspond directly
to the code in counter.compact. For example:
- The
Circuitstype for this contract includes anincrementfunction. - The
Ledgertype for this contract includes aroundfield, declared to be a JavaScriptbigint. - The
Witnessestype for this contract is empty, because the contract declares no witnesses.
Notice that many of the types are parameterized over some type T,
representing the DApp's private state implementation, which is opaque
to the circuits and ledger types emitted by the compiler.
Tests
Part 3 of the Midnight tutorial explores unit testing, so it will not
be discussed here, but you are free to examine the code in the
contract/src/test subdirectory. It defines just a few Jest tests to
exercise the contract in a simulation environment.
The counter-cli subproject
All the interactive logic for the counter DApp's command-line
interface is exposed through counter-cli/src/index.ts. Before examining
that file, though, it may be useful to see how the whole DApp is
launched.
Different configurations
There are four 'entry-point' files in the directory:
testnet-remote.ts- the first way you ran the counter DApp, so that it uses a local proof server, but a remote Midnight node and a remote Midnight Indexer, both connected to TestNettestnet-local.ts- the second way you ran the counter DApp, if you tried the optional step of running your own TestNet node and indexertestnet-remote-start-proof-server.ts- the same configuration astestnet-remote.ts, but not assuming you already have a proof server running and instead starting one as part of the scriptstandalone.ts- invokes aDockerComposeEnvironmentthat launches a proof server, Midnight node, and the Midnight Indexer, all disconnected from TestNet and running entirely locally, for unit testing.
If you look in testnet-remote.ts, you will see that it is very short:
Loading...
and the other entry points are similar, because they rely on the
configurations defined in config.ts. Each of those configurations
is quite similar. For example, here is the one that says to connect
to the Midnight-hosted instances of a Midnight node and an indexer:
Loading...
The various subsystems are all configured for operation on Midnight TestNet.
Notice the setNetworkId call in the TestnetRemoteConfig constructor.
Many Midnight APIs and packages require the API consumer to explicitly specify
the network they are targeting via a network ID parameter. Providing such a parameter to
each API would be tedious and error-prone. Midnight.js provides the setNetworkId function
so that the user can specify the network they are targeting once and have the correct network
ID used everywhere.
Generated types, instantiated
Recall that many of the types, such as Contract, in the source files
generated by the Compact compiler, are parameterized. Writing out the
instantiated forms of these types can become unwieldy, especially when
the type arguments will be the same, over and over. Thus, the
Midnight team often creates a short file defining instantiated versions
of some of these generated types and uses those instantiated versions
in the rest of the code.
You can see an example of this pattern in the file common-types.ts,
which defines types such as CounterContract, like this:
export type CounterContract = Contract<CounterPrivateState, Witnesses<CounterPrivateState>>;
The Dapp
The two main files implementing our application are counter-cli/src/cli.ts and
counter-cli/src/api.ts. The former contains the main run loop of the application.
The latter contains convenience functions for interacting with the Midnight network.
To understand counter-cli/src/cli.ts, start at the end, looking at the run function. Ignoring the
startup and error-handling code, the core actions of the DApp are to
- instantiate a wallet,
- instantiate a collection of 'providers' for working with the contract, and
- start the main user interaction loop.
Create a wallet
If this DApp were running in a web browser, it would begin by connecting to the browser's Lace wallet extension. Instead, the counter DApp is running outside any web browser, so it initializes a 'headless' wallet to be used for funding transactions.
Depending on the user's input in the interactive buildWallet
function, the DApp either prompts for a wallet seed or generates some
random bytes to serve as the seed. Either way, the flow eventually
reaches buildWalletAndWaitForFunds in counter-cli/src/api.ts.
Omitting the logging code, this function is as follows:
Loading...
The WalletBuilder class provides the main entry point into creating
a wallet: buildFromSeed. The wallet needs to know about the
indexer, the node, and the proof server, because it watches the ledger
for information about funds and transaction results (using the
indexer), submits transactions through the node, and calls on the
proof server to generate proofs that transactions are valid.
The counter DApp uses @midnight-ntwrk/wallet@4.0.0, which introduces the following breaking changes:
WalletBuilder.buildFromSeed()is deprecated. It still works, but will be removed in a future version. UseWalletBuilder.build()with a seed parameter instead.- Wallet addresses and keys are returned in the new Bech32m format by default.
- The seed is no longer embedded in the serialized wallet state. You must explicitly pass the seed again when restoring a wallet.
Notice the getZswapNetworkId argument to WalletBuilder.buildFromSeed.
The getZswapNetworkId function is provided by Midnight.js. It retrieves
the current network ID (set by the setNetworkId call we have already seen)
and converts it to a format the wallet can understand.
After the wallet is started, the DApp pauses to wait for funds to
appear. You may find it instructive to look at the definition of
waitForFunds just above buildWalletAndWaitForFunds in the file.
The wallet's state appears in TypeScript as an observable stream, so
waitForFunds uses RxJS functions to watch for the wallet's balance
to be non-zero.
Loading...
Notice that the wallet's state includes information about the degree to which it is synchronized with the total state of the blockchain.
Hypothetically, the wallet could track the balances of many different
tokens, so the code in waitForFunds asks specifically for the
nativeToken balance, meaning tDUST. Once the tDUST balance is
non-zero, the waitForFunds function returns, and the
buildWalletAndWaitForFunds function returns the wallet itself.
Providers
The Midnight programmatic interface to a smart contract is extremely flexible and thus highly parameterized. For example, a DApp developer might implement a new kind of private state storage or interpose some kind of balance-checking functionality between the contract and a wallet. The way that the Midnight libraries handle all this functional parameterization is through a JavaScript object with fields for different types of providers.
Of course, most of the time, a standard implementation for each type of provider is what you want, and the Midnight libraries define these for you. All you have to specify is some information about where to find the indexer, the proof server, and so on.
The counter-cli/src/api.ts file defines a function configureProviders that
returns an object containing the providers our application will need to interact
with the network.
Loading...
Back in the run function, you can see where the counter DApp creates
the providers.
Loading...
The exception to the pattern of using standard implementations for the
providers is seen in the value provided for the walletProvider and
midnightProvider fields.
- The wallet provider specifies the wallet's public key and defines a
function for balancing transactions (that is, attaching to the
transaction an appropriate amount of 'fuel' to run the transaction):
balanceTx. - The Midnight provider defines a function for submitting transactions
to the Midnight network:
submitTx.
The counter DApp defines a single object to satisfy both provider interfaces:
Loading...
You can read more about what happens when a transaction is balanced and submitted to the network in the portion of the Midnight developer documentation that describes how Midnight works.
Work with a contract
The bulk of the counter DApp uses input from the user to direct its actions, which include:
- deploying a new counter contract
- joining an existing counter contract
- calling the
incrementcircuit on the contract - examining the ledger state associated with the contract.
With the wallet and providers objects in hand, these actions
are performed by calling Midnight library functions. For example, the
call to find and join an existing contract can be seen in the join
function, near the top of `counter-cli/src/api.ts. The relevant call is:
Loading...
where counterContractInstance is simply a Contract object defined in the index.ts file
in the generated code.
Loading...
Submit a Transaction
Using the generated code and the Midnight.js library, creating and submitting an
increment transaction becomes a simple function call:
Loading...
The callTx access on the counterContract object indicates that we want to create a call
transaction for the counter contract. The increment() call creates and submits a call transaction for the increment
circuit. After the transaction is submitted and included in the blockchain, the result is stored in finalizedTxData.public, which contains additional public information about the transaction.
Viewing Contract State
The publicDataProvider in the providers object can be used to query for information about the blockchain.
To view the current value of round defined in the counter contract, we can query the publicDataProvider
and convert the result to a JavaScript object using the ledger function generated for the counter contract
by the compiler.
Loading...
At this point, you have seen enough to be able to read and understand the gist of the
counter-cli/src/api.ts and counter-cli/src/cli.ts files.