Skip to main content
Version: Canary 🚧

Deploy the hello world contract

In this tutorial, you'll deploy your compiled hello world contract to Midnight's Preprod network. You'll learn how to set up a wallet, manage deployment scripts, and submit your contract to the blockchain.

By the end of this tutorial, you'll have a live contract that you can interact with through the network.

Prerequisites​

Before you begin, ensure you have the following:

  • A compiled hello world contract in contracts/managed/hello-world/. You'll need to complete the build your first contract tutorial first.
  • Node.js version 20.x or higher installed. Install it using NVM.
  • Docker installed and running. This is required to run the proof server and generate Zero Knowledge (ZK) proofs.
  • Basic understanding of TypeScript and command-line operations.

On your project's root directory, initialize an npm project with default settings. This creates a package.json file:

npm init -y

Create the src directory to hold your deployment scripts:

mkdir src

Your project structure should look similar to this:

my-midnight-contract/
β”œβ”€β”€ contracts/
β”‚ β”œβ”€β”€ managed/
β”‚ β”‚ └── hello-world/
β”‚ β”‚ β”œβ”€β”€ compiler/
β”‚ β”‚ β”œβ”€β”€ contract/
β”‚ β”‚ β”œβ”€β”€ keys/
β”‚ β”‚ └── zkir/
β”‚ └── hello-world.compact
β”œβ”€β”€ src/
└── package.json
1

Install deployment dependencies​

Add all the dependencies needed for wallet management, contract deployment, and network connectivity.

Update the package.json file to include the deployment dependencies and scripts:

{
"name": "my-midnight-contract",
"version": "1.0.0",
"type": "module",
"scripts": {
"compile": "compact compile contracts/hello-world.compact contracts/managed/hello-world",
"build": "tsc",
"deploy": "tsx src/deploy.ts",
"start-proof-server": "docker run -p 6300:6300 midnightntwrk/proof-server:7.0.0 -- midnight-proof-server -v"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/ws": "^8.18.1",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
},
"dependencies": {
"@midnight-ntwrk/compact-runtime": "0.14.0",
"@midnight-ntwrk/ledger": "^4.0.0",
"@midnight-ntwrk/midnight-js-contracts": "3.0.0",
"@midnight-ntwrk/midnight-js-http-client-proof-provider": "3.0.0",
"@midnight-ntwrk/midnight-js-indexer-public-data-provider": "3.0.0",
"@midnight-ntwrk/midnight-js-level-private-state-provider": "3.0.0",
"@midnight-ntwrk/midnight-js-network-id": "3.0.0",
"@midnight-ntwrk/midnight-js-node-zk-config-provider": "3.0.0",
"@midnight-ntwrk/midnight-js-types": "3.0.0",
"@midnight-ntwrk/wallet-sdk-address-format": "3.0.0",
"@midnight-ntwrk/wallet-sdk-dust-wallet": "1.0.0",
"@midnight-ntwrk/wallet-sdk-facade": "1.0.0",
"@midnight-ntwrk/wallet-sdk-hd": "3.0.0",
"@midnight-ntwrk/wallet-sdk-shielded": "1.0.0",
"@midnight-ntwrk/wallet-sdk-unshielded-wallet": "1.0.0",
"ws": "^8.19.0"
}
}

These packages provide essential functionality:

  • Wallet SDK packages: Create and manage Midnight wallets with support for shielded, unshielded, and DUST operations.
  • Midnight.js packages: Handle contract deployment, proof generation, and blockchain interactions.
  • Development tools: TypeScript execution (tsx) and type definitions for Node.js.

Install all dependencies:

npm install
2

Configure TypeScript​

Create a TypeScript configuration file to define how your deployment scripts are compiled.

Create tsconfig.json in your project root:

{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"ts-node": {
"esm": true,
"experimentalSpecifierResolution": "node"
}
}

This configuration:

  • Targets modern JavaScript (ES2022) for better performance
  • Uses ESNext modules for compatibility with Midnight SDK packages
  • Enables strict type checking for safer code
  • Outputs compiled JavaScript to the dist directory
3

Create wallet utilities​

Create a shared utilities file to handle wallet creation, key derivation, and provider setup. This code is reused across deployment and interaction scripts for your contract.

Create the src/utils.ts file and add the following code:

import * as path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { WebSocket } from 'ws';
import * as Rx from 'rxjs';
import { Buffer } from 'buffer';

// Midnight SDK imports
import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client-proof-provider';
import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
import { levelPrivateStateProvider } from '@midnight-ntwrk/midnight-js-level-private-state-provider';
import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider';
import { setNetworkId, getNetworkId } from '@midnight-ntwrk/midnight-js-network-id';
import * as ledger from '@midnight-ntwrk/ledger-v7';
import { WalletFacade } from '@midnight-ntwrk/wallet-sdk-facade';
import { DustWallet } from '@midnight-ntwrk/wallet-sdk-dust-wallet';
import { HDWallet, Roles } from '@midnight-ntwrk/wallet-sdk-hd';
import { ShieldedWallet } from '@midnight-ntwrk/wallet-sdk-shielded';
import { createKeystore, InMemoryTransactionHistoryStorage, PublicKey, UnshieldedWallet } from '@midnight-ntwrk/wallet-sdk-unshielded-wallet';
import { CompiledContract } from '@midnight-ntwrk/compact-js';

// Enable WebSocket for GraphQL subscriptions
// @ts-expect-error Required for wallet sync
globalThis.WebSocket = WebSocket;

// Set network to Preprod
setNetworkId('preprod');

// Network configuration for Preprod
export const CONFIG = {
indexer: 'https://indexer.preprod.midnight.network/api/v3/graphql',
indexerWS: 'wss://indexer.preprod.midnight.network/api/v3/graphql/ws',
node: 'https://rpc.preprod.midnight.network',
proofServer: 'http://127.0.0.1:6300',
};

// Path configuration
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export const zkConfigPath = path.resolve(__dirname, '..', 'contracts', 'managed', 'hello-world');

// Load compiled contract
const contractPath = path.join(zkConfigPath, 'contract', 'index.js');
export const HelloWorld = await import(pathToFileURL(contractPath).href);

export const compiledContract = CompiledContract.make('hello-world', HelloWorld.Contract).pipe(
CompiledContract.withVacantWitnesses,
CompiledContract.withCompiledFileAssets(zkConfigPath),
);

This section sets up the foundation:

  • Configure WebSocket support for real-time blockchain updates.
  • Define network endpoints for the Preprod indexer, node, and proof server.
  • Load your compiled contract and prepare it for deployment.

Add the wallet creation functions:

// ─── Wallet Functions ──────────────────────────────────────────────────────────

export function deriveKeys(seed: string) {
const hdWallet = HDWallet.fromSeed(Buffer.from(seed, 'hex'));
if (hdWallet.type !== 'seedOk') throw new Error('Invalid seed');

const result = hdWallet.hdWallet
.selectAccount(0)
.selectRoles([Roles.Zswap, Roles.NightExternal, Roles.Dust])
.deriveKeysAt(0);

if (result.type !== 'keysDerived') throw new Error('Key derivation failed');

hdWallet.hdWallet.clear();
return result.keys;
}

export async function createWallet(seed: string) {
const keys = deriveKeys(seed);
const networkId = getNetworkId();

// Derive secret keys for different wallet components
const shieldedSecretKeys = ledger.ZswapSecretKeys.fromSeed(keys[Roles.Zswap]);
const dustSecretKey = ledger.DustSecretKey.fromSeed(keys[Roles.Dust]);
const unshieldedKeystore = createKeystore(keys[Roles.NightExternal], networkId);

const walletConfig = {
networkId,
indexerClientConnection: {
indexerHttpUrl: CONFIG.indexer,
indexerWsUrl: CONFIG.indexerWS
},
provingServerUrl: new URL(CONFIG.proofServer),
relayURL: new URL(CONFIG.node.replace(/^http/, 'ws')),
};

// Initialize wallet components
const shieldedWallet = ShieldedWallet(walletConfig)
.startWithSecretKeys(shieldedSecretKeys);

const unshieldedWallet = UnshieldedWallet({
networkId,
indexerClientConnection: walletConfig.indexerClientConnection,
txHistoryStorage: new InMemoryTransactionHistoryStorage(),
}).startWithPublicKey(PublicKey.fromKeyStore(unshieldedKeystore));

const dustWallet = DustWallet({
...walletConfig,
costParameters: {
additionalFeeOverhead: 300_000_000_000_000n,
feeBlocksMargin: 5
},
}).startWithSecretKey(dustSecretKey, ledger.LedgerParameters.initialParameters().dust);

const wallet = new WalletFacade(shieldedWallet, unshieldedWallet, dustWallet);
await wallet.start(shieldedSecretKeys, dustSecretKey);

return { wallet, shieldedSecretKeys, dustSecretKey, unshieldedKeystore };
}

These functions handle wallet creation:

  • deriveKeys: Uses HD (Hierarchical Deterministic) wallet derivation to generate keys for different roles from a single seed
  • createWallet: Creates a complete Midnight wallet with three components:
    • Shielded wallet: For private transactions using Zero Knowledge (ZK) proofs
    • Unshielded wallet: For public transactions visible on-chain
    • DUST wallet: Manages Midnight's gas resource required for transaction fees

Add the transaction signing and provider setup functions:

// Sign transaction intents with the wallet's private keys
export function signTransactionIntents(
tx: { intents?: Map<number, any> },
signFn: (payload: Uint8Array) => ledger.Signature,
proofMarker: 'proof' | 'pre-proof'
): void {
if (!tx.intents || tx.intents.size === 0) return;

for (const segment of tx.intents.keys()) {
const intent = tx.intents.get(segment);
if (!intent) continue;

const cloned = ledger.Intent.deserialize<
ledger.SignatureEnabled,
ledger.Proofish,
ledger.PreBinding
>('signature', proofMarker, 'pre-binding', intent.serialize());

const sigData = cloned.signatureData(segment);
const signature = signFn(sigData);

if (cloned.fallibleUnshieldedOffer) {
const sigs = cloned.fallibleUnshieldedOffer.inputs.map(
(_: any, i: number) =>
cloned.fallibleUnshieldedOffer!.signatures.at(i) ?? signature
);
cloned.fallibleUnshieldedOffer =
cloned.fallibleUnshieldedOffer.addSignatures(sigs);
}

if (cloned.guaranteedUnshieldedOffer) {
const sigs = cloned.guaranteedUnshieldedOffer.inputs.map(
(_: any, i: number) =>
cloned.guaranteedUnshieldedOffer!.signatures.at(i) ?? signature
);
cloned.guaranteedUnshieldedOffer =
cloned.guaranteedUnshieldedOffer.addSignatures(sigs);
}

tx.intents.set(segment, cloned);
}
}

export async function createProviders(
walletCtx: Awaited<ReturnType<typeof createWallet>>
) {
const state = await Rx.firstValueFrom(
walletCtx.wallet.state().pipe(Rx.filter((s) => s.isSynced))
);

const walletProvider = {
getCoinPublicKey: () => state.shielded.coinPublicKey.toHexString(),
getEncryptionPublicKey: () => state.shielded.encryptionPublicKey.toHexString(),
async balanceTx(tx: any, ttl?: Date) {
const recipe = await walletCtx.wallet.balanceUnboundTransaction(
tx,
{
shieldedSecretKeys: walletCtx.shieldedSecretKeys,
dustSecretKey: walletCtx.dustSecretKey
},
{ ttl: ttl ?? new Date(Date.now() + 30 * 60 * 1000) },
);

const signFn = (payload: Uint8Array) =>
walletCtx.unshieldedKeystore.signData(payload);

signTransactionIntents(recipe.baseTransaction, signFn, 'proof');
if (recipe.balancingTransaction) {
signTransactionIntents(recipe.balancingTransaction, signFn, 'pre-proof');
}

return walletCtx.wallet.finalizeRecipe(recipe);
},
submitTx: (tx: any) => walletCtx.wallet.submitTransaction(tx) as any,
};

const zkConfigProvider = new NodeZkConfigProvider(zkConfigPath);

return {
privateStateProvider: levelPrivateStateProvider({
privateStateStoreName: 'hello-world-state',
walletProvider
}),
publicDataProvider: indexerPublicDataProvider(
CONFIG.indexer,
CONFIG.indexerWS
),
zkConfigProvider,
proofProvider: httpClientProofProvider(CONFIG.proofServer, zkConfigProvider),
walletProvider,
midnightProvider: walletProvider,
};
}

These functions handle transaction management:

  • signTransactionIntents: Signs transaction intents with the wallet's private keys. This is required for unshielded transactions
  • createProviders: Creates all the providers needed for contract deployment:
    • privateStateProvider: Manages local contract state storage
    • publicDataProvider: Fetches blockchain data from the indexer
    • zkConfigProvider: Loads Zero Knowledge (ZK) circuit configurations
    • proofProvider: Communicates with the proof server
    • walletProvider: Handles transaction balancing and signing
4

Create the deployment script​

Create the main deployment script to handle the entire deployment process.

Create the src/deploy.ts file and add the following code:

import { createInterface } from 'node:readline/promises';
import { stdin, stdout } from 'node:process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as Rx from 'rxjs';
import { Buffer } from 'buffer';

// Midnight.js imports
import { deployContract } from '@midnight-ntwrk/midnight-js-contracts';
import { toHex } from '@midnight-ntwrk/midnight-js-utils';
import { unshieldedToken } from '@midnight-ntwrk/ledger-v7';
import { generateRandomSeed } from '@midnight-ntwrk/wallet-sdk-hd';

// Shared utilities from the utils.ts file
import {
createWallet,
createProviders,
compiledContract,
zkConfigPath
} from './utils.js';

This imports all necessary functions and checks that the contract is compiled before proceeding.

Add the main deployment logic below the imports:

// ─── Main Deploy Script ────────────────────────────────────────────────────────

async function main() {
console.log('\n╔══════════════════════════════════════════════════════════════╗');
console.log('β•‘ Deploy Hello World to Midnight Preprod β•‘');
console.log('β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•\n');

// Check if contract is compiled
if (!fs.existsSync(path.join(zkConfigPath, 'contract', 'index.js'))) {
console.error('Contract not compiled! Run: npm run compile');
process.exit(1);
}

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

try {
// 1. Wallet setup
console.log('─── Step 1: Wallet Setup ───────────────────────────────────────\n');
const choice = await rl.question(
' [1] Create new wallet\n [2] Restore from seed\n > '
);

const seed = choice.trim() === '2'
? await rl.question('\n Enter your 64-character seed: ')
: toHex(Buffer.from(generateRandomSeed()));

if (choice.trim() !== '2') {
console.log(
`\n ⚠️ SAVE THIS SEED (you'll need it later):\n ${seed}\n`
);
}

console.log(' Creating wallet...');
const walletCtx = await createWallet(seed);

console.log(' Syncing with network...');
const state = await Rx.firstValueFrom(
walletCtx.wallet.state().pipe(
Rx.throttleTime(5000),
Rx.filter((s) => s.isSynced)
)
);

const address = walletCtx.unshieldedKeystore.getBech32Address();
const balance = state.unshielded.balances[unshieldedToken().raw] ?? 0n;

console.log(`\n Wallet Address: ${address}`);
console.log(` Balance: ${balance.toLocaleString()} tNight\n`);

This section handles wallet creation:

  • Prompt the user to create a new wallet or restore from an existing seed.
  • Generate a cryptographically secure random seed for new wallets.
  • Create the wallet and sync it with the Preprod network.
  • Display the wallet address and current balance.

Continue with funding and DUST registration:

    // 2. Fund wallet if needed
if (balance === 0n) {
console.log('─── Step 2: Fund Your Wallet ───────────────────────────────────\n');
console.log(' Visit: https://faucet.preprod.midnight.network/');
console.log(` Address: ${address}\n`);
console.log(' Waiting for funds...');

await Rx.firstValueFrom(
walletCtx.wallet.state().pipe(
Rx.throttleTime(10000),
Rx.filter((s) => s.isSynced),
Rx.map((s) => s.unshielded.balances[unshieldedToken().raw] ?? 0n),
Rx.filter((b) => b > 0n),
),
);
console.log(' Funds received!\n');
}

// 3. Register for DUST
console.log('─── Step 3: DUST Token Setup ───────────────────────────────────\n');
const dustState = await Rx.firstValueFrom(
walletCtx.wallet.state().pipe(Rx.filter((s) => s.isSynced))
);

if (dustState.dust.walletBalance(new Date()) === 0n) {
const nightUtxos = dustState.unshielded.availableCoins.filter(
(c: any) => !c.meta?.registeredForDustGeneration
);

if (nightUtxos.length > 0) {
console.log(' Registering for DUST generation...');
const recipe = await walletCtx.wallet.registerNightUtxosForDustGeneration(
nightUtxos,
walletCtx.unshieldedKeystore.getPublicKey(),
(payload) => walletCtx.unshieldedKeystore.signData(payload),
);
await walletCtx.wallet.submitTransaction(
await walletCtx.wallet.finalizeRecipe(recipe)
);
}

console.log(' Waiting for DUST tokens...');
await Rx.firstValueFrom(
walletCtx.wallet.state().pipe(
Rx.throttleTime(5000),
Rx.filter((s) => s.isSynced),
Rx.filter((s) => s.dust.walletBalance(new Date()) > 0n)
),
);
}
console.log(' DUST tokens ready!\n');

This handles pre-deployment requirements:

  • Funding: If the wallet has no balance, then it displays instructions to visit the Preprod faucet and waits for funds to arrive.
  • DUST registration: DUST is Midnight's gas token. The script registers unshielded UTXOs (Unspent Transaction Outputs) for DUST generation and waits for the tokens to become available.

Add the actual deployment code:

    // 4. Deploy contract
console.log('─── Step 4: Deploy Contract ────────────────────────────────────\n');
console.log(' Setting up providers...');
const providers = await createProviders(walletCtx);

console.log(' Deploying contract (this may take 30-60 seconds)...\n');
const deployed = await deployContract(providers, {
compiledContract,
privateStateId: 'helloWorldState',
initialPrivateState: {},
});

const contractAddress = deployed.deployTxData.public.contractAddress;
console.log(' βœ… Contract deployed successfully!\n');
console.log(` Contract Address: ${contractAddress}\n`);

// 5. Save deployment info
const deploymentInfo = {
contractAddress,
seed,
network: 'preprod',
deployedAt: new Date().toISOString(),
};

fs.writeFileSync('deployment.json', JSON.stringify(deploymentInfo, null, 2));
console.log(' Saved to deployment.json\n');

await walletCtx.wallet.stop();
console.log('─── Deployment Complete! ───────────────────────────────────────\n');
} finally {
rl.close();
}
}

main().catch(console.error);

This final section:

  • Sets up all required providers for contract deployment.
  • Calls deployContract which:
    • Generates Zero Knowledge (ZK) proofs for the deployment transaction.
    • Submits the transaction to the Preprod network.
    • Waits for confirmation.
  • Saves deployment information to deployment.json for later use.
  • Stops the wallet and displays next steps.
5

Run the deployment​

You're ready to deploy your contract to Preprod.

Make sure the proof server is running:

npm run start-proof-server

Run the deployment script:

npm run deploy

The script guides you through the deployment process:

  1. Wallet creation: Choose to create a new wallet or restore from an existing seed. If creating a new wallet, then save the displayed seed. You'll need it for later interactions with the contract.
  2. Funding: If your wallet has no balance, then the script displays your address and waits. Visit the Preprod faucet to request test tokens.
  3. DUST registration: The script automatically registers your wallet for DUST generation and waits for tokens to arrive.
  4. Deployment: The contract is deployed to the network. This process takes 30-60 seconds as it generates Zero Knowledge (ZK) proofs and waits for blockchain confirmation.

When deployment completes, you'll see output similar to the following:

βœ… Contract deployed successfully!

Contract Address: 0x1234567890abcdef...

Saved to deployment.json

Understanding the deployment artifacts​

After successful deployment, your project will have a new deployment.json file in the root directory. Here's an example of what it contains:

{
"contractAddress": "0x1234567890abcdef...",
"seed": "1234567890abcdef...",
"network": "preprod",
"deployedAt": "2026-02-10T20:00:00.000Z"
}

This file stores important information about your deployed contract:

  • contractAddress: The unique address of your deployed contract. You'll use this to interact with the contract.
  • seed: Your wallet seed. This is stored for convenience in developmentβ€”never commit this to version control in production.
  • network: The network where the contract is deployed.
  • deployedAt: Timestamp of deployment.

Your project structure should now look similar to this:

my-midnight-contract/
β”œβ”€β”€ contracts/
β”‚ β”œβ”€β”€ managed/
β”‚ β”‚ └── hello-world/
β”‚ β”‚ β”œβ”€β”€ compiler/
β”‚ β”‚ β”œβ”€β”€ contract/
β”‚ β”‚ β”œβ”€β”€ keys/
β”‚ β”‚ └── zkir/
β”‚ └── hello-world.compact
β”œβ”€β”€ src/
β”‚ β”œβ”€β”€ deploy.ts
β”‚ └── utils.ts
β”œβ”€β”€ node_modules/
β”œβ”€β”€ deployment.json # Generated by the deployment script
β”œβ”€β”€ package-lock.json
β”œβ”€β”€ package.json
└── tsconfig.json

Troubleshoot​

This section covers common issues that you might encounter during deployment and their solutions.

Proof server connection errors​

If you see errors about connecting to the proof server, such as Wallet.Proving: Failed to prove transaction, then:

  • Verify Docker is running: docker ps.
  • Check the proof server is started: npm run start-proof-server.
  • Ensure port 6300 is not already in use.

Insufficient DUST tokens​

If deployment fails due to lack of DUST:

  • Wait longer for DUST generation. It can take several minutes to generate.
  • Ensure you have sufficient tNight tokens. DUST is generated from tNight.
  • Check that DUST registration was successful in the script output.

Contract compilation errors​

If the script reports missing contract files, then:

  • Run npm run compile to recompile your contract.
  • Verify the contract compiled successfully without errors.
  • Check that contracts/managed/hello-world/contract/index.js exists.

Security considerations​

When deploying contracts, keep these security practices in mind:

  • Never commit seeds: Add deployment.json to .gitignore in production projects. Your seed controls access to your wallet and funds.
  • Use environment variables: For production deployments, load seeds from secure environment variables instead of files.
  • Separate wallets: Use different wallets for development, testing, and production.
  • Verify deployments: Always verify the contract address and deployment transaction on a block explorer.

Next steps​

You've deployed your contract to Preprod. See the interact with hello world contract guide to build a CLI and call the storeMessage circuit.