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 used to pay for shielded operations. | 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.
Midnight wallets are connected today through an injected browser extension
(the 1am wallet). Embedded (social-auth) Midnight wallets are not yet
available. Until then, the 1am extension is the supported path and a user who
signs in with social auth will not have a derived Midnight wallet.
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 richer shielded handles (coin / encryption public keys), read
them from the connector:
const connector = primaryWallet.connector;
const { shieldedAddress, shieldedCoinPublicKey, shieldedEncryptionPublicKey } =
await connector.getShieldedAddresses();
const { unshieldedAddress } = await connector.getUnshieldedAddress();
const { dustAddress } = await connector.getDustAddress();
Reading Balances
getFormattedBalances() returns display-ready strings for all three surfaces in
one call:
const { shieldedBalance, unshieldedBalance, dustBalance } =
await primaryWallet.getFormattedBalances();
// shieldedBalance -> shielded NIGHT, e.g. "12.5"
// unshieldedBalance -> unshielded NIGHT, e.g. "3.0"
// dustBalance -> { balance, cap } for DUST generation, undefined if empty
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