Browser extension
- Injected at document start into
https://pages,http://localhostandhttp://127.0.0.1; Firefox also injects it into everyhttp://page - Approvals open in the extension's own window
- No
window.bread
Bread is the Miden wallet for Chrome, Firefox, iOS, Android and desktop. Connect with the Miden wallet adapter, ask for sends, consumes and custom transactions, sign people in with a key made for your site, and on phones take the whole screen with one meta tag.
npm i @miden-sdk/miden-wallet-adapter @miden-sdk/miden-sdk @miden-sdk/react
Every Bread app injects the same window.midenWallet provider, so one integration covers them all. The phone apps add window.bread for fullscreen and sign-in.
https:// pages, http://localhost and http://127.0.0.1; Firefox also injects it into every http:// pagewindow.breadwindow.bread.presentation for fullscreen and window.bread.auth for Google and Apple sign-inwindow.bread is an empty marker: no fullscreen, no sign-in sheetThree steps from an empty page to a confirmed transfer. React is the shortest path; the adapter also works on its own in plain TypeScript.
@miden-sdk/miden-wallet-adapter is the umbrella package: the Bread adapter, its types, the React hooks and the UI components in one install. It imports @miden-sdk/miden-sdk and @miden-sdk/react 0.17 but installs neither, so name both. npm 7+ and pnpm 8+ add the SDK on their own as a required peer; nothing adds @miden-sdk/react, an optional peer, and Yarn adds no peers.
npm i @miden-sdk/miden-wallet-adapter \
@miden-sdk/miden-sdk @miden-sdk/reactnpm i @miden-sdk/miden-sdk \
@miden-sdk/miden-wallet-adapter-miden \
@miden-sdk/miden-wallet-adapter-baseThe umbrella needs React 19.1 or later and pulls React into any app that imports it. Without React, or on React 18, install its parts, the @miden-sdk/miden-wallet-adapter-* packages: -miden and -base for the adapter, -react for the hooks (React 18 or 19) and -reactui for the UI (React 19.1). They export the same names: MidenWalletAdapter and MidenWalletName come from -miden, WalletProvider and useWallet from -react, and every other name in these samples from -base.
WalletProvider holds the connection. Its network, privateDataPermission and allowedPrivateData props are what it asks Bread for when it connects.
import {
MidenWalletAdapter,
PrivateDataPermission,
WalletAdapterNetwork,
WalletProvider
} from '@miden-sdk/miden-wallet-adapter';
// Create adapters once, outside render: a new adapter disconnects the old one.
const wallets = [new MidenWalletAdapter({ appName: 'My dApp' })];
export function Root() {
return (
<WalletProvider
wallets={wallets}
network={WalletAdapterNetwork.Testnet}
privateDataPermission={PrivateDataPermission.UponRequest}
>
<App />
</WalletProvider>
);
}
Select Bread once, connect on a click, then ask for a transfer. The call resolves with a transaction id when Bread accepts it; waitForTransaction tells you how it ended.
import { useEffect } from 'react';
import {
MidenWalletName,
PrivateDataPermission,
SendTransaction,
useWallet,
WalletAdapterNetwork
} from '@miden-sdk/miden-wallet-adapter';
export function App() {
const { wallet, select, connect, connected, address, requestSend, waitForTransaction } = useWallet();
useEffect(() => {
if (!wallet) select(MidenWalletName);
}, [wallet, select]);
async function pay(recipient: string, faucetId: string) {
// WalletProvider connects with its own props; these arguments only satisfy the type.
if (!connected) await connect(PrivateDataPermission.UponRequest, WalletAdapterNetwork.Testnet);
const txId = await requestSend!(new SendTransaction(address!, recipient, faucetId, 'public', 1_000_000));
const { txHash } = await waitForTransaction!(txId);
console.log('committed', txHash);
}
return <button onClick={() => pay(RECIPIENT, FAUCET_ID)}>Pay with Bread</button>;
}import { MidenWalletAdapter } from '@miden-sdk/miden-wallet-adapter-miden';
import {
PrivateDataPermission,
SendTransaction,
WalletAdapterNetwork,
WalletReadyState
} from '@miden-sdk/miden-wallet-adapter-base';
const adapter = new MidenWalletAdapter({ appName: 'My dApp' });
// The adapter polls for window.midenWallet and may find it at once, in its constructor.
if (adapter.readyState === WalletReadyState.Installed) enableConnectButton();
adapter.on('readyStateChange', state => {
if (state === WalletReadyState.Installed) enableConnectButton();
});
async function pay(recipient: string, faucetId: string) {
await adapter.connect(PrivateDataPermission.UponRequest, WalletAdapterNetwork.Testnet);
const txId = await adapter.requestSend(
new SendTransaction(adapter.address, recipient, faucetId, 'public', 1_000_000)
);
const { txHash, outputNotes } = await adapter.waitForTransaction(txId);
return { txHash, outputNotes };
}Pass amount in the token's smallest unit, as a whole number. Bread reads it with BigInt, so 1_000_000 of a token with 6 decimals is one token.
A connection is a grant from one account to one origin. You say what you want to read without asking each time; the user decides.
connect(
privateDataPermission: PrivateDataPermission,
network: WalletAdapterNetwork,
allowedPrivateData?: AllowedPrivateData
): Promise<void>| PrivateDataPermission | What Bread does |
|---|---|
UponRequest | Asks the user each time you read balances or notes. The default. |
Auto | Answers reads in the categories of allowedPrivateData without a prompt. Because that is standing access, the connect sheet asks the user to confirm it explicitly. |
| AllowedPrivateData | Value | Covers |
|---|---|---|
None | 0 | Nothing; the default |
Assets | 1 | requestAssets |
Notes | 2 | requestPrivateNotes, requestConsumableNotes |
Storage | 4 | Accepted, but no request reads it today |
All | 65535 | Every category |
Combine categories with a bitwise or, for example AllowedPrivateData.Assets | AllowedPrivateData.Notes.
disconnect() removes the session too.NOT_GRANTED, the extension with a plain Not Found error.connect() throws WalletNotReadyError. Through the React WalletProvider it also opens Bread's Chrome Web Store page.Bread's refusals carry a code at the start of the message. Through the adapter most arrive wrapped in its Wallet...Error classes with the same message; importPrivateNote, requestConsumableNotes and a refused waitForTransaction pass the provider's error through as it is.
| Code | Means |
|---|---|
NOT_GRANTED | The user declined, the origin has no session, or DApps Interaction is off (phone and desktop apps) |
NOT_FOUND | Nothing to act on, such as disconnecting without a session |
INVALID_PARAMS | A request field Bread refused, explained in the rest of the message |
NETWORK_NOT_GRANTED | An unknown network name, or mainnet while the wallet is not on mainnet |
LOGIN_KEY_UNAVAILABLE | The account has no login key on this device (see Sign in with Bread) |
Error instancesCalling window.midenWallet directly, failures are MidenWalletError objects that implement Error but do not extend it, so instanceof Error is false. Read error.message and match the code.
You name a network when you connect; Bread runs every request on the network the wallet is on.
| Network | RPC | Faucet | Explorer |
|---|---|---|---|
| mainnet | https://rpc.mainnet.miden.io | None | explorer.mainnet.miden.io |
| testnet | https://rpc.testnet.miden.io | faucet.testnet.miden.io | testnet.midenscan.com |
| devnet | https://rpc.devnet.miden.io | faucet.devnet.miden.io | devnet.midenscan.com |
| localnet | http://localhost:57291 | Your own | None |
mainnet, testnet, devnet and localnet. Any other name is refused with NETWORK_NOT_GRANTED.mainnet is granted only while the wallet itself is on mainnet.WalletAdapterNetwork 0.17 lists only Devnet, Testnet and Localnet, and WalletProvider defaults to Testnet, so a mainnet wallet shows the mismatch notice on every default connect. A mainnet dApp passes the string itself: 'mainnet' as WalletAdapterNetwork.
After connecting, address and publicKey are on the adapter. Everything else is a request, answered with a prompt or silently according to the grant.
import { NoteFilterTypes } from '@miden-sdk/miden-sdk';
const assets = await adapter.requestAssets();
// [{ faucetId, amount }]: faucetId as a bech32 address, amount as a decimal string
const consumable = await adapter.requestConsumableNotes();
const committed = await adapter.requestPrivateNotes(NoteFilterTypes.Committed);
// each: { noteId, senderAccountId, assets, noteType, nullifier, state }
const guardian = await adapter.requestGuardianInfo();
// { isGuardianAccount, guardianEndpoint, guardianProvider, guardianSyncStatus }requestPrivateNotes(filter, noteIds?) takes the SDK's NoteFilterTypes; pass NoteFilterTypes.List with ids to ask for specific notes.requestGuardianInfo reports its Guardian operator and sync state; the desktop provider does not implement it yet.Three shapes, one confirmation flow. Each call opens Bread's approval, and resolves with a transaction id once Bread has accepted the transaction.
import { SendTransaction } from '@miden-sdk/miden-wallet-adapter';
const txId = await adapter.requestSend(
new SendTransaction(
adapter.address, // sender: the connected account
recipient, // a Miden address
faucetId, // which token
'private', // 'public' or 'private' note
250_000 // integer, smallest unit
)
);import { ConsumeTransaction } from '@miden-sdk/miden-wallet-adapter';
// Claim a note addressed to the connected account.
const txId = await adapter.requestConsume(
new ConsumeTransaction(faucetId, noteId, 'public', amount)
);
// A private note the wallet has not seen yet travels with its bytes:
// new ConsumeTransaction(faucetId, noteId, 'private', amount, noteBytes)import { Transaction } from '@miden-sdk/miden-wallet-adapter';
import type { TransactionRequest } from '@miden-sdk/miden-sdk';
// Build any TransactionRequest with the Miden SDK, then hand it to Bread.
const request: TransactionRequest = buildMyRequest();
const txId = await adapter.requestTransaction(
Transaction.createCustomTransaction(
adapter.address, // the account that executes it
recipientAddress,
request,
inputNoteIds, // optional: notes the request consumes
inputNoteBytes // optional: Uint8Array[] of notes to import first
)
);try {
const { txHash, outputNotes } = await adapter.waitForTransaction(txId);
// outputNotes are SDK Note objects, for example a private note to hand on
} catch (error) {
// WalletTransactionError: it failed or did not settle in time.
// A refused request rejects with the provider's own error.
}requestTransaction also accepts Transaction.createSendTransaction(...) and createConsumeTransaction(...); the adapter routes those to the send and consume requests.CustomTransaction serializes the request in its constructor and Bread deserializes it with its own SDK, so build it with the same @miden-sdk/miden-sdk minor version Bread uses, 0.17.SendTransaction takes an optional sixth argument, recallBlocks.Bread resolves the request when the transaction enters its queue, not when it lands on chain. Show progress, then call waitForTransaction(txId) before treating it as done. If Bread answers without an id, the adapter throws instead of resolving.
Send with noteType: 'private' and the note's details stay off chain. To pass a private note to Bread yourself, for example one your server holds for the user, import its serialized bytes:
const noteId = await adapter.importPrivateNote(noteBytes); // Uint8Array
const notes = await adapter.requestConsumableNotes(); // listed if this account can consume itsignBytes asks the connected account to sign data you prepared with the Miden SDK. The user approves every signature.
import { Word } from '@miden-sdk/miden-sdk';
const signature = await adapter.signBytes(word.serialize(), 'word');
// or: adapter.signBytes(signingInputs.serialize(), 'signingInputs')
// signature: the SDK's serialized Signature bytes| kind | What Bread signs |
|---|---|
'word' | Signs the domain-separated commitment of that Word, tag Poseidon2 of miden-wallet/sign-bytes/v1, not the raw Word, and cannot authorize a transaction. |
'signingInputs' holding a transaction summary | Signs that summary's own commitment, which can authorize the transaction. |
'signingInputs' holding Blind or Arbitrary | Signs the domain-separated commitment of that payload and cannot authorize a transaction. |
To verify, take the word Bread signed and check publicKey.verify(word, Signature.deserialize(signature)). For a word, Blind, or Arbitrary payload, compute that word with domainSeparatedSignCommitment(data, kind) from @miden-sdk/miden-sdk 0.17.10; for a transaction summary it is signingInputs.toCommitment(). A page that calls the injected provider directly also gets the word back as signedCommitment. A word, Blind, or Arbitrary signature does not verify against the raw payload.
To sign people in, use the login key below instead: it is separate from the account's transaction keys.
Each account has one login key per site. It comes from the account's root key and your registrable domain, so the same recovery phrase brings the same login back on every device and in every Bread app. It signs only sign-in messages, never transactions.
// After connecting. Not in the adapter or its types: call the provider directly.
const wallet = (window as any).midenWallet;
if (wallet?.requestLoginKey) {
const { loginKey } = await wallet.requestLoginKey(); // 32 bytes, no prompt
const word = await fetchChallengeWord(); // 32 bytes from your server
const { signature } = await wallet.signLoginBytes(word, 'word'); // prompts
await fetch('/login', {
method: 'POST',
body: JSON.stringify({ loginKey: toHex(loginKey), signature: toHex(signature) })
});
}import { Felt, FeltArray, Poseidon2, Word } from '@miden-sdk/miden-sdk';
// Bread signs M, never the word itself.
function loginMessage(word: Word) {
const tag = new Uint8Array(16);
tag.set(new TextEncoder().encode('Bread login v1'));
const view = new DataView(tag.buffer);
const tagElements = [0, 4, 8, 12].map(at => new Felt(BigInt(view.getUint32(at, true))));
return Poseidon2.hashElements(new FeltArray([...tagElements, ...word.toFelts()]));
}requestLoginKey() resolves to { loginKey }, the 32-byte commitment of the login key, in the same form as the publicKey a connection returns. It needs a connected session and shows no prompt.signLoginBytes(data, 'word') always asks for approval, on a screen that names your site. It resolves to { signature }, a 66-byte recoverable ECDSA signature over secp256k1 with Keccak: [0x01, r, s, v].PublicKey.recoverFrom supports only Falcon keys, so recover the secp256k1 key with an ECDSA library from r, s and v over keccak256 of M's 32 serialized bytes. Deserialize it as an SDK PublicKey (0x01 and the 33-byte compressed point), check publicKey.verify(M, signature), and accept only when publicKey.toCommitment() equals loginKey. Checking against the raw word fails by design.app.example.com and www.example.com share one key. Pages must be https, except localhost and loopback addresses.| Refusal | When |
|---|---|
LOGIN_KEY_UNAVAILABLE | The account's root key is not on this device, which is always the case for an account imported from a single private key. Bread never falls back to another key. |
INVALID_PARAMS | The data is not a word (32 bytes holding four little-endian field elements), or the page is not https |
NOT_GRANTED | No session for this origin, or the user declined |
Older Bread builds have neither method. LOGIN_KEY_UNAVAILABLE arrives as a plain MidenWalletError; match it in error.message.
On iOS and Android, Explore opens your dApp in Bread's own browser. The adapter works unchanged; a few details differ from the extension.
window.midenWallet appears after each page load, not at document start. The adapter polls for it every second, so wait for readyState to be Installed instead of checking the global once at startup.NOT_GRANTED until it has confirmed your page's origin, which takes a moment after navigation.padding-bottom: calc(96px + env(safe-area-inset-bottom, 0px)) !important to your body so content clears its bottom toolbar.connect() repopulates the account.breadInside Bread on mobile, window.bread is a non-configurable, frozen property. A classic script must not declare a top-level bread with let, const, class, function or var, and a bundle must not expose a library global named bread. Module scripts and bundler-wrapped code are unaffected.
Games and immersive dApps can drop Bread's browser chrome on iOS and Android. Your page fills the display; a small translucent Bread capsule stays in the corner as the way back.
<head>
<meta name="bread-display" content="fullscreen" />
<meta name="bread-ready" content="manual" />
<meta name="theme-color" content="#101010" />
</head>// With bread-ready="manual", reveal once your first correctly sized frame is drawn.
await renderFirstFrame();
if (window.bread?.presentation) {
// Rejects where Bread falls back to browser mode; the page then shows as usual.
await window.bread.presentation.ready().catch(() => {});
}
// Leave fullscreen and close the page from your own UI.
quitButton.onclick = async () => {
if (window.bread?.presentation) await window.bread.presentation.close();
};/* Sit beside the capsule when the rectangle is known */
:root[data-bread-control-bounds='true'] .hud {
top: var(--bread-control-top);
height: var(--bread-control-height);
margin-left: calc(var(--bread-control-left) + var(--bread-control-width) + 12px);
}
/* No rectangle, as on older hosts: reserve the whole top row */
:root:not([data-bread-control-bounds='true']) .hud {
padding-top: var(--bread-control-inset-top, 0px);
}
/* Backgrounds may bleed; controls stay inside the safe area */
.controls {
padding-bottom: var(--bread-safe-area-bottom, 0px);
padding-left: var(--bread-safe-area-left, 0px);
padding-right: var(--bread-safe-area-right, 0px);
}| Meta tag | Effect |
|---|---|
bread-display | Only the exact value fullscreen opts in. Anything else keeps the ordinary browser layout. |
bread-ready | manual makes Bread wait for presentation.ready(). Without it, Bread reveals the page at DOMContentLoaded. |
theme-color | The color behind your page while it prepares, written as #RRGGBB. |
Bread reads these tags while the head parses, so put them in your initial HTML rather than adding them from a bundle. The settings are fixed at DOMContentLoaded.
Pick a region to see what Bread reserves and which variable describes it. Every value is in CSS pixels, updated on rotation, resize and safe-area changes.
Illustrative. Read the real rectangle from the variables; don't hardcode its position.
The same numbers are available to JavaScript, as frozen snapshots that change on every update:
function layout() {
const state = window.__breadPresentationState;
if (!state) return;
const { controlBounds, safeAreaInsets } = state; // either may be missing on older hosts
// controlBounds: { left, top, width, height }
// safeAreaInsets: { top, left, right, bottom }
resizeRenderer(controlBounds, safeAreaInsets);
}
document.addEventListener('breadpresentationchange', layout);
layout();theme-color and waits up to 30 seconds of foreground time for readiness, then offers Retry and Back to Bread.DOMContentLoaded, or when you call presentation.ready().presentation.close(). Closing rejects that page's pending wallet confirmation and drops its saved session. A transaction already submitted continues.ready() and close() resolve when the native side acknowledges and reject on refusal or after 30 seconds. At most 32 calls may be outstanding.Only the main document of Bread's mobile browser has window.bread.presentation; frames, the extension and the desktop app do not. Android WebViews without the required navigation support open the page in normal browser mode; window.bread.presentation still exists there, but ready() and close() reject. Keep your ordinary responsive layout working outside Bread and size your renderer from its container.
Google and Apple refuse to sign in inside embedded web views. On phones, window.bread.auth.open(url) runs the flow in a secure system sheet while your page stays loaded.
const hex = bytes => Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
const verifier = hex(crypto.getRandomValues(new Uint8Array(32)));
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
const url = new URL('/auth/start', location.origin);
url.searchParams.set('challenge', hex(new Uint8Array(digest)));
if (window.bread?.auth) {
const result = await window.bread.auth.open(url.href);
const handle = !result.cancelled && new URL(result.url).searchParams.get('login');
if (handle) {
await fetch('/auth/redeem', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ handle, verifier })
});
}
}<!doctype html>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Serve with Cache-Control: no-store. Origin and handle are written by your server. -->
<meta http-equiv="refresh" content="0; url=com.miden.wallet.auth://callback?origin=https%3A%2F%2Fexample.org&login=HANDLE" />
<title>Return to Bread</title>
<a href="com.miden.wallet.auth://callback?origin=https%3A%2F%2Fexample.org&login=HANDLE">Return to Bread</a>open resolves to a frozen { url }, the untouched callback URL, or { cancelled: true } when the user closes the sheet. It has no timeout.com.miden.wallet.auth://callback with exactly one origin parameter naming your page's origin. Bread reads nothing else from the URL, so every other parameter name is yours.Another app can register the callback scheme on Android's Custom Tab path. Keep the verifier in page memory and redeem a handle only with the secret whose hash started the flow, single use and short-lived. Take the callback's origin from your own configuration, never from the request. The sheet shares no cookies with your page.
| Rejection | When |
|---|---|
AUTH_NOT_MAIN_FRAME | A frame reached the native transport; frames get no window.bread at all |
AUTH_INSECURE_PAGE | The page is not https with a host; http://localhost too |
AUTH_INVALID_URL | The URL breaks a rule above |
AUTH_BUSY | A sheet is already open, for any dApp |
AUTH_UNAVAILABLE | Bread can't show a sheet now: hidden, parked, closing or in the background |
AUTH_ORIGIN_MISMATCH | The callback does not name this page's origin exactly once |
AUTH_FAILED | Anything else, such as a callback with a path |
Every URL and callback rule, character by character, is in the sign-in sheet reference.
Send people straight into your dApp inside Bread on their phone. Type an https address to build both link forms.
https://bread.miden.xyz/open#url=<page> carries the page in the fragment, which browsers never send to the link host. com.miden.wallet://open?url=<page> is the app scheme.Your dApp does not need to be listed. Any dApp connects to Bread and works in it without a listing, opened from its address, a link or the search box in Explore. The Explore catalog is only Bread's built-in discovery section.
Listing goes through a vetting process. Applications are welcome and each one is reviewed. The catalog's rollout is not defined yet: there is no timeline, and applying does not guarantee a listing.
To apply, open an issue on the wallet repository.
Start on testnet in a desktop browser with the extension, then check the phone apps.
http://localhost and http://127.0.0.1, so a local dev server works as is.https://, so write http:// yourself for a plain-http server.window.bread.auth needs an https page; the login key needs https or a loopback host such as localhost.The wallet's own end-to-end suite drives every request through a small test dApp, including the login key check. Read it in playwright/e2e/test-dapp.
The ones that cost people an afternoon.
| Symptom | Cause and fix |
|---|---|
Could not resolve @miden-sdk/react | The adapter's React hooks import it, but it is an optional peer, so no package manager installs it. Install it, or without React use the -miden and -base packages. |
| Bread never detected on phones | The provider arrives after page load. Wait for the adapter's readyState, don't read window.midenWallet once at startup. |
React connect() ignores my network | WalletProvider connects with its own props. Set network and the permissions there. |
| Connect works once, then the account is gone | Adapters created inside render are new each time, and a replaced adapter disconnects. Create them at module scope. |
| Mismatch notice on mainnet | The adapter defaults to Testnet and has no Mainnet member. Pass 'mainnet' as WalletAdapterNetwork. |
| "Success" but nothing on chain | A transaction id means queued. Call waitForTransaction. |
catch (e) misses wallet errors | Raw provider errors fail instanceof Error. Match e.message. |
Script error about bread | A classic script declared a global bread. Rename it or use a module. |
| Fullscreen ignored | The meta tag was added after DOMContentLoaded, for example by a bundle, or its value is not exactly fullscreen. |
theme-color ignored | Bread accepts only #RRGGBB. |
| Blank page on Android | Plain-http dev server. Use https. |
createAccount fails | Bread does not create accounts for dApps; the extension and phones refuse it, desktop does not define it. |
Native integration notes and specifications, for when you need every edge case.
Found something missing or wrong? Open an issue.