Skip to main content
Embedded Midnight wallets are MPC wallets created from a user’s social or email login — no browser extension required. Like all Midnight wallets, they expose three surfaces:
SurfaceWhat it isWhere Dynamic exposes it
UnshieldedPublic address and state. The wallet’s main address.wallet.address
ShieldedPrivate address and state. A separate token pool.wallet.additionalAddresses (midnight_shielded)
DUSTFee-generation state. DUST pays for Midnight transactions.wallet.additionalAddresses (midnight_dust)
NIGHT exists as both a shielded and an unshielded asset — different token types in different pools, not the same balance shown twice.
This page covers embedded Midnight wallets (created via social/email login). For wallets connected through the injected 1am browser extension, see Using Midnight wallets.

Enabling Midnight

1

Enable Midnight in the dashboard

Enable Midnight under Chains & Networks.
2

Enable Private Key Exports in the dashboard

Midnight embedded wallets internally derive signing keys via the key export mechanism. Private Key Exports must be enabled in your environment settings or all wallet operations (balance reads, signing, transfers) will fail with a 403 error.Go to Embedded Wallets → Security in the Dynamic dashboard and toggle Private Key Exports on.
Without this setting enabled, Midnight wallets cannot initialise — calls to getFormattedBalances(), signMessage(), and all other wallet methods will fail.
3

Install the connector

npm i @dynamic-labs/midnight
4

Add the embedded connector to DynamicContextProvider

Use DynamicWaasMidnightConnectors for embedded (MPC) wallets.
React
import { DynamicContextProvider, DynamicWidget } from '@dynamic-labs/sdk-react-core';
import { DynamicWaasMidnightConnectors } from '@dynamic-labs/midnight';

const App = () => (
  <DynamicContextProvider
    settings={{
      environmentId: 'YOUR_ENVIRONMENT_ID',
      walletConnectors: [DynamicWaasMidnightConnectors],
    }}
  >
    <DynamicWidget />
  </DynamicContextProvider>
);

export default App;

Getting a Midnight wallet

Narrow a wallet with isMidnightWallet before calling Midnight methods. The guard both confirms the chain and gives you the typed wallet — all operations below are called directly on it.
React
import { useDynamicContext } from '@dynamic-labs/sdk-react-core';
import { isMidnightWallet } from '@dynamic-labs/midnight';

const { primaryWallet } = useDynamicContext();

if (!primaryWallet || !isMidnightWallet(primaryWallet)) {
  throw new Error('This wallet is not a Midnight wallet');
}

// `primaryWallet` is now a typed Midnight wallet.

Reading balances

getFormattedBalances() returns display-ready strings for all three surfaces in one call:
React
const { unshieldedBalance, shieldedTokenCount, dustBalance, dustSyncing } =
  await primaryWallet.getFormattedBalances();

// unshieldedBalance  -> unshielded NIGHT as a display string, e.g. "3.0" (undefined if none)
// shieldedTokenCount -> number of distinct token types with a positive shielded balance
//                       (shielded NIGHT is a separate token type from unshielded NIGHT)
// dustBalance        -> { balance: string, cap: string } (undefined if no DUST)
// dustSyncing        -> true while DUST is still syncing in the background (WaaS only)
Poll dustSyncing to know when all surfaces are ready:
React
async function waitForSync() {
  const { dustSyncing } = await primaryWallet.getFormattedBalances();
  if (dustSyncing) {
    setTimeout(waitForSync, 2500); // retry until ready
  }
}
waitForSync();
Individual getters are also available:
React
const shielded   = await primaryWallet.getShieldedBalance();   // string | undefined
const unshielded = await primaryWallet.getUnshieldedBalance(); // string | undefined
const dust       = await primaryWallet.getDustBalance();       // { balance: string, cap: string }
For raw, per-token amounts (a pool can hold more than just NIGHT), use getBalances(), which returns { shielded, unshielded, dust } keyed by token type, with bigint values:
React
const { shielded, unshielded, dust } = await primaryWallet.getBalances();
The first balance read on a device runs a one-time background sync (the DUST state can take a while to fold). Subsequent reads resume from a local checkpoint and return quickly.

Signing a message

React
const signature = await primaryWallet.signMessage('Hello Midnight');

Sending tokens

Embedded Midnight wallets support both unshielded (public) and shielded (private) transfers for NIGHT and other tokens. Amounts are in the token’s smallest (atomic) unit unless token.decimals is set.

Simple send

sendBalance builds, proves, and broadcasts in one call, and routes to the right pool automatically based on the recipient prefix (mn_shield… → shielded, otherwise unshielded). It returns the transaction hash.
React
// Unshielded NIGHT transfer
const txHash = await primaryWallet.sendBalance({
  toAddress: 'mn_addr...',
  amount: '100000000', // atomic units
});

// Shielded NIGHT transfer — same call, shielded recipient
await primaryWallet.sendBalance({
  toAddress: 'mn_shield...',
  amount: '100000000',
});

// Shielded transfer of a non-NIGHT token
await primaryWallet.sendBalance({
  toAddress: 'mn_shield...',
  amount: '50',
  token: {
    address: 'tokenTypeHex', // token type identifier
    decimals: 6,             // optional, defaults to 0
  },
});
sendBalance routes based on the recipient address prefix: mn_addr draws from the unshielded pool; mn_shield draws from the shielded pool. Cross-pool transfers (unshielded → shielded or vice versa) are not supported.The optional token parameter lets you send tokens other than native NIGHT. Pass token.address with the token type identifier and token.decimals if the amount needs decimal conversion. Omit token entirely for NIGHT transfers.

Step-by-step send

For more control, run the three steps yourself — build → sign → submit. This lets you inspect or persist the serialized transaction between steps. Set type to 'unshielded' or 'shielded' per transfer.
React
// 1. Build an unsigned transaction.
const { serializedTransaction } = await primaryWallet.createTransferTransaction({
  transfers: [
    {
      type: 'unshielded',       // or 'shielded'
      recipientAddress: 'mn_addr...',
      amount: '100000000',      // atomic units
      tokenType: 'tokenTypeHex', // optional — omit for native NIGHT
    },
  ],
});

// 2. Sign + ZK-prove (unshielded segments are MPC-signed; shielded/dust are proven).
const finalizedTransaction =
  await primaryWallet.signTransaction(serializedTransaction);

// 3. Broadcast.
const { txHash } = await primaryWallet.submitTransaction(finalizedTransaction);
Transactions are paid for in DUST, generated by registered unshielded NIGHT. If a wallet has no DUST, register it first (see below).

Registering for DUST

DUST is the resource that pays for Midnight transactions. It is generated over time by unshielded NIGHT that has been registered for generation. Call registerDust() once to designate the wallet’s NIGHT for DUST generation.
React
const result = await primaryWallet.registerDust();
// result.status -> 'registered' | 'already_registered' | 'already_has_dust' | 'no_utxos'
Fund the wallet with unshielded NIGHT before registering — DUST is generated from NIGHT UTxOs, so a wallet with no NIGHT has nothing to register.

Recovering reserved UTxOs

If a transaction is built or signed but never submitted, its inputs stay reserved. These methods release them:
React
// Release the inputs of a specific built/signed transaction.
await primaryWallet.revertTransaction(serializedTransaction);

// Release every finalized-but-unsubmitted transaction the wallet knows about.
await primaryWallet.revertAllPending();
resetWalletCache() is a last resort. It clears the wallet’s persisted state and forces a full cold re-sync on the next operation. Use it only when inputs stay stuck after revertTransaction and revertAllPending.
React
await primaryWallet.resetWalletCache();

Resources

Last modified on June 29, 2026