Interact with a deployed contract
After deployment, you can use the CLI to interact with the Hello World contract on Midnight Testnet.
Prerequisites
The following is required:
- A
deployment.jsonfile containing contract address - A 64-character hexadecimal wallet seed
- Project with all dependencies installed using
npm install
Create an interactive CLI script
Add a script to your project that provides an interactive menu for calling contract functions.
Update package.json
To start, add the CLI script to the scripts section in package.json:
"scripts": {
"compile": "cd contracts && compact compile hello-world.compact .",
"build": "tsc",
"deploy": "node dist/deploy.js",
"cli": "node dist/cli.js"
}
Create the CLI script
Create a new TypeScript file for the CLI. This file contains all the logic for interacting with the deployed contract.
touch src/cli.ts
Import required libraries for contract interaction. These modules provide wallet management, contract connectivity, and command-line interface functionality.
Add to src/cli.ts:
import * as readline from "readline/promises";
import { WalletBuilder } from "@midnight-ntwrk/wallet";
import { findDeployedContract } from "@midnight-ntwrk/midnight-js-contracts";
import { httpClientProofProvider } from "@midnight-ntwrk/midnight-js-http-client-proof-provider";
import { indexerPublicDataProvider } from "@midnight-ntwrk/midnight-js-indexer-public-data-provider";
import { NodeZkConfigProvider } from "@midnight-ntwrk/midnight-js-node-zk-config-provider";
import { levelPrivateStateProvider } from "@midnight-ntwrk/midnight-js-level-private-state-provider";
import {
NetworkId,
setNetworkId,
getZswapNetworkId,
getLedgerNetworkId
} from "@midnight-ntwrk/midnight-js-network-id";
import { createBalancedTx } from "@midnight-ntwrk/midnight-js-types";
import { Transaction } from "@midnight-ntwrk/ledger";
import { Transaction as ZswapTransaction } from "@midnight-ntwrk/zswap";
import { WebSocket } from "ws";
import * as path from "path";
import * as fs from "fs";
import * as Rx from "rxjs";
Configure network settings for Midnight Testnet connection. This establishes the endpoints needed to communicate with the blockchain network.
// Fix WebSocket for Node.js environment
// @ts-ignore
globalThis.WebSocket = WebSocket;
// Configure for Midnight Testnet
setNetworkId(NetworkId.TestNet);
// Testnet connection endpoints
const TESTNET_CONFIG = {
indexer: "https://indexer.testnet-02.midnight.network/api/v1/graphql",
indexerWS: "wss://indexer.testnet-02.midnight.network/api/v1/graphql/ws",
node: "https://rpc.testnet-02.midnight.network",
proofServer: "http://127.0.0.1:6300"
};
Create the main function that initializes the CLI interface. This function checks for deployment files and prompts for wallet credentials.
async function main() {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log("Hello World Contract CLI\n");
try {
// Check for deployment file
if (!fs.existsSync("deployment.json")) {
console.error("No deployment.json found! Run npm run deploy first.");
process.exit(1);
}
const deployment = JSON.parse(fs.readFileSync("deployment.json", "utf-8"));
console.log(`Contract: ${deployment.contractAddress}\n`);
// Get wallet seed
const walletSeed = await rl.question("Enter your wallet seed: ");
console.log("\nConnecting to Midnight network...");
// Additional logic follows...
} catch (error) {
console.error("\nError:", error);
} finally {
rl.close();
}
}
Build and synchronize the wallet with the network. This creates a wallet instance from the seed and waits for it to sync with the blockchain state.
Add inside try block:
// Build wallet
const wallet = await WalletBuilder.buildFromSeed(
TESTNET_CONFIG.indexer,
TESTNET_CONFIG.indexerWS,
TESTNET_CONFIG.proofServer,
TESTNET_CONFIG.node,
walletSeed,
getZswapNetworkId(),
"info"
);
wallet.start();
// Wait for sync
await Rx.firstValueFrom(
wallet.state().pipe(Rx.filter((s) => s.syncProgress?.synced === true))
);
Load the compiled contract module from the file system. This imports the contract code that was compiled in the previous steps.
// Load contract
const contractPath = path.join(process.cwd(), "contracts");
const contractModulePath = path.join(
contractPath,
"managed",
"hello-world",
"contract",
"index.cjs"
);
const HelloWorldModule = await import(contractModulePath);
const contractInstance = new HelloWorldModule.Contract({});
Create a wallet provider to handle transaction operations. This provider manages the cryptographic signing and submission of transactions to the network.
// Create wallet provider
const walletState = await Rx.firstValueFrom(wallet.state());
const walletProvider = {
coinPublicKey: walletState.coinPublicKey,
encryptionPublicKey: walletState.encryptionPublicKey,
balanceTx(tx: any, newCoins: any) {
return wallet
.balanceTransaction(
ZswapTransaction.deserialize(
tx.serialize(getLedgerNetworkId()),
getZswapNetworkId()
),
newCoins
)
.then((tx) => wallet.proveTransaction(tx))
.then((zswapTx) =>
Transaction.deserialize(
zswapTx.serialize(getZswapNetworkId()),
getLedgerNetworkId()
)
)
.then(createBalancedTx);
},
submitTx(tx: any) {
return wallet.submitTransaction(tx);
}
};
Configure providers and establish connection to the deployed contract. These providers handle state management, data queries, and proof generation for contract interactions.
// Configure providers
const zkConfigPath = path.join(contractPath, "managed", "hello-world");
const providers = {
privateStateProvider: levelPrivateStateProvider({
privateStateStoreName: "hello-world-state"
}),
publicDataProvider: indexerPublicDataProvider(
TESTNET_CONFIG.indexer,
TESTNET_CONFIG.indexerWS
),
zkConfigProvider: new NodeZkConfigProvider(zkConfigPath),
proofProvider: httpClientProofProvider(TESTNET_CONFIG.proofServer),
walletProvider: walletProvider,
midnightProvider: walletProvider
};
// Connect to contract
const deployed: any = await findDeployedContract(providers, {
contractAddress: deployment.contractAddress,
contract: contractInstance,
privateStateId: "helloWorldState",
initialPrivateState: {}
});
console.log("Connected to contract\n");
Implement the interactive menu loop for user operations. This creates a command-line menu that allows users to store messages, read messages, or exit the application.
// Main menu loop
let running = true;
while (running) {
console.log("--- Menu ---");
console.log("1. Store message");
console.log("2. Read current message");
console.log("3. Exit");
const choice = await rl.question("\nYour choice: ");
switch (choice) {
case "1":
console.log("\nStoring custom message...");
const customMessage = await rl.question("Enter your message: ");
try {
const tx = await deployed.callTx.storeMessage(customMessage);
console.log("Success!");
console.log(`Message: "${customMessage}"`);
console.log(`Transaction ID: ${tx.public.txId}`);
console.log(`Block height: ${tx.public.blockHeight}\n`);
} catch (error) {
console.error("Failed to store message:", error);
}
break;
case "2":
console.log("\nReading message from blockchain...");
try {
const state = await providers.publicDataProvider.queryContractState(
deployment.contractAddress
);
if (state) {
const ledger = HelloWorldModule.ledger(state.data);
const message = Buffer.from(ledger.message).toString();
console.log(`Current message: "${message}"\n`);
} else {
console.log("No message found\n");
}
} catch (error) {
console.error("Failed to read message:", error);
}
break;
case "3":
running = false;
console.log("\nGoodbye!");
break;
default:
console.log("Invalid choice. Please enter 1, 2, or 3.\n");
}
}
// Clean up
await wallet.close();
Add the main function call to start the CLI application. This line at the end of the file initiates the script execution.
main().catch(console.error);
Start the proof server in a new terminal window. This Docker container generates the zero-knowledge proofs required for contract interactions.
Open a new terminal and run:
docker run -p 6300:6300 midnightnetwork/proof-server -- midnight-proof-server --network testnet
Keep this terminal open while running the CLI.
Compile the TypeScript CLI application to JavaScript. This converts the TypeScript code into executable JavaScript that Node.js can run.
In the original terminal:
npm run build
Run the CLI application to interact with the contract. This starts the interactive menu system for reading and writing messages to the blockchain.
npm run cli
Follow the prompts to:
- Enter wallet seed when prompted
- Wait for script to connect to network and sync wallet
- Use menu to interact with contract
Understand CLI options
The CLI presents an interactive menu with various options for interacting with the deployed contract. Each option performs a specific blockchain operation, from writing data to the contract to reading its current state.
- Store messages
- Read messages
- Exit
Store a new message on the blockchain
Calls storeMessage function in smart contract to write a new message:
- Prompts for custom message input
- Creates transaction and generates zero-knowledge proof
- Submits transaction to blockchain
- Costs gas fees and takes 10-30 seconds to complete
Example output:
Storing custom message...
Enter your message: Privacy is powerful!
Success!
Message: "Privacy is powerful!"
Transaction ID: 0x5678...efgh
Block height: 123457
Read the current message from the blockchain
Queries public state of contract to retrieve the stored message:
- Queries indexer for contract data
- No transaction created, no proof generated
- Free and instant operation
- Returns immediately with current message
Example output:
Reading message from blockchain...
Current message: "Privacy is powerful!"
Exit the CLI application
Safely terminates the program:
- Closes wallet connection
- Releases network resources
- Exits the application cleanly
Example output:
Goodbye!
You can call storeMessage() multiple times. Each call creates a new transaction that updates contract state with the latest message.
Next steps
The CLI successfully interacts with a privacy-preserving smart contract that accepts user input. You can explore advanced features:
Enhanced contract features
- Message validation: Add logic to contract to enforce length limits or filter content
- Message history: Modify contract to store an array of past messages
- Private messaging: Use private state and witnesses to store confidential data
UI improvements
- Web interface: Build frontend using React or Next.js
- Real-time updates: Use WebSocket connection to listen for blockchain events
- Transaction status: Display status of pending and confirmed transactions