Interacting with zkApps server-side
While user-facing zkApps can be written for the browser, sometimes it is useful to interact with a zkApp server-side or on your own machine. For example, when initializing a zkApp using programmatically generated information, deploying a zkApp in custom ways, or writing scripts that create transactions depending on real-world (periodically updating an on-chain value with signed data, like a keeper for an oracle) or on-chain events.
Interacting with zkApps server-side is useful for some use cases. For example, if you need to create a custom account for your zkApp to deploy a zkApp to a different key than the fee payer key. You can programmatically parameterize a zkApp before you initialize it. You can even create a smart contract programmatically for users as part of an application.
Prerequisites
Ensure your environment meets the Prerequisites for zkApp Developer Tutorials.
Before you start this tutorial, you must deploy a smart contract. To do this, read and complete Tutorial 3: Deploy to a Live Network that reuses the smart contract Square
from Tutorial 1: Hello World.
This tutorial has been tested with:
Interact with the deployed Smart Contract
Now that you successfully created and deployed your project following Tutorial 3: Deploy to a Live Network, you can write a script to interact with the smart contract.
Building on Tutorial 3
For this tutorial, you update the smart contract that you already built for Tutorial 3: Deploy to a Live Network.
You run commands from the root of the 03-deploying-to-a-live-network
directory as you work in the src
directory on files that contain the TypeScript code for the smart contract.
Each time you make updates, then build or deploy, the TypeScript code is compiled into JavaScript in the build
directory.
Helper functions in utils.ts
To make this more script convenient to write, use the provided helper functions.
Download the utils.ts file.
Place the file in the project
03-deploying-to-a-live-network/src
folder.Read through the code to understand what it is doing to implement its functionality.
The
utils.ts
file contains two functions:
loopUntilAccountExists()
waits until an account exists on Devnetdeploy()
programmatically deploys your zkApp
Connect to a remote network
Download the main.ts example file.
Because you are building on your earlier work, place the
main.ts
file in the project03-deploying-to-a-live-network/src
folder.Review the code that adds the imports and o1js setup:
import { Square } from './Square.js';
import { Mina, PrivateKey } from 'o1js';Now, set the active instance to the remote Devnet network. Earlier tutorials set the active instance to a simulated local blockchain, which is fast for development but only available on your local machine and is not decentralized.
The connection is made through the GraphQL endpoint exposed by the Mina node connected to the Devnet network. By connecting to the remote Devnet network, you 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.
Review the code that connects to Devnet:
...
const Network = Mina.Network(
'https://api.minascan.io/node/devnet/v1/graphql'
);
Mina.setActiveInstance(Network);
...Set a transaction fee that you use to pay for access to sending transactions and deploying smart contracts on Mina.
Transaction fees in code are declared as nanomina. Review the code that sets the default transaction fee to 0.1 MINA (100,000,000 nanomina):
...
const transactionFee = 100_000_000;
...
This example connects to a remote RPC served by minascan.io
. You could also run a Mina node locally and instead use its GraphQL endpoint. In other blockchains a local Mina node would be very heavyweight, but because Mina is succinct this is actually a reasonable option. See the Node Operator Getting Started docs.
Public/private key pair
You already generated a public/private key pair when you ran the zk config
command to configure the deployment in Tutorial 3: Deploy to a Live Network the zk config
command.
The public/private key pair was created in keys/devnet.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 EK
for easy differentiability.
Still in the
main.ts
file, review the code that specifies that the name of the key file must be provided through an argument on the command line: (process.argv[2]
):...
const transactionFee = 100_000_000;
const deployAlias = process.argv[2];
const deployerKeysFileContents = fs.readFileSync(
'keys/' + deployAlias + '.json',
'utf8'
);
const deployerPrivateKeyBase58 = JSON.parse(
deployerKeysFileContents
).privateKey;
const deployerPrivateKey = PrivateKey.fromBase58(deployerPrivateKeyBase58);
const deployerPublicKey = deployerPrivateKey.toPublicKey();
const zkAppPrivateKey = PrivateKey.fromBase58(
'EKFTMuvTirzrwpeHP8RKe7bGufBGiKs27nTMzD5XyMV8NcK3upt2'
);
...
You can run this code now with:
$ npm run build && node build/src/main.js devnet
The expected output is:
> [email protected] build
> tsc
state after init: 3
state after txn1: 9
Field.assertEquals(): 75 != 81
state after txn2: 9
state after txn3: 81
- The
npm run build
command creates JavaScript code in thebuild
directory. - The
&&
operator links two commands together. - The
node build/src/main.js
command runs the code insrc/main.ts
. - The keys are read from
keys/devnet.json
.
The SmartContract is also deployed to the same account you deployed from, set with zkAppPrivateKey = deployerPrivateKey
. Depending on the application, it can also be useful to have separate keys for the zkApp and deployer accounts.
Wait for accounts to be ready
Next, review the code that waits for the deployer account to be ready.
In main.ts
, the import to use the loopUntilAccountExists()
function from utils.ts
goes here:
import { Square } from './Square.js';
import { Mina, PrivateKey } from 'o1js';
import fs from 'fs';
import { loopUntilAccountExists, deploy } from './utils.js';
...
Wait until the new deployment account exists.
If the key created from the zk deploy
command earlier in this tutorial has already been funded, then find the account and move on. If that transaction hasn't finished yet, then wait until that has completed.
After the account is found, print out its nonce and its balance. This code compiles the smart contract and waits for it to be deployed:
...
// ----------------------------------------------------
console.log('Compiling smart contract...');
let { verificationKey } = await Square.compile();
const zkAppPublicKey = zkAppPrivateKey.toPublicKey();
let zkapp = new Square(zkAppPublicKey);
// Programmatic deploy:
// Besides the CLI, you can also create accounts programmatically. This is useful if you need
// more custom account creation - say deploying a zkApp to a different key than the fee payer
// key, programmatically parameterizing a zkApp before initializing it, or creating Smart
// Contracts programmatically for users as part of an application.
await deploy(deployerPrivateKey, zkAppPrivateKey, zkapp, verificationKey);
await loopUntilAccountExists({
account: zkAppPublicKey,
eachTimeNotExist: () =>
console.log('waiting for zkApp account to be deployed...'),
isZkAppAccount: true,
});
let num = (await zkapp.num.fetch())!;
console.log(`current value of num is ${num}`);
// ----------------------------------------------------
...
To do this, reuse the helper function loopUntilAccountExists()
from utils.js
. This time, pass in isZkappAccount: true
checks if the account exists and that there is a verification key on the account. An existing verification key indicates that the zkApp has been successfully deployed.
The smart contract was already deployed with zk deploy
so a programmatic deploy is not required and is commented out here. If you want to see how this works, or it's useful for your application, see the code in utils.ts.
After the zkApp has been deployed, fetch the current value of zkapp.num
(the on-chain defined on the SmartContract
) and log it. If this is the first time you have run this script, the value is 3
because that's how it is set in the smart contract's init()
function. The init()
function is called automatically during the first deploy (not during re-deploys).
Send an update transaction
Finally, here is code that sends an update to the transaction. If the zkApp was just initialized, this calls an update on the newly initialized account. Otherwise, it calls an update on whatever the current account state happens to be.
...
// ----------------------------------------------------
let transaction = await Mina.transaction(
{ sender: deployerPublicKey, fee: transactionFee },
async () => {
await zkapp.update(num.mul(num));
}
);
// fill in the proof - this can take a while...
console.log('Creating an execution proof...');
let time0 = performance.now();
await transaction.prove();
let time1 = performance.now();
console.log(`creating proof took ${(time1 - time0) / 1e3} seconds`);
// sign transaction with the deployer account
transaction.sign([deployerPrivateKey]);
console.log('Sending the transaction...');
let pendingTransaction = await transaction.send();
// ----------------------------------------------------
...
To send an update transaction, perform the following steps:
- Construct the transaction with
Mina.transaction
. This is where you callzkapp.update()
, the custom method defined on the smart contract. - Create a proof of the transaction. This can take up to a minute.
- Sign the transaction and send it to the network.
When sending the transaction using transaction.send()
, an object called pendingTransaction
is returned and provides information about how the transaction went and waits for inclusion in a block:
if (pendingTransaction.status === "rejected") {
console.log('error sending transaction (see above)');
process.exit(0);
}
console.log(
`See transaction at https://minascan.io/devnet/tx/${pendingTransaction.hash}
Waiting for transaction to be included...`
);
await pendingTransaction.wait();
console.log(`updated state! ${await zkapp.num.fetch()}`);
This code uses several functionalities of the pending transaction:
- `pendingTransaction.status indicates the initial processing status of the transaction, with pending signifying that the transaction has been accepted for processing by the network, and rejected indicating that the transaction was immediately deemed invalid and rejected by the GraphQL endpoint.
pendingTransaction.hash
is the transaction hash that you can use to look up the transaction in a block explorer. If the transaction failed, it returnsundefined
.pendingTransaction.wait()
is especially useful as it returns a promise that resolves whether a transaction is included into a block or rejected. This takes several minutes, so you might not want to block the main thread on this in a real application.
Finally, after the transaction was successfully applied on the Mina blockchain, you can double-check that your state was updated by fetching it again with zkapp.num.fetch()
.
Conclusion
You have finished writing a script to initialize the state and interact with it! You can also run this script multiple times to update x
to its square.
Check out other tutorials and documentation to keep going!