Skip to main content
info

Please note that zkApp programmability is not yet available on Mina Mainnet, but zkApps can now be deployed to Berkeley Testnet.

Interacting with zkApps Server-Side

Overview

While user-facing zkApps can be written for the browser, sometimes it is useful to write a zkApp that can run server-side.

This tutorial shows how to do this. This can be useful when initializing a zkApp using programmatically generated information, deploying a zkApp in custom ways, or writing scripts that create transactions depending on real world or on-chain events.

To start this tutorial, read and complete Tutorial 3, to see how to deploy a smart contract. We will use the same project used in Tutorial 3.

Note Tutorial 3, and this tutorial, reuse the smart contract Square from Tutorial 1.

Interacting with our Deployed Smart Contract

Once you've created your project and deployed it following Tutorial 3, we can write a script to interact with our smart contract.

utils.ts

We will be using a set of helper functions to make this more convenient to write, in a utils.ts file. You can find this file here. Please download this and place it in your project's src folder. It contains 3 functions:

  • loopUntilAccountExists: This function waits until an account exists on Berkeley.
  • makeAndSendTransaction: This function makes a transation, produces a proof for it, and sends it.
  • zkAppNeedsInitialization: This function checks if a zkApp has been initialized or not.

We strongly recommend reading through this code, to understand what they are doing to interact with the network and implement their functionality.

Imports, main, and Mina.Network

Open up main.ts in a text editor. You can find the complete source for this here.

To start, let's add the imports, and our empty main function:

  1 import { Square } from './Square.js';
2 import {
3 isReady,
4 shutdown,
5 Mina,
6 PrivateKey,
7 } from 'snarkyjs';
8
9 (async function main() {
10 await isReady;
11
12 console.log('SnarkyJS loaded');
13
14 // ----------------------------------------------------
15
16 // ----------------------------------------------------
17
18 console.log('Shutting down');
19
20 await shutdown();
21 })();

So far nothing new - but now, let's add connecting to Berkeley:

...
14 // ----------------------------------------------------
15
16 const Berkeley = Mina.Network(
17 'https://proxy.berkeley.minaexplorer.com/graphql'
18 );
19 Mina.setActiveInstance(Berkeley);
20
21 let transactionFee = 100_000_000;
22
23 // ----------------------------------------------------...

In past tutorials, we set the active instance to a local blockchain, which is fast for development, but only available on one's local machine, and not decentralized.

We are now setting the active instance to the remote Berkeley network. We are connecting to Berkeley through a GraphQL proxy, which is running a Mina node connected to the Berkeley network. By connecting to Berkeley, we can provide smart contracts that are globally accessible, and provide strong guarantees around state due to both Mina's decentralization and its succinct state proof.

With the Ethereum Bridge (to be included in a future tutorial), states and proofs from smart contracts deployed on Mina networks can also be available on Ethereum and other EVM chains.

We also set a transaction fee, which we'll use to pay for access to sending transactions and deploying smart contracts on Mina. Transaction fees in code are inputted as nano mina. We will use a 0.1 Mina fee as default (100,000,000 nano mina) here.

note

While in this case we are connecting to a remote RPC (run by minaexplorer.com), you can also run a Mina node locally, and instead use its GraphQL endpoint. While in other blockchains this would be very heavyweight, because Mina is succinct this is actually a reasonable option. See here to see how to do this.

A version of the Mina node is in the works that runs directly in the browser and within the local node process, so client side users can connect directly to Mina with low resource requirements, while keeping full decentralization guarantees, with no intermediaries that can go down, censor, or otherwise impact you or your users' connection to the network.

Now, to finish setting up our code, let's add to our main.ts:

...
7 } from 'snarkyjs';
8
9 import fs from 'fs';
10
11 (async function main() {
...
23 let transactionFee = 100_000_000;
24
25 const deployAlias = process.argv[2];
26
27 const deployerKeysFileContents = fs.readFileSync(
28 'keys/' + deployAlias + '.json',
29 'utf8'
30 );
31
32 const deployerPrivateKeyBase58 = JSON.parse(
33 deployerKeysFileContents
34 ).privateKey;
35
36 const deployerPrivateKey = PrivateKey.fromBase58(deployerPrivateKeyBase58);
37
38 const zkAppPrivateKey = deployerPrivateKey;
39
40 // ----------------------------------------------------
...

This uses the key generated by the zk config command, stored in keys/. The name of the key file will be provided through an argument on the command line (process.argv[2]).

You can run this now with:

npm run build && node build/src/main.js berkeley

Which will read keys from keys/berkeley.json. Public and Private Keys in Mina are commonly stored in Base58 for easily readability. In Mina, public keys start with B62, and private keys start with EKE for easy differentiability.

Our Smart Contract is also deployed to the same account we deployed from. So we set zkAppPrivateKey = deployerPrivateKey.

Waiting for accounts to be ready

Next, we will wait for the deployer account to be ready.

In main, we will import a and use the loopUntilAccountExists() function from utils.ts:

...
9 import fs from 'fs';
10 import { loopUntilAccountExists } from './utils.js';
11
12 (async function main()
...
42 // ----------------------------------------------------
43
44 let account = await loopUntilAccountExists({
45 account: deployerPrivateKey.toPublicKey(),
46 eachTimeNotExist: () => {
47 console.log(
48 'Deployer account does not exist. ' +
49 'Request funds at faucet ' +
50 'https://faucet.minaprotocol.com/?address=' +
51 deployerPrivateKey.toPublicKey().toBase58()
52 );
53 },
54 isZkAppAccount: false,
55 });
56
57 console.log(
58 `Using fee payer account with nonce ${account.nonce}, balance ${account.balance}`
59 );
60
61 // ----------------------------------------------------
...

We wait until our new deployment account exist - if it does not, we share a link with the user to go to the faucet and request funds.

If the key created from the zk deploy command earlier in this tutorial has already been funded, then this should find the account and move on. If that transaction hasn't finished yet, then this will wait until that has completed.

Once we find the account, we print out its nonce, and its balance.

Moving on, we compile the smart contract, and wait for it to have been deployed:

...
60 // ----------------------------------------------------
61
62 console.log('Compiling smart contract...');
63 let { verificationKey } = await Square.compile();
64
65 const zkAppPublicKey = zkAppPrivateKey.toPublicKey();
66 let zkapp = new Square(zkAppPublicKey);
67
68 // Programmatic deploy:
69 // Besides the CLI, you can also create accounts programmatically. This is useful if you need
70 // more custom account creation - say deploying a zkApp to a different key than the deployer
71 // key, programmatically parameterizing a zkApp before initializing it, or creating Smart
72 // Contracts programmatically for users as part of an application.
73 //await deploy(deployerPrivateKey, zkAppPrivateKey, zkAppPublicKey, zkapp, verificationKey)
74
75 // ----------------------------------------------------
76
77 let zkAppAccount = await loopUntilAccountExists({
78 account: zkAppPrivateKey.toPublicKey(),
79 eachTimeNotExist: () => console.log('waiting for zkApp account to be deployed...'),
80 isZkAppAccount: true
81 });
82
83 // ----------------------------------------------------...

To do this, we reuse the helper function loopUntilAccountExists() from utils.js

Note as we deployed our smart contract already with zk deploy, programmatic deploy is not needed, and is commented out here. If you would like to see how this works, or its useful for your application, see code for this here.

Initializing the smart contract

Next, let's check if our zkApp is initialized or not:

...
10 import { loopUntilAccountExists, zkAppNeedsInitialization } from './utils.js';
...
81 });
82
83 const needsInitialization = await zkAppNeedsInitialization({ zkAppAccount });
84
85 if (needsInitialization) {
86 console.log('initializing smart contract');
87 // TODO
88 }
89
90 // ----------------------------------------------------
...

We use a helper, zkAppNeedsInitialization(), to check if the zkApp has been initialized. Note that deploying a zkApp from the CLI, loads the verification key for the smart contract into the account, but does not initialize it yet. This code is still needed for that step.

The helper function checks if the zkApp has been initialized by seeing if every field is zero. This works for zkApps that after initialized, never return to a state where all of their fields are zero. This works for the Square smart contract for example, since it initializes to 3 and then only gets larger.

info

There is an isProved property on accounts present in the protocol, but yet to be exposed in SnarkyJS, that definitively answers if a smart contract has been initialized or not.

This isProved property starts out false when a contract is deployed. It is recommended that, once available, the a contract's init() function require isProved to be false - while other functions require isProved to be true. More details will be released as this feature is made available.

Now, to actually do the initialization:

...
10 import { loopUntilAccountExists, zkAppNeedsInitialization, makeAndSendTransaction } from './utils.js';
...
85 if (needsInitialization) {
86 console.log('initializing smart contract');
87 await makeAndSendTransaction({
88 feePayerPrivateKey: deployerPrivateKey,
89 zkAppPublicKey: zkAppPublicKey,
90 mutateZkApp: () => zkapp.init(),
91 transactionFee: transactionFee,
92 getState: () => zkapp.num.get(),
93 statesEqual: (num1, num2) => num1.equals(num2).toBoolean()
94 });
95
96 console.log('updated state!', zkapp.num.get().toString());
97 }
98
99 let num = (await zkapp.num.get())!;
100 console.log('current value of num is', num.toString());
101
102 // ----------------------------------------------------
...

Internally, makeAndSendSendTransaction() does the following steps:

  1. Fetches the fee payer account, to make sure it has up to date account information for sending the deploy transaction, such as its balance.
  2. Constructs the transaction, with some change to make (in this case, initializing the zkApp).
  3. Creates a proof of the transaction. This can take up to a minute or two.
  4. Sends the transaction, posting some information on how the send went.
  5. Loops until the account state has updated. Note after this, the zkApp state is updated, so calling .get() on zkApp state, as we do on lines 96 and 99, returns updated values.

Calling update on our transaction

And lastly, let's now send an update to our transaction. If the zkApp was just initialized, this will call update on the newly initialized account. Otherwise, it will call update on whatever the current account state happens to be.

...
102 // ----------------------------------------------------
103
104 await makeAndSendTransaction({
105 feePayerPrivateKey: deployerPrivateKey,
106 zkAppPublicKey: zkAppPublicKey,
107 mutateZkApp: () => zkapp.update(num.mul(num)),
108 transactionFee: transactionFee,
109 getState: () => zkapp.num.get(),
110 statesEqual: (num1, num2) => num1.equals(num2).toBoolean()
111 });
112
113 console.log('updated state!', zkapp.num.get().toString());
114
115 // ----------------------------------------------------...

Here we reuse our makeAndSendTransaction() function, to call the update() function on our zkapp.

Conclusion

We have finished writing a script to initialize the state and interact with it! You can also run this script multiple times, and it should each time, update x to its square.

Check out out our other tutorials and documentation to keep going!