midnight-wallet — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited midnight-wallet (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
The Midnight Wallet SDK provides a comprehensive set of tools for managing wallets on the Midnight Network. It includes support for shielded private transactions, unshielded public transactions, DUST token management, and hierarchical deterministic key derivation. The SDK is designed to work both in browser environments through wallet extensions and in Node.js environments through programmatic wallet creation.
The core entry point for wallet operations is the WalletFacade which unifies the various wallet types into a single interface. Underneath the facade the SDK implements several specialized wallet types each handling a different aspect of token management and transaction flow.
Wallets on Midnight manage three types of tokens and coins. Unshielded NIGHT tokens exist in public UTXOs visible on the ledger. Shielded coins exist in encrypted form known only to the wallet owner. DUST tokens are a special resource used to pay for transaction fees and must be generated before they can be spent.
The Wallet SDK integrates with the DApp Connector API for browser based DApps and provides standalone programmatic interfaces for server side and CLI applications.
The ShieldedWallet manages private transactions on the Midnight Network. It handles the creation and spending of shielded coins using zero knowledge proofs through the Zswap protocol. Shielded coins obscure the transaction amount and participants from public view.
Shielded operations include creating shielded coins from unshielded tokens through the shield operation. Spend operations consume existing shielded coins and produce new shielded coins or unshielded tokens as output. The wallet maintains a local state containing the user's shielded coin set that is synchronized with the Zswap protocol state.
class ShieldedWallet<S extends ShieldedCoinInfo> {
async createShieldedCoin(amount: bigint): Promise<ShieldedCoinInfo>
async spendShieldedCoin(coin: ShieldedCoinInfo): Promise<Transaction>
async getShieldedBalances(): Promise<ShieldedBalance[]>
async getShieldedCoins(): Promise<ShieldedCoinInfo[]>
}The UnshieldedWallet manages public token operations on the Midnight Network. It handles sending unshielded NIGHT tokens between addresses and managing the UTXO set. Unshielded transactions are visible on the public ledger and can be queried through the indexer.
class UnshieldedWallet {
async sendUnshieldedTokens(to: string, amount: bigint): Promise<Transaction>
async getUnshieldedBalances(): Promise<UnshieldedBalance[]>
async getUnshieldedUtxos(): Promise<Utxo[]>
}The DustWallet manages DUST tokens which are a separate resource used to pay transaction fees on the Midnight Network. Unlike unshielded and shielded tokens DUST cannot be transferred between users. It is generated by the network and consumed during transaction submission.
class DustWallet {
async registerDust(): Promise<DustRegistration>
async generateDust(amount: bigint): Promise<DustGeneration>
async spendDust(amount: bigint, target: Transaction): Promise<Transaction>
async getDustBalance(): Promise<DustBalance>
async getDustState(): Promise<DustState>
}The HDWallet implements BIP32 hierarchical deterministic key derivation for generating sequences of wallet keys from a single seed. It supports BIP340 derivation for signing keys and provides child key derivation for organizing keys by purpose.
class HDWallet {
static fromSeed(seed: Uint8Array): HDWallet
static fromMnemonic(mnemonic: string): HDWallet
deriveChild(path: DerivationPath): HDWallet
getPrivateKey(): Uint8Array
getPublicKey(): Uint8Array
}The WalletFacade composes the ShieldedWallet, UnshieldedWallet, DustWallet, and HDWallet into a single unified interface. It provides convenience methods that coordinate operations across multiple wallet types.
class WalletFacade {
readonly shieldedWallet: ShieldedWallet<S>;
readonly unshieldedWallet: UnshieldedWallet;
readonly dustWallet: DustWallet;
readonly hdWallet: HDWallet;
}Wallets can be created from BIP39 mnemonic seed phrases. The seed phrase is used to derive the master key which in turn derives the keys for shielded, unshielded, and DUST operations.
const wallet = await WalletFacade.fromMnemonic(
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
);For new wallets the SDK can generate a random mnemonic and create the corresponding wallet. This is the standard approach for creating new user wallets.
const randomWallet = await WalletFacade.generateRandom();
const mnemonic = randomWallet.getMnemonic();Wallets can also be created from existing key material such as private keys or extended keys. This is useful for importing wallets from other systems or restoring from a raw key backup.
const wallet = await WalletFacade.fromPrivateKey(privateKeyBytes);The FluentWalletBuilder provides a builder pattern for constructing wallets with specific configurations. It is especially useful in test environments where deterministic wallets are needed.
const wallet = await new FluentWalletBuilder()
.withNetworkId(NetworkId.Preprod)
.withMnemonic(testMnemonic)
.build();The WalletFactory provides factory methods for creating specific wallet types without going through the full facade. This is useful when only a subset of wallet functionality is needed.
const shieldedWallet = await WalletFactory.createShieldedWallet({
networkId: NetworkId.Preprod,
privateKey: signingKey
});
const unshieldedWallet = await WalletFactory.createUnshieldedWallet({
networkId: NetworkId.Preprod,
privateKey: signingKey
});The WalletSaveStateProvider handles persistence of wallet state including shielded coin information, DUST state, and key material. It serializes the wallet state to a format suitable for storage and handles deserialization for state restoration.
class WalletSaveStateProvider {
async saveState(wallet: WalletFacade): Promise<SerializedWalletState>
async restoreState(
serializedState: SerializedWalletState,
providers: MidnightProviders
): Promise<WalletFacade>
}Wallet state is serialized to a structured format that includes all shielded coins, their associated metadata, DUST registration state, and wallet keys. The serialized format is encrypted before storage.
interface SerializedWalletState {
readonly shieldedCoins: SerializedShieldedCoin[];
readonly dustState: SerializedDustState;
readonly keys: SerializedKeys;
readonly version: number;
}State restoration reconstructs a wallet from serialized state. This involves decrypting the state, deserializing the wallet structure, and reconnecting to the network to verify on chain state.
const serializedState = await fs.readFile('wallet-state.json', 'utf-8');
const wallet = await walletSaveStateProvider.restoreState(
JSON.parse(serializedState),
providers
);Wallet state files contain the serialized wallet state and should be stored securely. They are typically encrypted with a user provided password to protect against unauthorized access.
State files should be backed up regularly since they contain the user's shielded coins. Losing the state file means losing access to shielded coins since their information is not stored on chain.
Sending unshielded tokens transfers publicly visible NIGHT tokens from one address to another. The transaction creates output UTXOs for the recipient and returns any change to the sender.
const tx = await wallet.unshieldedWallet.sendUnshieldedTokens(
recipientAddress,
amount
);
await wallet.providers.waitForTxFinalization(tx.hash);Unshielded balances are queried from the indexer which tracks all unspent transaction outputs. The balance includes all UTXOs belonging to the wallet address.
const balances = await wallet.unshieldedWallet.getUnshieldedBalances();
for (const balance of balances) {
console.log(`Balance: ${balance.amount} NIGHT`);
}UTXOs are the building blocks of unshielded transactions. The wallet manages its UTXO set by tracking which outputs have been spent and which are still available. UTXO selection algorithms choose which outputs to spend for a given transaction.
const utxos = await wallet.unshieldedWallet.getUnshieldedUtxos();
const spendableUtxos = utxos.filter(utxo => !utxo.isSpent);These two methods provide fine grained access to the wallet's unshielded state. getUnshieldedBalances returns aggregated balance information while getUnshieldedUtxos returns the individual UTXOs.
const balances = await wallet.unshieldedWallet.getUnshieldedBalances();
const utxos = await wallet.unshieldedWallet.getUnshieldedUtxos();Creating shielded coins converts public unshielded tokens into private shielded coins. The shield operation consumes UTXOs as inputs and produces shielded coins as outputs in the Zswap protocol.
const shieldedCoin = await wallet.shieldedWallet.createShieldedCoin(amount);Spending shielded coins converts them back to unshielded tokens or creates new shielded coins with different owners. The spend operation uses zero knowledge proofs to prove ownership without revealing the coin details.
const outputCoins = await wallet.shieldedWallet.spendShieldedCoin(
shieldedCoin,
recipientAddress
);The ZswapLocalState maintains the wallet's local view of the shielded coin pool. It tracks which coins have been created and spent and synchronizes with the Zswap protocol state on chain.
class ZswapLocalState<S extends ShieldedCoinInfo> {
async addCoin(coin: ShieldedCoinInfo): Promise<void>
async removeCoin(coinId: string): Promise<void>
async getCoins(): Promise<ShieldedCoinInfo[]>
async sync(): Promise<void>
}ShieldedCoinInfo contains the metadata for a shielded coin including its value commitment, nullifier, and encryption details. This information is stored locally and never revealed on chain.
QualifiedShieldedCoinInfo extends ShieldedCoinInfo with additional verification data that proves the coin is valid and properly formed. This is used when spending coins to demonstrate that they were correctly created.
Before generating DUST a wallet must register for DUST generation. Registration associates the wallet with a DUST generation state and proves that the wallet is eligible to receive DUST.
const registration = await wallet.dustWallet.registerDust();DUST generation creates new DUST tokens for the wallet. This is an on chain operation that involves proving eligibility through the DUST protocol and receiving newly generated DUST tokens.
const generation = await wallet.dustWallet.generateDust(requestedAmount);DUST is spent automatically during transaction submission as fee payment. However the wallet provides manual DUST spend operations for advanced use cases where DUST must be applied to specific transactions.
const tx = await wallet.dustWallet.spendDust(dustAmount, targetTransaction);DustActions is an enum defining the possible operations on DUST state. It includes registration, generation, and spend actions each with their own state transition requirements.
enum DustActions {
Register = 'register',
Generate = 'generate',
Spend = 'spend'
}DustLocalState maintains the wallet's local view of its DUST balance and generation history. DustState represents the on chain DUST state for a wallet address.
interface DustLocalState {
readonly balance: bigint;
readonly generationHistory: DustGeneration[];
readonly registrationStatus: 'unregistered' | 'registered' | 'pending';
}
interface DustState {
readonly totalGenerated: bigint;
readonly totalSpent: bigint;
readonly lastGeneration: number;
}DustParameters configure the DUST generation behavior including the maximum generation amount per epoch and the generation rate. These parameters are determined by the network and may change through governance.
DustGenerationState tracks the progress of an ongoing DUST generation operation. It includes the amount requested, the amount generated so far, and the remaining amount to generate.
On the preprod testnet DUST can be generated programmatically through the faucet. The preprod faucet provides small amounts of DUST for testing purposes.
const faucetResponse = await fetch('https://faucet.preprod.midnight.network/dust', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address: walletAddress })
});The faucet client is a convenience wrapper around the faucet HTTP API. It handles request formatting and response parsing for DUST requests.
class FaucetClient {
async requestDust(address: string, amount: bigint): Promise<FaucetResponse>
async checkStatus(requestId: string): Promise<FaucetStatus>
}For programmatic DUST generation in test environments the dust generator provides a scriptable interface that can be integrated into test suites and CI pipelines.
async function generateTestDust(wallet: WalletFacade, amount: bigint): Promise<void> {
const registration = await wallet.dustWallet.registerDust();
const generation = await wallet.dustWallet.generateDust(amount);
}Signing keys are used to authorize transactions and prove ownership of wallet addresses. They are derived from the wallet's HD tree using BIP340 derivation paths.
const signingKey = wallet.hdWallet.deriveChild(DerivationPath.signing()).getPrivateKey();Coin secret keys are used for shielded coin operations. They enable the wallet to create and spend shielded coins without revealing the coin details.
const coinSecretKey = wallet.hdWallet.deriveChild(DerivationPath.coinSecret()).getPrivateKey();Encryption secret keys protect the privacy of shielded coin metadata stored locally. They encrypt the local state so that even if the state file is compromised the coin details remain hidden.
const encryptionKey = wallet.hdWallet.deriveChild(DerivationPath.encryption()).getPrivateKey();BIP340 defines the derivation scheme for Schnorr signature keys used in Midnight. The derivation path follows the BIP44 structure with Midnight specific coin types.
const bip340Key = wallet.hdWallet.deriveBip340('m/44'/0'/0'/0/0');Encryption public keys are used by other parties to encrypt data that only the wallet owner can decrypt. These keys are safe to share publicly.
const encryptionPublicKey = await wallet.keyMaterialProvider.getEncryptionPublicKey();The signData method creates a cryptographic signature over arbitrary data using the wallet's signing key. The signature can be verified by any party with the corresponding public key.
const signature = await wallet.keyMaterialProvider.signData(dataToSign);The verifySignature method checks whether a signature is valid for the given data and public key. It returns true if the signature is authentic.
const isValid = await wallet.keyMaterialProvider.verifySignature(
data,
signature,
publicKey
);The signature verifying key is the public key counterpart to the signing key. It is used by verifiers to check signatures produced by the wallet.
const verifyingKey = wallet.keyMaterialProvider.signatureVerifyingKey();This utility function extracts the Schnorr signing key from a BIP340 derivation path. It converts the HD derivation output into a signing key suitable for signature generation.
const signingKey = signingKeyFromBip340(wallet.hdWallet, DerivationPath.signing());The WalletProvider type defines the interface that all wallet implementations must satisfy. It includes methods for balance queries, token transfers, and key material access.
type WalletProvider = {
readonly getBalances: () => Promise<Balance[]>;
readonly getAddress: () => string;
readonly getNetworkId: () => NetworkId;
readonly keyMaterialProvider: KeyMaterialProvider;
};The MidnightWalletProvider is the concrete implementation of the WalletProvider interface that wraps the WalletFacade and exposes wallet operations in a provider compatible format.
class MidnightWalletProvider implements WalletProvider {
constructor(private readonly wallet: WalletFacade) {}
async getBalances(): Promise<Balance[]> {
return this.wallet.getBalances();
}
}The community wallet CLI provides command line access to wallet operations. It supports creating wallets, checking balances, sending tokens, and managing DUST.
npx @midnight-ntwrk/wallet-cli create --network preprod
npx @midnight-ntwrk/wallet-cli balance --address <address>
npx @midnight-ntwrk/wallet-cli send --to <address> --amount 100
npx @midnight-ntwrk/wallet-cli dust-generate --amount 50The wallet MCP server exposes wallet operations as MCP tools that AI agents can call. This enables AI agents to interact with the Midnight Network programmatically.
The MCP server provides tools for creating wallets, querying balances, sending transactions, and checking transaction status. Each tool maps to a corresponding WalletFacade method.
const walletMcpServer = new WalletMcpServer({
wallet,
allowedOperations: ['balance', 'send', 'dust']
});The walletConnectedAPI is obtained through the DApp Connector after successful authorization. It provides wallet operations through the browser wallet extension.
const connectedApi = await initialApi.requestAuthorization();
const walletApi = connectedApi.walletConnectedAPI;Connecting through the DApp Connector establishes a channel to the browser wallet. The connect method returns an initial API that requires authorization before it can be used for operations.
const midnight = window.midnight;
const initialApi = await midnight.connect();Disconnecting closes the channel to the browser wallet and releases any held resources. The wallet remains available for future connections.
await connectedApi.disconnect();Authorization prompts the user to approve the DApp's access to the wallet. The user can review the requested permissions and network before approving.
const connectedApi = await initialApi.requestAuthorization({
networkId: NetworkId.Preprod
});Test environments use FluentWalletBuilder to create deterministic wallets with known keys and balances. This ensures reproducible test outcomes.
const testWallet = await new FluentWalletBuilder()
.withNetworkId(NetworkId.Undeployed)
.withMnemonic(testMnemonic)
.build();The Midnight test kit provides standard test mnemonics that can be used across test suites. These mnemonics correspond to well known wallet addresses with predictable behavior.
const TEST_MNEMONIC = 'test test test test test test test test test test test junk';Wallet seeds provide an alternative to mnemonics for deterministic wallet creation. Seeds are raw byte arrays from which the HD tree is derived.
const seed = new Uint8Array(64).fill(0x01);
const wallet = await HDWallet.fromSeed(seed);Deterministic wallets produce the same keys and addresses given the same seed or mnemonic. This is essential for testing where predictable wallet behavior is needed.
Private states can be exported from a wallet for backup or transfer to another device. The export includes shielded coin information and DUST state.
const exportedState = await wallet.exportPrivateStates();
await fs.writeFile('wallet-backup.json', JSON.stringify(exportedState));Imported states restore the wallet's private state from a previously exported backup. The import must use the same encryption password used during export.
const exportedState = JSON.parse(await fs.readFile('wallet-backup.json', 'utf-8'));
await wallet.importPrivateStates(exportedState);Signing keys can be exported individually for backup or transfer. The exported keys are in a standard format that can be imported by other wallet implementations.
const exportedSigningKey = await wallet.exportSigningKey();Imported signing keys restore the wallet's signing capability from a previously exported key. The import verifies that the key is valid before accepting it.
await wallet.importSigningKey(exportedSigningKey);Wallet export formats use structured JSON with version markers and checksums. Import validation verifies the format version and checksum integrity before accepting the data.
StorageEncryption provides the encryption layer for wallet state persistence. It supports multiple encryption algorithms and key derivation functions.
class StorageEncryption {
static async encrypt(data: Uint8Array, password: string): Promise<EncryptedData>
static async decrypt(encryptedData: EncryptedData, password: string): Promise<Uint8Array>
}Password rotation changes the encryption password without requiring a full state re-export. The wallet re-encrypts all stored data with the new password.
await wallet.rotateEncryptionPassword('old-password', 'new-password');Multiple crypto backends are supported for wallet encryption including the Midnight default backend and platform specific implementations. Backends can be selected based on performance and security requirements.
Individual encrypted values can be decrypted using the wallet's encryption key. This is used for on demand access to encrypted state entries.
const value = await wallet.decryptValue(encryptedData);CAIP-372 defines the namespace and reference format for Midnight wallet addresses and chain identifiers. Wallets implement CAIP-372 for interoperability with cross chain tools and explorers.
The Midnight chain identifier follows the format midnight:<network> where network is one of mainnet, preprod, preview, or undeployed. Wallet addresses use the CAIP-10 account identifier format within this namespace.
Several community wallets support the Midnight Network providing users with different options for managing tokens.
Lace wallet provides Midnight support through its multi chain extension with built in proving and DApp Connector integration.
The 1AM wallet is purpose built for Midnight with native support for shielded transactions and DUST management.
Kuira wallet offers Midnight compatibility through its modular extension architecture supporting both shielded and unshielded operations.
Fireblocks provides institutional grade wallet infrastructure with Midnight support for enterprise custodial workflows.
Never store wallet mnemonics or private keys in plaintext. Always use encrypted storage with strong passwords. The mnemonic is the root of all wallet keys and its compromise means total loss of funds.
Use hardware security modules or secure enclaves for production wallet key storage. Development and testing should use separate wallets with no real value.
Rotate encryption passwords periodically especially if there is suspicion of compromise. Password rotation does not affect the underlying wallet keys only the encryption of the local state.
Backup wallet state files regularly since shielded coin information exists only in the local state. Without a state backup shielded coins become permanently inaccessible even though they still exist in the Zswap protocol.
Store backups in multiple secure locations. Consider using encrypted cloud storage with additional access controls. Test restore procedures periodically to ensure backups are valid.
Use strong unique passwords for wallet encryption. Consider using a password manager to generate and store passwords securely.
Never reuse wallet encryption passwords across different wallets. A compromise of one wallet should not affect others.
Always verify the network ID before performing operations. Sending tokens intended for preprod to a mainnet address results in permanent loss.
DApps should display the current network prominently to users and warn before operations on mainnet.
Implement retry logic for transient failures such as network timeouts and proof server unavailability. Use exponential backoff to avoid overwhelming services.
Handle permanent failures gracefully by presenting clear error messages to users. Log detailed error information for debugging while avoiding exposure of sensitive key material in logs.
This skill covers wallet SDK and DUST operations. For related areas see these skills.
Provide recovery paths for common error scenarios such as insufficient DUST, invalid recipient addresses, and network congestion. Educate users about error causes and remediation steps.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.