Guide to integrating external signers (Para, Turnkey, MidenFi wallet adapter) and building custom signers for Miden React frontends. Covers provider setup, passkey authentication, unified signer interface, custom SignerContext implementation, and custom account components. Use when adding wallet connection, authentication, or external key management to a Miden frontend.
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Um comando direto ignora o prompt de revisão. Verifique a origem antes de executá-lo.
Instruções da origem · Visualização somente leitura
name
signer-integration
description
Guide to integrating external signers (Para, Turnkey, MidenFi wallet adapter) and building custom signers for Miden React frontends. Covers provider setup, passkey authentication, unified signer interface, custom SignerContext implementation, and custom account components. Use when adding wallet connection, authentication, or external key management to a Miden frontend.
Miden Signer Integration
Overview
By default, MidenProvider uses a local keystore (keys in IndexedDB, no wallet connection needed). For production apps, wrap MidenProvider with a signer provider to use external key management.
Signer providers must wrap MidenProvider (outer → inner):
import { TurnkeySignerProvider } from"@miden-sdk/miden-turnkey-react";
// `config` is REQUIRED, and `defaultOrganizationId` is required within it.// Type: Pick<TurnkeySDKBrowserConfig, "defaultOrganizationId">// & Partial<Omit<TurnkeySDKBrowserConfig, "defaultOrganizationId">>// — only the other fields (e.g. `apiBaseUrl`) are optional; `apiBaseUrl`// defaults to https://api.turnkey.com. There is NO env-var fallback for the// org id (the provider does not read `VITE_TURNKEY_ORG_ID`).<TurnkeySignerProviderconfig={{defaultOrganizationId: "your-org-id" }}><MidenProviderconfig={{rpcUrl: "testnet" }}><App /></MidenProvider></TurnkeySignerProvider>// Or override the apiBaseUrl default:<TurnkeySignerProviderconfig={{apiBaseUrl: "https://api.turnkey.com",
defaultOrganizationId: "your-org-id",
}}>
...
</TurnkeySignerProvider>
TurnkeySignerProvider also accepts optional customComponents and importAccountId props, which it forwards into accountConfig (see "Custom Account Components").
With MidenFiSignerProvider in place, use useSigner() from the React SDK to manage connection state. The regular React SDK hooks (useSend, useConsume, etc.) automatically sign via the connected wallet — no additional wiring needed.
The provider accepts an accountType prop, but it is a no-op: account visibility is determined solely by storageMode (private/public), and the provider always imports the account by ID (importAccountId), bypassing the builder path entirely. Omit it.
Frontend-template-specific MidenFi pattern
The frontend template (on web-sdk 0.15 — @miden-sdk/miden-sdk@0.15.3, @miden-sdk/react@0.15.3, wallet adapters 0.15.1) deviates from the generic patterns above in three places worth knowing when the wallet extension is the primary signer:
Provider order is INVERTED: MidenProvider runs OUTSIDE MidenFiSignerProvider — see src/providers.tsx. This is the opposite of the canonical signer-outer / Miden-inner nesting at the top of this skill, and it is deliberate. In v0.15, when a signer provider is an ancestor of MidenProvider, MidenProvider treats it as its external keystore and does NOT create the WebClient until the signer connects (the init effect sees signerIsConnected === false and returns early before building the client). With a wallet that hasn't connected — or any environment without the extension — the app would hang on "Initializing…" and even public reads couldn't run. The template never signs throughMidenProvider (it signs its only write, the counter increment, through the local WebClient rather than the wallet), so it runs MidenProvider in local-keystore mode (no signer ancestor → it initializes immediately, reads work pre-connect) and keeps MidenFiSignerProviderinside, purely for the connect button and the wallet's requestTransaction. MidenFiSignerProvider works standalone (it provides its own WalletContext + SignerContext; no MultiSignerProvider needed). Use this inversion only when you do not sign through MidenProvider; if external-keystore signing IS the goal, keep the canonical signer-outer order so MidenProvider picks up the signer's signCb/accountConfig.
Wallet button uses useMidenFiWallet() + WalletReadyState — see src/components/AppContent.tsx. The button gates on wallet?.readyState (rendering a disabled "Install MidenFi Wallet" state unless readyState is Installed or Loadable) so it can show install state before the extension is detected. useSigner().connect() would silently fall through to the adapter's window.open(adapter.url, ...) install fallback; gating on readyState avoids that path.
The counter increment is a local two-transaction flow, not a wallet-signed tx — see src/hooks/useIncrementCounter.ts. It does not use the wallet at all. It creates a throwaway local sender (client.newWallet(...)), publishes a plain increment note as that sender's own output note (TransactionRequestBuilder().withOwnOutputNotes(...)), then consumes the note as the counter (client.newConsumeTransactionRequest([note])). Both transactions are submitted by the local WebClient via submitNewTransactionWithProver(accountId, request, prover) (remote prover), never by the wallet, so useWaitForCommit doesn't apply and the template polls the counter's storage map instead. This mirrors the project-template increment_count reference.
The note APIs in that hook (use as the reference): the JS NoteMetadata constructor is attachment-less — new NoteMetadata(sender, noteType, tag). Build the note with new Note(new NoteAssets(), metadata, recipient). The increment note carries no attachment and uses tag 0; the counter is a plain public NoAuth account, so anyone can consume the note against it with no signature. (Attachments still exist for other uses — NoteAttachment.fromWord(scheme, word) / fromWords(scheme, words), read back via .toWords(), or createNoteAttachment(...) — but the increment does not need one. v0.15 removed the network-account model, so there is no network-execution targeting.)
Two hard requirements (don't regress): (1) the client runs with useWorker: false on MidenProvider. The default worker shim keeps a separate in-memory SMT forest per thread; consuming against an imported (not locally-created) account applies a delta transaction whose apply step looks the account up in the executing (worker) forest, which never contains the late-imported counter, so it fails with account data wasn't found (web-sdk#222). One thread means one forest, which fixes it. (2) Submits go through the remote prover (submitNewTransactionWithProver) so the worker-less single thread only pays local execution, not minutes of local proving. The increment works end-to-end on v0.15 (verified on testnet); there is no INCREMENT_ONCHAIN_BLOCKED flag.
Unified Signer Interface
Works with any signer provider above. useSigner() returns null in local-keystore mode (no signer provider mounted), so guard before destructuring:
import { useSigner } from"@miden-sdk/react";
const signer = useSigner();
if (!signer) returnnull; // local keystore mode — no external signerconst { isConnected, connect, disconnect, name } = signer;
if (!isConnected) {
return<buttononClick={connect}>Connect {name}</button>;
}
Building a Custom Signer
Implement SignerContextValue via SignerContext.Provider:
storeName — Unique string per user (isolates IndexedDB data between users)
accountConfig — { publicKeyCommitment: Uint8Array; storageMode: AccountStorageMode; ... } (storage mode is an AccountStorageMode instance, e.g. AccountStorageMode.private(), not a string)
signCb — Callback that signs transaction data with your key management service
connect / disconnect — Session lifecycle handlers
Custom Account Components
Attach application-specific AccountComponent instances (e.g., DEX logic from .masp packages) to accounts created by the signer:
SignerAccountConfig has an accountType field, but it is ignored — account kind and code mutability are not encoded in the account, so visibility comes solely from storageMode. Omit it.
Components are appended to the AccountBuilder after the default basic wallet component. The field is optional — omitting it preserves default behavior.
Which Signer to Choose
Signer
Auth Method
Keys Stored
Best For
Local keystore (default)
None
Browser IndexedDB
Development, demos
Para
EVM wallet
Para servers
Apps with existing EVM users
Turnkey
Passkey (biometric)
Turnkey servers
Consumer apps, no seed phrases
MidenFi Wallet
Browser extension
Extension
Power users with MidenFi wallet
Custom
Your choice
Your infrastructure
Enterprise, custom auth flows
Key trade-off: Local keystore requires no setup but keys are lost if the user clears browser data. External signers persist keys server-side but add a dependency.