Help us improve
Share bugs, ideas, or general feedback.
From midnight-dapp-dev
This skill should be used when the user asks about the Midnight.js SDK, midnight-js packages, @midnight-ntwrk npm packages, setting up SDK providers, deploying or finding contracts with deployContract or findDeployedContract, calling circuits with callTx or submitCallTx, the transaction lifecycle, SDK provider types (WalletProvider, MidnightProvider, PublicDataProvider, ProofProvider, ZkConfigProvider, PrivateStateProvider), testkit-js testing, observable state subscriptions, contract maintenance and verifier keys, or connecting to the indexer or proof server.
npx claudepluginhub devrelaicom/midnight-expert --plugin midnight-dapp-devHow this skill is triggered — by the user, by Claude, or both
Slash command
/midnight-dapp-dev:midnight-sdkThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Comprehensive reference for the Midnight.js SDK: all 10 packages, the MidnightProviders architecture, the full transaction lifecycle, advanced contract operations, observable patterns, and testkit-js. For the deployment workflow, see `references/transaction-lifecycle.md`. For TypeScript witness implementation, see `compact-core:compact-witness-ts`. For browser wallet integration via the DApp Co...
This skill should be used when building a Midnight DApp frontend, "create a React component for contract interaction", "set up wallet connection", "add a contract state subscription", "configure Vite for Midnight", "write tests for a DApp component", "debug wallet connection", "provider assembly", "transaction flow in the browser", "DApp Connector API", "RxJS observable for contract state", "scaffold a Midnight DApp", "useContractState hook", or working with Midnight SDK packages (@midnight-ntwrk/*) in a Vite + React project.
This skill should be used when the user asks about Midnight network architecture, transaction structure, guaranteed vs fallible sections, Zswap/Kachina integration, ledger and state management, cryptographic binding, balance verification, nullifiers, address derivation, transaction merging, atomic swaps, fee handling, or the privacy model separating private and public domains.
This skill should be used when the user asks about Midnight transaction execution, guaranteed vs fallible phases, kernel.checkpoint(), transaction composition, state conflicts, DUST fees, gas limits, proof verification, partial transaction success, transaction merging, atomic swaps, or how Compact circuits map to on-chain execution. Also triggered by mentions of "transaction semantics", "fallible phase", "guaranteed phase", "checkpoint", "well-formedness", "Impact VM", or "Zswap offers".
Share bugs, ideas, or general feedback.
Comprehensive reference for the Midnight.js SDK: all 10 packages, the MidnightProviders architecture, the full transaction lifecycle, advanced contract operations, observable patterns, and testkit-js. For the deployment workflow, see references/transaction-lifecycle.md. For TypeScript witness implementation, see compact-core:compact-witness-ts. For browser wallet integration via the DApp Connector, see midnight-dapp-dev:dapp-connector.
| Package | Purpose | Key Exports |
|---|---|---|
@midnight-ntwrk/midnight-js-contracts | Contract deployment and interaction | deployContract, findDeployedContract, submitCallTx, callTx, submitCallTxAsync, submitTxAsync, createUnprovenCallTx, getStates, getPublicStates, getUnshieldedBalances, verifyContractState, submitInsertVerifierKeyTx, submitRemoveVerifierKeyTx, submitReplaceAuthorityTx, replaceAuthority |
@midnight-ntwrk/midnight-js-types | Core type definitions | MidnightProviders, WalletProvider, MidnightProvider, PublicDataProvider, PrivateStateProvider, ProofProvider, ZkConfigProvider |
@midnight-ntwrk/midnight-js-network-id | Network configuration | setNetworkId |
@midnight-ntwrk/midnight-js-indexer-public-data-provider | Indexer connection | indexerPublicDataProvider() |
@midnight-ntwrk/midnight-js-http-client-proof-provider | Proof server communication | httpClientProofProvider() |
@midnight-ntwrk/midnight-js-level-private-state-provider | LevelDB private state (Node.js) | levelPrivateStateProvider() |
@midnight-ntwrk/midnight-js-node-zk-config-provider | Node.js ZK asset loading | NodeZkConfigProvider |
@midnight-ntwrk/midnight-js-fetch-zk-config-provider | Browser ZK asset loading | FetchZkConfigProvider |
@midnight-ntwrk/midnight-js-logger-provider | Optional structured logging | loggerProvider() |
@midnight-ntwrk/midnight-js-utils | Utility functions | toHex (as of the current release, toHex is the primary utility export) |
All packages are published on the public npm registry under the @midnight-ntwrk scope. Do not configure custom registries or .npmrc overrides.
MidnightProviders<ICK, PSI, PS> bundles the six providers required for contract deployment and interaction:
import type { MidnightProviders } from "@midnight-ntwrk/midnight-js-types";
interface MidnightProviders<ICK extends string, PSI extends string, PS> {
walletProvider: WalletProvider;
midnightProvider: MidnightProvider;
publicDataProvider: PublicDataProvider;
privateStateProvider: PrivateStateProvider<PSI, PS>;
zkConfigProvider: ZkConfigProvider<ICK>;
proofProvider: ProofProvider<ICK>;
}
| Parameter | Meaning | Example |
|---|---|---|
ICK | Impure circuit keys -- union of circuit names that have witnesses | "transfer" | "mint" |
PSI | Private state identifier -- the key used in the private state store | typeof "myContractState" |
PS | Private state type -- the shape of off-chain state | { secretKey: Uint8Array } |
Handles transaction balancing (adding fee inputs/outputs) and provides signing keys:
interface WalletProvider {
/** Get the shielded coin public key for receiving funds */
getCoinPublicKey(): string;
/** Get the encryption public key for encrypted outputs */
getEncryptionPublicKey(): string;
/** Balance an unproven transaction by adding fee inputs/outputs */
balanceTx(
tx: UnprovenTransaction,
newCoins?: ShieldedCoinInfo[],
ttl?: Date,
): Promise<BalancedProvingRecipe>;
}
WalletFacade (see the Deployment section below)ConnectedAPI.balanceUnsealedTransaction (see midnight-dapp-dev:dapp-connector)Submits finalized transactions to the Midnight node:
interface MidnightProvider {
/** Submit a finalized (balanced + proven) transaction */
submitTx(tx: FinalizedTransaction): Promise<TransactionId>;
}
WalletFacadeConnectedAPI.submitTransactionConnects to the indexer for on-chain state queries and subscriptions:
interface PublicDataProvider {
queryContractState(address: ContractAddress): Promise<ContractState | null>;
watchForDeployTxData(address: ContractAddress): Observable<DeployTxData>;
contractStateObservable(
address: ContractAddress,
options?: { type: "latest" },
): Observable<ContractState>;
}
Created identically in both Node.js and browser via indexerPublicDataProvider(httpUrl, wsUrl).
Persists off-chain contract state that witnesses access:
interface PrivateStateProvider<PSI extends string, PS> {
get(id: PSI): Promise<PS | null>;
set(id: PSI, state: PS): Promise<void>;
remove(id: PSI): Promise<void>;
}
levelPrivateStateProvider() using LevelDBMap or IndexedDB (see midnight-dapp-dev:dapp-connector)Loads ZK circuit configurations from compiled assets:
interface ZkConfigProvider<ICK extends string> {
getZkConfig(circuitId: ICK): Promise<ZkConfig>;
}
NodeZkConfigProvider reads from filesystemFetchZkConfigProvider fetches via HTTPCommunicates with the proof server to generate ZK proofs:
interface ProofProvider<ICK extends string> {
prove(circuitId: ICK, inputs: ProveInputs): Promise<Proof>;
}
Created via httpClientProofProvider(proofServerUrl, zkConfigProvider) in both environments.
Every contract interaction follows a five-stage pipeline:
Build -> Prove -> Balance -> Submit -> Finalize
callTx proofProvider walletProvider midnightProvider publicDataProvider
(construct tx) (generate ZK (add fee (send to node) (confirm on-chain)
proof) inputs/outputs)
callTx on a deployed/found contract performs all five stages:
const deployed = await deployContract(providers, options);
// Single call does: build -> prove -> balance -> submit -> finalize
const txData = await deployed.callTx.myCircuit(arg1, arg2);
// txData contains:
txData.public.txId; // TransactionId
txData.public.txHash; // string
txData.public.blockHeight; // bigint
For fine-grained control, use individual functions:
import {
createUnprovenCallTx,
submitCallTx,
submitCallTxAsync,
} from "@midnight-ntwrk/midnight-js-contracts";
// Step 1: Build the unproven transaction
const unproven = await createUnprovenCallTx(
deployed,
providers,
"myCircuit",
[arg1, arg2],
);
// Step 2+3+4+5: Prove, balance, submit, and finalize
const txData = await submitCallTx(providers, unproven);
// OR: Steps 2+3+4 only (return after submission, don't wait for finalization)
const txId = await submitCallTxAsync(providers, unproven);
For operations that do not need to wait for on-chain confirmation:
| Function | Waits for finalization? | Returns |
|---|---|---|
callTx.circuitName() | Yes | FinalizedCallTxData |
submitCallTx(providers, unproven) | Yes | FinalizedCallTxData |
submitCallTxAsync(providers, unproven) | No | TransactionId |
submitTxAsync(providers, tx) | No | TransactionId |
Use async variants for fire-and-forget operations or when managing finalization separately via observables.
Query contract and balance state without submitting transactions:
import {
getStates,
getPublicStates,
getUnshieldedBalances,
verifyContractState,
} from "@midnight-ntwrk/midnight-js-contracts";
// Get both public and private state
const { publicState, privateState } = await getStates(
deployed,
providers,
);
// Get only public (on-chain) state
const publicState = await getPublicStates(deployed, providers);
// Get unshielded token balances for the connected wallet
const balances = await getUnshieldedBalances(providers);
// Returns Record<string, bigint>
// Verify that on-chain state matches expected state
const isValid = await verifyContractState(deployed, providers);
Verifier keys authorize which circuits can be called on a deployed contract. Manage them post-deployment:
import {
submitInsertVerifierKeyTx,
submitRemoveVerifierKeyTx,
} from "@midnight-ntwrk/midnight-js-contracts";
// Add a new verifier key (enables a new circuit)
await submitInsertVerifierKeyTx(providers, {
contractAddress,
circuitId: "newCircuit",
verifierKey: newVerifierKeyBytes,
});
// Remove a verifier key (disables a circuit)
await submitRemoveVerifierKeyTx(providers, {
contractAddress,
circuitId: "oldCircuit",
});
Verifier key insertion is required when upgrading contract logic or enabling circuits that were not included in the original deployment.
The contract authority controls who can modify verifier keys. Transfer authority to enable governance transitions:
import {
submitReplaceAuthorityTx,
replaceAuthority,
} from "@midnight-ntwrk/midnight-js-contracts";
// Replace the contract authority
await submitReplaceAuthorityTx(providers, {
contractAddress,
newAuthority: newAuthorityPublicKey,
});
// Alternative: replaceAuthority() for use within contract interactions
const result = await replaceAuthority(deployed, providers, newAuthorityPublicKey);
Authority replacement is irreversible. The new authority must be a valid public key that controls the signing key for subsequent verifier key operations.
The SDK uses RxJS observables for reactive state management:
import { map, distinctUntilChanged } from "rxjs";
import { MyContract } from "./managed/mycontract/contract/index.js";
// Subscribe to all state changes
const stateSubscription = providers.publicDataProvider
.contractStateObservable(contractAddress, { type: "latest" })
.pipe(
map((state) => MyContract.ledger(state.data)),
)
.subscribe({
next: (ledgerState) => {
console.log("State updated:", ledgerState);
},
error: (err) => {
console.error("Subscription error:", err);
},
});
// Clean up when done
stateSubscription.unsubscribe();
// Watch for a specific transaction to finalize
providers.publicDataProvider
.watchForDeployTxData(contractAddress)
.subscribe({
next: (deployData) => {
console.log("Contract deployed:", deployData.contractAddress);
},
});
import { combineLatest, map } from "rxjs";
// Combine contract state with balance updates
const appState$ = combineLatest([
providers.publicDataProvider
.contractStateObservable(contractAddress, { type: "latest" })
.pipe(map((s) => MyContract.ledger(s.data))),
balanceObservable$,
]).pipe(
map(([ledger, balance]) => ({
contractValue: ledger.counter,
userBalance: balance,
})),
);
The @midnight-ntwrk/testkit-js package provides utilities for integration testing without a live network:
import { TestWallet } from "@midnight-ntwrk/testkit-js";
// Create a test wallet with pre-funded balances
const testWallet = await TestWallet.create({
networkId: "undeployed",
initialBalance: 1_000_000n,
});
const walletProvider = testWallet.walletProvider();
const midnightProvider = testWallet.midnightProvider();
import { TestEnvironment } from "@midnight-ntwrk/testkit-js";
import { deployContract } from "@midnight-ntwrk/midnight-js-contracts";
describe("MyContract", () => {
let env: TestEnvironment;
beforeAll(async () => {
env = await TestEnvironment.start();
});
afterAll(async () => {
await env.stop();
});
it("deploys and calls a circuit", async () => {
const providers = env.createProviders(witnesses);
const deployed = await deployContract(providers, {
compiledContract: myCompiledContract,
privateStateId: "testState",
initialPrivateState: { secretKey: testKey },
});
const result = await deployed.callTx.increment();
expect(result.public.txId).toBeDefined();
const state = await getPublicStates(deployed, providers);
expect(state.counter).toBe(1n);
});
});
// Simulate two different users interacting with the same contract
const alice = await TestWallet.create({ networkId: "undeployed", initialBalance: 1_000_000n });
const bob = await TestWallet.create({ networkId: "undeployed", initialBalance: 1_000_000n });
// Alice deploys
const aliceProviders = env.createProvidersWithWallet(alice, witnesses);
const deployed = await deployContract(aliceProviders, deployOptions);
// Bob joins
const bobProviders = env.createProvidersWithWallet(bob, witnesses);
const found = await findDeployedContract(bobProviders, {
contractAddress: deployed.deployTxData.public.contractAddress,
compiledContract: myCompiledContract,
privateStateId: "bobState",
initialPrivateState: { secretKey: bobKey },
});
// Bob calls a circuit on Alice's contract
const result = await found.callTx.transfer(aliceAddress, 100n);
interface FinalizedDeployTxData<C> {
public: {
contractAddress: ContractAddress;
txId: TransactionId;
txHash: string;
blockHeight: bigint;
};
private: {
signingKey: Uint8Array;
initialPrivateState: PS;
};
}
interface FinalizedCallTxData<C, ICK> {
public: {
txId: TransactionId;
txHash: string;
blockHeight: bigint;
};
}
Both types provide the on-chain confirmation data needed to verify that the operation succeeded.
| Mistake | Fix |
|---|---|
Importing NodeZkConfigProvider in browser code | Use FetchZkConfigProvider for browser environments |
Not calling setNetworkId() before creating providers | Call once at application startup, before any provider construction |
| Forgetting to unsubscribe from observables | Store subscription references and call unsubscribe() in cleanup |
Using callTx when you only need to submit | Use submitCallTxAsync to avoid blocking on finalization |
| Mixing circuit keys from different contracts | Each contract has its own ImpureCircuitId type; do not share providers across contracts with different circuit sets |
Using mismatched versions of @midnight-ntwrk packages | Pin all SDK dependencies to the same release version. |
| Topic | Reference File |
|---|---|
| Detailed exports, constructor signatures, and configuration for all 10 SDK packages | references/package-reference.md |
| Complete transaction lifecycle with low-level API, proving flow, balancing internals, and finalization | references/transaction-lifecycle.md |