Use mera with wagmi
The mera connector turns a passkey account into a wagmi connection, so wagmi’s actions and hooks sign with it as they do with a wallet app.
Prerequisites:
@category-labs/mera,@wagmi/core3, andvieminstalled. React apps installwagmi, which brings@wagmi/core.- A PRF-capable authenticator (authenticator support).
Add the connector
Section titled “Add the connector”import { function mera({ rp, accountIndex, webAuthnClient, keepSessionOpen, }: MeraOptions): CreateConnectorFn<Provider, Properties, StorageItem>
Creates a wagmi connector for a passkey account: an EVM account whose key
derives from a passkey's PRF output.
mera } from "@category-labs/mera/wagmi";import { function createConfig<const chains extends readonly [Chain, ...Chain[]], transports extends Record<chains[number]["id"], Transport>, const connectorFns extends readonly CreateConnectorFn[]>(parameters: CreateConfigParameters<chains, transports, connectorFns>): Config<chains, transports, connectorFns>
createConfig, function http<rpcSchema extends RpcSchema | undefined = undefined, raw extends boolean = false>(url?: string | undefined, config?: HttpTransportConfig<rpcSchema, raw>): HttpTransport<rpcSchema, raw>
http } from "@wagmi/core";import { const sepolia: { blockExplorers: { readonly default: { readonly name: "Etherscan"; readonly url: "https://sepolia.etherscan.io"; readonly apiUrl: "https://api-sepolia.etherscan.io/api"; }; }; blockTime?: number | undefined | undefined; contracts: { readonly multicall3: { readonly address: "0xca11bde05977b3631167028862be2a173976ca11"; readonly blockCreated: 751532; }; readonly ensUniversalResolver: { readonly address: "0xeeeeeeee14d718c2b47d9923deab1335e144eeee"; readonly blockCreated: 8928790; }; }; ... 16 more ...; verifyHash?: ((client: Client<...>, parameters: VerifyHashParameters) => Promise<boolean>) | undefined;}
sepolia } from "viem/chains";
const const rpId: "account.example.com"
rpId = "account.example.com";
export const const config: Config<readonly [{ blockExplorers: { readonly default: { readonly name: "Etherscan"; readonly url: "https://sepolia.etherscan.io"; readonly apiUrl: "https://api-sepolia.etherscan.io/api"; }; }; blockTime?: number | undefined | undefined; contracts: { readonly multicall3: { readonly address: "0xca11bde05977b3631167028862be2a173976ca11"; readonly blockCreated: 751532; }; readonly ensUniversalResolver: { readonly address: "0xeeeeeeee14d718c2b47d9923deab1335e144eeee"; readonly blockCreated: 8928790; }; }; ... 16 more ...; verifyHash?: ((client: Client<...>, parameters: VerifyHashParameters) => Promise<boolean>) | undefined;}], { ...;}, readonly [...]>
config = createConfig<readonly [{ blockExplorers: { readonly default: { readonly name: "Etherscan"; readonly url: "https://sepolia.etherscan.io"; readonly apiUrl: "https://api-sepolia.etherscan.io/api"; }; }; blockTime?: number | undefined | undefined; contracts: { readonly multicall3: { readonly address: "0xca11bde05977b3631167028862be2a173976ca11"; readonly blockCreated: 751532; }; readonly ensUniversalResolver: { readonly address: "0xeeeeeeee14d718c2b47d9923deab1335e144eeee"; readonly blockCreated: 8928790; }; }; ... 16 more ...; verifyHash?: ((client: Client<...>, parameters: VerifyHashParameters) => Promise<boolean>) | undefined;}], { ...;}, readonly [...]>(parameters: CreateConfigParameters<...>): Config<...>
createConfig({ chains: readonly [{ blockExplorers: { readonly default: { readonly name: "Etherscan"; readonly url: "https://sepolia.etherscan.io"; readonly apiUrl: "https://api-sepolia.etherscan.io/api"; }; }; blockTime?: number | undefined | undefined; contracts: { readonly multicall3: { readonly address: "0xca11bde05977b3631167028862be2a173976ca11"; readonly blockCreated: 751532; }; readonly ensUniversalResolver: { readonly address: "0xeeeeeeee14d718c2b47d9923deab1335e144eeee"; readonly blockCreated: 8928790; }; }; ... 16 more ...; verifyHash?: ((client: Client<...>, parameters: VerifyHashParameters) => Promise<boolean>) | undefined;}]
chains: [const sepolia: { blockExplorers: { readonly default: { readonly name: "Etherscan"; readonly url: "https://sepolia.etherscan.io"; readonly apiUrl: "https://api-sepolia.etherscan.io/api"; }; }; blockTime?: number | undefined | undefined; contracts: { readonly multicall3: { readonly address: "0xca11bde05977b3631167028862be2a173976ca11"; readonly blockCreated: 751532; }; readonly ensUniversalResolver: { readonly address: "0xeeeeeeee14d718c2b47d9923deab1335e144eeee"; readonly blockCreated: 8928790; }; }; ... 16 more ...; verifyHash?: ((client: Client<...>, parameters: VerifyHashParameters) => Promise<boolean>) | undefined;}
sepolia], connectors?: readonly [CreateConnectorFn<Provider, Properties, StorageItem>] | undefined
connectors: [function mera({ rp, accountIndex, webAuthnClient, keepSessionOpen, }: MeraOptions): CreateConnectorFn<Provider, Properties, StorageItem>
Creates a wagmi connector for a passkey account: an EVM account whose key
derives from a passkey's PRF output.
mera({ rp: PasskeyRelyingParty
Relying party the passkeys belong to. rp.name is shown when a passkey
is created.
rp: { id: string
Relying party ID: the host the passkey is scoped to.
id: const rpId: "account.example.com"
rpId, name: string
Relying party name the authenticator may show.
name: "Example" } })], transports: { 11155111: HttpTransport<undefined, false>;}
transports: { [const sepolia: { blockExplorers: { readonly default: { readonly name: "Etherscan"; readonly url: "https://sepolia.etherscan.io"; readonly apiUrl: "https://api-sepolia.etherscan.io/api"; }; }; blockTime?: number | undefined | undefined; contracts: { readonly multicall3: { readonly address: "0xca11bde05977b3631167028862be2a173976ca11"; readonly blockCreated: 751532; }; readonly ensUniversalResolver: { readonly address: "0xeeeeeeee14d718c2b47d9923deab1335e144eeee"; readonly blockCreated: 8928790; }; }; ... 16 more ...; verifyHash?: ((client: Client<...>, parameters: VerifyHashParameters) => Promise<boolean>) | undefined;}
sepolia.id: 11155111
id]: http<undefined, false>(url?: string | undefined, config?: HttpTransportConfig<undefined, false> | undefined): HttpTransport<undefined, false>
http() },});
export const [const passkey: Connector<CreateConnectorFn<Provider, Properties, StorageItem>>
passkey] = const config: Config<readonly [{ blockExplorers: { readonly default: { readonly name: "Etherscan"; readonly url: "https://sepolia.etherscan.io"; readonly apiUrl: "https://api-sepolia.etherscan.io/api"; }; }; blockTime?: number | undefined | undefined; contracts: { readonly multicall3: { readonly address: "0xca11bde05977b3631167028862be2a173976ca11"; readonly blockCreated: 751532; }; readonly ensUniversalResolver: { readonly address: "0xeeeeeeee14d718c2b47d9923deab1335e144eeee"; readonly blockCreated: 8928790; }; }; ... 16 more ...; verifyHash?: ((client: Client<...>, parameters: VerifyHashParameters) => Promise<boolean>) | undefined;}], { 11155111: HttpTransport<undefined, false>;}, readonly [...]>
config.connectors: readonly [Connector<CreateConnectorFn<Provider, Properties, StorageItem>>]
connectors;Create an account
Section titled “Create an account”connect with create creates a passkey and connects its account:
import { function connect<config extends Config, connector extends Connector | CreateConnectorFn, withCapabilities extends boolean = false>(config: config, parameters: ConnectParameters<config, connector, withCapabilities>): Promise<ConnectReturnType<config, connector, withCapabilities>>
connect } from "@wagmi/core";
const { const accounts: readonly [`0x${string}`, ...`0x${string}`[]]
accounts } = await connect<Config, Connector<CreateConnectorFn<Provider, Properties, StorageItem>>, false>(config: Config, parameters: ConnectParameters<Config, Connector<CreateConnectorFn<Provider, Properties, StorageItem>>, false, Omit<{ chainId?: number | undefined; isReconnecting?: boolean | undefined; withCapabilities?: boolean | undefined; create?: CreatePasskey | undefined; immediate?: boolean | undefined;}, "isReconnecting">>): Promise<...>
connect(const config: Config
config, { connector: CreateConnectorFn | Connector<CreateConnectorFn<Provider, Properties, StorageItem>>
connector: const passkey: Connector<CreateConnectorFn<Provider, Properties, StorageItem>>
passkey, create?: CreatePasskey | undefined
Creates a passkey for this user and connects its account. Without it,
connecting asks for any passkey the relying party already has. When the
second prompt that some authenticators need is dismissed, the new
passkey stays, and a later connect without create offers it.
create: { user: { name: string; displayName: string;}
user: { name: string
User name displayed or stored by the authenticator.
name: "account@example.com", displayName: string
Human-readable display name for the authenticator UI.
displayName: "Example account" }, },});A returning visitor connects without create, and the browser offers the passkeys it holds for the relying party:
import { function connect<config extends Config, connector extends Connector | CreateConnectorFn, withCapabilities extends boolean = false>(config: config, parameters: ConnectParameters<config, connector, withCapabilities>): Promise<ConnectReturnType<config, connector, withCapabilities>>
connect } from "@wagmi/core";
await connect<Config, Connector<CreateConnectorFn<Provider, Properties, StorageItem>>, false>(config: Config, parameters: ConnectParameters<Config, Connector<CreateConnectorFn<Provider, Properties, StorageItem>>, false, Omit<{ chainId?: number | undefined; isReconnecting?: boolean | undefined; withCapabilities?: boolean | undefined; create?: CreatePasskey | undefined; immediate?: boolean | undefined;}, "isReconnecting">>): Promise<...>
connect(const config: Config
config, { connector: CreateConnectorFn | Connector<CreateConnectorFn<Provider, Properties, StorageItem>>
connector: const passkey: Connector<CreateConnectorFn<Provider, Properties, StorageItem>>
passkey });With immediate: true, connect rejects at once with UserRejectedRequestError when this device holds no passkey, including when the passkey is on a phone; Create passkey accounts shows the one-button flow built on it.
Sign and send
Section titled “Sign and send”Signing works through wagmi’s usual actions, and each one asks for the passkey:
import { function sendTransaction<config extends Config, chainId extends config["chains"][number]["id"]>(config: config, parameters: SendTransactionParameters<config, chainId>): Promise<SendTransactionReturnType>
sendTransaction, function signMessage(config: Config, parameters: SignMessageParameters): Promise<SignMessageReturnType>
signMessage } from "@wagmi/core";import { function parseEther(ether: string, unit?: "wei" | "gwei"): bigint
Converts a string representation of ether to numerical wei.
parseEther } from "viem";
const const signature: `0x${string}`
signature = await function signMessage(config: Config, parameters: SignMessageParameters): Promise<SignMessageReturnType>
signMessage(const config: Config
config, { message: SignableMessage
message: "Sign in to Example" });
const const recipient: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
recipient = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8";const const hash: `0x${string}`
hash = await sendTransaction<Config, number>(config: Config, parameters: { type?: "legacy" | undefined; account?: `0x${string}` | Account | null | undefined; to?: `0x${string}` | null | undefined; data?: `0x${string}` | undefined; nonce?: number | undefined; value?: bigint | undefined; blobs?: undefined; blobVersionedHashes?: undefined; kzg?: undefined; gasPrice?: bigint | undefined; maxFeePerBlobGas?: undefined | undefined; maxFeePerGas?: undefined | undefined; maxPriorityFeePerGas?: undefined | undefined; accessList?: undefined; sidecars?: undefined; authorizationList?: undefined; assertChainId?: boolean | undefined | undefined; dataSuffix?: `0x${string}` | undefined; chainId?: number | undefined; connector?: Connector | undefined; gas?: bigint | ... 1 more ... | undefined;} | ... 4 more ... | { ...;}): Promise<SendTransactionReturnType>
sendTransaction(const config: Config
config, { to: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
to: const recipient: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
recipient, value: bigint
value: function parseEther(ether: string, unit?: "wei" | "gwei"): bigint
Converts a string representation of ether to numerical wei.
parseEther("0.01"),});After a page load, reconnect, which wagmi’s React provider runs on mount, restores the account without a prompt.
Sign without a prompt each time
Section titled “Sign without a prompt each time”mera({ rp, keepSessionOpen: true }) keeps the signing session that connecting opens, so signing after a connect shows no prompt and only the first signature after a page load asks for the passkey. The derived key stays in memory until disconnect or a page unload.
Handle a dismissed prompt
Section titled “Handle a dismissed prompt”A dismissed or timed-out prompt rejects with viem’s UserRejectedRequestError, the error a wallet app gives when the person declines. Other failures reject with a MeraError, such as PRF_UNAVAILABLE for an authenticator without PRF:
import { class MeraError
Error thrown by this package.
MeraError } from "@category-labs/mera";import { function connect<config extends Config, connector extends Connector | CreateConnectorFn, withCapabilities extends boolean = false>(config: config, parameters: ConnectParameters<config, connector, withCapabilities>): Promise<ConnectReturnType<config, connector, withCapabilities>>
connect } from "@wagmi/core";import { class UserRejectedRequestError
UserRejectedRequestError } from "viem";
try { await connect<Config, Connector<CreateConnectorFn<Provider, Properties, StorageItem>>, false>(config: Config, parameters: ConnectParameters<Config, Connector<CreateConnectorFn<Provider, Properties, StorageItem>>, false, Omit<{ chainId?: number | undefined; isReconnecting?: boolean | undefined; withCapabilities?: boolean | undefined; create?: CreatePasskey | undefined; immediate?: boolean | undefined;}, "isReconnecting">>): Promise<...>
connect(const config: Config
config, { connector: CreateConnectorFn | Connector<CreateConnectorFn<Provider, Properties, StorageItem>>
connector: const passkey: Connector<CreateConnectorFn<Provider, Properties, StorageItem>>
passkey });} catch (var error: unknown
error) { if (var error: unknown
error instanceof class UserRejectedRequestError
UserRejectedRequestError) { // The person closed the prompt, or had no passkey to offer. } else if (var error: unknown
error instanceof class MeraError
Error thrown by this package.
MeraError && var error: MeraError
error.MeraError.code: MeraErrorCode
Stable machine-readable category for the failure.
code === "PRF_UNAVAILABLE") { // Point at a PRF-capable authenticator. } else { throw var error: unknown
error; }}Pad gas estimates
Section titled “Pad gas estimates”The connector sends transactions through viem with the config’s chain, so a chain’s prepareTransactionRequest hook applies to them. On a chain that charges the full gas limit, such as Monad, the hook can add a margin to each estimate:
import { function defineChain<formatters extends ChainFormatters, const chain extends Chain<formatters>>(chain: chain): DefineChainReturnType<Assign<Chain<undefined>, chain>>
defineChain } from "viem";import { const monad: { blockExplorers: { readonly default: { readonly name: "Monadscan"; readonly url: "https://monadscan.com"; readonly apiUrl: "https://api.etherscan.io/v2/api?chainid=143"; }; readonly monadvision: { readonly name: "MonadVision"; readonly url: "https://monadvision.com"; }; }; blockTime: 300; contracts: { readonly multicall3: { readonly address: "0xcA11bde05977b3631167028862bE2a173976CA11"; readonly blockCreated: 9248132; }; }; ... 16 more ...; verifyHash?: ((client: Client<...>, parameters: VerifyHashParameters) => Promise<boolean>) | undefined;}export monad
monad as const monadBase: { blockExplorers: { readonly default: { readonly name: "Monadscan"; readonly url: "https://monadscan.com"; readonly apiUrl: "https://api.etherscan.io/v2/api?chainid=143"; }; readonly monadvision: { readonly name: "MonadVision"; readonly url: "https://monadvision.com"; }; }; blockTime: 300; contracts: { readonly multicall3: { readonly address: "0xcA11bde05977b3631167028862bE2a173976CA11"; readonly blockCreated: 9248132; }; }; ... 16 more ...; verifyHash?: ((client: Client<...>, parameters: VerifyHashParameters) => Promise<boolean>) | undefined;}
monadBase } from "viem/chains";
const const monad: { blockExplorers: { readonly default: { readonly name: "Monadscan"; readonly url: "https://monadscan.com"; readonly apiUrl: "https://api.etherscan.io/v2/api?chainid=143"; }; readonly monadvision: { readonly name: "MonadVision"; readonly url: "https://monadvision.com"; }; }; blockTime: 300; contracts: { readonly multicall3: { readonly address: "0xcA11bde05977b3631167028862bE2a173976CA11"; readonly blockCreated: 9248132; }; }; ... 16 more ...; readonly formatters?: undefined | undefined;}
monad = defineChain<ChainFormatters, { readonly prepareTransactionRequest: [(request: PrepareTransactionRequestParameters) => Promise<{ gas: bigint | undefined; type?: "legacy" | undefined; data?: `0x${string}` | undefined; nonce?: number | undefined; to?: `0x${string}` | null | undefined; value?: bigint | undefined; blobs?: undefined; blobVersionedHashes?: undefined; kzg?: undefined; gasPrice?: bigint | undefined; maxFeePerBlobGas?: undefined | undefined; maxFeePerGas?: undefined | undefined; maxPriorityFeePerGas?: undefined | undefined; accessList?: undefined; sidecars?: undefined; ... 5 more ...; chainId?: number | undefined; } | ... 4 more ... | { ...; }>, { ...; }]; ... 18 more ...; readonly verifyHash?: ((client: Client<...>, parameters: VerifyHashParameters) => Promise<boolean>) | undefined;}>(chain: { ...;}): { ...;}
defineChain({ ...const monadBase: { blockExplorers: { readonly default: { readonly name: "Monadscan"; readonly url: "https://monadscan.com"; readonly apiUrl: "https://api.etherscan.io/v2/api?chainid=143"; }; readonly monadvision: { readonly name: "MonadVision"; readonly url: "https://monadvision.com"; }; }; blockTime: 300; contracts: { readonly multicall3: { readonly address: "0xcA11bde05977b3631167028862bE2a173976CA11"; readonly blockCreated: 9248132; }; }; ... 16 more ...; verifyHash?: ((client: Client<...>, parameters: VerifyHashParameters) => Promise<boolean>) | undefined;}
monadBase, prepareTransactionRequest: [(request: PrepareTransactionRequestParameters) => Promise<{ gas: bigint | undefined; type?: "legacy" | undefined; data?: `0x${string}` | undefined; nonce?: number | undefined; to?: `0x${string}` | null | undefined; value?: bigint | undefined; blobs?: undefined; blobVersionedHashes?: undefined; kzg?: undefined; gasPrice?: bigint | undefined; maxFeePerBlobGas?: undefined | undefined; maxFeePerGas?: undefined | undefined; maxPriorityFeePerGas?: undefined | undefined; accessList?: undefined; sidecars?: undefined; authorizationList?: undefined; nonceManager?: NonceManager | undefined; parameters?: readonly PrepareTransactionRequestParameterType[] | undefined; account?: `0x${string}` | ... 2 more ... | undefined; chain: Chain | ... 1 more ... | undefined; chainId?: number | undefined;} | ... 4 more ... | { ...;}>, { ...;}]
prepareTransactionRequest: [ async (request: PrepareTransactionRequestParameters
request) => ({ ...request: PrepareTransactionRequestParameters
request, gas: bigint | undefined
gas: request: PrepareTransactionRequestParameters
request.gas?: bigint | undefined
Gas provided for transaction execution
gas === var undefined
undefined ? var undefined
undefined : (request: PrepareTransactionRequestParameters
request.gas?: bigint
Gas provided for transaction execution
gas * 11n) / 10n, }), { runAt: readonly ["afterFillParameters"]
runAt: ["afterFillParameters"] }, ],});Disconnect
Section titled “Disconnect”disconnect(config) deletes the stored account, so the next page load does not reconnect.
See also
Section titled “See also”- mera: every provider method the connector answers, and its limits.
- Send a transaction with viem: the same account in an app without wagmi.
- Signing sessions: how long a session holds the key, and the trade-off.