Skip to main content

Deploy a Hello World contract

Continue from the Hello World contract creation, deploy the contract to Midnight Testnet, and establish your MN app on-chain.

Prerequisites

The following is required:

Update project for deployment

Replace existing package.json with this complete version that includes all deployment dependencies and scripts:

{
"name": "my-mn-app",
"version": "1.0.0",
"type": "module",
"scripts": {
"compile": "compact compile contracts/hello-world.compact contracts/managed/hello-world",
"build": "tsc",
"deploy": "node dist/deploy.js"
},
"dependencies": {
"@midnight-ntwrk/compact-runtime": "^0.8.1",
"@midnight-ntwrk/ledger": "^4.0.0",
"@midnight-ntwrk/midnight-js-contracts": "2.0.2",
"@midnight-ntwrk/midnight-js-http-client-proof-provider": "2.0.2",
"@midnight-ntwrk/midnight-js-indexer-public-data-provider": "2.0.2",
"@midnight-ntwrk/midnight-js-level-private-state-provider": "2.0.2",
"@midnight-ntwrk/midnight-js-node-zk-config-provider": "2.0.2",
"@midnight-ntwrk/midnight-js-network-id": "2.0.2",
"@midnight-ntwrk/midnight-js-types": "2.0.2",
"@midnight-ntwrk/wallet": "5.0.0",
"@midnight-ntwrk/wallet-api": "5.0.0",
"@midnight-ntwrk/zswap": "^4.0.0",
"ws": "^8.18.3"
},
"devDependencies": {
"@types/node": "^24.4.0",
"@types/ws": "^8.5.10",
"typescript": "^5.9.2"
}
}

Install dependencies:

npm install

Build deployment script

1

Create a TypeScript configuration file to define compilation settings. This file tells TypeScript how to compile the project into JavaScript.

touch tsconfig.json

Add configuration to tsconfig.json:

{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "node",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": ["src/**/*"]
}
2

Create a source directory and deployment script file. These form the foundation for the deployment code.

mkdir src
touch src/deploy.ts
3

Add import statements to access required libraries. These modules provide wallet management, contract deployment, and network connectivity functions.

Add to src/deploy.ts:

import { WalletBuilder } from "@midnight-ntwrk/wallet";
import { deployContract } 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 { nativeToken, Transaction } from "@midnight-ntwrk/ledger";
import { Transaction as ZswapTransaction } from "@midnight-ntwrk/zswap";
import { WebSocket } from "ws";
import * as fs from "fs";
import * as path from "path";
import * as readline from "readline/promises";
import * as Rx from "rxjs";
import { type Wallet } from "@midnight-ntwrk/wallet-api";
4

Configure network settings and define helper functions. This establishes connection to Midnight Testnet and creates a function to monitor wallet funding.

// 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"
};

const waitForFunds = (wallet: Wallet) =>
Rx.firstValueFrom(
wallet.state().pipe(
Rx.tap((state) => {
if (state.syncProgress) {
console.log(
`Sync progress: synced=${state.syncProgress.synced}, sourceGap=${state.syncProgress.lag.sourceGap}, applyGap=${state.syncProgress.lag.applyGap}`
);
}
}),
Rx.filter((state) => state.syncProgress?.synced === true),
Rx.map((s) => s.balances[nativeToken()] ?? 0n),
Rx.filter((balance) => balance > 0n),
Rx.tap((balance) => console.log(`Wallet funded with balance: ${balance}`))
)
);
5

Create a main function to orchestrate the deployment process. This function handles user input for wallet seed generation or recovery.

async function main() {
console.log("Midnight Hello World Deployment\n");

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});

try {
// Ask user if they have an existing wallet seed
const choice = await rl.question("Do you have a wallet seed? (y/n): ");

let walletSeed: string;
if (choice.toLowerCase() === "y" || choice.toLowerCase() === "yes") {
// Use existing seed
walletSeed = await rl.question("Enter your 64-character seed: ");
} else {
// Generate new wallet seed
const bytes = new Uint8Array(32);
// @ts-ignore
crypto.getRandomValues(bytes);
walletSeed = Array.from(bytes, (b) =>
b.toString(16).padStart(2, "0")
).join("");
console.log(`\nSAVE THIS SEED: ${walletSeed}\n`);
}

// Rest of deployment logic follows...
} catch (error) {
console.error("Failed:", error);
} finally {
rl.close();
}
}
6

Build a wallet from the seed and check funding status. This step creates a wallet instance and waits for funds if necessary.

Add inside try block after seed generation:

// Build wallet from seed
console.log("Building wallet...");
const wallet = await WalletBuilder.buildFromSeed(
TESTNET_CONFIG.indexer,
TESTNET_CONFIG.indexerWS,
TESTNET_CONFIG.proofServer,
TESTNET_CONFIG.node,
walletSeed,
getZswapNetworkId(),
"info"
);

wallet.start();
const state = await Rx.firstValueFrom(wallet.state());

console.log(`Your wallet address is: ${state.address}`);

let balance = state.balances[nativeToken()] || 0n;

if (balance === 0n) {
console.log(`Your wallet balance is: 0`);
console.log("Visit: https://midnight.network/test-faucet to get some funds.");
console.log(`Waiting to receive tokens...`);
balance = await waitForFunds(wallet);
}

console.log(`Balance: ${balance}`);
7

Load the compiled contract module from disk. This imports the compiled contract artifacts needed for deployment.

// Load compiled contract files
console.log("Loading contract...");
const contractPath = path.join(process.cwd(), "contracts");
const contractModulePath = path.join(
contractPath,
"managed",
"hello-world",
"contract",
"index.cjs"
);

if (!fs.existsSync(contractModulePath)) {
console.error("Contract not found! Run: npm run compile");
process.exit(1);
}

const HelloWorldModule = await import(contractModulePath);
const contractInstance = new HelloWorldModule.Contract({});
8

Create a wallet provider for transaction management. This provider handles transaction signing and submission to the network.

// Create wallet provider for transactions
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);
}
};
9

Configure all required providers for contract deployment. These providers handle state management, data fetching, proof generation, and wallet interaction.

// Configure all required providers
console.log("Setting up 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
};
10

Deploy the contract to the blockchain and save deployment information. This executes the deployment transaction and stores the contract address for future use.

// Deploy contract to blockchain
console.log("Deploying contract (30-60 seconds)...");

const deployed = await deployContract(providers, {
contract: contractInstance,
privateStateId: "helloWorldState",
initialPrivateState: {}
});

const contractAddress = deployed.deployTxData.public.contractAddress;

// Save deployment information
console.log("\nDEPLOYED!");
console.log(`Contract: ${contractAddress}\n`);

const info = {
contractAddress,
deployedAt: new Date().toISOString()
};

fs.writeFileSync("deployment.json", JSON.stringify(info, null, 2));
console.log("Saved to deployment.json");

// Close wallet connection
await wallet.close();
11

Add the main function call to initialize the script. This line at the end of the file starts the deployment process when the script runs.

main().catch(console.error);
12

Start the proof server in a new terminal window. This Docker container generates zero-knowledge proofs required for deployment.

Open a new terminal and run:

docker run -p 6300:6300 midnightnetwork/proof-server -- midnight-proof-server --network testnet

Keep this terminal open throughout the deployment process.

13

Compile TypeScript code to JavaScript. This converts the TypeScript deployment script into executable JavaScript.

In the original terminal:

npm run build
14

Execute the deployment script to deploy the contract. This runs the compiled script which handles wallet creation and contract deployment.

npm run deploy

The script prompts for an existing wallet seed or generates a new one. If balance is zero, it displays the wallet address and waits for funding from the faucet.

Created artifacts

After successful deployment, project contains:

  • deployment.json: File containing contract address and deployment timestamp
  • dist/: Folder containing compiled JavaScript output
  • Wallet seed: 64-character seed phrase displayed in console for new wallets

Final project structure:

my-mn-app/
├── contracts/
│ ├── managed/
│ │ └── hello-world/
│ │ ├── compiler/
│ │ ├── contract/ # Contains index.cjs
│ │ ├── keys/
│ │ └── zkir/
│ └── hello-world.compact
├── src/
│ └── deploy.ts
├── dist/ # Generated by TypeScript
├── node_modules/
├── deployment.json # Generated after deployment
├── package-lock.json
├── package.json
└── tsconfig.json
Important to save
  • Wallet seed: 64-character hex string. Keep private and secure. Losing it means losing wallet access.
  • Contract address: Found in deployment.json. Required for contract interaction.

Next steps

The Hello World contract is now live on Midnight Testnet. You can interact with the deployed contract.