Midnight is a privacy-focused chain. Unlike most chains, a functional Midnight
wallet is not a single address — it exposes three distinct surfaces:
| Surface | What it is | Where Dynamic exposes it |
|---|
| Unshielded | Public address/state. The wallet’s main address. | wallet.address |
| Shielded | Private (shielded) address/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 — they are
different token types in different pools, not the same balance shown twice.
This page covers Midnight wallets connected through an injected browser
extension (the 1am wallet). For embedded Midnight wallets created via
social or email login, see Using Midnight embedded wallets.
Enabling Midnight
Enable Midnight in the dashboard
Install the connector
npm i @dynamic-labs/midnight
Add the connector to DynamicContextProvider
import { DynamicContextProvider, DynamicWidget } from '@dynamic-labs/sdk-react-core';
import { MidnightWalletConnectors } from '@dynamic-labs/midnight';
const App = () => (
<DynamicContextProvider
settings={{
environmentId: 'YOUR_ENVIRONMENT_ID',
walletConnectors: [MidnightWalletConnectors],
}}
>
<DynamicWidget />
</DynamicContextProvider>
);
export default App;
Checking if a Wallet is a Midnight Wallet
import { isMidnightWallet } from '@dynamic-labs/midnight';
if (!isMidnightWallet(wallet)) {
throw new Error('This wallet is not a Midnight wallet');
}
Retrieving the Address Surfaces
The unshielded address is the wallet’s main address. The shielded and DUST
addresses are exposed through additionalAddresses, keyed by WalletAddressType.
import { useDynamicContext } from '@dynamic-labs/sdk-react-core';
import { isMidnightWallet } from '@dynamic-labs/midnight';
import { WalletAddressType } from '@dynamic-labs/sdk-api-core';
const { primaryWallet } = useDynamicContext();
if (!primaryWallet || !isMidnightWallet(primaryWallet)) {
throw new Error('This wallet is not a Midnight wallet');
}
// Unshielded — the main address
const unshieldedAddress = primaryWallet.address;
// Shielded and DUST — from additionalAddresses
const shieldedAddress = primaryWallet.additionalAddresses?.find(
(a) => a.type === WalletAddressType.MidnightShielded,
)?.address;
const dustAddress = primaryWallet.additionalAddresses?.find(
(a) => a.type === WalletAddressType.MidnightDust,
)?.address;
Deposits go to either the unshielded or shielded address depending on
which pool the sender is paying into — surface both in your deposit UI. DUST is
generated, not deposited to.
If you need the cryptographic public keys for the shielded surface (coin and
encryption keys required by some dApps), read them from the connector:
const { shieldedCoinPublicKey, shieldedEncryptionPublicKey } =
await primaryWallet.connector.getShieldedAddresses();
Reading Balances
getFormattedBalances() returns display-ready strings for all three surfaces in
one call:
const { unshieldedBalance, shieldedTokenCount, dustBalance } =
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
// dustBalance -> { balance: string, cap: string } (undefined if no DUST)
Individual getters are also available:
await primaryWallet.getShieldedBalance(); // shielded NIGHT
await primaryWallet.getUnshieldedBalance(); // unshielded NIGHT
await primaryWallet.getDustBalance(); // { balance, cap }
For raw, per-token amounts (a pool can hold more than just NIGHT), use
getBalances(), which returns { shielded, unshielded, dust } keyed by token
type:
const { shielded, unshielded, dust } = await primaryWallet.getBalances();
Sending
sendBalance routes to the correct pool automatically based on the recipient
address prefix (mn_shield... = shielded, otherwise unshielded). Cross-pool
transfers are not supported — sender and recipient must be in the same pool.
await primaryWallet.sendBalance({
toAddress: 'mn_shield...', // shielded recipient
amount: '1.5', // human-readable NIGHT
});
Ownership Boundaries
When integrating, it helps to know who owns what:
- Dynamic — connection lifecycle, the
MidnightWalletConnectors, the
MidnightWallet object, address/balance accessors, and send routing.
- 1am wallet (extension) — key custody, address derivation, proving, message
signing, and transaction submission. Dynamic talks to it through the official
@midnight-ntwrk/dapp-connector-api.
- Your app — the deposit/receive UX (surface unshielded and shielded),
which pool to display, and how you present DUST generation.
Resources