Skip to main content
Version: v1

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.