> ## Documentation Index
> Fetch the complete documentation index at: https://docs.monad.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# 如何使用 Mera 创建 passkey 账户

> 使用 passkey 创建和恢复 Monad 账户，无需助记词。

[mera](https://mera.category.xyz/) 从 passkey 派生 EVM 账户。用户使用 Face ID、Touch ID 或安全密钥创建账户，并且同一个 passkey 在每次登录时都会再生成相同的账户。

这些账户是普通的 EOA。没有需要部署的东西，也没有需要运行的 bundler 或 MPC 服务。一旦您拥有了地址，其余部分就是针对 Monad RPC 使用普通的 viem。

<Note>
  **先试试演示**

  建议您检查[演示](https://mera.category.xyz/demo/)是否能与您计划支持的浏览器和设备上的 passkey 提供商配合使用。[Authenticator 支持](https://mera.category.xyz/authenticator-support/)页面列出了已测试过的组合。
</Note>

<Tip>
  在构建原生移动应用？请参阅[如何在 React Native 中使用 Mera](/zh/guides/mera/react-native)，了解平台关联、原生 passkey API 以及跨设备账户复用。
</Tip>

## 安装

```bash theme={null}
npm install @category-labs/mera viem @scure/bip32 @scure/bip39
```

## 要求

* HTTPS，或在开发中使用 `localhost`。
* 一个支持 WebAuthn PRF 扩展的 passkey 提供商，例如 iCloud Keychain、1Password 或 Google Password Manager。[Authenticator 支持](https://mera.category.xyz/authenticator-support/)页面列出了完整列表。

<Warning>
  **桌面版 Chrome**

  在桌面版 Chrome 上，只有保存到 Google Password Manager 的 passkey 才会返回 PRF。当 Chrome 保存到本地配置文件时，passkey 虽会被创建，但 mera 会抛出 `PRF_UNAVAILABLE`。这是最常见的设置失败原因。
</Warning>

## 创建 passkey

`createPasskeyWithPrfOutput` 提示用户创建 passkey，并返回 32 字节的密钥。

```ts theme={null}
import { createPasskeyWithPrfOutput } from "@category-labs/mera";

const created = await createPasskeyWithPrfOutput({
  rp: { id: location.hostname, name: "My Monad App" },
  user: { name: "player@example.com", displayName: "Player One" },
});

// created.prfOutput — Uint8Array(32), the root of every account
localStorage.setItem(
  "app.credential",
  JSON.stringify({
    credentialId: created.credentialId,
    transports: created.transports,
  }),
);
```

每次调用都会创建一个新的 passkey，每个 passkey 都会产生一组不同的账户。在初次引导过程中运行一次，并存储返回的 `credentialId`。回访时使用带有该凭据的 `getPasskeyPrfOutput`，这在[登录](#signing-in)一节中有介绍。

凭据元数据不包含任何密钥材料，因此使用 `localStorage` 存储是可以的。在下一次登录时将其传回，可以让浏览器直接跳到相同的 passkey，而不是要求用户挑选。

## 派生密钥

由 passkey 返回的 32 字节 `prfOutput` 可用于派生标准的 BIP-44 账户。以这种方式派生可以保持账户的可移植性，因此导出的助记词可以导入 MetaMask 或 Rabby，并产生相同的地址。

```ts theme={null}
import { HDKey } from "@scure/bip32";
import { entropyToMnemonic, mnemonicToSeedSync } from "@scure/bip39";
import { wordlist } from "@scure/bip39/wordlists/english";

function deriveEvmKey(prfOutput: Uint8Array, index = 0): Uint8Array {
  const seed = mnemonicToSeedSync(entropyToMnemonic(prfOutput, wordlist));
  const node = HDKey.fromMasterSeed(seed).derive(`m/44'/60'/0'/0/${index}`);
  if (node.privateKey === null) throw new Error("derivation produced no key");
  return node.privateKey;
}
```

递增 `index` 以从同一个 passkey 派生更多账户。

## 发送交易

签名会话持有密钥，`toViemAccount` 将其包装为 viem 的 `LocalAccount`。它与任何其他 viem 账户表现一致，支持相同的签名方法。

```ts theme={null}
import { createSecp256k1SigningSession } from "@category-labs/mera";
import { toViemAccount } from "@category-labs/mera/viem";
import { createWalletClient, http, parseEther } from "viem";
import { monadTestnet } from "viem/chains";

const session = createSecp256k1SigningSession({
  privateKey: deriveEvmKey(created.prfOutput),
});

const client = createWalletClient({
  account: toViemAccount(session),
  chain: monadTestnet,
  transport: http(),
});

const hash = await client.sendTransaction({
  to: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
  value: parseEther("0.01"),
  gas: 21_000n,
});

session.end();
```

`end()` 会清零密钥，并且无法撤销，因此下一笔交易需要一个新会话。要保持会话开启多久，请参见[整合起来](#putting-it-together)。

主网使用 `monad` 而不是 `monadTestnet`。两者均在 `viem/chains` 中提供。

| 网络            | 链 ID  | viem 导出        |
| ------------- | ----- | -------------- |
| Monad         | 143   | `monad`        |
| Monad Testnet | 10143 | `monadTestnet` |

<Tip>
  **Gas**

  Monad 按您声明的 `gasLimit` 收费，而不是按实际使用的 gas。请传入显式的 `gas` 值，例如原生转账使用 `21_000n`，而不是使用估算值。参见 [Gas 定价](/zh/developer-essentials/gas-pricing)。
</Tip>

## 登录

```ts theme={null}
import { getPasskeyPrfOutput } from "@category-labs/mera";

const stored = localStorage.getItem("app.credential");
const known = stored ? JSON.parse(stored) : undefined;

const { prfOutput, credentialId } = await getPasskeyPrfOutput({
  rpId: location.hostname,
  credential: known,
});

// Nothing stored? The browser may have used a different passkey, so record it.
localStorage.setItem(
  "app.credential",
  JSON.stringify(known?.credentialId === credentialId ? known : { credentialId }),
);
```

在新设备上，`getPasskeyPrfOutput` 会以可发现模式提示 authenticator，用户应该能够选择先前保存的 passkey。

## 整合起来

有两种方式可以组织这些内容，区别在于密钥在内存中保留的时间长度。

### 保持会话

一次性提示 authenticator，派生密钥，并在用户登录期间一直保持会话。这适合诸如交易之类的高频工作流程，其中每笔交易都提示是行不通的。密钥会在整个会话期间存在于页面内存中，任何页面上的脚本在其存活期间都可以用它签名。

```ts theme={null}
// src/lib/wallet.ts
import {
  createSecp256k1SigningSession,
  getPasskeyPrfOutput,
  type Secp256k1SigningSession,
} from "@category-labs/mera";
import { toViemAccount } from "@category-labs/mera/viem";
import { createWalletClient, http, parseEther, type Hex } from "viem";
import { monadTestnet } from "viem/chains";

let session: Secp256k1SigningSession | undefined;

export async function connect() {
  const stored = localStorage.getItem("app.credential");
  const { prfOutput } = await getPasskeyPrfOutput({
    rpId: location.hostname,
    credential: stored ? JSON.parse(stored) : undefined,
  });

  session = createSecp256k1SigningSession({
    privateKey: deriveEvmKey(prfOutput),
  });

  return toViemAccount(session).address;
}

export function send(to: Hex, mon: string) {
  if (!session) throw new Error("not connected");

  const client = createWalletClient({
    account: toViemAccount(session),
    chain: monadTestnet,
    transport: http(),
  });

  return client.sendTransaction({ to, value: parseEther(mon), gas: 21_000n });
}

export function disconnect() {
  session?.end();
  session = undefined;
}
```

在登出时调用 `disconnect()`，并考虑在空闲超时时也调用它。

### 每笔交易都提示

为每笔交易执行整套流程，并在发送后立即结束会话。密钥仅在发送期间存在，代价是每次都要弹出一个 passkey 提示。

```ts theme={null}
export async function send(to: Hex, mon: string) {
  const stored = localStorage.getItem("app.credential");
  const { prfOutput } = await getPasskeyPrfOutput({
    rpId: location.hostname,
    credential: stored ? JSON.parse(stored) : undefined,
  });

  using session = createSecp256k1SigningSession({
    privateKey: deriveEvmKey(prfOutput),
  });

  const client = createWalletClient({
    account: toViemAccount(session),
    chain: monadTestnet,
    transport: http(),
  });

  return client.sendTransaction({ to, value: parseEther(mon), gas: 21_000n });
}
```

`using` 会在作用域退出时结束会话。如果您的构建目标不支持显式资源管理，请在 `finally` 块中调用 `session.end()`。

## 错误

mera 会抛出带有 `code` 的 `MeraError`。请使用代码来识别错误，因为消息文本可能会在不同版本之间变化。

```ts theme={null}
import { isMeraError } from "@category-labs/mera";

if (isMeraError(error)) {
  switch (error.code) {
    case "PRF_UNAVAILABLE":          // unsupported passkey provider
    case "PASSKEY_OPERATION_FAILED": // prompt dismissed or failed; offer a retry
    case "CRYPTO_UNAVAILABLE":       // not a secure context
    case "SESSION_ENDED":            // signing call after end()
  }
}
```

## 注意事项

* passkey 与其创建时使用的 `rpId` 绑定。如果应用迁移到新域名，账户就无法再被派生出来，用户将需要用 24 词助记词来恢复它们。
* 上面的所有内容都是从 passkey 派生密钥。当密钥来自其他地方，比如用户已经拥有的助记词时，[secret vaults](https://mera.category.xyz/concepts/secret-vaults/) 会用 passkey 对其加密一次，并解密它。
* `createEd25519SigningSession` 和 `getSolanaAddress` 可以从同一个 passkey 派生 Solana 账户。

## 更多资源

您可以查看 [mera 文档](https://mera.category.xyz/)了解更多详情。该项目是开源的，因此您也可以[在 GitHub 上查看代码](https://github.com/category-labs/mera)。
