New Fullscreen dApps on iOS and Android

Build dApps that feel at home in 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
Detected in this browser right now window.midenWallet window.bread bread.presentation bread.auth Open this page in Bread to light them up.

One provider, three places your dApp runs

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.

Browser extension

Chrome and Firefox

  • Injected at document start into https:// pages, http://localhost and http://127.0.0.1; Firefox also injects it into every http:// page
  • Approvals open in the extension's own window
  • No window.bread

Mobile browser

iOS and Android, inside the Bread app

  • Bread's in-app browser injects the provider after each page load
  • Approvals open in a dialog over your page
  • window.bread.presentation for fullscreen and window.bread.auth for Google and Apple sign-in

Desktop app

macOS, Windows and Linux

  • Explore opens your page in its own window, provider injected at document start in the top frame
  • Approvals appear in the main Bread window
  • window.bread is an empty marker: no fullscreen, no sign-in sheet
On this page Quickstart

    Quickstart

    Three steps from an empty page to a confirmed transfer. React is the shortest path; the adapter also works on its own in plain TypeScript.

    Install the adapter

    @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/react

    The 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.

    Wrap your app

    WalletProvider holds the connection. Its network, privateDataPermission and allowedPrivateData props are what it asks Bread for when it connects.

    main.tsx
    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>
      );
    }

    Connect and send

    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>;
    }
    Amounts are integers

    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.

    Connect and permissions

    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.

    signature
    connect(
      privateDataPermission: PrivateDataPermission,
      network: WalletAdapterNetwork,
      allowedPrivateData?: AllowedPrivateData
    ): Promise<void>
    PrivateDataPermissionWhat Bread does
    UponRequestAsks the user each time you read balances or notes. The default.
    AutoAnswers reads in the categories of allowedPrivateData without a prompt. Because that is standing access, the connect sheet asks the user to confirm it explicitly.
    AllowedPrivateDataValueCovers
    None0Nothing; the default
    Assets1requestAssets
    Notes2requestPrivateNotes, requestConsumableNotes
    Storage4Accepted, but no request reads it today
    All65535Every category

    Combine categories with a bitwise or, for example AllowedPrivateData.Assets | AllowedPrivateData.Notes.

    What the user sees

    • Approval screens name your site by its verified origin, never by a name you send.
    • Connecting again from the same origin returns the saved grant without a prompt. A locked wallet asks the user to unlock first.
    • Users review and revoke connections in Settings, Authorized DApps. disconnect() removes the session too.
    • If the user turns off DApps Interaction in that screen, every request fails: the phone and desktop apps refuse it with NOT_GRANTED, the extension with a plain Not Found error.
    • If Bread is not installed, connect() throws WalletNotReadyError. Through the React WalletProvider it also opens Bread's Chrome Web Store page.

    Errors

    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.

    CodeMeans
    NOT_GRANTEDThe user declined, the origin has no session, or DApps Interaction is off (phone and desktop apps)
    NOT_FOUNDNothing to act on, such as disconnecting without a session
    INVALID_PARAMSA request field Bread refused, explained in the rest of the message
    NETWORK_NOT_GRANTEDAn unknown network name, or mainnet while the wallet is not on mainnet
    LOGIN_KEY_UNAVAILABLEThe account has no login key on this device (see Sign in with Bread)
    Raw provider errors are not Error instances

    Calling 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.

    Networks

    You name a network when you connect; Bread runs every request on the network the wallet is on.

    NetworkRPCFaucetExplorer
    mainnethttps://rpc.mainnet.miden.ioNoneexplorer.mainnet.miden.io
    testnethttps://rpc.testnet.miden.iofaucet.testnet.miden.iotestnet.midenscan.com
    devnethttps://rpc.devnet.miden.iofaucet.devnet.miden.iodevnet.midenscan.com
    localnethttp://localhost:57291Your ownNone
    • Bread accepts mainnet, testnet, devnet and localnet. Any other name is refused with NETWORK_NOT_GRANTED.
    • Asking for a valid network the wallet is not on is not refused. The connect sheet tells the user: "This app asked for Miden Testnet, but your wallet is on Miden Mainnet." Requests then run on the wallet's network.
    • mainnet is granted only while the wallet itself is on mainnet.
    The adapter has no Mainnet member

    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.

    Read balances and notes

    After connecting, address and publicKey are on the adapter. Everything else is a request, answered with a prompt or silently according to the grant.

    reads.ts
    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.
    • Every Bread account is a Guardian account. requestGuardianInfo reports its Guardian operator and sync state; the desktop provider does not implement it yet.

    Transactions

    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
      )
    );
    • 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.
    A transaction id is not a confirmation

    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.

    Private notes

    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:

    import-note.ts
    const noteId = await adapter.importPrivateNote(noteBytes); // Uint8Array
    const notes = await adapter.requestConsumableNotes();     // listed if this account can consume it

    Sign bytes

    signBytes asks the connected account to sign data you prepared with the Miden SDK. The user approves every signature.

    sign.ts
    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
    kindWhat 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 summarySigns that summary's own commitment, which can authorize the transaction.
    'signingInputs' holding Blind or ArbitrarySigns 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.

    Sign in with Bread

    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) })
      });
    }
    • Key: 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.
    • Signature: 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].
    • Verify: on your server, compute the login message M from your own word. Bread never sends the public key and the SDK's 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.
    • Domain: Bread takes the domain from your page's real origin, never from the request. app.example.com and www.example.com share one key. Pages must be https, except localhost and loopback addresses.
    RefusalWhen
    LOGIN_KEY_UNAVAILABLEThe 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_PARAMSThe data is not a word (32 bytes holding four little-endian field elements), or the page is not https
    NOT_GRANTEDNo session for this origin, or the user declined
    Detect the methods, not a version

    Older Bread builds have neither method. LOGIN_KEY_UNAVAILABLE arrives as a plain MidenWalletError; match it in error.message.

    The mobile browser

    On iOS and Android, Explore opens your dApp in Bread's own browser. The adapter works unchanged; a few details differ from the extension.

    • Injected after load. 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.
    • Origin first. Bread refuses requests with NOT_GRANTED until it has confirmed your page's origin, which takes a moment after navigation.
    • Dialogs, not windows. Approvals open in a dialog over your page. Requests time out in the page after five minutes.
    • Toolbar space. In ordinary browser mode Bread adds padding-bottom: calc(96px + env(safe-area-inset-bottom, 0px)) !important to your body so content clears its bottom toolbar.
    • Kept alive. A dApp the user leaves is parked, and picking it again restores the same page and JavaScript state without a reload. A repeat connect() repopulates the account.
    Don't name a global bread

    Inside 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.

    Fullscreen

    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>
    Meta tagEffect
    bread-displayOnly the exact value fullscreen opts in. Anything else keeps the ordinary browser layout.
    bread-readymanual makes Bread wait for presentation.ready(). Without it, Bread reveals the page at DOMContentLoaded.
    theme-colorThe 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.

    Layout anatomy

    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:

    layout.js
    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();

    Lifecycle

    1. PrepareBread shows your theme-color and waits up to 30 seconds of foreground time for readiness, then offers Retry and Back to Bread.
    2. RevealAutomatic at DOMContentLoaded, or when you call presentation.ready().
    3. MinimizeA tap on the capsule parks your page in Bread like any other app. Nothing reloads.
    4. RestorePicking it again brings back the same document and session, still fullscreen.
    • Motion: dApps open and minimize with a smooth morph, a native spring animation that runs at your display's full refresh rate, up to 120 Hz on ProMotion iPhones. Tap the Bread capsule to minimize, or grab the window mid-flight and the motion follows your finger and carries your velocity.
    • Overlays: wallet confirmations, alerts and locking suspend your page; it resumes when the last overlay closes.
    • Closing: users close a fullscreen page with Bread's usual parked-app controls; you can call presentation.close(). Closing rejects that page's pending wallet confirmation and drops its saved session. A transaction already submitted continues.
    • Rotation: fullscreen opens in the current orientation and follows the device.
    • Promises: ready() and close() resolve when the native side acknowledges and reject on refusal or after 30 seconds. At most 32 calls may be outstanding.
    Where fullscreen does not apply

    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 sign-in

    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 })
        });
      }
    }
    • Result: open resolves to a frozen { url }, the untouched callback URL, or { cancelled: true } when the user closes the sheet. It has no timeout.
    • URL rules: the URL must be an absolute https URL on your page's own origin, up to 4096 characters. Inside the sheet the flow can redirect to Google or Apple as usual.
    • Ending the flow: navigate to 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.
    • One at a time: only one sheet can be open across Bread, and only while your page is visible and Bread is in the foreground.
    • Platforms: iOS uses the system sign-in sheet, which shares Safari's cookies. Android uses an Auth Tab where the browser supports it, otherwise a Custom Tab.
    A handle alone must never sign anyone in

    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.

    RejectionWhen
    AUTH_NOT_MAIN_FRAMEA frame reached the native transport; frames get no window.bread at all
    AUTH_INSECURE_PAGEThe page is not https with a host; http://localhost too
    AUTH_INVALID_URLThe URL breaks a rule above
    AUTH_BUSYA sheet is already open, for any dApp
    AUTH_UNAVAILABLEBread can't show a sheet now: hidden, parked, closing or in the background
    AUTH_ORIGIN_MISMATCHThe callback does not name this page's origin exactly once
    AUTH_FAILEDAnything else, such as a callback with a path

    Every URL and callback rule, character by character, is in the sign-in sheet reference.

    Explore listing (optional)

    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.

    Status: applications welcome, rollout to be announced

    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.

    Test your dApp

    Start on testnet in a desktop browser with the extension, then check the phone apps.

    Get a test wallet

    Open your build

    • Extension: the provider is injected on http://localhost and http://127.0.0.1, so a local dev server works as is.
    • Phones: type your address into Explore's search box. An address typed without a scheme gets https://, so write http:// yourself for a plain-http server.
    • iOS loads plain-http pages. Android store builds allow no cleartext to your server, so expose your dev server over https, for example through a tunnel.
    • Desktop: Explore accepts http and https addresses and opens them in Bread's dApp window, which the next address reuses.
    • window.bread.auth needs an https page; the login key needs https or a loopback host such as localhost.
    A complete reference dApp

    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.

    Gotchas

    The ones that cost people an afternoon.

    SymptomCause and fix
    Could not resolve @miden-sdk/reactThe 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 phonesThe provider arrives after page load. Wait for the adapter's readyState, don't read window.midenWallet once at startup.
    React connect() ignores my networkWalletProvider connects with its own props. Set network and the permissions there.
    Connect works once, then the account is goneAdapters created inside render are new each time, and a replaced adapter disconnects. Create them at module scope.
    Mismatch notice on mainnetThe adapter defaults to Testnet and has no Mainnet member. Pass 'mainnet' as WalletAdapterNetwork.
    "Success" but nothing on chainA transaction id means queued. Call waitForTransaction.
    catch (e) misses wallet errorsRaw provider errors fail instanceof Error. Match e.message.
    Script error about breadA classic script declared a global bread. Rename it or use a module.
    Fullscreen ignoredThe meta tag was added after DOMContentLoaded, for example by a bundle, or its value is not exactly fullscreen.
    theme-color ignoredBread accepts only #RRGGBB.
    Blank page on AndroidPlain-http dev server. Use https.
    createAccount failsBread does not create accounts for dApps; the extension and phones refuse it, desktop does not define it.

    Under the hood

    Native integration notes and specifications, for when you need every edge case.

    Found something missing or wrong? Open an issue.