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:
| Surface | What it is | Where Dynamic exposes it |
|---|
| Unshielded | Public address and state. The wallet’s main address. | wallet.address |
| Shielded | Private address and state. A separate token pool. | wallet.additionalAddresses (midnight_shielded) |
| DUST | Fee-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
Enable Midnight in the dashboard
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.
Install the connector
npm i @dynamic-labs/midnight
Add the embedded connector to DynamicContextProvider
Use DynamicWaasMidnightConnectors for embedded (MPC) wallets.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.
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:
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:
async function waitForSync() {
const { dustSyncing } = await primaryWallet.getFormattedBalances();
if (dustSyncing) {
setTimeout(waitForSync, 2500); // retry until ready
}
}
waitForSync();
Individual getters are also available:
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:
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
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.
// 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.
// 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.
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:
// 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.await primaryWallet.resetWalletCache();
Resources