midnight-js
The Midnight.js TypeScript SDK for building DApps on Midnight Network. Covers all provider setup, wallet SDK integration (HDWallet, WalletFacade, ShieldedWallet, UnshieldedWallet, DustWallet), contract deployment, circuit calls, private state management, DUST generation, and testkit usage. Use this skill whenever a user is wiring up providers, managing wallets in a Node.js or browser context, deploying or calling Compact contracts from TypeScript, handling DUST/fee flow, reading ledger state, or writing tests for Midnight DApps.
How do I install this agent skill?
npx skills add https://github.com/kali-decoder/midnight-skills --skill midnight-jsIs this agent skill safe to install?
- Gen Agent Trust Hubpass
The skill provides a detailed guide for using the Midnight.js SDK to build decentralized applications on the Midnight Network. It covers wallet configuration, contract deployment, and network settings using official packages and resources. No security risks or malicious patterns were identified.
- Socketpass
No alerts
- Snykwarn
Risk: MEDIUM · 2 issues
What does this agent skill do?
Midnight.js Skill
Midnight.js is the TypeScript SDK for building DApps on Midnight Network — analogous to Web3.js for Ethereum. The official SDK page is "coming soon"; this skill is grounded in the official Counter CLI tutorial and the 1AM starter template.
Primary references:
docs.midnight.network/tutorials/counter/counter-cli— official full implementationgithub.com/midnightntwrk/example-counter— reference repo (Node.js / headless wallet)github.com/webisoftSoftware/1AM-starter-template— browser wallet variant
1) Package Map
| Package | Purpose |
|---|---|
@midnight-ntwrk/midnight-js-contracts | deployContract, findDeployedContract, submitCallTx |
@midnight-ntwrk/midnight-js-types | MidnightProviders, WalletProvider, MidnightProvider, createProofProvider |
@midnight-ntwrk/midnight-js-indexer-public-data-provider | indexerPublicDataProvider (GraphQL indexer client) |
@midnight-ntwrk/midnight-js-fetch-zk-config-provider | FetchZkConfigProvider (browser / CDN) |
@midnight-ntwrk/midnight-js-node-zk-config-provider | NodeZkConfigProvider (Node.js / filesystem) |
@midnight-ntwrk/midnight-js-http-client-proof-provider | httpClientProofProvider (self-hosted proof server) |
@midnight-ntwrk/midnight-js-level-private-state-provider | levelPrivateStateProvider (persistent LevelDB private state) |
@midnight-ntwrk/midnight-js-network-id | setNetworkId, getNetworkId |
@midnight-ntwrk/compact-runtime | ContractState (indexer deserialization) |
@midnight-ntwrk/ledger-v8 | Transaction, LedgerParameters, ZswapSecretKeys, DustSecretKey |
@midnight-ntwrk/compact-js | CompiledContract, ImpureCircuitId |
@midnight-ntwrk/wallet-sdk-facade | WalletFacade (unified wallet) |
@midnight-ntwrk/wallet-sdk-hd | HDWallet, generateRandomSeed, Roles |
@midnight-ntwrk/wallet-sdk-shielded | ShieldedWallet |
@midnight-ntwrk/wallet-sdk-unshielded-wallet | UnshieldedWallet, createKeystore, PublicKey |
@midnight-ntwrk/wallet-sdk-dust-wallet | DustWallet |
@midnight-ntwrk/wallet-sdk-address-format | MidnightBech32m, ShieldedAddress, etc. |
2) Network Configuration
Always call setNetworkId before any SDK operation. It sets a global that all libraries use automatically.
import { setNetworkId, getNetworkId } from '@midnight-ntwrk/midnight-js-network-id';
// Call once at startup, in the config constructor or before building providers
setNetworkId('preprod'); // 'preprod' | 'preview' | 'mainnet' | 'undeployed'
Network endpoints:
| Network | Indexer HTTP | Indexer WS | RPC |
|---|---|---|---|
preprod | https://indexer.preprod.midnight.network/api/v4/graphql | wss://indexer.preprod.midnight.network/api/v4/graphql/ws | https://rpc.preprod.midnight.network |
preview | https://indexer.preview.midnight.network/api/v4/graphql | wss://indexer.preview.midnight.network/api/v4/graphql/ws | wss://rpc.preview.midnight.network |
mainnet | https://indexer.mainnet.midnight.network/api/v4/graphql | wss://indexer.mainnet.midnight.network/api/v4/graphql/ws | https://rpc.mainnet.midnight.network |
undeployed (local) | http://localhost:8088/api/v4/graphql | ws://localhost:8088/api/v4/graphql/ws | ws://localhost:9944 |
3) Compiled Contract Setup
Pre-compile the contract once at module load — not on every deploy/call.
import { CompiledContract } from '@midnight-ntwrk/compact-js';
import { Contract, witnesses } from './managed/counter'; // generated by compact compiler
// Node.js: load ZK assets from filesystem
import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider';
const zkConfigPath = path.resolve('contract/src/managed/counter');
const compiledContract = CompiledContract.make('counter', Contract).pipe(
CompiledContract.withVacantWitnesses,
CompiledContract.withCompiledFileAssets(zkConfigPath),
);
// Browser / CDN: load ZK assets via fetch
import { FetchZkConfigProvider } from '@midnight-ntwrk/midnight-js-fetch-zk-config-provider';
const compiledContract = CompiledContract.make('YourContract', Contract).pipe(
CompiledContract.withVacantWitnesses,
CompiledContract.withCompiledFileAssets('/zk/your-contract'),
);
4) Type Definitions (Important Pattern)
Define typed aliases for your contract's providers — this is the Midnight.js pattern:
import type { MidnightProviders } from '@midnight-ntwrk/midnight-js-types';
import type { DeployedContract, FoundContract } from '@midnight-ntwrk/midnight-js-contracts';
import type { ImpureCircuitId } from '@midnight-ntwrk/compact-js';
import { Counter, type CounterPrivateState } from './managed/counter';
// Extract circuit IDs from the contract type
export type CounterCircuits = ImpureCircuitId<Counter.Contract<CounterPrivateState>>;
// Private state key (string literal type)
export const CounterPrivateStateId = 'counterPrivateState' as const;
export type CounterPrivateStateId = typeof CounterPrivateStateId;
// Full providers type for this contract
export type CounterProviders = MidnightProviders<
CounterCircuits,
CounterPrivateStateId,
CounterPrivateState
>;
// Contract instance types
export type CounterContract = Counter.Contract<CounterPrivateState>;
export type DeployedCounterContract = DeployedContract<CounterContract> | FoundContract<CounterContract>;
5) Wallet Setup (Node.js / Headless)
HD Key Derivation
import { HDWallet, generateRandomSeed, Roles } from '@midnight-ntwrk/wallet-sdk-hd';
import * as ledger from '@midnight-ntwrk/ledger-v8';
import { Buffer } from 'buffer';
// Generate a fresh random seed (returns Uint8Array)
const seed = generateRandomSeed();
const seedHex = Buffer.from(seed).toString('hex'); // save this
// Or restore from existing hex seed
const seedHex = '...'; // from user input
const hdWallet = HDWallet.fromSeed(Buffer.from(seedHex, 'hex'));
if (hdWallet.type !== 'seedOk') throw new Error('Invalid seed');
const derivationResult = hdWallet.hdWallet
.selectAccount(0)
.selectRoles([Roles.Zswap, Roles.NightExternal, Roles.Dust])
.deriveKeysAt(0);
if (derivationResult.type !== 'keysDerived') throw new Error('Key derivation failed');
hdWallet.hdWallet.clear(); // wipe secret material from memory
const keys = derivationResult.keys;
const shieldedSecretKeys = ledger.ZswapSecretKeys.fromSeed(keys[Roles.Zswap]);
const dustSecretKey = ledger.DustSecretKey.fromSeed(keys[Roles.Dust]);
Wallet Construction
import { ShieldedWallet } from '@midnight-ntwrk/wallet-sdk-shielded';
import { UnshieldedWallet, createKeystore, PublicKey, InMemoryTransactionHistoryStorage } from '@midnight-ntwrk/wallet-sdk-unshielded-wallet';
import { DustWallet } from '@midnight-ntwrk/wallet-sdk-dust-wallet';
import { WalletFacade } from '@midnight-ntwrk/wallet-sdk-facade';
const unshieldedKeystore = createKeystore(keys[Roles.NightExternal], getNetworkId());
const shieldedWallet = ShieldedWallet({
networkId: getNetworkId(),
indexerClientConnection: { indexerHttpUrl: indexer, indexerWsUrl: indexerWS },
provingServerUrl: new URL(proofServer),
relayURL: new URL(node.replace(/^http/, 'ws')), // convert http → ws
}).startWithSecretKeys(shieldedSecretKeys);
const unshieldedWallet = UnshieldedWallet({
networkId: getNetworkId(),
indexerClientConnection: { indexerHttpUrl: indexer, indexerWsUrl: indexerWS },
txHistoryStorage: new InMemoryTransactionHistoryStorage(),
}).startWithPublicKey(PublicKey.fromKeyStore(unshieldedKeystore));
const dustWallet = DustWallet({
networkId: getNetworkId(),
costParameters: {
additionalFeeOverhead: 300_000_000_000_000n,
feeBlocksMargin: 5,
},
indexerClientConnection: { indexerHttpUrl: indexer, indexerWsUrl: indexerWS },
provingServerUrl: new URL(proofServer),
relayURL: new URL(node.replace(/^http/, 'ws')),
}).startWithSecretKey(dustSecretKey, ledger.LedgerParameters.initialParameters().dust);
const wallet = new WalletFacade(shieldedWallet, unshieldedWallet, dustWallet);
await wallet.start(shieldedSecretKeys, dustSecretKey);
Wallet Sync (RxJS pattern — always used with WalletFacade)
import * as Rx from 'rxjs';
// Wait until wallet is fully synced
const syncedState = await Rx.firstValueFrom(
wallet.state().pipe(
Rx.throttleTime(5_000),
Rx.filter((state) => state.isSynced),
),
);
// Wait until wallet has non-zero unshielded balance
import { unshieldedToken } from '@midnight-ntwrk/ledger-v8';
const balance = await Rx.firstValueFrom(
wallet.state().pipe(
Rx.throttleTime(10_000),
Rx.filter((s) => s.isSynced),
Rx.map((s) => s.unshielded.balances[unshieldedToken().raw] ?? 0n),
Rx.filter((balance) => balance > 0n),
),
);
Node.js WebSocket requirement:
import { WebSocket } from 'ws';
// Required for GraphQL subscriptions (wallet sync) to work in Node.js
globalThis.WebSocket = WebSocket as unknown as typeof globalThis.WebSocket;
// Put this at the very top of your entry file, before any wallet imports
6) DUST Generation (Preprod / Undeployed Only)
NIGHT tokens generate DUST over time, but only after UTXOs are explicitly registered on-chain. DUST is the non-transferable fee resource for all transactions.
// 1. Wait for sync
const state = await Rx.firstValueFrom(wallet.state().pipe(Rx.filter((s) => s.isSynced)));
// 2. Check if DUST already available
if (state.dust.availableCoins.length > 0) {
console.log('DUST already available:', state.dust.walletBalance(new Date()));
return;
}
// 3. Register unregistered NIGHT UTXOs
const nightUtxos = state.unshielded.availableCoins.filter(
(coin: any) => coin.meta?.registeredForDustGeneration !== true,
);
if (nightUtxos.length > 0) {
const recipe = await wallet.registerNightUtxosForDustGeneration(
nightUtxos,
unshieldedKeystore.getPublicKey(),
(payload) => unshieldedKeystore.signData(payload),
);
const finalized = await wallet.finalizeRecipe(recipe);
await wallet.submitTransaction(finalized);
}
// 4. Wait for DUST balance > 0 (may take a few minutes)
await Rx.firstValueFrom(
wallet.state().pipe(
Rx.throttleTime(5_000),
Rx.filter((s) => s.isSynced),
Rx.filter((s) => s.dust.walletBalance(new Date()) > 0n),
),
);
DUST troubleshooting:
- DUST balance drops to 0 after failed deploy → restart DApp to release locked DUST coins
pendingCoins > 0 && availableCoins === 0→ DUST locked by a pending/failed transaction- NIGHT registered but DUST = 0 → still accruing, wait a few minutes
7) Provider Configuration
Node.js (self-hosted proof server)
import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client-proof-provider';
import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider';
import { levelPrivateStateProvider } from '@midnight-ntwrk/midnight-js-level-private-state-provider';
const zkConfigProvider = new NodeZkConfigProvider<CounterCircuits>(zkConfigPath);
const providers: CounterProviders = {
privateStateProvider: levelPrivateStateProvider<CounterPrivateStateId>({
privateStateStoreName: 'counter-private-state', // LevelDB store name
walletProvider: walletAndMidnightProvider,
}),
publicDataProvider: indexerPublicDataProvider(indexerHttp, indexerWs),
zkConfigProvider,
proofProvider: httpClientProofProvider(proofServerUrl, zkConfigProvider),
walletProvider: walletAndMidnightProvider,
midnightProvider: walletAndMidnightProvider,
};
Browser (1AM wallet / CDN ZK assets)
import { FetchZkConfigProvider } from '@midnight-ntwrk/midnight-js-fetch-zk-config-provider';
import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
import { createProofProvider } from '@midnight-ntwrk/midnight-js-types';
const config = await connectedAPI.getConfiguration();
setNetworkId(config.networkId);
const zkConfigProvider = new FetchZkConfigProvider(
new URL(zkAssetBasePath, window.location.origin).toString(),
window.fetch.bind(window),
);
const provingProvider = await connectedAPI.getProvingProvider(zkConfigProvider);
const providers = {
publicDataProvider: indexerPublicDataProvider(config.indexerUri, config.indexerWsUri),
zkConfigProvider,
proofProvider: createProofProvider(provingProvider), // wraps 1AM proving provider
walletProvider: { /* see 1AM skill */ },
midnightProvider: { /* see 1AM skill */ },
privateStateProvider: createPrivateStateProvider(), // in-memory for browser
};
8) WalletProvider & MidnightProvider (Node.js headless)
This is the bridge between WalletFacade and the midnight-js contracts API:
import type { WalletProvider, MidnightProvider } from '@midnight-ntwrk/midnight-js-types';
import * as ledger from '@midnight-ntwrk/ledger-v8';
const state = await Rx.firstValueFrom(wallet.state().pipe(Rx.filter((s) => s.isSynced)));
const walletAndMidnightProvider: WalletProvider & MidnightProvider = {
getCoinPublicKey() {
return state.shielded.coinPublicKey.toHexString();
},
getEncryptionPublicKey() {
return state.shielded.encryptionPublicKey.toHexString();
},
async balanceTx(tx, ttl?) {
const recipe = await wallet.balanceUnboundTransaction(
tx,
{ shieldedSecretKeys, dustSecretKey },
{ ttl: ttl ?? new Date(Date.now() + 30 * 60 * 1000) },
);
// ⚠️ KNOWN BUG WORKAROUND: wallet SDK signRecipe hardcodes 'pre-proof' but
// proven (UnboundTransaction) intents contain 'proof' data → "Failed to clone intent"
// Sign intents manually with the correct proof marker (see signTransactionIntents below)
signTransactionIntents(recipe.baseTransaction, signFn, 'proof');
if (recipe.balancingTransaction) {
signTransactionIntents(recipe.balancingTransaction, signFn, 'pre-proof');
}
return wallet.finalizeRecipe(recipe);
},
submitTx(tx) {
return wallet.submitTransaction(tx) as any;
},
};
The "Failed to clone intent" Bug Fix
/**
* Workaround for wallet SDK bug where signRecipe hardcodes 'pre-proof',
* causing failures when signing proven (UnboundTransaction) intents.
* Call with proofMarker='proof' for baseTransaction, 'pre-proof' for balancingTransaction.
*/
const 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 signature = signFn(cloned.signatureData(segment));
if (cloned.fallibleUnshieldedOffer) {
const sigs = cloned.fallibleUnshieldedOffer.inputs.map(
(_: ledger.UtxoSpend, i: number) =>
cloned.fallibleUnshieldedOffer!.signatures.at(i) ?? signature,
);
cloned.fallibleUnshieldedOffer = cloned.fallibleUnshieldedOffer.addSignatures(sigs);
}
if (cloned.guaranteedUnshieldedOffer) {
const sigs = cloned.guaranteedUnshieldedOffer.inputs.map(
(_: ledger.UtxoSpend, i: number) =>
cloned.guaranteedUnshieldedOffer!.signatures.at(i) ?? signature,
);
cloned.guaranteedUnshieldedOffer = cloned.guaranteedUnshieldedOffer.addSignatures(sigs);
}
tx.intents.set(segment, cloned);
}
};
9) Deploy & Call Contracts
Deploy
import { deployContract } from '@midnight-ntwrk/midnight-js-contracts';
const deployed = await deployContract(providers, {
compiledContract,
privateStateId: CounterPrivateStateId,
initialPrivateState: { privateCounter: 0 },
});
console.log('Contract address:', deployed.deployTxData.public.contractAddress);
Call a Circuit
// Using callTx (recommended — automatically builds, proves, balances, submits)
const result = await deployed.callTx.increment();
console.log('txId:', result.public.txId);
console.log('blockHeight:', result.public.blockHeight);
Join an Existing Contract
import { findDeployedContract } from '@midnight-ntwrk/midnight-js-contracts';
const contract = await findDeployedContract(providers, {
contractAddress: '09dbe05f...', // hex string
compiledContract,
privateStateId: CounterPrivateStateId,
initialPrivateState: { privateCounter: 0 },
});
// Now call circuits on it
await contract.callTx.increment();
Read Ledger State
import { ContractState } from '@midnight-ntwrk/compact-runtime';
import { Counter } from './managed/counter';
const contractState = await providers.publicDataProvider.queryContractState(contractAddress);
if (contractState === null) {
console.log('Contract not found');
} else {
// Apply generated ledger() function to deserialize typed state
const ledgerState = Counter.ledger(contractState.data);
console.log('Counter value:', ledgerState.round);
}
10) Private State Provider (In-Memory — Browser / Minimal)
import type {
PrivateStateProvider, PrivateStateId, PrivateStateExport,
SigningKeyExport,
} from '@midnight-ntwrk/midnight-js-types';
function createPrivateStateProvider() {
let scope = '';
const stateStore = new Map<string, unknown>();
const signingKeyStore = new Map<string, unknown>();
const key = (id: string) => `${scope}:${id}`;
return {
setContractAddress(address: string) { scope = address; },
async set(id: string, state: unknown) { stateStore.set(key(id), state); },
async get(id: string) { return stateStore.get(key(id)) ?? null; },
async remove(id: string) { stateStore.delete(key(id)); },
async clear() { stateStore.clear(); },
async setSigningKey(addr: string, k: unknown) { signingKeyStore.set(addr, k); },
async getSigningKey(addr: string) { return signingKeyStore.get(addr) ?? null; },
async removeSigningKey(addr: string) { signingKeyStore.delete(addr); },
async clearSigningKeys() { signingKeyStore.clear(); },
async exportPrivateStates(): Promise<PrivateStateExport> { throw new Error('Not implemented'); },
async importPrivateStates() { throw new Error('Not implemented'); },
async exportSigningKeys(): Promise<SigningKeyExport> { throw new Error('Not implemented'); },
async importSigningKeys() { throw new Error('Not implemented'); },
};
}
For persistent Node.js private state, use levelPrivateStateProvider instead.
11) Testkit (Integration Testing)
import { getTestEnvironment } from '@midnight-ntwrk/testkit-js';
import pino from 'pino';
const logger = pino({ level: 'info' });
let testEnvironment: Awaited<ReturnType<typeof getTestEnvironment>>;
beforeAll(async () => {
testEnvironment = getTestEnvironment(logger);
const environmentConfig = await testEnvironment.start();
// environmentConfig contains indexer, node, proofServer URLs
});
afterAll(async () => {
await testEnvironment.shutdown();
});
// Get wallet providers for testing
const walletProvider = await testEnvironment.getMidnightWalletProvider();
// or multiple wallets (max 4 on local):
const [wallet1, wallet2] = await testEnvironment.startMidnightWalletProviders(2);
Environment selection via env var:
# Local Docker (default)
MN_TEST_ENVIRONMENT=undeployed yarn test
# Against preprod
MN_TEST_ENVIRONMENT=devnet yarn test
# Custom endpoints
MN_TEST_ENVIRONMENT=env-var-remote \
MN_TEST_NETWORK_ID=undeployed \
MN_TEST_INDEXER=http://localhost:8088/api/ \
MN_TEST_INDEXER_WS=ws://localhost:8088/ws/ \
MN_TEST_NODE=http://localhost:9944 \
yarn test
12) Proof Server
For Node.js / headless use, run the proof server locally via Docker:
# Auto-started proof server (preprod, pulls image automatically)
cd counter-cli && npm run preprod-ps
# Or manually
docker compose -f proof-server.yml up
# Wait for: "starting service... listening on: 0.0.0.0:6300"
# Direct Docker run
docker run -p 6300:6300 midnightntwrk/proof-server:latest midnight-proof-server -v
Proof server URL: http://127.0.0.1:6300
Mac ARM (Apple Silicon) fix: If the proof server hangs, go to Docker Desktop → Settings → General → Virtual Machine Options → select Docker VMM → restart Docker.
13) Common Pitfalls
setNetworkId not called → cryptic type mismatch errors deep in SDK. Call it immediately at startup, before any SDK operations.
WebSocket not set globally in Node.js → wallet sync GraphQL subscriptions silently fail. Put globalThis.WebSocket = WebSocket at the top of your entry file.
ZK assets not found → double-check zkConfigPath is absolute, points to the compiled managed folder, and contains keys/ and zkir/ subdirectories. Run the sync:assets npm script before dev.
"Failed to clone intent" → wallet SDK bug, use the signTransactionIntents workaround. Occurs when a proven UnboundTransaction is balanced — the SDK hardcodes 'pre-proof' but the intents contain 'proof' data.
DUST = 0 after failed deploy → known wallet SDK issue. Restart the DApp to release the locked DUST coins. Use the DUST monitor to verify status before deploying.
Wallet 0 balance after faucet → wait for sync to complete first. If still 0, verify you used the unshielded address (mn_addr_preprod1...), not the shielded address.
relayURL must be WebSocket → convert https:// → wss://, http:// → ws://. The wallet SDK expects a WebSocket URL for the relay.
CompiledContract.make called on every deploy → call it once at module load, not inside the deploy function. Loading ZK keys on every call is slow.
levelPrivateStateProvider in browser → LevelDB is Node.js only. Use the in-memory createPrivateStateProvider for browser contexts.
Old contract address after recompile → verifier keys change when the Compact contract changes. Old addresses will fail proof verification. Always redeploy after any contract change.
14) Dependency Versions (Pinned — Official Counter Example)
{
"@midnight-ntwrk/compact-runtime": "0.15.0",
"@midnight-ntwrk/ledger-v8": "8.0.3",
"@midnight-ntwrk/midnight-js-contracts": "4.0.2",
"@midnight-ntwrk/midnight-js-http-client-proof-provider": "4.0.2",
"@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.0.2",
"@midnight-ntwrk/midnight-js-level-private-state-provider": "4.0.2",
"@midnight-ntwrk/midnight-js-network-id": "4.0.2",
"@midnight-ntwrk/midnight-js-node-zk-config-provider": "4.0.2",
"@midnight-ntwrk/midnight-js-types": "4.0.2",
"@midnight-ntwrk/midnight-js-utils": "4.0.2",
"@midnight-ntwrk/wallet-sdk-address-format": "3.1.0",
"@midnight-ntwrk/wallet-sdk-dust-wallet": "3.0.0",
"@midnight-ntwrk/wallet-sdk-facade": "3.0.0",
"@midnight-ntwrk/wallet-sdk-hd": "3.0.1",
"@midnight-ntwrk/wallet-sdk-shielded": "2.1.0",
"@midnight-ntwrk/wallet-sdk-unshielded-wallet": "2.1.0",
"@midnight-ntwrk/compact-js": "2.5.0"
}
Node.js version: 22+ (required)
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/kali-decoder/midnight-skills/midnight-js">View midnight-js on skillZs</a>