diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 5394a8b..8a4a564 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,4 +1,6 @@ import { HeadConfig, defineConfig } from 'vitepress'; +import zhNav from './zh.nav'; +import zhSidebar from './zh.sidebar'; // https://vitepress.dev/reference/site-config export default defineConfig({ @@ -198,7 +200,7 @@ export default defineConfig({ { text: 'Rename from Frames v2', link: '/reference/frames-redirect', - } + }, ], }, { @@ -495,8 +497,15 @@ export default defineConfig({ { text: 'Mini Apps', items: [ - { text: 'Specification', link: 'https://miniapps.farcaster.xyz/docs/specification', target: '_self' }, - { text: 'Rename from Frames v2', link: '/reference/frames-redirect'} + { + text: 'Specification', + link: 'https://miniapps.farcaster.xyz/docs/specification', + target: '_self', + }, + { + text: 'Rename from Frames v2', + link: '/reference/frames-redirect', + }, ], }, { @@ -721,4 +730,19 @@ export default defineConfig({ vite: { assetsInclude: ['**/*.avifs'], }, + locales: { + root: { + label: 'English', + lang: 'en', + }, + zh: { + label: '简体中文', + lang: 'zh', + link: '/zh/', + themeConfig: { + nav: zhNav, + sidebar: zhSidebar, + }, + }, + }, }); diff --git a/docs/.vitepress/zh.nav.js b/docs/.vitepress/zh.nav.js new file mode 100644 index 0000000..3463e64 --- /dev/null +++ b/docs/.vitepress/zh.nav.js @@ -0,0 +1,27 @@ +export default [ + { + "text": "学习", + "link": "/zh/learn/" + }, + { + "text": "构建应用", + "link": "/zh/developers/" + }, + { + "text": "AuthKit", + "link": "/zh/auth-kit/" + }, + { + "text": "Hubble", + "link": "/zh/hubble/hubble", + "activeMatch": "/hubble/" + }, + { + "text": "参考文档", + "link": "/zh/reference/" + }, + { + "text": "开发者交流", + "link": "https://warpcast.com/~/group/X2P7HNc4PHTriCssYHNcmQ" + } +] \ No newline at end of file diff --git a/docs/.vitepress/zh.sidebar.js b/docs/.vitepress/zh.sidebar.js new file mode 100644 index 0000000..02195b0 --- /dev/null +++ b/docs/.vitepress/zh.sidebar.js @@ -0,0 +1,712 @@ +export default { + "/zh/learn/": [ + { + "text": "简介", + "items": [ + { + "text": "入门指南", + "link": "/zh/learn/" + } + ] + }, + { + "text": "核心概念", + "items": [ + { + "text": "迷你应用", + "link": "https://miniapps.farcaster.xyz/", + "target": "_self" + }, + { + "text": "账户", + "link": "/zh/learn/what-is-farcaster/accounts", + "target": "_self" + }, + { + "text": "用户名", + "link": "/zh/learn/what-is-farcaster/usernames", + "target": "_self" + }, + { + "text": "消息", + "link": "/zh/learn/what-is-farcaster/messages", + "target": "_self" + }, + { + "text": "频道", + "link": "/zh/learn/what-is-farcaster/channels", + "target": "_self" + }, + { + "text": "应用", + "link": "/zh/learn/what-is-farcaster/apps", + "target": "_self" + } + ] + }, + { + "text": "架构", + "items": [ + { + "text": "概述", + "link": "/zh/learn/architecture/overview" + }, + { + "text": "合约", + "link": "/zh/learn/architecture/contracts" + }, + { + "text": "枢纽", + "link": "/zh/learn/architecture/hubs" + }, + { + "text": "ENS域名", + "link": "/zh/learn/architecture/ens-names" + } + ] + }, + { + "text": "贡献指南", + "items": [ + { + "text": "概述", + "link": "/zh/learn/contributing/overview" + }, + { + "text": "治理", + "link": "/zh/learn/contributing/governance" + }, + { + "text": "FIP提案", + "link": "/zh/learn/contributing/fips" + } + ] + } + ], + "/zh/developers/": [ + { + "text": "概述", + "link": "/zh/developers/" + }, + { + "text": "资源", + "link": "/zh/developers/resources" + }, + { + "text": "迷你应用", + "items": [ + { + "text": "简介", + "link": "https://miniapps.farcaster.xyz", + "target": "_self" + }, + { + "text": "入门指南", + "link": "https://miniapps.farcaster.xyz/docs/getting-started", + "target": "_self" + }, + { + "text": "与钱包交互", + "link": "https://miniapps.farcaster.xyz/docs/guides/wallets", + "target": "_self" + }, + { + "text": "发送通知", + "link": "https://miniapps.farcaster.xyz/docs/guides/notifications", + "target": "_self" + }, + { + "text": "用户认证", + "link": "https://miniapps.farcaster.xyz/docs/guides/auth", + "target": "_self" + }, + { + "text": "规范", + "link": "https://miniapps.farcaster.xyz/docs/specification", + "target": "_self" + }, + { + "text": "从Frames v2重命名", + "link": "/zh/reference/frames-redirect" + } + ] + }, + { + "text": "旧版Frames", + "items": [ + { + "text": "简介", + "link": "/zh/developers/frames/" + }, + { + "text": "入门指南", + "link": "/zh/developers/frames/getting-started" + }, + { + "text": "规范", + "link": "/zh/developers/frames/spec" + }, + { + "text": "最佳实践", + "link": "/zh/developers/frames/best-practices" + }, + { + "text": "高级", + "link": "/zh/developers/frames/advanced" + }, + { + "text": "资源", + "link": "/zh/developers/frames/resources" + } + ] + }, + { + "text": "Farcaster登录", + "items": [ + { + "text": "简介", + "link": "/zh/developers/siwf/" + }, + { + "text": "AuthKit工具包", + "link": "/zh/auth-kit/" + } + ] + }, + { + "text": "Farcaster协议", + "items": [ + { + "text": "账户管理", + "collapsed": true, + "items": [ + { + "text": "创建账户", + "link": "/zh/developers/guides/accounts/create-account" + }, + { + "text": "创建账户密钥", + "link": "/zh/developers/guides/accounts/create-account-key" + }, + { + "text": "按名称查找账户", + "link": "/zh/developers/guides/accounts/find-by-name" + }, + { + "text": "更改Farcaster名称", + "link": "/zh/developers/guides/accounts/change-fname" + }, + { + "text": "更改托管地址", + "link": "/zh/developers/guides/accounts/change-custody" + }, + { + "text": "更改恢复地址", + "link": "/zh/developers/guides/accounts/change-recovery" + } + ] + }, + { + "text": "数据查询", + "collapsed": true, + "items": [ + { + "text": "获取账户消息", + "link": "/zh/developers/guides/querying/fetch-casts" + }, + { + "text": "获取账户资料", + "link": "/zh/developers/guides/querying/fetch-profile" + }, + { + "text": "获取频道广播", + "link": "/zh/developers/guides/querying/fetch-channel-casts" + } + ] + }, + { + "text": "数据写入", + "collapsed": true, + "items": [ + { + "text": "创建账户", + "link": "/zh/developers/guides/accounts/create-account" + }, + { + "text": "创建消息", + "link": "/zh/developers/guides/writing/messages" + }, + { + "text": "创建广播", + "link": "/zh/developers/guides/writing/casts" + }, + { + "text": "创建验证", + "link": "/zh/developers/guides/writing/verify-address" + }, + { + "text": "提交消息", + "link": "/zh/developers/guides/writing/submit-messages" + } + ] + }, + { + "text": "构建应用", + "collapsed": true, + "items": [ + { + "text": "同步至Postgres", + "link": "/zh/developers/guides/apps/replicate" + } + ] + }, + { + "text": "高级", + "collapsed": true, + "items": [ + { + "text": "按日统计注册量", + "link": "/zh/developers/guides/advanced/query-signups" + }, + { + "text": "解码密钥元数据", + "link": "/zh/developers/guides/advanced/decode-key-metadata" + } + ] + } + ] + }, + { + "text": "第三方服务", + "items": [ + { + "text": "Neynar", + "link": "/zh/reference/third-party/neynar/index" + } + ] + } + ], + "/zh/auth-kit/": [ + { + "text": "概述", + "items": [ + { + "text": "简介", + "link": "/zh/auth-kit/" + }, + { + "text": "示例", + "link": "/zh/auth-kit/examples" + } + ] + }, + { + "text": "快速开始", + "items": [ + { + "text": "安装", + "link": "/zh/auth-kit/installation" + }, + { + "text": "登录按钮", + "link": "/zh/auth-kit/sign-in-button" + }, + { + "text": "AuthKit提供者", + "link": "/zh/auth-kit/auth-kit-provider" + } + ] + }, + { + "text": "高级", + "items": [ + { + "text": "钩子函数", + "collapsed": true, + "items": [ + { + "text": "useSignIn", + "link": "/zh/auth-kit/hooks/use-sign-in" + }, + { + "text": "useSignInMessage", + "link": "/zh/auth-kit/hooks/use-sign-in-message" + }, + { + "text": "useProfile", + "link": "/zh/auth-kit/hooks/use-profile" + } + ] + }, + { + "text": "认证客户端", + "collapsed": true, + "items": [ + { + "text": "简介", + "link": "/zh/auth-kit/client/introduction" + }, + { + "text": "应用操作", + "collapsed": true, + "items": [ + { + "text": "AppClient", + "link": "/zh/auth-kit/client/app/client" + }, + { + "text": "createChannel", + "link": "/zh/auth-kit/client/app/create-channel" + }, + { + "text": "status", + "link": "/zh/auth-kit/client/app/status" + }, + { + "text": "watchStatus", + "link": "/zh/auth-kit/client/app/watch-status" + }, + { + "text": "verifySignInMessage", + "link": "/zh/auth-kit/client/app/verify-sign-in-message" + } + ] + }, + { + "text": "钱包操作", + "collapsed": true, + "items": [ + { + "text": "WalletClient", + "link": "/zh/auth-kit/client/wallet/client" + }, + { + "text": "parseSignInURI", + "link": "/zh/auth-kit/client/wallet/parse-sign-in-uri" + }, + { + "text": "buildSignInMessage", + "link": "/zh/auth-kit/client/wallet/build-sign-in-message" + }, + { + "text": "authenticate", + "link": "/zh/auth-kit/client/wallet/authenticate" + } + ] + } + ] + } + ] + } + ], + "/zh/hubble/": [ + { + "text": "开始使用", + "items": [ + { + "text": "Hubble", + "link": "/zh/hubble/hubble" + }, + { + "text": "迁移至Snapchain", + "link": "/zh/hubble/migrating" + }, + { + "text": "安装", + "link": "/zh/hubble/install" + }, + { + "text": "网络", + "link": "/zh/hubble/networks" + }, + { + "text": "监控", + "link": "/zh/hubble/monitoring" + }, + { + "text": "教程", + "link": "/zh/hubble/tutorials" + }, + { + "text": "故障排除", + "link": "/zh/hubble/troubleshooting" + } + ] + } + ], + "/zh/reference/": [ + { + "text": "参考", + "items": [ + { + "text": "概述", + "link": "/zh/reference/index" + } + ] + }, + { + "text": "迷你应用", + "items": [ + { + "text": "规范", + "link": "https://miniapps.farcaster.xyz/docs/specification", + "target": "_self" + }, + { + "text": "从Frames v2重命名", + "link": "/zh/reference/frames-redirect" + } + ] + }, + { + "text": "操作", + "items": [ + { + "text": "规范", + "link": "/zh/reference/actions/spec" + } + ] + }, + { + "text": "Warpcast", + "items": [ + { + "text": "API", + "link": "/zh/reference/warpcast/api" + }, + { + "text": "签名者请求", + "link": "/zh/reference/warpcast/signer-requests" + }, + { + "text": "意图URL", + "link": "/zh/reference/warpcast/cast-composer-intents" + }, + { + "text": "直接广播", + "link": "/zh/reference/warpcast/direct-casts" + }, + { + "text": "嵌入内容", + "link": "/zh/reference/warpcast/embeds" + }, + { + "text": "视频", + "link": "/zh/reference/warpcast/videos" + } + ] + }, + { + "text": "Hubble", + "items": [ + { + "text": "架构", + "link": "/zh/reference/hubble/architecture" + }, + { + "text": "数据类型", + "collapsed": true, + "items": [ + { + "text": "消息", + "link": "/zh/reference/hubble/datatypes/messages" + }, + { + "text": "事件", + "link": "/zh/reference/hubble/datatypes/events" + } + ] + }, + { + "text": "GRPC接口", + "collapsed": true, + "items": [ + { + "text": "使用GRPC接口", + "link": "/zh/reference/hubble/grpcapi/grpcapi" + }, + { + "text": "广播API", + "link": "/zh/reference/hubble/grpcapi/casts" + }, + { + "text": "反应API", + "link": "/zh/reference/hubble/grpcapi/reactions" + }, + { + "text": "链接API", + "link": "/zh/reference/hubble/grpcapi/links" + }, + { + "text": "用户数据API", + "link": "/zh/reference/hubble/grpcapi/userdata" + }, + { + "text": "用户名证明API", + "link": "/zh/reference/hubble/grpcapi/usernameproof" + }, + { + "text": "验证API", + "link": "/zh/reference/hubble/grpcapi/verification" + }, + { + "text": "消息API", + "link": "/zh/reference/hubble/grpcapi/message" + }, + { + "text": "Fids接口", + "link": "/zh/reference/hubble/grpcapi/fids" + }, + { + "text": "存储API", + "link": "/zh/reference/hubble/grpcapi/storagelimits" + }, + { + "text": "链上API", + "link": "/zh/reference/hubble/grpcapi/onchain" + }, + { + "text": "事件API", + "link": "/zh/reference/hubble/grpcapi/events" + }, + { + "text": "同步API", + "link": "/zh/reference/hubble/grpcapi/sync" + } + ] + }, + { + "text": "HTTP接口", + "collapsed": true, + "items": [ + { + "text": "使用HTTP接口", + "link": "/zh/reference/hubble/httpapi/httpapi" + }, + { + "text": "信息API", + "link": "/zh/reference/hubble/httpapi/info" + }, + { + "text": "广播API", + "link": "/zh/reference/hubble/httpapi/casts" + }, + { + "text": "反应API", + "link": "/zh/reference/hubble/httpapi/reactions" + }, + { + "text": "链接API", + "link": "/zh/reference/hubble/httpapi/links" + }, + { + "text": "用户数据API", + "link": "/zh/reference/hubble/httpapi/userdata" + }, + { + "text": "用户名证明API", + "link": "/zh/reference/hubble/httpapi/usernameproof" + }, + { + "text": "验证API", + "link": "/zh/reference/hubble/httpapi/verification" + }, + { + "text": "消息API", + "link": "/zh/reference/hubble/httpapi/message" + }, + { + "text": "Fids接口", + "link": "/zh/reference/hubble/httpapi/fids" + }, + { + "text": "存储API", + "link": "/zh/reference/hubble/httpapi/storagelimits" + }, + { + "text": "链上API", + "link": "/zh/reference/hubble/httpapi/onchain" + }, + { + "text": "事件API", + "link": "/zh/reference/hubble/httpapi/events" + } + ] + }, + { + "text": "复制器模式", + "link": "/zh/reference/replicator/schema" + } + ] + }, + { + "text": "合约", + "items": [ + { + "text": "概述", + "link": "/zh/reference/contracts/index" + }, + { + "text": "参考", + "collapsed": true, + "items": [ + { + "text": "ID网关", + "link": "/zh/reference/contracts/reference/id-gateway" + }, + { + "text": "ID注册表", + "link": "/zh/reference/contracts/reference/id-registry" + }, + { + "text": "密钥网关", + "link": "/zh/reference/contracts/reference/key-gateway" + }, + { + "text": "密钥注册表", + "link": "/zh/reference/contracts/reference/key-registry" + }, + { + "text": "存储注册表", + "link": "/zh/reference/contracts/reference/storage-registry" + }, + { + "text": "打包器", + "link": "/zh/reference/contracts/reference/bundler" + }, + { + "text": "签名密钥请求验证器", + "link": "/zh/reference/contracts/reference/signed-key-request-validator" + } + ] + }, + { + "text": "部署", + "link": "/zh/reference/contracts/deployments" + }, + { + "text": "常见问题", + "link": "/zh/reference/contracts/faq" + } + ] + }, + { + "text": "FName服务", + "items": [ + { + "text": "API参考", + "link": "/zh/reference/fname/api" + } + ] + }, + { + "text": "第三方服务", + "items": [ + { + "text": "Neynar", + "link": "/zh/reference/third-party/neynar/index" + } + ] + } + ] +} \ No newline at end of file diff --git a/docs/zh/auth-kit/auth-kit-provider.md b/docs/zh/auth-kit/auth-kit-provider.md new file mode 100644 index 0000000..c1710cf --- /dev/null +++ b/docs/zh/auth-kit/auth-kit-provider.md @@ -0,0 +1,36 @@ +# `AuthKitProvider` + +使用 Farcaster Auth 时,请将您的应用程序包裹在 `AuthKitProvider` 中。该提供者组件存储了关于您应用的配置信息,并使其可供 auth-kit 组件和钩子使用。 + +**注意:** 您必须创建一个 `AuthKitProvider` 才能使用 Farcaster Connect。别忘了在应用的顶层创建该组件。 + +```tsx +const config = { + domain: 'example.com', + siweUri: 'https://example.com/login', + rpcUrl: process.env.OP_MAINNET_RPC_URL, + relay: 'https://relay.farcaster.xyz', +}; + +const App = () => { + return ( + {/* 您的应用 */} + ); +}; +``` + +# 属性 + +| 属性 | 类型 | 必填 | 描述 | +| -------- | --------------- | ---- | ---------------------------------- | +| `config` | `AuthKitConfig` | 否 | 配置对象。请参阅下方表格中的选项。 | + +`config` 对象选项: + +| 参数 | 类型 | 必填 | 描述 | 默认值 | +| --------- | -------- | ---- | ----------------------------- | ----------------------------- | +| `domain` | `string` | 否 | 您的应用程序域名。 | `window.location.host` | +| `siweUri` | `string` | 否 | 您的应用程序登录 URL。 | `window.location.href` | +| `relay` | `string` | 否 | Farcaster Auth 中继服务器 URL | `https://relay.farcaster.xyz` | +| `rpcUrl` | `string` | 否 | Optimism RPC 服务器 URL | `https://mainnet.optimism.io` | +| `version` | `string` | 否 | Farcaster Auth 版本 | `v1` | diff --git a/docs/zh/auth-kit/client/app/client.md b/docs/zh/auth-kit/client/app/client.md new file mode 100644 index 0000000..5df4ba7 --- /dev/null +++ b/docs/zh/auth-kit/client/app/client.md @@ -0,0 +1,22 @@ +# App 客户端 + +如果您正在构建一个[关联应用](https://docs.farcaster.xyz/learn/what-is-farcaster/apps#connected-apps)并希望用户通过 Farcaster 登录,可以使用 `AppClient`。 + +通过 `AppClient`,您可以创建 Farcaster Auth 中继通道,生成深度链接以请求用户 Farcaster 钱包应用的签名,并验证返回的签名。 + +```ts +import { createAppClient, viemConnector } from '@farcaster/auth-client'; + +const appClient = createAppClient({ + relay: 'https://relay.farcaster.xyz', + ethereum: viemConnector(), +}); +``` + +## 参数 + +| 参数 | 类型 | 描述 | 必填 | +| ---------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- | +| `ethereum` | `EthereumConnector` |

以太坊连接器,用于查询 Farcaster 合约并验证智能合约钱包签名。当前 `@farcaster/auth-client` 仅提供 `viem` 连接器类型。

如需使用自定义 RPC,请将 RPC URL 传递给 viem 连接器。

| 是 | +| `relay` | `string` | 中继服务器 URL。默认为公共中继地址 `https://relay.farcaster.xyz` | 否 | +| `version` | `string` | Farcaster Auth 版本号。默认为 `"v1"` | 否 | diff --git a/docs/zh/auth-kit/client/app/create-channel.md b/docs/zh/auth-kit/client/app/create-channel.md new file mode 100644 index 0000000..488c21f --- /dev/null +++ b/docs/zh/auth-kit/client/app/create-channel.md @@ -0,0 +1,47 @@ +# `createChannel` + +创建一个 Farcaster 认证中继通道。 + +返回一个标识通道的密钥令牌,以及一个可展示给终端用户作为链接或二维码的 URI。 + +```ts +const channel = await appClient.createChannel({ + siweUri: 'https://example.com/login', + domain: 'example.com', +}); +``` + +## 参数 + +| 参数 | 类型 | 描述 | 必填 | 示例 | +| ---------------- | -------- | --------------------------------------------- | ---- | -------------------------------------- | +| `siweUri` | `string` | 应用程序的登录 URL。 | 是 | `https://example.com/login` | +| `domain` | `string` | 应用程序的域名。 | 是 | `example.com` | +| `nonce` | `string` | 自定义随机数。必须至少包含 8 个字母数字字符。 | 否 | `ESsxs6MaFio7OvqWb` | +| `notBefore` | `string` | 签名生效的开始时间。ISO 8601 格式的日期时间。 | 否 | `2023-12-20T23:21:24.917Z` | +| `expirationTime` | `string` | 签名失效的过期时间。ISO 8601 格式的日期时间。 | 否 | `2023-12-20T23:21:24.917Z` | +| `requestId` | `string` | 应用程序可用于引用登录请求的系统特定 ID。 | 否 | `8d0494d9-e0cf-402b-ab0a-394ac7fe07a0` | + +## 返回值 + +```ts +{ + response: Response; + data: { + channelToken: string; + url: string; + nonce: string; + } + isError: boolean; + error: Error; +} +``` + +| 参数 | 描述 | +| ------------------- | ----------------------------------------------------------------------------- | +| `response` | 来自 Connect 中继服务器的 HTTP 响应。 | +| `data.channelToken` | Connect 中继通道令牌 UUID。 | +| `data.url` | 展示给用户的 Sign in With Farcaster URL。在 v1 版本中链接到 Warpcast 客户端。 | +| `data.nonce` | 包含在 Sign in With Farcaster 消息中的随机数。 | +| `isError` | 当发生错误时为 true。 | +| `error` | `Error` 实例。 | diff --git a/docs/zh/auth-kit/client/app/status.md b/docs/zh/auth-kit/client/app/status.md new file mode 100644 index 0000000..e8eeb82 --- /dev/null +++ b/docs/zh/auth-kit/client/app/status.md @@ -0,0 +1,63 @@ +# `status` + +获取 Farcaster 认证请求的当前状态。 + +返回请求的当前状态:如果用户的 Farcaster 钱包应用尚未返回签名,则为 `'pending'`;一旦钱包应用返回响应,则为 `'completed'`。 + +在 `'completed'` 状态下,响应包含生成的 "Sign in With Farcaster" 消息、用户托管地址的签名、用户已验证的 fid 以及用户资料信息。 + +```ts +const status = await appClient.status({ + channelToken: '210f1718-427e-46a4-99e3-2207f21f83ec', +}); +``` + +## 参数 + +| 参数 | 类型 | 描述 | 必填 | 示例 | +| -------------- | -------- | ------------------------ | ---- | -------------------------------------- | +| `channelToken` | `string` | Farcaster 认证通道令牌。 | 是 | `8d0494d9-e0cf-402b-ab0a-394ac7fe07a0` | + +## 返回值 + +```ts +{ + response: Response + data: { + state: "pending"; + nonce: string; + } | { + state: "completed"; + nonce: string; + url: string; + message: string; + signature: `0x${string}`; + fid: number; + username?: string; + bio?: string; + displayName?: string; + pfpUrl?: string; + verifications?: Hex[]; + custody?: Hex; + } + isError: boolean + error: Error +} +``` + +| 参数 | 描述 | +| -------------------- | ---------------------------------------------------------------------------------- | +| `response` | 来自 Connect 中继服务器的 HTTP 响应。 | +| `data.state` | 登录请求的状态,`"pending"` 或 `"complete"` 之一 | +| `data.nonce` | SIWE 消息中使用的随机 nonce。如果你没有向钩子提供自定义 nonce 参数,则应读取此值。 | +| `data.message` | 生成的 SIWE 消息。 | +| `data.signature` | 用户 Warpcast 钱包生成的十六进制签名。 | +| `data.fid` | 用户的 Farcaster ID。 | +| `data.username` | 用户的 Farcaster 用户名。 | +| `data.bio` | 用户的 Farcaster 个人简介。 | +| `data.displayName` | 用户的 Farcaster 显示名称。 | +| `data.pfpUrl` | 用户的 Farcaster 头像 URL。 | +| `data.custody` | 用户的 FID 托管地址。 | +| `data.verifications` | 用户已验证地址的列表。 | +| `isError` | 当发生错误时为 true。 | +| `error` | `Error` 实例。 | diff --git a/docs/zh/auth-kit/client/app/verify-sign-in-message.md b/docs/zh/auth-kit/client/app/verify-sign-in-message.md new file mode 100644 index 0000000..88a04ad --- /dev/null +++ b/docs/zh/auth-kit/client/app/verify-sign-in-message.md @@ -0,0 +1,43 @@ +# `verifySignInMessage` + +验证一个 "使用 Farcaster 登录" 的消息。您的应用应在通过 Connect 通道读取用户 Farcaster 钱包提供的消息和签名后调用此函数,并检查验证是否成功。 + +返回解析后的 "使用 Farcaster 登录" 消息、用户的 fid 以及验证是否成功。 + +```ts +const { data, success, fid } = await appClient.verifySignInMessage({ + nonce: 'abcd1234', + domain: 'example.com', + message: 'example.com wants you to sign in with your Ethereum account…', + signature: '0x9335c3055d47780411a3fdabad293c68c84ea350a11794cd11fd51b…', +}); +``` + +## 参数 + +| 参数 | 类型 | 描述 | 必填 | +| ----------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ---- | +| `domain` | `string` | 您的应用程序域名。必须与提供的 SIWF 消息中的域名匹配。 | 是 | +| `nonce` | `string` | 自定义随机数。必须与提供的 SIWF 消息中的随机数匹配。 | 是 | +| `message` | `string` 或 `SiweMessage` | 要验证的 "使用 Farcaster 登录" 消息。可以是字符串或已解析的 `SiweMessage` 对象。您的应用应在请求完成后从 Connect 通道读取此值。 | 是 | +| `signature` | `Hex` | 用户 Farcaster 钱包提供的签名。您的应用应在请求完成后从 Connect 通道读取此值。 | 是 | + +## 返回值 + +```ts +{ + data: SiweMessage, + success: boolean, + fid: number + isError: boolean + error: Error +} +``` + +| 参数 | 描述 | +| --------- | --------------------------------------------- | +| `data` | 解析后的 SIWF 消息,作为 `SiweMessage` 对象。 | +| `success` | 如果提供的签名有效则为 true。 | +| `fid` | 用户的 FID。 | +| `isError` | 当发生错误时为 true。 | +| `error` | `Error` 实例。 | diff --git a/docs/zh/auth-kit/client/app/watch-status.md b/docs/zh/auth-kit/client/app/watch-status.md new file mode 100644 index 0000000..e519d20 --- /dev/null +++ b/docs/zh/auth-kit/client/app/watch-status.md @@ -0,0 +1,66 @@ +# `watchStatus` + +轮询 Farcaster Auth 请求的当前状态。 + +当状态变为 `'complete'` 时,该操作将返回最终的通道值,包括 "Sign In With Farcaster" 消息、签名和用户资料信息。 + +```ts +const status = await appClient.watchStatus({ + channelToken: '210f1718-427e-46a4-99e3-2207f21f83ec', + timeout: 60_000, + interval: 1_000, + onResponse: ({ response, data }) => { + console.log('Response code:', response.status); + console.log('Status data:', data); + }, +}); +``` + +## 参数 + +| 参数 | 类型 | 描述 | 必填 | 示例 | +| -------------- | ---------- | ------------------------------------------------------------------------------------------ | ---- | -------------------------------------- | +| `channelToken` | `string` | Farcaster Auth 通道令牌。 | 是 | `8d0494d9-e0cf-402b-ab0a-394ac7fe07a0` | +| `timeout` | `number` | 轮询超时时间(毫秒)。如果在超时前连接请求未完成,`watchStatus` 将返回错误。 | 否 | `300_000` | +| `interval` | `number` | 轮询间隔时间(毫秒)。客户端将按此频率检查更新。 | 否 | `1_000` | +| `onResponse` | `function` | 每次客户端轮询更新并从转发服务器收到响应时调用的回调函数。接收最新 `status` 请求的返回值。 | 否 | `({ data }) => console.log(data.fid)` | + +## 返回值 + +```ts +{ + response: Response + data: { + state: 'pending' | 'completed' + nonce: string + message?: string + signature?: Hex + fid?: number + username?: string + bio?: string + displayName?: string + pfpUrl?: string + custody?: Hex; + verifications?: Hex[]; + } + isError: boolean + error: Error +} +``` + +| 参数 | 描述 | +| -------------------- | --------------------------------------------------------------------- | +| `response` | 来自 Connect 转发服务器的 HTTP 响应。 | +| `data.state` | 登录请求的状态,`"pending"`(待处理)或 `"complete"`(已完成)。 | +| `data.nonce` | SIWE 消息中使用的随机数。如果未向钩子提供自定义随机数,则应读取此值。 | +| `data.message` | 生成的 SIWE 消息。 | +| `data.signature` | 用户 Warpcast 钱包生成的十六进制签名。 | +| `data.fid` | 用户的 Farcaster ID。 | +| `data.username` | 用户的 Farcaster 用户名。 | +| `data.bio` | 用户的 Farcaster 个人简介。 | +| `data.displayName` | 用户的 Farcaster 显示名称。 | +| `data.pfpUrl` | 用户的 Farcaster 头像 URL。 | +| `data.custody` | 用户的 FID 托管地址。 | +| `data.verifications` | 用户已验证地址的列表。 | +| `isError` | 发生错误时为 true。 | +| `error` | `Error` 实例。 | diff --git a/docs/zh/auth-kit/client/introduction.md b/docs/zh/auth-kit/client/introduction.md new file mode 100644 index 0000000..8ce722c --- /dev/null +++ b/docs/zh/auth-kit/client/introduction.md @@ -0,0 +1,47 @@ +# 认证客户端 + +[![NPM 版本](https://img.shields.io/npm/v/@farcaster/auth-client)](https://www.npmjs.com/package/@farcaster/auth-client) + +`@farcaster/auth-client` 库提供了一个与框架无关的 Farcaster 认证客户端。如果您不使用 React、需要更高的自定义性,或希望直接与 Farcaster 认证中继交互,可以使用该认证客户端库构建自己的「使用 Farcaster 登录」流程。 + +## 快速开始 + +### 安装 + +安装认证客户端及其对等依赖 [viem](https://viem.sh/)。 + +```sh +npm install @farcaster/auth-client viem +``` + +**注意:** 这是一个底层客户端库。如果您使用 React,请查看 [auth-kit](../) 替代方案。 + +### 创建客户端 + +通过中继服务器 URL 和以太坊连接器设置客户端。 + +```tsx +import { createAppClient, viemConnector } from '@farcaster/auth-client'; + +const appClient = createAppClient({ + relay: 'https://relay.farcaster.xyz', + ethereum: viemConnector(), +}); +``` + +根据您构建的应用类型,可以使用 `AppClient` 或 `WalletClient`。如果构建的是连接应用并需要用户登录,请使用 _应用客户端_;如果构建的是 Farcaster 钱包应用,请使用 _钱包客户端_。 + +### 执行操作 + +客户端设置完成后,即可通过它与 Farcaster 认证操作进行交互。 + +```tsx +const { data: { channelToken } } = await appClient.createChannel({ + siweUri: "https://example.com/login", + domain: "example.com"; +}); + +const status = await appClient.watchStatus({ + channelToken, +}); +``` diff --git a/docs/zh/auth-kit/client/wallet/authenticate.md b/docs/zh/auth-kit/client/wallet/authenticate.md new file mode 100644 index 0000000..261fc61 --- /dev/null +++ b/docs/zh/auth-kit/client/wallet/authenticate.md @@ -0,0 +1,66 @@ +# `authenticate` + +向 Connect 中继服务器提交使用 Farcaster 登录的消息、用户签名和个人资料数据。 + +```ts +const params = await walletClient.authenticate({ + message: 'example.com 希望您使用以太坊账户登录…', + signature: '0x9335c3055d47780411a3fdabad293c68c84ea350a11794cdc811fd5…', + fid: 1, + username: 'alice', + bio: '我是一个没有填写个人简介的小茶壶', + displayName: '爱丽丝茶壶', + pfpUrl: 'https://images.example.com/profile.png', +}); +``` + +## 参数 + +| 参数 | 类型 | 描述 | 必填 | +| -------------- | -------- | --------------------------------------------------------------------------------------- | ---- | +| `authKey` | `string` | Farcaster Auth API 密钥。Farcaster Auth v1 限制对 `/authenticate` 的调用仅限 Warpcast。 | 是 | +| `channelToken` | `string` | Farcaster Auth 频道令牌。 | 是 | +| `message` | `string` | 由您的钱包应用生成并由用户签名的 "使用 Farcaster 登录" 消息。 | 是 | +| `message` | `string` | 由您的钱包应用生成并由用户签名的 "使用 Farcaster 登录" 消息。 | 是 | +| `signature` | `Hex` | 由钱包用户账户创建的 SIWE 签名。 | 是 | +| `fid` | `number` | 钱包用户的 fid。 | 是 | +| `username` | `string` | 钱包用户的 Farcaster 用户名。 | 是 | +| `bio` | `string` | 钱包用户的个人简介。 | 是 | +| `displayName` | `string` | 钱包用户的显示名称。 | 是 | +| `pfpUrl` | `string` | 钱包用户的个人资料照片 URL。 | 是 | + +## 返回值 + +```ts +{ + response: Response + data: { + state: 'completed' + nonce: string + message?: string + signature?: `Hex` + fid?: number + username?: string + bio?: string + displayName?: string + pfpUrl?: string + } + isError: boolean + error: Error +} +``` + +| 参数 | 描述 | +| ------------------ | ---------------------------------------------------- | +| `response` | 来自 Connect 中继服务器的 HTTP 响应。 | +| `data.state` | 登录请求的状态,可能是 `"pending"` 或 `"complete"`。 | +| `data.nonce` | 用于 SIWE 消息的随机数。 | +| `data.message` | 生成的 SIWE 消息。 | +| `data.signature` | 由用户的 Warpcast 钱包生成的十六进制签名。 | +| `data.fid` | 用户的 Farcaster ID。 | +| `data.username` | 用户的 Farcaster 用户名。 | +| `data.bio` | 用户的 Farcaster 个人简介。 | +| `data.displayName` | 用户的 Farcaster 显示名称。 | +| `data.pfpUrl` | 用户的 Farcaster 个人资料照片 URL。 | +| `isError` | 当发生错误时为 true。 | +| `error` | `Error` 实例。 | diff --git a/docs/zh/auth-kit/client/wallet/build-sign-in-message.md b/docs/zh/auth-kit/client/wallet/build-sign-in-message.md new file mode 100644 index 0000000..f0f938d --- /dev/null +++ b/docs/zh/auth-kit/client/wallet/build-sign-in-message.md @@ -0,0 +1,48 @@ +# `buildSignInMessage` + +构建一个"使用 Farcaster 登录"消息,供终端用户签名。 + +该方法会为所有提供的参数添加必要的"使用 Farcaster 登录"消息属性。这些参数大部分应从提供的协议 URI 中解析获取。您的钱包应用必须提供用户的托管地址和 fid。 + +返回一个 `SiweMessage` 对象及消息字符串。 + +```ts +const { siweMessage, message } = walletClient.buildSignInMessage({ + address: '0x63C378DDC446DFf1d831B9B96F7d338FE6bd4231', + fid: 1, + uri: 'https://example.com/login', + domain: 'example.com', + nonce: 'ESsxs6MaFio7OvqWb', +}); +``` + +## 参数 + +| 参数名 | 类型 | 描述 | 必填 | +| ---------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- | ---- | +| `address` | `Hex` | 钱包用户的托管地址。该地址必须与签署生成的"使用 Farcaster 登录"消息的账户一致。您的钱包应用应提供已认证用户的托管地址。 | 是 | +| `fid` | `string` | 钱包用户的 fid。您的钱包应用应提供已认证用户的 fid。 | 是 | +| `uri` | `string` | 依赖连接应用的登录 URL。从提供的"使用 Farcaster 登录" URI 中解析此参数。 | 是 | +| `domain` | `string` | 依赖连接应用的域名。从提供的"使用 Farcaster 登录" URI 中解析此参数。 | 是 | +| `nonce` | `string` | 包含在"使用 Farcaster 登录"消息中的随机数。必须至少包含 8 个字母数字字符。从提供的"使用 Farcaster 登录" URI 中解析此参数。 | 是 | +| `notBefore` | `string` | SIWE 签名生效的开始时间。ISO 8601 格式的日期时间。从提供的"使用 Farcaster 登录" URI 中解析此参数。 | 否 | +| `expirationTime` | `string` | SIWE 签名失效的截止时间。ISO 8601 格式的日期时间。从提供的"使用 Farcaster 登录" URI 中解析此参数。 | 否 | +| `requestId` | `string` | 依赖应用提供的系统特定 ID。从提供的"使用 Farcaster 登录" URI 中解析此参数。 | 否 | + +## 返回值 + +```ts +{ + siweMessage: SiweMessage; + message: string; + isError: boolean; + error: Error; +} +``` + +| 参数名 | 描述 | +| ------------- | ------------------------------------ | +| `siweMessage` | 构建完成的"使用以太坊登录"消息对象。 | +| `message` | 序列化为字符串的 SIWE 消息。 | +| `isError` | 当发生错误时为 true。 | +| `error` | `Error` 实例。 | diff --git a/docs/zh/auth-kit/client/wallet/client.md b/docs/zh/auth-kit/client/wallet/client.md new file mode 100644 index 0000000..2adc88f --- /dev/null +++ b/docs/zh/auth-kit/client/wallet/client.md @@ -0,0 +1,22 @@ +# 钱包客户端 + +如果您正在开发一个[钱包应用](https://docs.farcaster.xyz/learn/what-is-farcaster/apps#wallet-apps)并需要处理签名请求,请使用 `WalletClient`。 + +您可以通过 `WalletClient` 解析收到的 "Sign In With Farcaster" 请求 URL,构建 "Sign In With Farcaster" 消息展示给用户,并将签名后的消息提交至 Farcaster Auth 中继通道。 + +```ts +import { createWalletClient, viemConnector } from '@farcaster/auth-client'; + +const walletClient = createWalletClient({ + relay: 'https://relay.farcaster.xyz', + ethereum: viemConnector(), +}); +``` + +## 参数说明 + +| 参数 | 类型 | 描述 | 必填 | +| ---------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- | +| `ethereum` | `EthereumConnector` |

以太坊连接器,用于查询 Farcaster 合约并验证智能合约钱包签名。当前 `@farcaster/auth-client` 仅提供 `viem` 连接器类型。

如需使用自定义 RPC,请将 RPC URL 传递给 viem 连接器。

| 是 | +| `relay` | `string` | 中继服务器 URL。默认为公共中继地址 `https://relay.farcaster.xyz` | 否 | +| `version` | `string` | Farcaster Auth 版本号。默认为 `"v1"` | 否 | diff --git a/docs/zh/auth-kit/client/wallet/parse-sign-in-uri.md b/docs/zh/auth-kit/client/wallet/parse-sign-in-uri.md new file mode 100644 index 0000000..d6a5b9c --- /dev/null +++ b/docs/zh/auth-kit/client/wallet/parse-sign-in-uri.md @@ -0,0 +1,49 @@ +# `parseSignInURI` + +解析由已连接应用用户提供的 Sign In With Farcaster URI。 + +返回解析后的参数。您的应用应使用这些参数来构造 Sign In With Farcaster 消息。 + +如果 URI 无效则返回错误。 + +```ts +const params = walletClient.parseSignInURI({ + uri: 'farcaster://connect?channelToken=76be6229-bdf7-4ad2-930a-540fb2de1e08&nonce=ESsxs6MaFio7OvqWb&siweUri=https%3A%2F%2Fexample.com%2Flogin&domain=example.com', +}); +``` + +## 参数 + +| 参数 | 类型 | 描述 | 必填 | +| ----- | -------- | ---------------------------- | ---- | +| `uri` | `string` | Sign In With Farcaster URI。 | 是 | + +## 返回值 + +```ts +{ + channelToken: string + params: { + domain: string + uri: string + nonce: string + notBefore?: string + expirationTime?: string + requestId?: string + } + isError: boolean + error: Error +} +``` + +| 参数 | 描述 | +| ----------------------- | ------------------------------ | +| `channelToken` | 连接中继通道令牌 UUID。 | +| `params.uri` | 依赖连接应用的登录 URI。 | +| `params.domain` | 依赖应用的域名。 | +| `params.nonce` | 依赖应用提供的随机数。 | +| `params.notBefore` | 此消息生效的时间。 | +| `params.expirationTime` | 此消息过期的时间。 | +| `params.requestId` | 依赖应用提供的系统特定标识符。 | +| `isError` | 当发生错误时为 true。 | +| `error` | `Error` 实例。 | diff --git a/docs/zh/auth-kit/examples.md b/docs/zh/auth-kit/examples.md new file mode 100644 index 0000000..276031e --- /dev/null +++ b/docs/zh/auth-kit/examples.md @@ -0,0 +1,17 @@ +# 示例 + +## 客户端示例 + +一个纯前端应用,允许用户通过 Farcaster 登录,并显示他们的个人资料图片和用户名。 + + + +## 后续步骤 + +1. [构建你的第一个 mini app](https://miniapps.farcaster.xyz/docs/getting-started){target="\_self"} +2. 阅读 [Mini Apps 正式规范](https://miniapps.farcaster.xyz/docs/specification){target="\_self"} +3. 加入 Farcaster 开发者社区 [/fc-devs](https://warpcast.com/~/channel/fc-devs) 频道 diff --git a/docs/zh/developers/frames/resources.md b/docs/zh/developers/frames/resources.md new file mode 100644 index 0000000..acd62a3 --- /dev/null +++ b/docs/zh/developers/frames/resources.md @@ -0,0 +1,40 @@ +--- +title: 框架资源 +--- + +# 框架资源 + +一份精选的热门资源集合,用于构建 Frames。 + +#### 学习资源 + +- [/fc-devs](https://warpcast.com/~/channel/fc-devs) - Farcaster 开发者频道 +- [Frames 规范](./spec) - 官方 Frames 规范 +- [dTech 零基础入门指南](https://dtech.vision/farcaster/frames/) +- [Frames.js 指南](https://framesjs.org/guides/create-frame) - Frames.js 提供的指南 +- [Frog 指南](https://frog.fm/getting-started) - Frog 框架的指南 +- [Frame 界面设计指南 (FIG)](https://github.com/paradigmxyz/Fig) - 构建 frames 的指导和最佳实践 +- [Pinata 教程](https://docs.pinata.cloud/farcaster/frames#frame-tutorials) - 利用 IPFS 构建 frames 的教程 +- [Frames.js 示例](https://framesjs.org/examples/basic) - 20+ 涵盖基础和高级主题的示例集合 + +#### 开发框架 + +- [frog](https://frog.fm) - 极简轻量的 Farcaster Frames 框架 +- [FrogUI](https://frog.fm/ui) - 类型安全的布局和样式基础组件 +- [frames.js](https://framesjs.org/) - 构建、调试和渲染 Frames +- [onchainkit](https://github.com/coinbase/onchainkit) - 用于创建 frames 的 React 工具包 +- [simplest frame](https://github.com/depatchedmode/simplest-frame) - 零成本、无框架的 Frame 模板 +- [framebuilder](https://framebuilder.xyz) - 无代码 Farcaster Frames 构建工具 + +#### 开发者工具 + +- [Warpcast Frame 验证器](https://warpcast.com/~/developers/frames-legacy) - 在 Warpcast 中测试 frames 的调试工具 +- [Neynar](https://docs.neynar.com/docs/how-to-build-farcaster-frames-with-neynar) - 为 frame 服务器提供的基础设施和工具 +- [Neynar Frame Studio](https://neynar.com/nfs) - 构建无代码 Frames +- [Pinata](https://docs.pinata.cloud/farcaster/frames) - Frame 分析、Hub API 等功能 +- [Airstack Frames SDK](https://github.com/Airstack-xyz/airstack-frames-sdk) - 将链上数据集成到 Frames 中 +- [Vercel OG](https://vercel.com/docs/functions/og-image-generation) - 使用 satori 和 resvg-js 从 HTML 和 CSS 生成 PNG 图像 + +
+ +更全面的资源列表请访问 [awesome-frames](https://github.com/davidfurlong/awesome-frames)。 diff --git a/docs/zh/developers/frames/safari_frame.gif b/docs/zh/developers/frames/safari_frame.gif new file mode 100644 index 0000000..6ddfd41 Binary files /dev/null and b/docs/zh/developers/frames/safari_frame.gif differ diff --git a/docs/zh/developers/frames/spec.md b/docs/zh/developers/frames/spec.md new file mode 100644 index 0000000..038fe3a --- /dev/null +++ b/docs/zh/developers/frames/spec.md @@ -0,0 +1,367 @@ +--- +title: 帧规范 +--- + +# 帧规范 + +帧(Frame)是用于在 Farcaster 上创建交互式认证体验的标准,可嵌入任何 Farcaster 客户端。 + +帧是一组位于 HTML 页面 `` 中的 `` 标签。如果页面包含所有必需的帧属性,Farcaster 应用会将该页面渲染为帧。帧的 `` 标签扩展了 [OpenGraph 协议](https://ogp.me/)。 + +帧可以相互链接,从而在 cast 中创建嵌入式动态应用。 + +## 帧应用的生命周期 + +帧应用始于初始帧,该帧会被应用缓存并展示给用户。帧必须包含图像,可以包含按钮,点击按钮会加载其他帧或将用户重定向到外部网站。 + +![帧应用](/assets/frame_app.png) + +### 初始帧 + +帧是位于网络服务器 URL(如 foo.com/app)上的 HTML 网页应用。我们将此网络服务器称为“帧服务器”。 + +帧服务器: + +- 必须在 HTML `` 部分返回有效的帧。 +- 应返回有效的 HTML ``,以防用户在浏览器中点击进入帧。 +- 不应在初始帧中包含动态内容,因为它会被 Farcaster 客户端缓存。 +- 不应包含 `fc:frame:state` 标签。 + +### 响应帧 + +当用户点击帧上的按钮时,应用会向帧服务器发送带有[帧签名](#frame-signature)的 POST 请求,以证明请求来自用户。服务器必须返回一个新帧发送给用户。 + +当帧服务器收到 POST 请求时: + +- 必须在 5 秒内响应。 +- 对于 `post` 按钮点击,必须返回 200 OK 和另一个帧以表示成功。 +- 对于 `post_redirect` 按钮点击,必须返回 302 OK 和 Location 标头以表示成功。 +- 可以返回 4XX 状态码、`content-type: application/json` 标头和包含 `message` 属性(不超过 90 个字符)的 JSON 正文以表示应用级错误。 +- 提供的 Location 标头必须包含以 `http://` 或 `https://` 开头的 URL。 + +### 渲染帧 + +当用户创建 cast 并在其中嵌入帧 URL 时,帧进入 Farcaster。支持帧的应用必须: + +- 检查所有 cast 嵌入 URL 是否包含有效帧。 +- 如果帧有效,则在查看 cast 时渲染该帧。 +- 如果帧格式错误,则回退到将其视为 OpenGraph 嵌入。 +- 遵循帧的[安全模型](#securing-frames)。 + +## 构建帧 + +帧必须包含必需属性,也可以包含可选属性。可以使用 Warpcast 提供的[帧验证工具](https://warpcast.com/~/developers/frames-legacy)验证帧。 + +### 属性 + +帧属性是具有属性名和内容值的 meta 标签。属性名始终以 `fc:frame` 为前缀。 + +```html + + + +``` + +### 必需属性 + +| 键 | 描述 | +| ---------------- | ------------------------------------------------------------------------------------------------------------------------- | +| `fc:frame` | 有效的帧版本字符串。字符串必须是发布日期(如 2020-01-01)或 vNext。应用必须忽略不理解的版本。目前唯一有效的版本是 vNext。 | +| `fc:frame:image` | 图像,宽高比应为 1.91:1 或 1:1 | +| `og:image` | 图像,宽高比应为 1.91:1。用于不支持帧的客户端回退。 | + +### 可选属性 + +| 键 | 描述 | +| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `fc:frame:post_url` | 不超过 256 字节的字符串,包含发送签名数据包的有效 URL。 | +| `fc:frame:button:$idx` | 不超过 256 字节的字符串,表示位置 $idx 处按钮的标签。页面可以包含 0 到 4 个按钮。如果存在多个按钮,索引值必须从 1 开始按顺序排列(如 1、2、3)。如果序列不连续(如 1、2、4),则帧无效。 | +| `fc:frame:button:$idx:action` | 必须是 `post`、`post_redirect`、`link`、`mint` 或 `tx`。默认为 `post`。详见[按钮操作](#button-actions)。 | +| `fc:frame:button:$idx:target` | 不超过 256 字节的字符串,决定操作的目标。 | +| `fc:frame:button:$idx:post_url` | 不超过 256 字节的字符串,定义按钮特定的 URL 以发送签名数据包。如果设置,会覆盖 `fc:frame:post_url`。 | +| `fc:frame:input:text` | 添加此属性启用文本字段。内容是不超过 32 字节的标签,显示给用户(如“输入消息”)。 | +| `fc:frame:image:aspect_ratio` | 必须是 `1.91:1` 或 `1:1`。默认为 `1.91:1` | +| `fc:frame:state` | 包含传递给帧服务器的序列化状态(如 JSON)的字符串。最多 4096 字节。 | + +## 按钮操作 + +### `post` + +```html + + +``` + +`post` 操作向帧或按钮的 `post_url` 发送 HTTP POST 请求。这是默认按钮类型。 + +帧服务器在 POST 正文中接收签名数据包,其中包含点击的按钮、文本输入和 cast 上下文信息。帧服务器必须返回 200 OK 和另一个帧。 + +### `post_redirect` + +```html + + + +``` + +`post_redirect` 操作向帧或按钮的 `post_url` 发送 HTTP POST 请求。可以使用此操作基于帧状态或用户输入重定向到 URL。 + +帧服务器在 POST 正文中接收签名数据包。帧服务器必须返回 302 Found 和以 `http://` 或 `https://` 开头的 Location 标头。 + +### `link` + +```html + + + +``` + +`link` 操作将用户重定向到外部 URL。可以使用此操作重定向到 URL,而无需向帧服务器处理 POST 请求。 + +客户端不会为 `link` 操作向帧服务器发送请求,而是直接将用户重定向到 `target` URL。 + +### `mint` + +```html + + + +``` + +`mint` 操作允许用户铸造 NFT。支持中继或发起链上交易的客户端可以通过中继交易或与用户钱包交互来增强铸造按钮功能。不支持的客户端会回退到链接到外部 URL。 + +`target` 属性必须是有效的 `CAIP-10` 地址,加上可选的代币 ID。 + +### `tx` + +```html + + + + +``` + +`tx` 操作允许帧请求用户在其连接的钱包中执行操作。与其他操作类型不同,`tx` 操作包含多个步骤。 + +首先,客户端向 `target` URL 发送 POST 请求以获取钱包操作数据。帧服务器在 POST 正文中接收签名数据包,包括连接的钱包地址。帧服务器必须返回 200 OK 和描述钱包操作的 JSON 响应: + +```json +{ + method: "eth_sendTransaction", + chainId: "eip155:10", + params: { + abi: [...], // 函数选择器和任何错误的 JSON ABI + to: "0x00000000fcCe7f938e7aE6D3c335bD6a1a7c593D", + data: "0x783a112b0000000000000000000000000000000000000000000000000000000000000e250000000000000000000000000000000000000000000000000000000000000001", + value: "984316556204476", + }, +} +``` + +客户端使用此数据请求用户钱包中的操作。如果用户完成操作,客户端会向 `post_url` 发送 POST 请求,其中签名数据包包含 `transaction_id` 中的交易或签名哈希以及 `address` 中使用的地址。帧服务器必须返回 200 OK 和另一个帧。帧服务器可以监控交易哈希以确定交易是否成功、回滚或超时。 + +#### 钱包操作响应类型 + +钱包操作响应必须是以下之一: + +##### EthSendTransactionAction + +- `chainId`:标识交易网络的 CAIP-2 链 ID(如以太坊主网)。 +- `method`:必须是 `"eth_sendTransaction"`。 +- `attribution`:可选。返回 `false` 以省略[调用数据归属后缀](https://www.notion.so/Frame-Transactions-Public-9d9f9f4f527249519a41bd8d16165f73?pvs=21)。如果此值为 `undefined` 或 `true`,客户端会附加归属后缀。 +- `params`: + - `to`:交易目标地址。 + - `abi`:JSON ABI,**必须**包含编码的函数类型,**应**包含潜在错误类型。可以为空。 + - `value`:以 wei 为单位的交易发送的以太币值。可选。 + - `data`:交易调用数据。可选。 + +```ts +type EthSendTransactionAction = { + chainId: string; + method: 'eth_sendTransaction'; + attribution?: boolean; + params: { + abi: Abi | []; + to: string; + value?: string; + data?: string; + }; +}; +``` + +##### EthSignTypedDataV4 + +参见 [EIP-712](https://eips.ethereum.org/EIPS/eip-712)。 + +- `chainId`:标识交易网络的 CAIP-2 链 ID(如以太坊主网)。 +- `method`:必须是 `"eth_signTypedData_v4"`。 +- `params`: + - `domain`:类型化域。 + - `types`:类型化数据的类型定义。 + - `primaryType`:从类型中提取并在值中使用的主要类型。 + - `message`:类型化消息。 + +```ts +type EthSignTypedDataV4Action = { + chainId: string; + method: 'eth_signTypedData_v4'; + params: { + domain: { + name?: string; + version?: string; + chainId?: number; + verifyingContract?: string; + }; + types: Record; + primaryType: string; + message: Record; + }; +}; +``` + +**支持的链** + +| 网络 | 链 ID | +| -------- | ------------------ | +| 以太坊 | `eip155:1` | +| Arbitrum | `eip155:42161` | +| Base | `eip155:8453` | +| Degen | `eip155:666666666` | +| Gnosis | `eip155:100` | +| Optimism | `eip155:10` | +| Zora | `eip155:7777777` | +| Polygon | `eip155:137` | + +| 测试网 | 链 ID | +| ---------------- | ----------------- | +| Sepolia | `eip155:11155111` | +| Arbitrum Sepolia | `eip155:421614` | +| Base Sepolia | `eip155:84532` | +| Optimism Sepolia | `eip155:11155420` | + +### 图像 + +`fc:frame:image` 标签中的图像需遵循以下规则: + +- 图像大小必须 < 10 MB。 +- 图像类型必须是 jpg、png 或 gif。 +- 图像源必须是带有内容标头的外部资源或数据 URI。 + +客户端可能会调整较大图像的尺寸或裁剪不符合其宽高比的图像。不允许使用 SVG 图像,因为它们可能包含脚本,客户端需要额外工作来清理它们。 + +帧服务器可以使用缓存标头刷新图像并提供更动态的初始帧体验: + +- 帧服务器可以在 HTTP `Cache-Control` 标头中使用 `max-age` 指令确保初始帧中的图像自动刷新。较低的 `max-age` 确保图像无需用户交互即可定期更新。 +- 应用开发者应尊重原始帧图像设置的缓存标头,其图像代理实现不应干扰持续时间。 + +## 在信息流中显示帧 + +Farcaster 应用负责向用户渲染帧,并代表用户将其交互代理回帧服务器。 + +### 解析帧 + +当 cast 中嵌入 URL 时: + +1. 应用必须抓取标头以检查 URL 是否为帧。 +2. 如果帧标签有效,应用必须渲染该帧。 +3. 如果帧标签无效或不存在,应用必须回退到 OpenGraph 标签。 +4. 如果 OpenGraph 标签也不存在,应用必须渲染占位错误消息。 + +### 渲染帧 + +应用可以在向查看者显示 cast 时随时渲染帧。以下规则适用于帧的渲染: + +1. 按钮必须按升序索引顺序显示在图像下方。 +2. 如果空间受限,按钮可以多行显示。 +3. 文本输入必须显示在按钮上方和图像下方。 +4. 文本输入标签必须显示在文本输入上方或内部。 +5. 应用必须尊重 `fc:frame:image:aspect_ratio` 属性中设置的宽高比。 + +如果按钮是 `post_redirect` 或 `link` 操作: + +1. 必须用重定向符号进行视觉标记。 +2. 用户离开应用前往不受信任的站点时应收到警告。 + +如果按钮是 `mint` 操作: + +1. 必须验证 `target` 属性中是否存在有效的 [CAIP-10](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md) URL。 +2. 如果所有属性有效,必须将项目显示为 NFT。 + +如果按钮是 `tx` 操作: + +1. 必须视觉指示 `tx` 按钮将请求钱包交易。 +2. 必须显示帧提供的按钮标签。 + +### 处理点击 + +如果点击的按钮是 `post` 或 `post_redirect`,应用必须: + +1. 构建帧签名数据包。 +2. 如果存在 `fc:frame:button:$idx:target`,则向该目标 POST 数据包。 +3. 如果目标不存在,则向 `fc:frame:post_url` POST 数据包。 +4. 如果目标和操作都不存在,则向帧的嵌入 URL POST 数据包。 +5. 至少等待 5 秒以获取帧服务器的响应。 + +如果点击的按钮是 `mint`,应用应: + +1. 允许用户铸造 NFT 或打开允许此功能的外部页面。 + +### 处理响应 + +应用在提交 POST 请求后会收到帧服务器的响应。以下规则适用于这些响应的处理: + +1. 如果按钮操作是 `post`,将所有非 200 响应视为错误。 +2. 如果按钮操作是 `post_redirect`,将所有非 30X 响应视为错误。 +3. 处理 30X 响应时,应用必须将用户重定向到 URL 位置值。 +4. 处理 30X 响应时,应用必须确保 URL 以 `http://` 或 `https://` 开头。 +5. 处理 30X 响应时,在将用户定向到不受信任的站点前发出警告。 +6. 处理应用级错误响应时,向最终用户显示 `message`。 + +## 保护帧安全 + +帧开发者和实现帧的应用都必须解决重要的安全问题。 + +### 帧开发者 + +1. 清理从用户通过文本输入接收的所有输入。 +2. 验证帧签名数据包的签名。 +3. 验证帧签名数据包中的原始 URL。 +4. 仅从受信任的来源加载交易调用数据。 + +### 应用开发者 + +1. 代理图像请求以防止帧服务器跟踪用户。 +2. 清理重定向 URL 以确保它们以 `http://` 或 `https://` 开头。 +3. 仅接受数据 URI 如果是图像。 +4. 避免渲染 SVG,因为它们可能包含可执行代码。 + +应用应考虑以下机制保护用户免受恶意交易侵害: + +1. 交易模拟。 +2. 域名允许列表和禁止列表以阻止已知攻击者。 +3. 社交图谱分析以检测潜在的不良或不受信任的参与者。 +4. 教育用户交易的危险性并使用余额有限的钱包。 + +## 数据结构 + +### 帧签名 + +帧签名证明用户点击了帧按钮。它由 Farcaster 应用创建,由用户账户签名并发送到帧服务器。 + +当点击帧按钮时,Farcaster 应用必须生成 `FrameAction` protobuf。FrameAction 是一种新型的 [Farcaster 消息](https://github.com/farcasterxyz/protocol/blob/main/docs/SPECIFICATION.md#2-message-specifications)。与所有 FC 消息一样,它必须使用属于用户的活跃 Ed25519 账户密钥(即签名者)签名。 + +```proto +message FrameActionBody { + bytes frame_url = 1; // 帧应用的 URL + bytes button_index = 2; // 点击的按钮索引 + CastId cast_id = 3; // 包含帧 URL 的 cast +``` diff --git a/docs/zh/developers/guides/accounts/change-custody.md b/docs/zh/developers/guides/accounts/change-custody.md new file mode 100644 index 0000000..a822344 --- /dev/null +++ b/docs/zh/developers/guides/accounts/change-custody.md @@ -0,0 +1,102 @@ +# 更改托管地址 + +账户由托管地址拥有,该地址是 OP 主网上的一个以太坊地址。 + +用户可能出于安全考虑或需要转移整个账户所有权而更改此地址。 + +### 要求 + +- 一个在 OP 主网上拥有该账户的 ETH 钱包,且钱包中有少量 ETH。 +- 一个 OP 主网的以太坊提供商 URL(例如通过 [Alchemy](https://www.alchemy.com/)、[Infura](https://www.infura.io/) 或 [QuickNode](https://www.quicknode.com/))。 + +### 更改托管地址 + +调用 Id Registry 合约的 `transfer` 函数。接收地址必须提供一个 EIP-712 签名以接受转移。 + +::: code-group + +```ts [@farcaster/hub-web] +import { ViemWalletEip712Signer } from '@farcaster/hub-web'; +import { walletClient, account } from './clients.ts'; +import { readNonce, getDeadline } from './helpers.ts'; + +const nonce = await readNonce(); +const deadline = getDeadline(); + +const eip712Signer = new ViemWalletEip712Signer(walletClient); +const signature = await eip712signer.signTransfer({ + fid: 1n, + to: account, + nonce, + deadline, +}); + +const { request: transferRequest } = await publicClient.simulateContract({ + ...IdContract, + functionName: 'transfer', + args: [account, deadline, signature], // to, deadline, signature +}); + +await walletClient.writeContract(transferRequest); +``` + +```ts [Viem] +import { + ID_REGISTRY_EIP_712_TYPES, + idRegistryABI, + ID_GATEWAY_ADDRESS, +} from '@farcaster/hub-web'; +import { walletClient, account } from './clients.ts'; +import { readNonce, getDeadline } from './helpers.ts'; + +const nonce = await readNonce(); +const deadline = getDeadline(); +const IdContract = { + abi: idRegistryABI, + address: ID_GATEWAY_ADDRESS, + chain: optimism, +}; + +const signature = await walletClient.signTypedData({ + account, + ...ID_REGISTRY_EIP_712_TYPES, + primaryType: 'Transfer', + message: { + fid: 1n, + to: account, + nonce, + deadline, + }, +}); +``` + +```ts [helpers.ts] +import { ID_REGISTRY_ADDRESS, idRegistryABI } from '@farcaster/hub-web'; +import { publicClient, account } from './clients.ts'; + +export const getDeadline = () => { + const now = Math.floor(Date.now() / 1000); + const oneHour = 60 * 60; + return now + oneHour; +}; + +export const readNonce = async () => { + return await publicClient.readContract({ + address: ID_REGISTRY_ADDRESS, + abi: idRegistryABI, + functionName: 'nonces', + args: [account], + }); +}; +``` + +<<< @/examples/contracts/clients.ts + +::: + +::: warning +转移 FID 不会重置其恢复地址。如需转移 FID 并更新其恢复地址, +请调用 [`transferAndChangeRecovery`](/zh/reference/contracts/reference/id-registry#transferandchangerecovery)。 +::: + +更多详情请参阅 [Id Registry](/zh/reference/contracts/reference/id-registry#transfer) 部分。 diff --git a/docs/zh/developers/guides/accounts/change-fname.md b/docs/zh/developers/guides/accounts/change-fname.md new file mode 100644 index 0000000..bc57e02 --- /dev/null +++ b/docs/zh/developers/guides/accounts/change-fname.md @@ -0,0 +1,57 @@ +# 更改 Farcaster 用户名 + +用户可以更改其链下 ENS 名称或 Fname,而不会影响账户历史记录。此操作最多每 28 天可执行一次。 + +::: warning + +- 若违反[使用政策](/zh/learn/architecture/ens-names#offchain-ens-names-fnames),Fname 可能会被撤销。 +- 频繁更改 Fname 可能导致应用降低您的信誉评级。 + ::: + +### 前提条件 + +- 在 OP Mainnet 上拥有该账户的 ETH 钱包(无需持有 ETH)。 + +### 更改用户名 + +要转移 Fname(例如 `Hubble`),请向 `/transfers` 发起 POST 请求,请求体如下: + +```yaml +{ + "name": "hubble", // 待转移的名称 + "from": 123, // 转出方 FID + "to": 321, // 转入方 FID + "fid": 123, // 发起请求的 FID(必须与 from 匹配) + "owner": "0x...", // 发起请求 FID 的托管地址 + "timestamp": 1641234567, // 当前时间戳(秒) + "signature": "0x..." // 由 FID 托管地址签名的 EIP-712 签名 +} +``` + +生成 EIP-712 签名的代码示例: + +```js +import { makeUserNameProofClaim, EIP712Signer } from '@farcaster/hub-nodejs'; + +const accountKey: EIP712Signer = undefined; // 托管地址的账户密钥(使用 hub-nodejs 中适用于 ethers 或 viem 的子类) + +const claim = makeUserNameProofClaim({ + name: 'hubble', + owner: '0x...', + timestamp: Math.floor(Date.now() / 1000), +}); +const signature = ( + await accountKey.signUserNameProofClaim(claim) +)._unsafeUnwrap(); +``` + +通过 curl 的请求示例: + +```bash +curl -X POST https://fnames.farcaster.xyz/transfers \ + -H "Content-Type: application/json" \ + -d \ +'{"name": "hubble", "owner": "0x...", "signature": "0x...", "from": 123, "to": 321, "timestamp": 1641234567, fid: 123}' +``` + +更多关于 Fname 注册表 API 的细节,请参阅[此处](/zh/reference/fname/api.md)。 diff --git a/docs/zh/developers/guides/accounts/change-recovery.md b/docs/zh/developers/guides/accounts/change-recovery.md new file mode 100644 index 0000000..d7eecc8 --- /dev/null +++ b/docs/zh/developers/guides/accounts/change-recovery.md @@ -0,0 +1,67 @@ +# 更改恢复地址 + +账户可以配置一个恢复地址,以防止托管地址丢失。恢复地址将更改账户的托管地址。 + +### 要求 + +- 一个在 OP 主网上拥有 FID 的 ETH 钱包,并准备一些 ETH 用于支付 gas 费用。 +- OP 主网的 ETH RPC URL(例如通过 [Alchemy](https://www.alchemy.com/)、[Infura](https://www.infura.io/) 或 [QuickNode](https://www.quicknode.com/) 获取)。 + +### 更改地址 + +调用 Id Registry 合约中的 `changeRecovery` 函数。 + +::: code-group + +```ts [@farcaster/hub-web] +import { walletClient, account, IdContract } from './clients.ts'; + +const newRecoveryAddress = '0x...'; + +const { request: transferRequest } = await walletClient.simulateContract({ + ...IdContract, + functionName: 'changeRecovery', + args: [newRecoveryAddress], // 新的恢复地址 +}); + +await walletClient.writeContract(transferRequest); +``` + +```ts [clients.ts] +import { + ID_REGISTRY_EIP_712_TYPES, + idRegistryABI, + ID_GATEWAY_ADDRESS, +} from '@farcaster/hub-web'; +import { walletClient, account } from './clients.ts'; + +const IdContract = { + abi: idRegistryABI, + address: ID_GATEWAY_ADDRESS, + chain: optimism, +}; + +import { createWalletClient, createPublicClient, custom, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { optimism } from 'viem/chains'; + +export const publicClient = createPublicClient({ + chain: optimism, + transport: http(), +}); + +export const walletClient = createWalletClient({ + chain: optimism, + transport: custom(window.ethereum), +}); + +// JSON-RPC 账户 +export const [account] = await walletClient.getAddresses(); + +// 本地账户 +export const account = privateKeyToAccount('0x...'); +``` + +::: + +更多详情请参阅 [Id Registry](/zh/reference/contracts/reference/id-registry#transfer) 部分。 diff --git a/docs/zh/developers/guides/accounts/create-account-key.md b/docs/zh/developers/guides/accounts/create-account-key.md new file mode 100644 index 0000000..7a0b536 --- /dev/null +++ b/docs/zh/developers/guides/accounts/create-account-key.md @@ -0,0 +1,361 @@ +# 创建账户密钥 + +一个账户可以授权账户密钥,使其能够代表该账户创建消息。 + +账户所有者可以随时撤销账户密钥。要添加账户密钥,您需要遵循以下六个步骤: + +1. 设置 [Viem](https://viem.sh/) 客户端和 [`@farcaster/hub-web`](https://www.npmjs.com/package/@farcaster/hub-web) 账户密钥。 +2. 如果您的应用尚未拥有,请注册一个 [应用 FID](/zh/reference/contracts/faq#what-is-an-app-fid-how-do-i-get-one)。 +3. 为用户创建一个新的账户密钥。 +4. 使用您的应用账户创建一个 [签名密钥请求](/zh/reference/contracts/reference/signed-key-request-validator)。 +5. 从用户处收集 [`Add`](/zh/reference/contracts/reference/key-gateway#add-signature) 签名。 +6. 调用 [密钥网关](https://docs.farcaster.xyz/reference/contracts/reference/key-gateway#addFor) 合约将密钥添加到链上。 + +### 要求 + +- OP 主网上的 ETH 钱包,并拥有一定数量的 ETH。 +- OP 主网的 ETH RPC URL(例如通过 [Alchemy](https://www.alchemy.com/)、[Infura](https://www.infura.io/) 或 [QuickNode](https://www.quicknode.com/))。 + +### 1. 设置客户端和账户密钥 + +首先,设置 Viem 客户端和 `@farcaster/hub-web` 账户密钥。在此示例中,我们将使用 Viem 本地账户和账户密钥,但您也可以使用 `ViemWalletEip712Signer` 连接到用户的钱包而非本地账户。 + +```ts +import * as ed from '@noble/ed25519'; +import { + ID_GATEWAY_ADDRESS, + ID_REGISTRY_ADDRESS, + ViemLocalEip712Signer, + idGatewayABI, + idRegistryABI, + NobleEd25519Signer, + KEY_GATEWAY_ADDRESS, + keyGatewayABI, +} from '@farcaster/hub-nodejs'; +import { bytesToHex, createPublicClient, createWalletClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { optimism } from 'viem/chains'; + +const APP_PRIVATE_KEY = '0x00'; +const ALICE_PRIVATE_KEY = '0x00'; + +const publicClient = createPublicClient({ + chain: optimism, + transport: http(), +}); + +const walletClient = createWalletClient({ + chain: optimism, + transport: http(), +}); + +const app = privateKeyToAccount(APP_PRIVATE_KEY); +const appAccountKey = new ViemLocalEip712Signer(app as any); + +const alice = privateKeyToAccount(ALICE_PRIVATE_KEY); +const aliceAccountKey = new ViemLocalEip712Signer(alice as any); + +const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600); // 将签名的截止时间设置为当前时间 1 小时后 + +const WARPCAST_RECOVERY_PROXY = '0x00000000FcB080a4D6c39a9354dA9EB9bC104cd7'; +``` + +### 2. 注册应用 FID + +如果尚未拥有,请注册一个应用 FID。要注册 FID,您需要从 ID 网关读取价格,然后调用 ID 网关并支付注册费用。您可以从 ID 注册表合约中读取新的 FID,或从 `Register` 事件中解析。这里,我们将从注册表合约中读取。 + +```ts +const price = await publicClient.readContract({ + address: ID_GATEWAY_ADDRESS, + abi: idGatewayABI, + functionName: 'price', + args: [0n], +}); + +const { request } = await publicClient.simulateContract({ + account: app, + address: ID_GATEWAY_ADDRESS, + abi: idGatewayABI, + functionName: 'register', + args: [WARPCAST_RECOVERY_PROXY, 0n], + value: price, +}); +await walletClient.writeContract(request); + +const APP_FID = await publicClient.readContract({ + address: ID_REGISTRY_ADDRESS, + abi: idRegistryABI, + functionName: 'idOf', + args: [app.address], +}); +``` + +### 3. 创建新的账户密钥 + +为用户创建一个新的 Ed25519 密钥对。在实际应用中,请确保私钥保密。 + +```ts +const privateKeyBytes = ed.utils.randomPrivateKey(); +const accountKey = new NobleEd25519Signer(privateKeyBytes); + +let accountPubKey = new Uint8Array(); +const accountKeyResult = await accountKey.getSignerKey(); +``` + +### 4. 使用应用账户创建签名密钥请求 + +创建一个由应用账户签名的签名密钥请求。为此,您可以使用 `getSignedKeyRequestMetadata` 辅助函数,它会生成并签署签名密钥请求。 + +```ts +if (accountKeyResult.isOk()) { + accountPubKey = accountKeyResult.value; + + const signedKeyRequestMetadata = + await appAccountKey.getSignedKeyRequestMetadata({ + requestFid: APP_FID, + key: accountPubKey, + deadline, + }); +} +``` + +### 5. 从用户处收集 `Add` 签名 + +从用户处收集 EIP-712 `Add` 签名,以授权将账户密钥添加到其 FID。在实际应用中,您可能会在前端从用户的钱包中收集此签名。在前端环境中,可以使用 `ViemEip712WalletSigner` 连接到浏览器钱包而非本地签名器。 + +```ts +if (signedKeyRequestMetadata.isOk()) { + const metadata = bytesToHex(signedKeyRequestMetadata.value); + + const aliceNonce = await publicClient.readContract({ + address: KEY_GATEWAY_ADDRESS, + abi: keyGatewayABI, + functionName: 'nonces', + args: [alice.address], + }); + + const aliceSignature = await aliceAccountKey.signAdd({ + owner: alice.address as `0x${string}`, + keyType: 1, + key: accountPubKey, + metadataType: 1, + metadata, + nonce, + deadline, + }); +} +``` + +### 6. 调用密钥网关合约将密钥添加到链上 + +调用密钥网关合约并提供用户的签名,将密钥添加到链上。 + +```ts +if (aliceSignature.isOk()) { + const { request } = await publicClient.simulateContract({ + account: app, + address: KEY_GATEWAY_ADDRESS, + abi: keyGatewayABI, + functionName: 'addFor', + args: [ + alice.address, + 1, + bytesToHex(accountPubKey), + 1, + metadata, + deadline, + bytesToHex(aliceSignature.value), + ], + }); + await walletClient.writeContract(request); +} +``` + +### 完整代码示例 + +查看以下完整代码示例,包含上述所有步骤。 + +::: code-group + +```ts [@farcaster/hub-web] +import * as ed from '@noble/ed25519'; +import { + ID_GATEWAY_ADDRESS, + ID_REGISTRY_ADDRESS, + ViemLocalEip712Signer, + idGatewayABI, + idRegistryABI, + NobleEd25519Signer, + KEY_GATEWAY_ADDRESS, + keyGatewayABI, +} from '@farcaster/hub-nodejs'; +import { bytesToHex, createPublicClient, createWalletClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { optimism } from 'viem/chains'; + +const APP_PRIVATE_KEY = '0x00'; +const ALICE_PRIVATE_KEY = '0x00'; + +/******************************************************************************* + * 设置 - 创建本地账户、Viem 客户端、辅助工具和常量。 + *******************************************************************************/ + +/** + * 创建 Viem 公共(读取)和钱包(写入)客户端。 + */ +const publicClient = createPublicClient({ + chain: optimism, + transport: http(), +}); + +const walletClient = createWalletClient({ + chain: optimism, + transport: http(), +}); + +/** + * 代表您应用的本地账户。您将使用此账户签署密钥元数据并代表用户发送交易。 + */ +const app = privateKeyToAccount(APP_PRIVATE_KEY); +const appAccountKey = new ViemLocalEip712Signer(app as any); +console.log('应用:', app.address); + +/** + * 代表用户 Alice 的本地账户。 + */ +const alice = privateKeyToAccount(ALICE_PRIVATE_KEY); +const aliceAccountKey = new ViemLocalEip712Signer(alice as any); +console.log('Alice:', alice.address); + +/** + * 生成一个当前时间 1 小时后的截止时间戳。 + * 所有 Farcaster EIP-712 签名都包含一个截止时间,即签名不再有效的区块时间戳。 + */ +const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600); // 将签名的截止时间设置为当前时间 1 小时后 + +const WARPCAST_RECOVERY_PROXY = '0x00000000FcB080a4D6c39a9354dA9EB9bC104cd7'; + +/******************************************************************************* + * IdGateway - register - 注册应用 FID。 + *******************************************************************************/ + +/** + * 获取当前注册价格。我们不打算注册任何额外存储,因此将 0n 作为唯一参数传递。 + */ +const price = await publicClient.readContract({ + address: ID_GATEWAY_ADDRESS, + abi: idGatewayABI, + functionName: 'price', + args: [0n], +}); + +/** + * 调用 `register` 将 FID 注册到应用账户。 + */ +const { request } = await publicClient.simulateContract({ + account: app, + address: ID_GATEWAY_ADDRESS, + abi: idGatewayABI, + functionName: 'register', + args: [WARPCAST_RECOVERY_PROXY, 0n], + value: price, +}); +await walletClient.writeContract(request); + +/** + * 从 ID 注册表合约中读取应用 FID。 + */ +const APP_FID = await publicClient.readContract({ + address: ID_REGISTRY_ADDRESS, + abi: idRegistryABI, + functionName: 'idOf', + args: [app.address], +}); + +/******************************************************************************* + * KeyGateway - addFor - 将账户密钥添加到 Alice 的 FID。 + *******************************************************************************/ + +/** + * 要将账户密钥添加到 Alice 的 FID,我们需要遵循四个步骤: + * + * 1. 为 Alice 创建一个新的账户密钥对。 + * 2. 使用我们的应用账户创建签名密钥请求。 + * 3. 收集 Alice 的 `Add` 签名。 + * 4. 调用合约将密钥添加到链上。 + */ + +/** + * 1. 为 Alice 创建 Ed25519 账户密钥对并获取公钥。 + */ +const privateKeyBytes = ed.utils.randomPrivateKey(); +const accountKey = new NobleEd25519Signer(privateKeyBytes); + +let accountPubKey = new Uint8Array(); +const accountKeyResult = await accountKey.getSignerKey(); +if (accountKeyResult.isOk()) { + accountPubKey = accountKeyResult.value; + + /** + * 2. 从应用账户生成签名密钥请求。 + */ + const signedKeyRequestMetadata = + await appAccountKey.getSignedKeyRequestMetadata({ + requestFid: APP_FID, + key: accountPubKey, + deadline, + }); + + if (signedKeyRequestMetadata.isOk()) { + const metadata = bytesToHex(signedKeyRequestMetadata.value); + /** + * 3. 从密钥网关读取 Alice 的 nonce。 + */ + const aliceNonce = await publicClient.readContract({ + address: KEY_GATEWAY_ADDRESS, + abi: keyGatewayABI, + functionName: 'nonces', + args: [alice.address], + }); + + /** + * 然后,收集她的 `Add` 签名。 + */ + const aliceSignature = await aliceAccountKey.signAdd({ + owner: alice.address as `0x${string}`, + keyType: 1, + key: accountPubKey, + metadataType: 1, + metadata, + nonce: aliceNonce, + deadline, + }); + + if (aliceSignature.isOk()) { + /** + * 使用 Alice 的签名和签名密钥请求调用 `addFor`。 + */ + const { request } = await publicClient.simulateContract({ + account: app, + address: KEY_GATEWAY_ADDRESS, + abi: keyGatewayABI, + functionName: 'addFor', + args: [ + alice.address, + 1, + bytesToHex(accountPubKey), + 1, + metadata, + deadline, + bytesToHex(aliceSignature.value), + ], + }); + await walletClient.writeContract(request); + } + } +} +``` + +::: + +有关更多详细信息,请参阅 [密钥注册表](/zh/reference/contracts/reference/key-registry#add) 参考文档。 diff --git a/docs/zh/developers/guides/accounts/create-account.md b/docs/zh/developers/guides/accounts/create-account.md new file mode 100644 index 0000000..c79bf10 --- /dev/null +++ b/docs/zh/developers/guides/accounts/create-account.md @@ -0,0 +1,432 @@ +# 创建账户 + +::: info 前提条件 + +- 一个 OP 主网的以太坊钱包,且拥有足够的 ETH 支付 gas 费用。 +- 一个 OP 主网的以太坊提供商 URL(可通过 [Alchemy](https://www.alchemy.com/)、[Infura](https://www.infura.io/) 或 [QuickNode](https://www.quicknode.com/) 获取)。 + +::: + +您可以使用 Bundler 合约注册新用户。具体步骤如下: + +1. 设置 [Viem](https://viem.sh/) 客户端和 [`@farcaster/hub-web`](https://www.npmjs.com/package/@farcaster/hub-web) 账户密钥。 +2. 如果您的应用尚未拥有 [app FID](/zh/reference/contracts/faq#what-is-an-app-fid-how-do-i-get-one),请先注册一个。 +3. 从用户处收集 [`Register`](/zh/reference/contracts/reference/id-gateway#register-signature) 签名。 +4. 为用户创建新的账户密钥对。 +5. 使用您的应用账户创建 [Signed Key Request](/zh/reference/contracts/reference/signed-key-request-validator)。 +6. 从用户处收集 [`Add`](/zh/reference/contracts/reference/key-gateway#add-signature) 签名。 +7. 调用 [Bundler](https://docs.farcaster.xyz/reference/contracts/reference/bundler#register) 合约完成链上注册。 + +### 1. 设置客户端和账户密钥 + +首先设置 Viem 客户端和 `@farcaster/hub-web` 账户密钥。本示例使用 Viem 本地账户和密钥,但您也可以使用 `ViemWalletEip712Signer` 连接用户钱包而非本地账户。 + +```ts +import * as ed from '@noble/ed25519'; +import { + ID_GATEWAY_ADDRESS, + ID_REGISTRY_ADDRESS, + ViemLocalEip712Signer, + idGatewayABI, + idRegistryABI, + NobleEd25519Signer, + BUNDLER_ADDRESS, + bundlerABI, + KEY_GATEWAY_ADDRESS, + keyGatewayABI, +} from '@farcaster/hub-nodejs'; +import { bytesToHex, createPublicClient, createWalletClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { optimism } from 'viem/chains'; + +const APP_PRIVATE_KEY = '0x00'; +const ALICE_PRIVATE_KEY = '0x00'; + +const publicClient = createPublicClient({ + chain: optimism, + transport: http(), +}); + +const walletClient = createWalletClient({ + chain: optimism, + transport: http(), +}); + +const app = privateKeyToAccount(APP_PRIVATE_KEY); +const appAccountKey = new ViemLocalEip712Signer(app as any); + +const alice = privateKeyToAccount(ALICE_PRIVATE_KEY); +const aliceAccountKey = new ViemLocalEip712Signer(alice as any); + +const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600); // 设置签名有效期为当前时间1小时后 + +const WARPCAST_RECOVERY_PROXY = '0x00000000FcB080a4D6c39a9354dA9EB9bC104cd7'; +``` + +### 2. 注册 app FID + +如果尚未拥有 app FID,请先注册。注册 FID 需要从 ID Gateway 读取价格,然后调用 ID Gateway 并支付注册费用。您可以从 Id Registry 合约读取新 FID,或从 `Register` 事件解析。此处我们从注册表合约读取。 + +```ts +const price = await publicClient.readContract({ + address: ID_GATEWAY_ADDRESS, + abi: idGatewayABI, + functionName: 'price', + args: [0n], +}); + +const { request } = await publicClient.simulateContract({ + account: app, + address: ID_GATEWAY_ADDRESS, + abi: idGatewayABI, + functionName: 'register', + args: [WARPCAST_RECOVERY_PROXY, 0n], + value: price, +}); +await walletClient.writeContract(request); + +const APP_FID = await publicClient.readContract({ + address: ID_REGISTRY_ADDRESS, + abi: idRegistryABI, + functionName: 'idOf', + args: [app.address], +}); +``` + +### 3. 收集用户的 `Register` 签名 + +从用户处收集 EIP-712 `Register` 签名。实际应用中,您可能在前端通过用户钱包收集此签名。前端环境中可使用 `ViemEip712WalletSigner` 连接浏览器钱包而非本地签名器。 + +```ts +let nonce = await publicClient.readContract({ + address: ID_REGISTRY_ADDRESS, + abi: idRegistryABI, + functionName: 'nonces', + args: [alice.address], +}); + +const registerSignatureResult = await aliceAccountKey.signRegister({ + to: alice.address as `0x${string}`, + recovery: WARPCAST_RECOVERY_PROXY, + nonce, + deadline, +}); + +let registerSignature; +if (registerSignatureResult.isOk()) { + registerSignature = registerSignatureResult.value; +} else { + throw new Error('生成注册签名失败'); +} +``` + +### 4. 创建新账户密钥对 + +为用户创建新的 Ed25519 账户密钥对。实际应用中请确保妥善保管用户私钥。 + +```ts +const privateKeyBytes = ed.utils.randomPrivateKey(); +const accountKey = new NobleEd25519Signer(privateKeyBytes); + +let accountPubKey = new Uint8Array(); +const accountKeyResult = await accountKey.getSignerKey(); +``` + +### 5. 使用应用账户创建 Signed Key Request + +由您的应用账户创建并签名 Signed Key Request。可使用 `getSignedKeyRequestMetadata` 辅助函数生成并签名。 + +```ts +if (accountKeyResult.isOk()) { + accountPubKey = accountKeyResult.value; + + const signedKeyRequestMetadata = + await appAccountKey.getSignedKeyRequestMetadata({ + requestFid: APP_FID, + key: accountPubKey, + deadline, + }); +} +``` + +### 6. 收集用户的 `Add` 签名 + +从用户处收集 EIP-712 `Add` 签名,授权将其账户密钥添加到 FID。 + +```ts +if (signedKeyRequestMetadata.isOk()) { + const metadata = bytesToHex(signedKeyRequestMetadata.value); + + nonce = await publicClient.readContract({ + address: KEY_GATEWAY_ADDRESS, + abi: keyGatewayABI, + functionName: 'nonces', + args: [alice.address], + }); + + const addSignatureResult = await aliceAccountKey.signAdd({ + owner: alice.address as `0x${string}`, + keyType: 1, + key: accountPubKey, + metadataType: 1, + metadata, + nonce, + deadline, + }); +} +``` + +### 7. 调用 Bundler 合约完成链上注册 + +调用 Key Gateway 合约并提供用户签名以在链上添加密钥。 + +```ts +if (addSignatureResult.isOk()) { + const addSignature = addSignatureResult.value; + + const price = await publicClient.readContract({ + address: BUNDLER_ADDRESS, + abi: bundlerABI, + functionName: 'price', + args: [0n], + }); + + const { request } = await publicClient.simulateContract({ + account: app, + address: BUNDLER_ADDRESS, + abi: bundlerABI, + functionName: 'register', + args: [ + { + to: alice.address, + recovery: WARPCAST_RECOVERY_PROXY, + sig: bytesToHex(registerSignature), + deadline, + }, + [ + { + keyType: 1, + key: bytesToHex(accountPubKey), + metadataType: 1, + metadata: metadata, + sig: bytesToHex(addSignature.value), + deadline, + }, + ], + 0n, + ], + value: price, + }); + await walletClient.writeContract(request); +} +``` + +### 完整代码示例 + +查看以下完整代码示例包含上述所有步骤。 + +::: code-group + +```ts [@farcaster/hub-web] +import * as ed from '@noble/ed25519'; +import { + ID_GATEWAY_ADDRESS, + ID_REGISTRY_ADDRESS, + ViemLocalEip712Signer, + idGatewayABI, + idRegistryABI, + NobleEd25519Signer, + BUNDLER_ADDRESS, + bundlerABI, + KEY_GATEWAY_ADDRESS, + keyGatewayABI, +} from '@farcaster/hub-nodejs'; +import { bytesToHex, createPublicClient, createWalletClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { optimism } from 'viem/chains'; + +const APP_PRIVATE_KEY = '0x00'; +const ALICE_PRIVATE_KEY = '0x00'; + +const publicClient = createPublicClient({ + chain: optimism, + transport: http('http://localhost:8545'), +}); + +const walletClient = createWalletClient({ + chain: optimism, + transport: http('http://localhost:8545'), +}); + +const app = privateKeyToAccount(APP_PRIVATE_KEY); +const appAccountKey = new ViemLocalEip712Signer(app as any); + +const alice = privateKeyToAccount(ALICE_PRIVATE_KEY); +const aliceAccountKey = new ViemLocalEip712Signer(alice as any); + +const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600); // 设置签名有效期为当前时间1小时后 + +const WARPCAST_RECOVERY_PROXY = '0x00000000FcB080a4D6c39a9354dA9EB9bC104cd7'; + +/******************************************************************************* + * IdGateway - register - 注册 app FID + *******************************************************************************/ + +/** + * 获取当前注册价格。我们不注册额外存储,因此传入 0n 作为唯一参数 + */ +const price = await publicClient.readContract({ + address: ID_GATEWAY_ADDRESS, + abi: idGatewayABI, + functionName: 'price', + args: [0n], +}); + +/** + * 调用 `register` 将 FID 注册到应用账户 + */ +const { request } = await publicClient.simulateContract({ + account: app, + address: ID_GATEWAY_ADDRESS, + abi: idGatewayABI, + functionName: 'register', + args: [WARPCAST_RECOVERY_PROXY, 0n], + value: price, +}); +await walletClient.writeContract(request); + +/** + * 从 Id Registry 合约读取 app fid + */ +const APP_FID = await publicClient.readContract({ + address: ID_REGISTRY_ADDRESS, + abi: idRegistryABI, + functionName: 'idOf', + args: [app.address], +}); + +/******************************************************************************* + * 从 Alice 收集 Register 签名 + *******************************************************************************/ + +let nonce = await publicClient.readContract({ + address: KEY_GATEWAY_ADDRESS, + abi: keyGatewayABI, + functionName: 'nonces', + args: [alice.address], +}); + +const registerSignatureResult = await aliceAccountKey.signRegister({ + to: alice.address as `0x${string}`, + recovery: WARPCAST_RECOVERY_PROXY, + nonce, + deadline, +}); + +let registerSignature; +if (registerSignatureResult.isOk()) { + registerSignature = registerSignatureResult.value; +} else { + throw new Error('生成注册签名失败'); +} + +/******************************************************************************* + * 从 Alice 收集 Add 签名 + *******************************************************************************/ + +/** + * 1. 为 Alice 创建 Ed25519 账户密钥对并获取公钥 + */ +const privateKeyBytes = ed.utils.randomPrivateKey(); +const accountKey = new NobleEd25519Signer(privateKeyBytes); + +let accountPubKey = new Uint8Array(); +const accountKeyResult = await accountKey.getSignerKey(); +if (accountKeyResult.isOk()) { + accountPubKey = accountKeyResult.value; + + /** + * 2. 从应用账户生成 Signed Key Request + */ + const signedKeyRequestMetadata = + await appAccountKey.getSignedKeyRequestMetadata({ + requestFid: APP_FID, + key: accountPubKey, + deadline, + }); + + if (signedKeyRequestMetadata.isOk()) { + const metadata = bytesToHex(signedKeyRequestMetadata.value); + /** + * 3. 从 Key Gateway 读取 Alice 的 nonce + */ + nonce = await publicClient.readContract({ + address: KEY_GATEWAY_ADDRESS, + abi: keyGatewayABI, + functionName: 'nonces', + args: [alice.address], + }); + + /** + * 然后收集她的 `Add` 签名 + */ + const addSignatureResult = await aliceAccountKey.signAdd({ + owner: alice.address as `0x${string}`, + keyType: 1, + key: accountPubKey, + metadataType: 1, + metadata, + nonce, + deadline, + }); + + if (addSignatureResult.isOk()) { + const addSignature = addSignatureResult.value; + /** + * 读取当前注册价格 + */ + const price = await publicClient.readContract({ + address: BUNDLER_ADDRESS, + abi: bundlerABI, + functionName: 'price', + args: [0n], + }); + + /** + * 使用 Alice 的签名、注册和密钥参数调用 `register` + */ + const { request } = await publicClient.simulateContract({ + account: app, + address: BUNDLER_ADDRESS, + abi: bundlerABI, + functionName: 'register', + args: [ + { + to: alice.address, + recovery: WARPCAST_RECOVERY_PROXY, + sig: bytesToHex(registerSignature), + deadline, + }, + [ + { + keyType: 1, + key: bytesToHex(accountPubKey), + metadataType: 1, + metadata: metadata, + sig: bytesToHex(addSignature), + deadline, + }, + ], + 0n, + ], + value: price, + }); + await walletClient.writeContract(request); + } + } +} +``` + +::: + +更多详情请参阅 [Bundler](/zh/reference/contracts/reference/bundler#register) 参考文档。 diff --git a/docs/zh/developers/guides/accounts/find-by-name.md b/docs/zh/developers/guides/accounts/find-by-name.md new file mode 100644 index 0000000..6f5640e --- /dev/null +++ b/docs/zh/developers/guides/accounts/find-by-name.md @@ -0,0 +1,43 @@ +# 通过用户名查找账户 + +如果您知道用户的名称并希望查找其账户,需要根据用户名的类型选择以下方法之一。 + +## 链下 ENS 名称 (Fnames) + +如果用户拥有类似 `@alice` 的链下 ENS 名称,您需要调用 [Fname 注册表](/zh/reference/fname/api#get-current-fname-or-fid)。 + +```bash +curl https://fnames.farcaster.xyz/transfers/current?name=farcaster | jq +{ + "transfers": [ + { + "id": 1, + "timestamp": 1628882891, + "username": "farcaster", + "owner": "0x8773442740c17c9d0f0b87022c722f9a136206ed", + "from": 0, + "to": 1, + "user_signature": "0xa6fdd2a69deab5633636f32a30a54b21b27dff123e6481532746eadca18cd84048488a98ca4aaf90f4d29b7e181c4540b360ba0721b928e50ffcd495734ef8471b", + "server_signature": "0xb7181760f14eda0028e0b647ff15f45235526ced3b4ae07fcce06141b73d32960d3253776e62f761363fb8137087192047763f4af838950a96f3885f3c2289c41b" + } + ] +} +``` + +如果名称已注册,此接口将返回与该名称关联的最新转移记录。请注意,Fname 的创建是从零地址到托管地址的转移。`to` 字段表示当前拥有该名称的 FID。 + +## 链上 ENS 名称 + +如果用户拥有类似 `@alice.eth` 的链上 ENS 名称,最简单的方法是使用 Hubble [复制器](../apps/replicate.md)。它会索引链上和链下数据,让您轻松找到所需信息。 + +设置完成后,在复制器数据库中查询 `fnames` 表以获取账户的 FID: + +```sql +SELECT username, fid +FROM fnames +WHERE username = 'farcaster.eth' +order by updated_at desc +limit 1; +``` + +有关复制器表结构的更多详情,请参阅[此处](/zh/reference/replicator/schema#fnames)。 diff --git a/docs/zh/developers/guides/advanced/decode-key-metadata.md b/docs/zh/developers/guides/advanced/decode-key-metadata.md new file mode 100644 index 0000000..7d8ea0e --- /dev/null +++ b/docs/zh/developers/guides/advanced/decode-key-metadata.md @@ -0,0 +1,66 @@ +# 解码密钥元数据 + +当用户向密钥注册表添加新密钥时,合约会在事件中发出[编码后的元数据](/zh/reference/contracts/reference/signed-key-request-validator#signedkeyrequestmetadata-struct)。您可以使用这些元数据来确定是谁请求了该密钥。 + +要解码密钥元数据,可以使用 Viem 的 `decodeAbiParameters` 函数: + +::: code-group + +```ts [Viem] +import { decodeAbiParameters } from 'viem'; + +const encodedMetadata = + '0x' + + '0000000000000000000000000000000000000000000000000000000000000020' + + '00000000000000000000000000000000000000000000000000000000000023C0' + + '00000000000000000000000002EF790DD7993A35FD847C053EDDAE940D055596' + + '0000000000000000000000000000000000000000000000000000000000000080' + + '00000000000000000000000000000000000000000000000000000000657C8AF5' + + '0000000000000000000000000000000000000000000000000000000000000041' + + 'F18569F4364BB2455DB27A4E8464A83AD8555416B1AAB21FF26E8501168F471F' + + '7EC17BB096EA4438E5BF82CE01224B2F67EF9042737B7B101C41A1A05CB469DC' + + '1B00000000000000000000000000000000000000000000000000000000000000'; + +const decoded = decodeAbiParameters( + [ + { + components: [ + { + name: 'requestFid', + type: 'uint256', + }, + { + name: 'requestSigner', + type: 'address', + }, + { + name: 'signature', + type: 'bytes', + }, + { + name: 'deadline', + type: 'uint256', + }, + ], + type: 'tuple', + }, + ], + encodedMetadata +); + +console.log(decoded); +/* +[ + { + requestFid: 9152n, + requestSigner: '0x02ef790Dd7993A35fD847C053EDdAE940D055596', + signature: '0xF185...69F4', + deadline: 1702660853n + } +] +*/ +``` + +::: + +更多详情请参阅[签名密钥请求验证器](/zh/reference/contracts/reference/signed-key-request-validator#signedkeyrequestmetadata-struct)参考文档。 diff --git a/docs/zh/developers/guides/advanced/query-signups.md b/docs/zh/developers/guides/advanced/query-signups.md new file mode 100644 index 0000000..f595d04 --- /dev/null +++ b/docs/zh/developers/guides/advanced/query-signups.md @@ -0,0 +1,19 @@ +# 按日统计注册量 + +::: info 前提条件 + +- 对 replicator 数据库拥有读取权限 + +::: + +要统计每日的注册数量,我们可以使用 `chain_events` 表来查询 [`ID_REGISTER`](/zh/reference/hubble/datatypes/events#onchaineventtype) 事件的数量,并按日分组。 + +```sql +SELECT DATE_TRUNC('day', created_at) AS day, COUNT(*) AS count +FROM chain_events +WHERE type = 3 -- ID_REGISTER (参见事件类型参考页面) +GROUP BY day +ORDER BY day desc; +``` + +有关数据库模式的更多信息,请参阅 [replicator 模式文档](/zh/reference/replicator/schema)。 diff --git a/docs/zh/developers/guides/apps/feed.md b/docs/zh/developers/guides/apps/feed.md new file mode 100644 index 0000000..a541022 --- /dev/null +++ b/docs/zh/developers/guides/apps/feed.md @@ -0,0 +1,162 @@ +# 为用户生成按时间排序的内容流 + +本示例将展示如何使用官方 [hub-nodejs](https://github.com/farcasterxyz/hub-monorepo/tree/main/packages/hub-nodejs) 在 TypeScript 中从 Farcaster 网络读取数据。 + +我们将从一个 FID 列表中创建一个简单的 casts 内容流,并按时间倒序显示它们。 + +## 安装 + +创建一个空的 TypeScript 项目并安装 hub-nodejs 包 + +```bash +yarn add @farcaster/hub-nodejs +``` + +## 1. 创建客户端 + +首先,设置一些常量并创建一个客户端来连接到 hub。 + +```typescript +import { getSSLHubRpcClient } from '@farcaster/hub-nodejs'; + +/** + * 用你自己的值填充以下常量 + */ + +const HUB_URL = 'hoyt.farcaster.xyz:2283'; // Hub 的 URL +const FIDS = [2, 3]; // 要获取 casts 的用户 ID + +// const client = getInsecureHubRpcClient(HUB_URL); // 如果不使用 SSL,请使用这个 +const client = getSSLHubRpcClient(HUB_URL); +``` + +## 2. 缓存 FID 对应的 fnames + +查询提供的 FID 的 UserData,以便我们可以显示友好的用户名。将它们缓存起来以备后用。 + +```typescript +// 创建一个 fid 到 fname 的映射,稍后我们需要用它来显示消息 +const fidToFname = new Map(); + +const fnameResultPromises = FIDS.map((fid) => + client.getUserData({ fid, userDataType: UserDataType.USERNAME }) +); +const fnameResults = await Promise.all(fnameResultPromises); + +fnameResults.map((result) => + result.map((uData) => { + if (isUserDataAddMessage(uData)) { + const fid = uData.data.fid; + const fname = uData.data.userDataBody.value; + fidToFname.set(fid, fname); + } + }) +); +``` + +## 3. 获取顶级 casts + +向 hub 查询每个 FID 的所有 casts,按时间倒序排序,然后筛选出仅顶级 casts。 + +```typescript +/** + * 返回用户未回复任何其他 casts 的 casts,按时间倒序排列。 + */ +const getPrimaryCastsByFid = async ( + fid: number, + client: HubRpcClient +): HubAsyncResult => { + const result = await client.getCastsByFid({ + fid: fid, + pageSize: 10, + reverse: true, + }); + if (result.isErr()) { + return err(result.error); + } + // 将 Messages 强制转换为 Casts,实际上不应过滤掉任何消息 + const casts = result.value.messages.filter(isCastAddMessage); + return ok(casts.filter((message) => !message.data.castAddBody.parentCastId)); +}; + +// 为每个 fid 获取主要 casts +const castResultPromises = FIDS.map((fid) => getPrimaryCastsByFid(fid, client)); +const castsResult = Result.combine(await Promise.all(castResultPromises)); + +if (castsResult.isErr()) { + console.error('获取 casts 失败'); + console.error(castsResult.error); + return; +} +``` + +## 4. 美化输出 cast 的函数 + +原始 cast 数据不太易读。我们将编写一个函数将时间戳转换为人类可读的格式,并将任何提及(仅存储为 fids 及其在 cast 中的位置)解析为它们的 fnames。 + +```typescript +const castToString = async ( + cast: CastAddMessage, + nameMapping: Map, + client: HubRpcClient +) => { + const fname = nameMapping.get(cast.data.fid) ?? `${cast.data.fid}!`; // 如果用户没有设置用户名,使用他们的 FID + + // 将时间戳转换为人类可读的字符串 + const unixTime = fromFarcasterTime(cast.data.timestamp)._unsafeUnwrap(); + const dateString = timeAgo.format(new Date(unixTime)); + + const { text, mentions, mentionsPositions } = cast.data.castAddBody; + const bytes = new TextEncoder().encode(text); + + const decoder = new TextDecoder(); + let textWithMentions = ''; + let indexBytes = 0; + for (let i = 0; i < mentions.length; i++) { + textWithMentions += decoder.decode( + bytes.slice(indexBytes, mentionsPositions[i]) + ); + const result = await getFnameFromFid(mentions[i], client); + result.map((fname) => (textWithMentions += fname)); + indexBytes = mentionsPositions[i]; + } + textWithMentions += decoder.decode(bytes.slice(indexBytes)); + + // 从消息文本中移除换行符 + const textNoLineBreaks = textWithMentions.replace(/(\r\n|\n|\r)/gm, ' '); + + return `${fname}: ${textNoLineBreaks}\n${dateString}\n`; +}; +``` + +## 5. 排序并打印 casts + +最后,我们可以再次按时间戳对 casts 进行排序(以便它们正确交错)并打印出来。 + +```typescript +/** + * 按时间戳比较两个 CastAddMessages,按时间倒序排列。 + */ +const compareCasts = (a: CastAddMessage, b: CastAddMessage) => { + if (a.data.timestamp < b.data.timestamp) { + return 1; + } + if (a.data.timestamp > b.data.timestamp) { + return -1; + } + return 0; +}; + +const sortedCasts = castsResult.value.flat().sort(compareCasts); // 按时间戳排序 casts +const stringifiedCasts = await Promise.all( + sortedCasts.map((c) => castToString(c, fidToFname, client)) +); // 将 casts 转换为可打印的字符串 + +for (const outputCast of stringifiedCasts) { + console.log(outputCast); +} +``` + +## 完整示例 + +查看完整示例 [这里](https://github.com/farcasterxyz/hub-monorepo/tree/main/packages/hub-nodejs/examples/chron-feed) diff --git a/docs/zh/developers/guides/apps/replicate.md b/docs/zh/developers/guides/apps/replicate.md new file mode 100644 index 0000000..23dd8c0 --- /dev/null +++ b/docs/zh/developers/guides/apps/replicate.md @@ -0,0 +1,14 @@ +# 将 Hubble 数据复制到 Postgres + +::: info 前提条件 + +- 本地或远程运行的 Hubble 实例(以获得更佳性能) + +::: + +虽然某些应用可以直接通过查询 Hubble 来编写,但大多数正式应用需要以更结构化的方式访问数据。 + +[Shuttle](https://github.com/farcasterxyz/hub-monorepo/tree/main/packages/shuttle) 是一个工具包, +可用于将 Hubble 的数据镜像到 Postgres 数据库,从而方便地访问底层数据。 + +查看文档以获取更多信息。 diff --git a/docs/zh/developers/guides/basics/hello-world.md b/docs/zh/developers/guides/basics/hello-world.md new file mode 100644 index 0000000..45e6cbd --- /dev/null +++ b/docs/zh/developers/guides/basics/hello-world.md @@ -0,0 +1,251 @@ +# Hello World + +通过编程方式创建您的 Farcaster 账户并发布第一条 "Hello World" 消息。 + +本示例将展示如何: + +- 通过链上交易创建账户 +- 租赁存储单元以便发布消息 +- 添加账户密钥用于消息签名 +- 为账户获取 fname +- 创建、签名并发布消息 + +完整可运行的代码仓库可在此处查看:[这里](https://github.com/farcasterxyz/hub-monorepo/tree/main/packages/hub-nodejs/examples/hello-world) + +### 前提条件 + +- 对 hub 的写入权限(可以是自建或第三方 hub) +- 一个 ETH 钱包,其中包含约 10 美元等值的 ETH 并已桥接至 [Optimism](https://www.optimism.io/) +- OP 主网的 ETH RPC URL(可通过 [Alchemy](https://www.alchemy.com/)、[Infura](https://www.infura.io/) 或 [QuickNode](https://www.quicknode.com/) 获取) + +## 1. 设置常量 + +```typescript +import { + ID_GATEWAY_ADDRESS, + idGatewayABI, + KEY_GATEWAY_ADDRESS, + keyGatewayABI, + ID_REGISTRY_ADDRESS, + idRegistryABI, + FarcasterNetwork, +} from '@farcaster/hub-web'; +import { zeroAddress } from 'viem'; +import { optimism } from 'viem/chains'; + +/** + * 用您自己的值填充以下常量 + */ +const MNEMONIC = '<必填>'; +const OP_PROVIDER_URL = '<必填>'; // Alchemy 或 Infura 的 URL +const RECOVERY_ADDRESS = zeroAddress; // 可选,使用默认值意味着如果助记词丢失,账户将无法恢复 +const ACCOUNT_KEY_PRIVATE_KEY: Hex = zeroAddress; // 可选,使用默认值意味着每次都会创建新的账户密钥 + +// 注意:hoyt 是 Farcaster 团队的主网 hub,受密码保护以防止滥用。请使用第三方 hub +// 提供商如 https://neynar.com/ 或者运行您自己的主网 hub 并无许可地向其广播。 +const HUB_URL = 'hoyt.farcaster.xyz:2283'; // Hub 的 URL + 端口 +const HUB_USERNAME = ''; // 认证用户名,如果不使用 TLS 则留空 +const HUB_PASS = ''; // 认证密码,如果不使用 TLS 则留空 +const USE_SSL = false; // 如果连接到使用 SSL 的 hub(第三方托管的 hub 或需要认证的 hub)则设为 true +const FC_NETWORK = FarcasterNetwork.MAINNET; // Hub 的网络 + +const CHAIN = optimism; + +const IdGateway = { + abi: idGatewayABI, + address: ID_GATEWAY_ADDRESS, + chain: CHAIN, +}; +const IdContract = { + abi: idRegistryABI, + address: ID_REGISTRY_ADDRESS, + chain: CHAIN, +}; +const KeyContract = { + abi: keyGatewayABI, + address: KEY_GATEWAY_ADDRESS, + chain: CHAIN, +}; +``` + +## 2. 注册并支付存储费用 + +创建一个函数来注册 FID 并支付存储费用。该函数会检查账户是否已有 FID,如果有则直接返回。 + +```typescript +const getOrRegisterFid = async (): Promise => { + const balance = await walletClient.getBalance({ address: account.address }); + // 检查是否已有 fid + const existingFid = (await walletClient.readContract({ + ...IdContract, + functionName: 'idOf', + args: [account.address], + })) as bigint; + + if (existingFid > 0n) { + return parseInt(existingFid.toString()); + } + + const price = await walletClient.readContract({ + ...IdGateway, + functionName: 'price', + }); + if (balance < price) { + throw new Error(`余额不足以租赁存储,需要: ${price},当前余额: ${balance}`); + } + const { request: registerRequest } = await walletClient.simulateContract({ + ...IdGateway, + functionName: 'register', + args: [RECOVERY_ADDRESS], + value: price, + }); + const registerTxHash = await walletClient.writeContract(registerRequest); + const registerTxReceipt = await walletClient.waitForTransactionReceipt({ + hash: registerTxHash, + }); + // 现在从日志中提取 FID + const registerLog = decodeEventLog({ + abi: idRegistryABI, + data: registerTxReceipt.logs[0].data, + topics: registerTxReceipt.logs[0].topics, + }); + const fid = parseInt(registerLog.args['id']); + return fid; +}; + +const fid = await getOrRegisterFid(); +``` + +## 3. 添加账户密钥 + +现在,我们将向密钥注册表添加一个账户密钥。每个账户密钥都必须有来自请求它的应用的 fid 的签名元数据字段。 +在本例中,我们将使用自己的 fid。注意,这需要使用持有 fid 的地址的私钥对消息进行签名。如果无法做到这一点, +请先为应用注册一个单独的 fid 并使用它。 + +```typescript +const getOrRegisterAccountKey = async (fid: number) => { + if (ACCOUNT_KEY_PRIVATE_KEY !== zeroAddress) { + // 如果提供了私钥,我们假设账户密钥已在密钥注册表中 + const privateKeyBytes = fromHex(ACCOUNT_KEY_PRIVATE_KEY, 'bytes'); + const publicKeyBytes = ed25519.getPublicKey(privateKeyBytes); + return privateKeyBytes; + } + + const privateKey = ed25519.utils.randomPrivateKey(); + const publicKey = toHex(ed25519.getPublicKey(privateKey)); + + const eip712signer = new ViemLocalEip712Signer(appAccount); + // 要添加密钥,我们需要用应用 fid 对元数据进行签名 + // 使用您的个人 fid,或为应用注册单独的 fid + const metadata = await eip712signer.getSignedKeyRequestMetadata({ + requestFid: APP_FID, + key: APP_PRIVATE_KEY, + deadline: Math.floor(Date.now() / 1000) + 60 * 60, // 从现在起 1 小时 + }); + + const { request: signerAddRequest } = await walletClient.simulateContract({ + ...KeyContract, + functionName: 'add', + args: [1, publicKey, 1, metadata], // keyType, publicKey, metadataType, metadata + }); + + const accountKeyAddTxHash = await walletClient.writeContract( + signerAddRequest + ); + await walletClient.waitForTransactionReceipt({ hash: accountKeyAddTxHash }); + // 等待 30 秒以便 hub 获取 accountKey 交易 + await new Promise((resolve) => setTimeout(resolve, 30000)); + return privateKey; +}; + +const accountPrivateKey = await getOrRegisterAccountKey(fid); +``` + +## 4. 注册 fname + +现在链上操作已完成,让我们使用 farcaster 的链下 fname 注册表注册一个 fname。 +注册 fname 需要 fid 的托管地址的签名。 + +```typescript +const registerFname = async (fid: number) => { + try { + // 首先检查此 fid 是否已有 fname + const response = await axios.get( + `https://fnames.farcaster.xyz/transfers/current?fid=${fid}` + ); + const fname = response.data.transfer.username; + return fname; + } catch (e) { + // 没有用户名,忽略并继续注册 + } + + const fname = `fid-${fid}`; + const timestamp = Math.floor(Date.now() / 1000); + const userNameProofSignature = await walletClient.signTypedData({ + domain: USERNAME_PROOF_DOMAIN, + types: USERNAME_PROOF_TYPE, + primaryType: 'UserNameProof', + message: { + name: fname, + timestamp: BigInt(timestamp), + owner: account.address, + }, + }); + + const response = await axios.post('https://fnames.farcaster.xyz/transfers', { + name: fname, // 要注册的名称 + from: 0, // 转移来源的 Fid(0 表示新注册) + to: fid, // 转移目标的 Fid(0 表示注销) + fid: fid, // 发起请求的 Fid(必须与 from 或 to 匹配) + owner: account.address, // 发起请求的 fid 的托管地址 + timestamp: timestamp, // 当前时间戳(秒) + signature: userNameProofSignature, // 由 fid 当前托管地址签名的 EIP-712 签名 + }); + return fname; +}; + +const fname = await registerFname(fid); +``` + +注意,这只是将名称与我们的 fid 关联,我们仍需将其设置为用户名。 + +## 5. 写入 hub + +最后,我们现在可以向 hub 提交消息了。首先,我们将 fname 设置为用户名,然后发布一条 cast。 + +```typescript +const submitMessage = async (resultPromise: HubAsyncResult) => { + const result = await resultPromise; + if (result.isErr()) { + throw new Error(`创建消息时出错: ${result.error}`); + } + await hubClient.submitMessage(result.value); +}; + +const accountKey = new NobleEd25519Signer(accountPrivateKey); +const dataOptions = { + fid: fid, + network: FC_NETWORK, +}; +const userDataUsernameBody = { + type: UserDataType.USERNAME, + value: fname, +}; +// 设置用户名 +await submitMessage( + makeUserDataAdd(userDataUsernameBody, dataOptions, accountKey) +); + +// 发布 cast +await submitMessage( + makeCastAdd( + { + text: 'Hello World!', + }, + dataOptions, + accountKey + ) +); +``` + +现在,您可以在任何 farcaster 客户端上查看您的个人资料。要在 Warpcast 上查看,请访问 `https://warpcast.com/@` diff --git a/docs/zh/developers/guides/querying/fetch-casts.md b/docs/zh/developers/guides/querying/fetch-casts.md new file mode 100644 index 0000000..ba59894 --- /dev/null +++ b/docs/zh/developers/guides/querying/fetch-casts.md @@ -0,0 +1,43 @@ +# 获取账户消息 + +::: info 前提条件 + +- 对 hubble 实例拥有只读访问权限 + +::: + +有关如何设置本地 hubble 实例的更多信息,请参阅 [hubble 安装指南](/zh/hubble/install)。 + +要查询特定 FID 的所有 casts(广播消息),可以使用 castsByFid HTTP 端点: + +```bash +# 默认 http 端口为 2281 +$ curl http://localhost:2281/v1/castsByFid\?fid\=1 | jq ".messages[].data.castAddBody.text | select( . != null)" +"testing" +"test" +" +"another test" +"another testy test" +``` + +这将返回该 fid 所有与 cast 相关的消息。对于 reactions(反应)和 follows(关注)也有类似的端点。更多详情请参阅 [http api 参考文档](/zh/reference/hubble/httpapi/httpapi)。 + +如果是从源码安装的 hubble,可以使用内置的 `console` 工具。这将使用 grpc API: + +```bash +# 确保当前在 hubble 子目录下 +$ cd apps/hubble +# 如果主机使用 TLS 请移除 `--insecure` 参数 +$ yarn console --insecure -s localhost:2283 +> res = await rpcClient.getCastsByFid({fid: 1}) +Ok { + value: { + messages: [ [Object], [Object], [Object], [Object] ], + nextPageToken: + } +} +> res.value.messages.map( m => m.data.castAddBody.text) +[ 'testing', 'test', 'another test', 'another testy test' ] +``` + +有关 GRPC API 的更多详情,请参阅 [grpc api 参考文档](/zh/reference/hubble/grpcapi/grpcapi)。 diff --git a/docs/zh/developers/guides/querying/fetch-channel-casts.md b/docs/zh/developers/guides/querying/fetch-channel-casts.md new file mode 100644 index 0000000..12510b7 --- /dev/null +++ b/docs/zh/developers/guides/querying/fetch-channel-casts.md @@ -0,0 +1,60 @@ +# 获取频道中的 casts(广播消息) + +::: info 前提条件 + +- 拥有 hubble 实例的读取权限 + +::: + +要从频道获取 casts,Hubble 提供了 `getCastsByParent` API 调用。 + +例如,要查询所有发送到 `ethereum` 频道的 casts: + +```bash +$ curl http://localhost:2281/v1/castsByParent\?fid\=1\&url\="https://ethereum.org" | jq " .messages | limit(10;.[]) | .data.castAddBody.text" +"cue " +"Commandeering this channel for the one true Ethereum, Ethereum classic." +"Pretty amazing that even during a bear market, the 30 day average burn gives us deflationary ETH. \n\nSource: Ultrasound.Money" +"So, Ethereum is frequently called the Base Layer or L1 when talking about scalability.\n\nBut how can you call the whole Ethereum + Ethereum L2s + L3s that commit to Ethereum L2s?\n\nWe’re building a unified multi-chain explorer for that, but we don’t know how to call it: https://ethereum.routescan.io" +"This, we're already doing.\n\nBut we call it, the Full-Index Ecosystem Explorer." +". By subdomains do you mean more specific namespaces, e.g. Farcaster fnames being name.farcaster.eth?\n\nMy guess is if the root .eth namespace is available it will command higher status and value.\n\nhttps://x.com/0xfoobar/status/1687209523239604230" +"what are the best examples of DAOs with independent core working groups/multisigs?" +"Anyone here use Rabby?\n\nhttps://x.com/0xfoobar/status/1687474090150416385" +"Who needs stablecoins when we have ETH" +"782,672 active + pending validators! 🤯 \n\n(also... I haven't proposed a block for a few months)" +``` + +使用 GRPC API 的示例: + +```bash +> res = await rpcClient.getCastsByParent({parentUrl: "https://ethereum.org"}) +> res.value.messages.slice(0, 10).map( m => m.data.castAddBody.text) +[ + 'cue ', + 'Commandeering this channel for the one true Ethereum, Ethereum classic.', + 'Pretty amazing that even during a bear market, the 30 day average burn gives us deflationary ETH. \n' + + '\n' + + 'Source: Ultrasound.Money', + 'So, Ethereum is frequently called the Base Layer or L1 when talking about scalability.\n' + + '\n' + + 'But how can you call the whole Ethereum + Ethereum L2s + L3s that commit to Ethereum L2s?\n' + + '\n' + + 'We’re building a unified multi-chain explorer for that, but we don’t know how to call it: https://ethereum.routescan.io', + "This, we're already doing.\n" + + '\n' + + 'But we call it, the Full-Index Ecosystem Explorer.', + '. By subdomains do you mean more specific namespaces, e.g. Farcaster fnames being name.farcaster.eth?\n' + + '\n' + + 'My guess is if the root .eth namespace is available it will command higher status and value.\n' + + '\n' + + 'https://x.com/0xfoobar/status/1687209523239604230', + 'what are the best examples of DAOs with independent core working groups/multisigs?', + 'Anyone here use Rabby?\n' + + '\n' + + 'https://x.com/0xfoobar/status/1687474090150416385', + 'Who needs stablecoins when we have ETH', + '782,672 active + pending validators! 🤯 \n' + + '\n' + + "(also... I haven't proposed a block for a few months)" +] +``` diff --git a/docs/zh/developers/guides/querying/fetch-profile.md b/docs/zh/developers/guides/querying/fetch-profile.md new file mode 100644 index 0000000..18fe10f --- /dev/null +++ b/docs/zh/developers/guides/querying/fetch-profile.md @@ -0,0 +1,59 @@ +# 获取账户资料 + +::: info 前提条件 + +- 对 hubble 实例拥有只读访问权限 + +::: + +要获取账户资料详情,可使用以下命令: + +```bash +$ curl http://localhost:2281/v1/userDataByFid\?fid\=1 | jq ".messages[].data.userDataBody" +{ + "type": "USER_DATA_TYPE_PFP", + "value": "https://i.imgur.com/I2rEbPF.png" +} +{ + "type": "USER_DATA_TYPE_BIO", + "value": "一个充分去中心化的社交网络。farcaster.xyz" +} +{ + "type": "USER_DATA_TYPE_DISPLAY", + "value": "Farcaster" +} +{ + "type": "USER_DATA_TYPE_USERNAME", + "value": "farcaster" +} +``` + +更多详情请参阅 [HTTP API 参考文档](/zh/reference/hubble/httpapi/userdata)。 + +如果是从源码安装的 hubble,可以使用内置的 `console` 工具。这将使用 gRPC API: + +```bash +# 确保当前在 hubble 子目录下 +$ cd apps/hubble +# 如果主机使用 TLS 请移除 `--insecure` 参数 +$ yarn console --insecure -s localhost:2283 +> res = await rpcClient.getUserDataByFid({fid: 1}) +Ok { + value: { + messages: [ [Object], [Object], [Object], [Object] ], + nextPageToken: + } +} +> res.value.messages.map(m => m.data.userDataBody) +[ + { type: 1, value: 'https://i.imgur.com/I2rEbPF.png' }, + { + type: 3, + value: '一个充分去中心化的社交网络。farcaster.xyz' + }, + { type: 2, value: 'Farcaster' }, + { type: 6, value: 'farcaster' } +] +``` + +关于 gRPC API 的更多详情,请参阅 [gRPC API 参考文档](/zh/reference/hubble/grpcapi/grpcapi)。 diff --git a/docs/zh/developers/guides/querying/setting-up.md b/docs/zh/developers/guides/querying/setting-up.md new file mode 100644 index 0000000..3cc0499 --- /dev/null +++ b/docs/zh/developers/guides/querying/setting-up.md @@ -0,0 +1,60 @@ +# 连接 Hubble + +::: info 前提条件 + +- 对 hubble 实例的只读访问权限 + +::: + +有关如何设置本地 hubble 实例的更多信息,请参阅 [hubble 安装指南](/zh/hubble/install)。 + +当 hub 运行后,可以通过查询 HTTP API 来验证是否可以正常访问: + +```bash +# 默认 HTTP 端口为 2281 +$ curl http://localhost:2281/v1/castsByFid\?fid\=1 | jq ".messages[1]" +{ + "data": { + "type": "MESSAGE_TYPE_CAST_ADD", + "fid": 1, + "timestamp": 62108253, + "network": "FARCASTER_NETWORK_MAINNET", + "castAddBody": { + "embedsDeprecated": [], + "mentions": [], + "parentCastId": { + "fid": 3, + "hash": "0x2d8c167ac383d51328c0ffd785ccdbaf54be45e7" + }, + "text": "test", + "mentionsPositions": [], + "embeds": [] + } + }, + "hash": "0x0e38d339e175e4df88c553102ea7f4db43d39f1b", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "dVsNn061CoMhQbleRlPTOL8a/rn9wNCIJnwcXzJnHLXK9RyceVGVPkmxtP7vAnpb+2UYhUwncnHgDHaex/lqBw==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0xb85cf7feef230f30925b101223fd3e3dc4e1120bacd677f5ad3523288f8f7102" +} +``` + +有关 HTTP API 的更多详细信息,请参阅 [HTTP API 参考文档](/zh/reference/hubble/httpapi/httpapi)。 + +或者,如果您已从源代码安装了 hubble,可以使用内置的 `console`。这将使用 GRPC API: + +```bash +# 确保当前位于 hubble 子目录中 +$ cd apps/hubble +# 如果主机使用 TLS,请移除 `--insecure` 参数 +$ yarn console --insecure -s localhost:2283 +> res = await rpcClient.getCastsByFid({fid: 1}) +Ok { + value: { + messages: [ [Object], [Object], [Object], [Object] ], + nextPageToken: + } +} +``` + +有关 GRPC API 的更多详细信息,请参阅 [GRPC API 参考文档](/zh/reference/hubble/grpcapi/grpcapi)。 diff --git a/docs/zh/developers/guides/writing/casts.md b/docs/zh/developers/guides/writing/casts.md new file mode 100644 index 0000000..6cdc49f --- /dev/null +++ b/docs/zh/developers/guides/writing/casts.md @@ -0,0 +1,138 @@ +# 创建广播 + +[消息](./messages.md)教程中已涵盖创建简单广播的内容。本教程将介绍高级主题,如提及、嵌入内容、表情符号、回复和频道。 + +## 设置 + +- 参见[创建消息](./messages.md)中的设置说明 + +## 提及 + +用户可以通过@用户名提及(例如"你好 @bob!")在广播中被标记,这会触发客户端发送通知。 + +提及不会直接包含在广播文本中。目标 fid 和提及在文本中的位置通过`mentions`和`mentionPositions`数组指定,客户端在渲染时会将它们填充到广播中。 + +```typescript +/** + * "@dwr 和 @v 是 @farcaster 的忠实粉丝" + */ +const castWithMentions = await makeCastAdd( + { + text: ' 和 是 的忠实粉丝', + embeds: [], + embedsDeprecated: [], + mentions: [3, 2, 1], + mentionsPositions: [0, 5, 22], // 以字节为单位的位置(非字符) + }, + dataOptions, + ed25519Signer +); +``` + +## 嵌入内容 + +URL 可以被嵌入到广播中,指示客户端渲染预览。 + +一条广播最多可包含 2 个嵌入内容,每个最长 256 字节。嵌入内容不进行其他验证。 + +```typescript +/** + * 包含提及和附件的广播 + * + * "嘿 @dwr,看看这个!" + */ +const castWithMentionsAndAttachment = await makeCastAdd( + { + text: '嘿,看看这个!', + embeds: [{ url: 'https://farcaster.xyz' }], + embedsDeprecated: [], + mentions: [3], + mentionsPositions: [4], + }, + dataOptions, + ed25519Signer +); +``` + +## 表情符号 + +表情符号可以直接包含在广播文本中,并由客户端渲染。 + +由于一个表情符号字符通常占用多个字节但显示为单个字符,这会影响提及位置和广播长度的计算方式。 + +```typescript +/** + * 包含表情符号和提及的广播 + * + * "🤓@farcaster 可以在表情符号后立即提及" + */ +const castWithEmojiAndMentions = await makeCastAdd( + { + text: '🤓 可以在表情符号后立即提及', + embeds: [], + embedsDeprecated: [], + mentions: [1], + mentionsPositions: [4], + }, + dataOptions, + ed25519Signer +); +``` + +## 回复 + +广播可以是对另一条广播、URL 或 NFT 的回复。对另一条广播的回复被视为线程,而对 URL 或 NFT 的回复可被视为评论或[频道](#channels)。 + +可选的 parentUrl 属性可以设置为 URL 或所回复广播的 fid-hash 值,如下例所示。有关回复类型的更多详情,请参阅[FIP-2](https://github.com/farcasterxyz/protocol/discussions/71)。 + +```typescript +/** + * 回复URL的广播 + * + * "我认为这是个很棒的协议 🚀" + */ +const castReplyingToAUrl = await makeCastAdd( + { + text: '我认为这是个很棒的协议 🚀', + embeds: [], + embedsDeprecated: [], + mentions: [], + mentionsPositions: [], + parentUrl: 'https://www.farcaster.xyz/', + }, + dataOptions, + ed25519Signer +); +``` + +## 频道 + +广播可以发送到频道中,这指示客户端它与特定主题相关。客户端可以选择以不同方式使用此元数据,例如将频道广播分组或向特定用户推荐。 + +频道本质上是一个`parentURL`,可以是 URL 或 NFT,所有客户端通过松散共识将其识别为频道。目前协议层面尚未就频道列表达成一致,但可以使用复制器数据库中的`casts`表获取常用频道列表。 + +```sql +/* 查询频道列表 */ +select parent_url, + count(*) as count +from casts +where parent_url is not null +group by parent_url +order by count desc; +``` + +```typescript +// 发送到Farcaster频道的广播 +const channelCast = await makeCastAdd( + { + text: '我爱farcaster', + embeds: [], + embedsDeprecated: [], + mentions: [], + mentionsPositions: [], + parentUrl: 'https://www.farcaster.xyz/', // 此为示例,并非实际频道URL + }, + dataOptions, + ed25519Signer +); +``` diff --git a/docs/zh/developers/guides/writing/messages.md b/docs/zh/developers/guides/writing/messages.md new file mode 100644 index 0000000..29b322e --- /dev/null +++ b/docs/zh/developers/guides/writing/messages.md @@ -0,0 +1,166 @@ +# 创建消息 + +消息代表用户执行的操作(例如 Alice 发送了"hello world") + +消息有多种类型,本教程将介绍最常见的几种。其他教程会涵盖更高级的消息类型。 + +## 准备工作 + +你需要: + +- 对 hubble 实例的写入权限 +- 已注册到 fid 的签名者私钥 +- 导入如下所示的 `hub-nodejs` 和辅助函数 + +```ts +import { + makeCastAdd, + makeCastRemove, + makeLinkAdd, + makeLinkRemove, + makeReactionAdd, + makeReactionRemove, + makeUserDataAdd, + NobleEd25519Signer, + FarcasterNetwork, +} from '@farcaster/hub-nodejs'; + +const ACCOUNT_PRIVATE_KEY: Hex = '0x...'; // 你的账户私钥 +const FID = -1; // 你的 fid +const ed25519Signer = new NobleEd25519Signer(ACCOUNT_PRIVATE_KEY); +const dataOptions = { + fid: FID, + network: FC_NETWORK, +}; +const FC_NETWORK = FarcasterNetwork.MAINNET; +``` + +## Casts(广播消息) + +Casts 是用户创建的公开消息。 + +通过发送包含文本内容及可选嵌入内容、提及和表情的 CastAdd 消息来创建 cast。以下示例展示了一个简单 cast 的创建过程。 + +```typescript +const cast = await makeCastAdd( + { + text: '这是一条广播消息!', // 文本最长可达 320 字节 + embeds: [], + embedsDeprecated: [], + mentions: [], + mentionsPositions: [], + }, + dataOptions, + ed25519Signer +); +``` + +可以通过发送带有 CastAdd 消息哈希和更晚时间戳的 CastRemove 消息来删除 cast。 + +```typescript +const castRemove = await makeCastRemove( + { + targetHash: castReplyingToAUrl._unsafeUnwrap().hash, + }, + dataOptions, + ed25519Signer +); +``` + +要创建包含嵌入内容、提及、频道表情的 casts,请参阅 [casts](../writing/casts.md) 教程。 + +## 互动(Reactions) + +互动是用户与 cast 之间的强类型关系(例如点赞)。 + +用户通过发送类型设为 `like` 且目标设为 cast 哈希及其作者 fid 的 ReactionAdd 消息来"点赞"一条 cast。 + +```typescript +const reactionAdd = await makeReactionAdd( + { + type: ReactionType.LIKE, + targetCastId: { fid: createdCast.data.fid, hash: createdCast.hash }, + }, + dataOptions, + ed25519Signer +); +``` + +可以通过广播带有相同信息和更晚时间戳的 ReactionRemove 消息来取消点赞。 + +```typescript +const reactionRemove = await makeReactionRemove( + { + type: ReactionType.LIKE, + targetCastId: { fid: createdCast.data.fid, hash: createdCast.hash }, + }, + dataOptions, // 必须提供更高的时间戳 + ed25519Signer +); +``` + +用户可以通过非常相似的过程进行"转发"(recast)。 + +```typescript +const recastAdd = await makeReactionAdd( + { + type: ReactionType.RECAST, + targetCastId: { fid: createdCast.data.fid, hash: createdCast.hash }, + }, + dataOptions, + ed25519Signer +); + +const recastRemove = await makeReactionRemove( + { + type: ReactionType.RECAST, + targetCastId: { fid: createdCast.data.fid, hash: createdCast.hash }, + }, + dataOptions, + ed25519Signer +); +``` + +## 用户数据(User Data) + +UserData 是一组强类型消息,代表用户的元数据(例如个人简介、头像)。 + +`UserData` 消息包含一个类型和一个可设置的字符串值。以下示例展示了用户更新其个人简介。 + +```typescript +// 更新用户简介。其他字段类似,只需更改类型。值始终为字符串。 +const bioUpdate = await makeUserDataAdd( + { + type: UserDataType.BIO, + value: '新简介', + }, + dataOptions, + ed25519Signer +); +``` + +## 链接(Links) + +链接是用户之间的松散类型关系(例如 Alice 关注 Bob)。 + +用户通过发送带有字符串类型和目标用户 fid 的 LinkAdd 消息来创建链接。所有客户端最常支持的链接类型是'follow'(关注)。 + +```typescript +const follow = await makeLinkAdd( + { + type: 'follow', + targetFid: 1, + }, + dataOptions, + ed25519Signer +); + +const unfollow = await makeLinkRemove( + { + type: 'unfollow', + targetFid: 1, + }, + dataOptions, + ed25519Signer +); +``` diff --git a/docs/zh/developers/guides/writing/submit-messages.md b/docs/zh/developers/guides/writing/submit-messages.md new file mode 100644 index 0000000..8bb507f --- /dev/null +++ b/docs/zh/developers/guides/writing/submit-messages.md @@ -0,0 +1,12 @@ +# 向 hub 提交数据 + +::: info 前提条件 + +- 对 hubble 实例的写入权限 +- 已注册到 fid 的账户密钥的私钥 +- 本地 typescript 开发环境 + +::: + +要查看如何向 hub 提交不同类型消息的完整示例, +请参阅[此处](https://github.com/farcasterxyz/hub-monorepo/tree/main/packages/hub-nodejs/examples/write-data) diff --git a/docs/zh/developers/guides/writing/verify-address.md b/docs/zh/developers/guides/writing/verify-address.md new file mode 100644 index 0000000..ab6543b --- /dev/null +++ b/docs/zh/developers/guides/writing/verify-address.md @@ -0,0 +1,123 @@ +# 创建地址验证 + +::: info 前提条件 + +- 拥有对 hubble 实例的写入权限 +- 已注册到 fid 的账户私钥 +- OP 主网的以太坊提供商 URL(例如通过 [Alchemy](https://www.alchemy.com/)、[Infura](https://www.infura.io/) 或 [QuickNode](https://www.quicknode.com/)) + +::: + +要创建证明外部以太坊地址所有权的[验证](https://docs.farcaster.xyz/reference/hubble/datatypes/messages.html#_6-verification),可以使用 `Eip712Signer` 签署验证消息,并使用 `makeVerificationAddEthAddress` 函数构建发送至 Hub 的消息。 + +首先,设置客户端和账户密钥: + +```ts +import { + NobleEd25519Signer, + ViemLocalEip712Signer, + FarcasterNetwork, + makeVerificationAddEthAddress, +} from '@farcaster/hub-nodejs'; +import { createPublicClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { optimism } from 'viem/chains'; + +const publicClient = createPublicClient({ + chain: optimism, + transport: http(), +}); + +const alice = privateKeyToAccount('0xab..12'); +const eip712Signer = new ViemLocalEip712Signer(alice); + +const ed25519Signer = new NobleEd25519Signer('0xcd..34'); +``` + +然后使用 `Eip712Signer` 创建验证签名。需要查询 OP 主网上的最新区块哈希并将其包含在签名消息中。 + +```ts +const latestBlock = await publicClient.getBlock(); + +const fid = 1n; +const network = FarcasterNetwork.MAINNET; +const address = alice.address; +const blockHash = latestBlock.blockhash; + +const ethSignature = eip712Signer.signVerificationEthAddressClaim({ + fid, + address, + network, + blockHash, +}); +``` + +最后,使用 `makeVerificationAddEthAddress` 构建可发送至 Hub 的[`验证`](https://docs.farcaster.xyz/reference/hubble/datatypes/messages.html#_6-verification)消息。 + +```ts +if (ethSignature.isOk()) { + const message = await makeVerificationAddEthAddress( + { + address, + blockHash, + ethSignature, + }, + { fid, network }, + ed25519Signer + ); +} +``` + +### 完整代码示例 + +::: code-group + +```ts [@farcaster/hub-nodejs] +import { + NobleEd25519Signer, + ViemLocalEip712Signer, + FarcasterNetwork, + makeVerificationAddEthAddress, +} from '@farcaster/hub-nodejs'; +import { createPublicClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { optimism } from 'viem/chains'; + +const publicClient = createPublicClient({ + chain: optimism, + transport: http(), +}); + +const alice = privateKeyToAccount('0xab..12'); +const eip712Signer = new ViemLocalEip712Signer(alice); + +const ed25519Signer = new NobleEd25519Signer('0xcd..34'); + +const latestBlock = await publicClient.getBlock(); + +const fid = 1n; +const network = FarcasterNetwork.MAINNET; +const address = alice.address; +const blockHash = latestBlock.blockhash; + +const ethSignature = eip712Signer.signVerificationEthAddressClaim({ + fid, + address, + network, + blockHash, +}); + +if (ethSignature.isOk()) { + const message = await makeVerificationAddEthAddress( + { + address, + blockHash, + ethSignature, + }, + { fid, network }, + ed25519Signer + ); +} +``` + +::: diff --git a/docs/zh/developers/index.md b/docs/zh/developers/index.md new file mode 100644 index 0000000..231e965 --- /dev/null +++ b/docs/zh/developers/index.md @@ -0,0 +1,32 @@ +:::tip 加入讨论 +在 Farcaster 的 [/fc-devs](https://warpcast.com/~/channel/fc-devs) 频道中与其他开发者交流提问。 +::: + +## 创建 mini apps + +学习如何构建在 Farcaster 信息流中运行的 mini apps(原称为 Frames)。 + + +- [简介](https://miniapps.farcaster.xyz/){target="_self"} - 了解什么是 mini app 及其工作原理 + +- [快速开始](https://miniapps.farcaster.xyz/docs/getting-started){target="_self"} - 构建你的第一个 mini app + +## 使用 Farcaster 登录 + +让用户能够轻松使用他们的 Farcaster 账户登录你的应用。 + +- [示例](/zh/auth-kit/examples.md) - 查看 Sign in with Farcaster (SIWF) 的实际应用 +- [AuthKit](/zh/auth-kit/installation.md) - 用于集成 SIWF 的 React 工具包 +- [FIP-11](https://github.com/farcasterxyz/protocol/discussions/110) - SIWF 的正式标准文档 + +## 分析 Farcaster 数据 + +将 Farcaster 网络同步到本地机器,以便对数据运行查询。 + +- [运行 hub](/zh/hubble/install.md) - 在本地机器上实时获取 Farcaster 数据 +- [编写第一个 hub 查询](./guides/querying/fetch-casts.md) - 从 hub 获取账户的 casts +- [设置 replicator](./guides/apps/replicate.md) - 将 hub 同步到 postgres 数据库以运行高级查询 + +## 写入 Farcaster + +- [Hello World](/zh/developers/guides/basics/hello-world) - 通过编程方式创建账户并发布 cast diff --git a/docs/zh/developers/resources.md b/docs/zh/developers/resources.md new file mode 100644 index 0000000..6f97498 --- /dev/null +++ b/docs/zh/developers/resources.md @@ -0,0 +1,55 @@ +# 资源列表 + +这是一份精选的 Farcaster 开发者最佳库和工具清单。更全面的资源列表可在 a16z 的 [awesome-farcaster](https://github.com/a16z/awesome-farcaster) 代码库中找到。 + +欢迎通过 Pull Request 提交资源。提交的资源必须专为 Farcaster 构建、积极维护,并符合协议的精神和规范。 + +:::warning +这些资源通常来自第三方且未经审查。使用时请自行承担风险。 +::: + +## 库 + +### Mini Apps + + +- [@farcaster/mini-app](https://miniapps.farcaster.xyz/docs/getting-started){target="_self"} - 用于构建 mini app 的 CLI 工具。 +- [@neynar/create-farcaster-mini-app](https://www.npmjs.com/package/@neynar/create-farcaster-mini-app) - Neynar 提供的 mini app 快速启动 npx 脚本。 + +### 传统 Frames + +- [@frame-js/frames](https://framesjs.org/) - 用于构建和调试 frames 的 next.js 模板。 +- [@coinbase/onchainkit](https://github.com/coinbase/onchainkit) - 创建 frames 的 React 工具包。 +- [@frog](https://frog.fm) - frames 开发框架。 + +### 应用 + +- [farcaster kit](https://www.farcasterkit.com/) - 用于构建 Farcaster 应用的 React hooks。 + +### Hubs + +- [@farcaster/hub-nodejs](https://www.npmjs.com/package/@farcaster/hub-nodejs) - 轻量、快速的 Farcaster Hubs TypeScript 接口。 + +### 链上 + +- [farcaster-solidity](https://github.com/pavlovdog/farcaster-solidity/) - 用于在链上验证和解析 Farcaster 消息的库 + +## 仪表盘 + +- [Farcaster Hub Map](https://farcaster.spindl.xyz/) - Farcaster hubs 地理分布图 +- [Dune: Farcaster](https://dune.com/pixelhack/farcaster) - @pixelhack 的网络统计仪表盘 +- [Dune: Farcaster User Onchain Activities](https://dune.com/yesyes/farcaster-users-onchain-activities) - Farcaster 用户链上活动仪表盘 + +## 学习资源 + +- [dTech Farcaster 教程](https://dtech.vision/farcaster) + +## 开源示例 + +- [quikcast](https://github.com/farcasterxyz/quikcast) - Farcaster 端到端连接应用实现 +- [fc-polls](https://github.com/farcasterxyz/fc-polls) - 使用 frames 构建的简单投票应用 + +## 服务 + +- [Neynar](https://neynar.com/) - 构建 Farcaster 应用的基础设施和服务。 +- [dTech](https://dtech.vision) - Farcaster 开发与咨询机构 diff --git a/docs/zh/developers/siwf/index.md b/docs/zh/developers/siwf/index.md new file mode 100644 index 0000000..9fa13c9 --- /dev/null +++ b/docs/zh/developers/siwf/index.md @@ -0,0 +1,23 @@ +--- +title: 使用 Farcaster 登录 +--- + +# 简介 + +使用 Farcaster 登录(SIWF)是一种让用户通过其 Farcaster 身份登录任何应用的方式。 + +当用户通过 Farcaster 登录您的应用程序时,您将能够利用其公开的社交数据(如社交图谱和个人资料信息)来提供简化的注册流程和社交驱动的功能。 + +### 工作原理 + +1. 向用户展示“使用 Farcaster 登录”按钮。 +2. 等待用户点击按钮,扫描二维码并在 Warpcast 中批准请求。 +3. 接收并验证来自 Warpcast 的签名。 +4. 显示已登录用户的头像和用户名。 + +![使用 Farcaster 登录演示](./siwf_demo.avifs) + +## 后续步骤 + +- 立即通过 [AuthKit](/zh/auth-kit/) 将 SIWF 集成到您的应用中。 +- 阅读底层标准 [FIP-11: 使用 Farcaster 登录](https://github.com/farcasterxyz/protocol/discussions/110)。 diff --git a/docs/zh/developers/siwf/siwf_demo.avifs b/docs/zh/developers/siwf/siwf_demo.avifs new file mode 100644 index 0000000..ff21a11 Binary files /dev/null and b/docs/zh/developers/siwf/siwf_demo.avifs differ diff --git a/docs/zh/developers/utilities.md b/docs/zh/developers/utilities.md new file mode 100644 index 0000000..9be567e --- /dev/null +++ b/docs/zh/developers/utilities.md @@ -0,0 +1,3 @@ +# 链接 + +- https://github.com/a16z/awesome-farcaster diff --git a/docs/zh/hubble/hubble.md b/docs/zh/hubble/hubble.md new file mode 100644 index 0000000..92665fb --- /dev/null +++ b/docs/zh/hubble/hubble.md @@ -0,0 +1,25 @@ +# Hubble + +[Hubble](https://github.com/farcasterxyz/hub-monorepo) 是 [Farcaster Hub 协议](https://github.com/farcasterxyz/protocol) 的一个实现,采用 [TypeScript](https://www.typescriptlang.org/) 和 [Rust](https://www.rust-lang.org/) 编写。 + +Hubble 会在您的机器上创建一个私有的 Farcaster 实例。它会与其他实例建立对等连接,并下载整个网络的副本。上传到您 Hubble 实例的消息将被广播到整个网络。 + +如果您正在构建应用程序、需要访问最新数据或希望帮助网络去中心化,我们建议您运行 Hubble。 + +## 托管实例 + +Hubble 实例也可以由其他服务提供商为您托管。 + +- [Hubs x Neynar](https://hubs.neynar.com/) +- [Hubs x Pinata](https://pinata.cloud/pinata-hub) + +## 公共实例 + +Farcaster 团队运行了一个供公众使用的 Hubble 实例。该实例不保证稳定性,目前为只读模式。 + +```bash +url: hoyt.farcaster.xyz +httpapi_port: 2281 +gossipsub_port: 2282 +grpc_port: 2283 +``` diff --git a/docs/zh/hubble/install.md b/docs/zh/hubble/install.md new file mode 100644 index 0000000..f97f9fe --- /dev/null +++ b/docs/zh/hubble/install.md @@ -0,0 +1,169 @@ +# 安装指南 + +我们推荐在已安装 [Docker](https://docs.docker.com/desktop/install/linux-install/) 的常驻服务器上运行 Hubble。 + +## 系统要求 + +Hubble 可在 30 分钟内完成部署。您需要准备以下配置的机器: + +- 16 GB 内存 +- 4 核 CPU 或 vCPU +- 1TB 可用存储空间 +- 具备公网 IP 地址并开放 2281 - 2283 端口 +- 以太坊和 Optimism 主网的 RPC 端点(可使用 [Alchemy](https://www.alchemy.com/)、[Infura](https://www.infura.io/)、[QuickNode](https://www.quicknode.com/),或自行运行 [以太坊](https://geth.ethereum.org/docs/getting-started) 和 [Optimism](https://docs.optimism.io/builders/node-operators/tutorials/mainnet) 节点) + +关于如何在云服务商部署 Hubble,请参阅 [教程指南](./tutorials)。 + +## 脚本安装 + +安装脚本是最简单的 Hubble 部署方式。 + +```bash +curl -sSL https://download.thehubble.xyz/bootstrap.sh | bash +``` + +_若使用 macOS 系统,需提前安装并运行 Docker。_ + +Hubble 将被安装到 `~/hubble` 目录,并通过 Docker 在后台运行,同时会启动 Grafana 和 Prometheus 用于 [监控](monitoring.md)。如遇脚本执行问题,可尝试 [Docker 安装方式](#install-via-docker)。 + +### 升级 Hubble + +安装脚本会自动创建定时任务,每周执行一次升级。如需手动升级,请运行: + +```bash +cd ~/hubble && ./hubble.sh upgrade +``` + +## Docker 安装 + +您也可以直接运行 Docker 镜像来部署 Hubble: + +1. 本地克隆 [hub-monorepo](https://github.com/farcasterxyz/hub-monorepo) 仓库 +2. 进入仓库根目录下的 `apps/hubble` 文件夹 +3. 使用 docker compose 生成身份密钥对 + +```bash +docker compose run hubble yarn identity create +``` + +4. 在 `apps/hubble` 目录创建 .env 文件并配置以太坊 RPC 端点: + +```bash +# 设置主网 ETH RPC URL +ETH_MAINNET_RPC_URL=您的-ETH-主网-RPC-URL + +# 设置 Optimism L2 主网 RPC URL +OPTIMISM_L2_RPC_URL=您的-L2-optimism-RPC-URL + +# 设置您的 Farcaster FID +HUB_OPERATOR_FID=您的-fid +``` + +5. 按照 [网络连接指南](./networks.md) 完成网络配置 + +6. 使用 docker compose 启动 Hubble(后台模式): + +```bash +docker compose up hubble -d +``` + +Docker compose 将启动 Hubble 容器,开放网络端口并将数据写入 `.hub` 和 `.rocks` 目录。Hubble 将开始与合约及其他节点同步,下载网络中的所有消息。 + +7. 查看同步状态和运行日志: + +```bash +docker compose logs -f hubble +``` + +8. 参照 [监控指南](monitoring.md) 配置 Grafana 实时查看节点状态 + +### 升级 Hubble + +进入 hub-monorepo 的 `apps/hubble` 目录运行: + +```bash +git checkout main && git pull +docker compose stop && docker compose up -d --force-recreate --pull always +``` + +## 源码编译安装 + +您也可以通过源码直接编译运行 Hubble(无需 Docker)。 + +#### 环境依赖 + +首先确保系统已安装: + +- [Node.js 18.7+](https://nodejs.org/en/download/releases) +- [Yarn](https://classic.yarnpkg.com/lang/en/docs/install) +- [Foundry](https://book.getfoundry.sh/getting-started/installation#using-foundryup) +- [Rust](https://www.rust-lang.org/tools/install) + +#### 编译步骤 + +- `git clone https://github.com/farcasterxyz/hub-monorepo.git` 克隆仓库 +- `cd hub-monorepo` 进入目录 +- `yarn install` 安装依赖 +- `yarn build` 编译 Hubble 及其依赖 +- `yarn test` 运行测试套件验证 + +#### 运行 Hubble + +进入 Hubble 应用目录 (`cd apps/hubble`) 后执行 yarn 命令: + +1. `yarn identity create` 创建身份 ID +2. 按照 [网络连接指南](./networks.md) 完成配置 +3. `yarn start --eth-mainnet-rpc-url <您的-ETH-主网-RPC-URL> --l2-rpc-url <您的-Optimism-L2-RPC-URL> --hub-operator-fid <您的-FID>` + +### 升级 Hubble + +查找最新 [发布版本](https://github.com/farcasterxyz/hub-monorepo/releases),检出对应版本后重新编译: + +```bash +git fetch --tags # 获取最新标签 +git checkout @farcaster/hubble@latest # 或指定版本号 +yarn install && yarn build # 在根目录执行 +``` + +## 常用命令 + +查看运行日志确保节点正常: + +```bash +docker compose logs -f hubble +``` + +进入 Hubble 容器 shell: + +```bash +docker compose exec hubble /bin/sh +``` + +## 故障排查 + +- 从非 Docker 环境升级时,请确保 `.hub` 和 `.rocks` 目录具有全局写入权限 + +- 从 1.3.3 或更早版本升级时: + + - Docker 用户需设置 `ETH_MAINNET_RPC_URL=您的-ETH-主网-RPC-URL` + - 非 Docker 用户需添加 `--eth-mainnet-rpc-url` 参数 + +- 切换网络时需清空数据库: + +```bash +docker compose stop && docker compose run --rm hubble yarn dbreset +``` + +- 手动拉取镜像: + +```bash +# 获取最新镜像 +docker pull farcasterxyz/hubble:latest + +# 获取特定版本 (v1.4.0) +docker pull farcasterxyz/hubble@v1.4.0 +``` + +- 设置 Hub 运营者 FID: + - Docker/脚本用户:在 `.env` 文件设置 `HUB_OPERATOR_FID=您的-fid` + - 源码用户:运行 `yarn start --hub-operator-fid <您的-fid>` diff --git a/docs/zh/hubble/migrating.md b/docs/zh/hubble/migrating.md new file mode 100644 index 0000000..4fb4db8 --- /dev/null +++ b/docs/zh/hubble/migrating.md @@ -0,0 +1,65 @@ +# 迁移至 snapchain + +[Snapchain](https://github.com/farcasterxyz/snapchain) 是 Farcaster 协议的一个更具扩展性的实现。要与 snapchain 交互,您可以运行自己的读取节点。 + +## 运行节点 + +```bash +mkdir snapchain +wget https://raw.githubusercontent.com/farcasterxyz/snapchain/refs/heads/main/docker-compose.mainnet.yml -O docker-compose.yml +docker compose up # -d 表示在后台运行 +``` + +注意,默认的 HTTP 端口是 `3381`,默认的 gossip 端口是 `3382`,默认的 gRPC 端口是 `3383`。您可能需要开放这些端口。 + +## 从 snapchain 读取数据 + +读取 API 完全向后兼容 hubs,因此无需迁移。通过客户端库读取数据的操作请参考[现有文档](https://docs.farcaster.xyz/developers/guides/querying/fetch-casts)。 + +运行节点后,通过 HTTP 访问: + +```bash +curl http://locaalhost:3381/v1/info +``` + +通过 gRPC 访问: + +```bash +git clone git@github.com:farcasterxyz/snapchain.git +cd snapchain +grpcurl -plaintext -proto src/proto/rpc.proto -import-path src/proto localhost:3383 HubService/GetInfo +``` + +如果您使用 Shuttle,其操作方式与之前相同,只需将其指向 snapchain 节点即可。 + +## 向 snapchain 写入数据 + +写入 API 也向后兼容 hubs,但有一些注意事项(见下文)。 + +要向 snapchain 写入数据,您应该运行一个节点并直接向其提交。通过客户端库写入数据的操作请参考[现有文档](https://docs.farcaster.xyz/developers/guides/writing/submit-messages)。注意,您**必须**使用 `hub-nodejs` 库的 `0.16` 或更高版本。 + +### 注意事项 + +- 如果您没有使用 `hub-nodejs` 库中的构建器,请确保在每个消息中填充 `dataBytes` 而不是 `data`,如下所示: + + ```ts + if (message.dataBytes === undefined) { + message.dataBytes = protobufs.MessageData.encode(message.data).finish(); + message.data = undefined; + } + ``` + +- Snapchain 中 `submitMessage` 返回的部分错误信息与 Hubs 不同。 +- Snapchain 的提交是尽力而为的。可能会出现 `submitMessage` 成功但消息未被包含进区块的情况。关于当消息被内存池接受但无法包含进区块时向客户端提供反馈的计划,请关注[此问题](https://github.com/farcasterxyz/snapchain/issues/353)。 + +## 测试网 + +snapchain 有一个测试网,您可以使用以下 docker compose 文件运行节点: + +```bash +mkdir snap_test +wget https://raw.githubusercontent.com/farcasterxyz/snapchain/refs/heads/main/docker-compose.testnet.yml -O docker-compose.yml +docker compose up # -d 表示在后台运行 +``` + +注意,测试网不稳定,会不时重置。 diff --git a/docs/zh/hubble/monitoring.md b/docs/zh/hubble/monitoring.md new file mode 100644 index 0000000..15b93d0 --- /dev/null +++ b/docs/zh/hubble/monitoring.md @@ -0,0 +1,35 @@ +# 监控 Hubble + +Hubble 默认提供了 Grafana 配置,用于监控同步和性能指标。 + +如果使用了安装脚本,请在浏览器中打开 localhost:3000 查看仪表盘。如果是手动使用 docker 或从源码安装,可能需要自行设置。 + +如果在远程服务器上部署了 hubble,可通过 `ssh -L 3000:localhost:3000 xyz@1.1.1.1` 建立 SSH 隧道,然后在浏览器中打开 localhost:3000 查看仪表盘。 + +## 监控设置步骤 + +1. 启动 grafana 和 statsd + +```bash +docker compose up statsd grafana +``` + +2. 在 Hub 的 `.env` 文件中启用监控 + +```bash +STATSD_METRICS_SERVER=statsd:8125 +``` + +如果从源码运行 hubble,可通过命令行参数传递 + +```bash +yarn start --statsd-metrics-server 127.0.0.1:8125 +``` + +3. 重启 hub + +4. 在浏览器中访问 `127.0.0.1:3000` 打开 Grafana。默认用户名/密码为 `admin`/`admin`。首次登录需要修改密码 + +5. 进入 `Settings -> Datasource -> Add new data source`,选择 `Graphite`。将 URL 设置为 `http://statsd:80` 并点击 `Save & Test` 确保连接正常 + +6. 进入 `Settings -> Dashboard -> Add New -> Import`,在 `Import from Panel JSON` 中粘贴 [默认 Grafana 仪表盘](https://github.com/farcasterxyz/hub-monorepo/blob/main/apps/hubble/grafana/grafana-dashboard.json) 的内容 diff --git a/docs/zh/hubble/networks.md b/docs/zh/hubble/networks.md new file mode 100644 index 0000000..db29a9a --- /dev/null +++ b/docs/zh/hubble/networks.md @@ -0,0 +1,48 @@ +# 网络 + +Farcaster 账户必须选择将消息发布到哪个网络。任何在[合约](../learn/architecture/contracts.md)中注册的账户都可以向任何网络发布消息。但发布在某个网络中的消息在其他网络中不可见。 + +目前有两个主要网络: + +- **Testnet** - 面向开发者的最新测试版本 +- **Mainnet** - 供所有人使用的稳定版本 + +在[安装你的 hub](./install.md)时,需要选择要连接的网络。 + +## Testnet + +Testnet 是开发者测试新功能的沙盒环境。系统每 10 秒会广播虚拟消息以模拟活动。 + +在 `apps/hubble` 目录下的 .env 文件中设置以下变量: + +```sh +FC_NETWORK_ID=2 +BOOTSTRAP_NODE=/dns/testnet1.farcaster.xyz/tcp/2282 +``` + +如果从源代码运行,请将这些参数添加到 `yarn start` 命令中: + +```sh +yarn start ... \ + -n 2 \ + -b /dns/testnet1.farcaster.xyz/tcp/2282 +``` + +## Mainnet + +Mainnet 是供所有人使用的生产环境。 + +在 `apps/hubble` 目录下的 .env 文件中设置以下变量: + +```sh +FC_NETWORK_ID=1 +BOOTSTRAP_NODE=/dns/hoyt.farcaster.xyz/tcp/2282 +``` + +如果从源代码运行,请将这些参数添加到 `yarn start` 命令中: + +```sh +yarn start ... \ + -n 1 \ + -b /dns/hoyt.farcaster.xyz/tcp/2282 +``` diff --git a/docs/zh/hubble/troubleshooting.md b/docs/zh/hubble/troubleshooting.md new file mode 100644 index 0000000..f49639f --- /dev/null +++ b/docs/zh/hubble/troubleshooting.md @@ -0,0 +1,55 @@ +# Hubble 故障排查 + +## 获取 Hubble 日志 + +在 hubble 目录下,通过以下命令获取日志: + +```bash +$ ./hubble.sh logs +``` + +或者直接使用 docker-compose: + +```bash +# 如果使用 hubble 脚本安装 hub,需要 sudo +# 根据你的 docker 配置,可能需要使用 `docker-compose` 而非 `docker compose` +$ sudo docker compose logs -f hubble +``` + +## 重启 Hubble + +在 hubble 目录下,通过以下命令停止并重新启动实例: + +```bash +$ ./hubble.sh down +$ ./hubble.sh up +``` + +## 重置数据库 + +重置数据库的最佳方式是彻底删除 `.rocks` 目录,这会强制 hub 获取最新快照并重新同步。 + +```bash +$ ./hubble.sh down # 确保 hub 未运行 +$ rm -rf .rocks # 确认当前位于 hub 目录 +``` + +## 无对等节点或传入的 gossip 连接 + +如果看不到任何对等节点或传入的 gossip 连接,可能是网络问题阻碍了 hub 与其他 hub 的通信。 + +检查防火墙和 NAT 设置,确保 2282(gossip)和 2283(rpc,用于同步)端口可访问。 + +你也可以尝试通过向 `.env` 文件添加以下行,使用非默认节点引导 hub: + +```dotenv +BOOTSTRAP_NODE=/dns/hoyt.farcaster.xyz/tcp/2282 +``` + +## 我的 hub 同步了吗? + +使用 [grafana 仪表盘](/zh/hubble/monitoring) 监控你的 hub。状态页会显示 hub 相对于其节点的消息同步百分比。如果低于 100%,尝试重启 hub 并等待一段时间。如果问题持续,请在 [hub 代码库](https://github.com/farcasterxyz/hub-monorepo/issues/new?assignees=&labels=&projects=&template=bug_report.md&title=bug%20%28hubble%29%3A) 提交问题。 + +## 管理 Peer ID + +Hubble 使用存储在 `.hub` 目录中的密钥对(`...._id.protobuf` 文件)来签名点对点消息。文件名包含公钥或 Peer ID,而文件内容包含私钥。 diff --git a/docs/zh/hubble/tutorials.md b/docs/zh/hubble/tutorials.md new file mode 100644 index 0000000..4172721 --- /dev/null +++ b/docs/zh/hubble/tutorials.md @@ -0,0 +1,7 @@ +# 教程 + +使用不同云服务提供商搭建 Hubble 实例的指南。 + +- [AWS EC2](https://warpcast.notion.site/Set-up-Hubble-on-EC2-Public-23b4e81d8f604ca9bf8b68f4bb086042) +- [Digital Ocean](https://warpcast.notion.site/Set-up-Hubble-on-DigitalOcean-Public-e38173c487874c91828665e73eac94c1) +- [Google 云平台 (GCP)](tutorials/gcp.md) diff --git a/docs/zh/hubble/tutorials/gcp.md b/docs/zh/hubble/tutorials/gcp.md new file mode 100644 index 0000000..8229598 --- /dev/null +++ b/docs/zh/hubble/tutorials/gcp.md @@ -0,0 +1,126 @@ +# 在 GCP 上运行 Hubble + +## 简介 + +本指南将逐步指导您在 GCP 上搭建 Hubble。 +通常整个流程可在 30 分钟内完成。 + +### 前提条件 + +- [GCP](https://console.cloud.google.com/) 账户 +- [Alchemy](https://www.alchemy.com/) 账户 + +### 费用说明 + +- 本教程推荐的 GCP 配置每月费用约 70 美元 +- Alchemy 使用量应保持在免费额度内 + +## 创建 GCP 虚拟机 + +打开 **Google Cloud Shell** 并执行以下命令: + +

点击 Google Cloud Shell 图标

+ +在 Cloud Shell 中执行以下命令: + +
mkdir farcaster-hub
+cd farcaster-hub
+nano main.tf
+
+ +将以下内容粘贴到 main.tf 文件中 \ +请将 "$YOUR_PROJECT_ID" 替换为您的个人项目 ID。 + +
+ +这是即将创建的 GCP 虚拟机配置。 + +``` +provider "google" { + project = "$YOUR_PROJECT_ID" + region = "us-central1" +} + +resource "google_compute_instance" "farcaster-hub-vm" { + name = "farcaster-hub-vm" + machine_type = "e2-standard-4" # 4 个 vCPU,16 GB 内存 + zone = "us-central1-a" # 指定可用区 + + + boot_disk { + initialize_params { + image = "ubuntu-2004-focal-v20231213" # Ubuntu 20.04 LTS 镜像 URL + size = 160 # 160 GB 磁盘容量 + } + } + + network_interface { + network = "default" + access_config { + // 这将为实例分配公网 IP 地址 + } + } + + tags = ["allow-farcaster-p2p-ports"] # 用于防火墙规则 + + metadata = { + # 如需可在此添加额外元数据 + } +} + +resource "google_compute_firewall" "farcaster-p2p-ports" { + name = "farcaster-p2p-ports" + network = "default" + + # 允许 2282-2285 端口的入站流量 + allow { + protocol = "tcp" + ports = ["2282-2285"] + } + + source_ranges = ["0.0.0.0/0"] +} +``` + +执行以下命令: + +``` +terraform init # 在 farcaster-hub 文件夹中初始化 terraform +``` + +执行以下命令: + +``` +terraform plan # 模拟 terraform 配置并检查是否正确 +``` + +示例输出: + +

terraform plan 示例输出

+ +启用 Compute Engine API + +
+ +现在执行以下命令: + +```bash +terraform apply +``` + +

Terraform apply 示例输出

+ +虚拟机创建需要几分钟时间。现在可以享受您的 :coffee: 了 + +
+ +现在您可以通过点击 **SSH** 按钮连接到虚拟机。 + +\ +接下来按照 [https://docs.docker.com/engine/install/ubuntu/](https://docs.docker.com/engine/install/ubuntu/) 说明安装 Docker + +然后按照 [安装页面](../install.md) 的步骤操作 \ +\ +当您看到以下标志时,表示 Hubble 已成功运行 :white_check_mark: + +
diff --git a/docs/zh/index.md b/docs/zh/index.md new file mode 100644 index 0000000..af9d7ea --- /dev/null +++ b/docs/zh/index.md @@ -0,0 +1,43 @@ +--- +layout: home + +hero: + name: Farcaster 文档中心 + tagline: 自由构建与分发社交应用 + actions: + - theme: brand + text: 构建 mini app + link: https://miniapps.farcaster.xyz/docs/getting-started + target: _self + - theme: alt + text: 探索 Farcaster 登录 + link: /zh/developers/siwf/ + - theme: alt + text: 了解协议规范 + link: /zh/learn/ +--- + +### 构建 Mini App + +学习如何构建运行在 Farcaster 信息流中的 Mini Apps(原 Frames v2)。 + + +- [Mini App 介绍](https://miniapps.farcaster.xyz/){target="_self"} - 理解 mini app 的概念与运行机制 + +- [开发首个 Mini App](https://miniapps.farcaster.xyz/docs/getting-started){target="_self"} - 构建运行在 Farcaster 中的 mini apps + +### 探索 Farcaster 登录 + +允许用户通过 Farcaster 登录,并在您的应用中利用社交数据。 + +- [功能简介](/zh/developers/siwf/) - 了解 Farcaster 登录机制 +- [使用 AuthKit 集成登录](/zh/auth-kit/installation) - 通过 React 工具包快速实现登录功能 +- [示例演示](/zh/auth-kit/examples) - 查看实际应用场景 + +### 分析 Farcaster 数据 + +将 Farcaster 网络同步至本地设备,实现数据查询功能。 + +- [编写首个 Hub 查询](/zh/developers/guides/querying/fetch-casts.md) - 从 Hub 获取账户的 Casts 数据 +- [配置数据同步器](/zh/developers/guides/apps/replicate.md) - 将 Hub 数据同步至 PostgreSQL 数据库以执行高级查询 +- [运行 Hub 节点](/zh/hubble/install.md) - 在本地设备实时获取 Farcaster 数据 diff --git a/docs/zh/learn/architecture/contracts.md b/docs/zh/learn/architecture/contracts.md new file mode 100644 index 0000000..ac1709b --- /dev/null +++ b/docs/zh/learn/architecture/contracts.md @@ -0,0 +1,35 @@ +# 合约 + +Farcaster 账户通过链上合约进行管理和安全保护。本节提供高层次概述,省略部分实现细节。完整信息请参阅[合约代码库](https://github.com/farcasterxyz/contracts/)。 + +
+ +在 OP 主网上部署了三个主要合约: + +- **ID 注册表** - 创建新账户 +- **存储注册表** - 为账户租赁存储空间 +- **密钥注册表** - 为账户添加或移除应用密钥 + +
+ +![注册表合约](/assets/registry-contracts.png) + +合约部署地址如下: + +| 合约 | 地址 | +| --------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| IdRegistry | [0x00000000fc6c5f01fc30151999387bb99a9f489b](https://optimistic.etherscan.io/address/0x00000000fc6c5f01fc30151999387bb99a9f489b) | +| StorageRegistry | [0x00000000fcce7f938e7ae6d3c335bd6a1a7c593d](https://optimistic.etherscan.io/address/0x00000000fcce7f938e7ae6d3c335bd6a1a7c593d) | +| KeyRegistry | [0x00000000fc1237824fb747abde0ff18990e59b7e](https://optimistic.etherscan.io/address/0x00000000fc1237824fb747abde0ff18990e59b7e) | + +### ID 注册表 + +ID 注册表允许用户注册、转移和恢复 Farcaster 账户。每个账户通过唯一的数字标识符(fid)识别,该标识符在注册时分配给一个以太坊地址。一个以太坊地址同一时间只能拥有一个账户,但可以自由将其转移给其他地址。账户还可设置恢复地址,该地址可随时转移账户所有权。 + +### 存储注册表 + +存储注册表允许账户通过支付 ETH 来租赁[存储空间](../what-is-farcaster/messages.md#storage)。存储价格由管理员以美元设定,并通过 Chainlink 预言机转换为 ETH。价格会根据供需关系动态调整。 + +### 密钥注册表 + +密钥注册表允许账户向应用颁发密钥,使其能够代表账户发布消息。密钥可随时添加或移除。添加密钥时,账户需提交 EdDSA 密钥对的公钥及请求者签名。请求者可以是账户本身,也可以是希望代表其操作的应用程序。 diff --git a/docs/zh/learn/architecture/ens-names.md b/docs/zh/learn/architecture/ens-names.md new file mode 100644 index 0000000..8461fcf --- /dev/null +++ b/docs/zh/learn/architecture/ens-names.md @@ -0,0 +1,45 @@ +# ENS 名称 + +Farcaster 使用 ENS 名称作为账户的人类可读标识符。支持两种类型的 ENS 名称: + +- **链下 ENS 名称**:免费且由 Farcaster 控制(例如 @alice) +- **链上 ENS 名称**:需付费且由您的钱包控制(例如 @alice.eth) + +ENS 名称在 Farcaster 上使用时必须满足以下条件:长度不超过 16 个字符,且仅包含小写字母、数字和连字符。 + +![用户名](/assets/usernames.png) + +## 链上 ENS 名称 + +用户可以在 Farcaster 上使用如 `@alice.eth` 这样的链上 ENS 名称。 + +链上 ENS 名称由 ENS 发行,以 .eth 结尾,必须在以太坊 L1 区块链上注册。任何人都可以通过 [ENS 应用](https://app.ens.domains/) 注册 ENS 名称。 + +用户需要支付费用来注册链上 ENS 名称,但一旦注册成功,该名称将由用户控制且无法被撤销。 + +## 链下 ENS 名称(Fnames) + +用户可以在 Farcaster 上使用如 `@alice` 这样的链下 ENS 名称。 + +链下 ENS 名称,也称为 Farcaster 名称或 Fnames,符合 ENS 标准但采用链下注册。Fnames 是免费的,但需遵守使用政策以防止抢注和冒充行为。同时还需满足以下要求: + +1. 一个账户同一时间只能拥有一个 Fname。 +2. 一个账户每 28 天只能更改一次 Fname。 + +### 使用政策 + +注册 Fname 是免费的,但需遵守以下政策: + +1. 与公众人物或实体相关的名称可能会被回收(例如 @google)。 +2. 超过 60 天未使用的名称可能会被回收。 +3. 仅用于转售目的而注册的名称可能会被回收。 + +相关决定由 Farcaster 团队做出,通常需要人工判断。希望完全自主控制名称的用户应使用链上 ENS 名称。 + +### 注册机制 + +Fnames 作为链下名称在 `fcast.id` 子域下发行。 + +Bob 可以注册链下 ENS 名称 `bob.fcast.id`,并在任何 Farcaster 应用中以简称 `@bob` 使用。该名称可通过向 Fname 注册服务器发送签名请求来注册。有关查询和创建 Fnames 的详细信息,请参阅 [FName API 参考](/zh/reference/fname/api)。 + +要了解更多关于 Fnames 工作原理的信息,请参阅 [ENSIP-16](https://docs.ens.domains/ens-improvement-proposals/ensip-16-offchain-metadata) 和 [ERC-3668](https://eips.ethereum.org/EIPS/eip-3668)。 diff --git a/docs/zh/learn/architecture/hubs.md b/docs/zh/learn/architecture/hubs.md new file mode 100644 index 0000000..2c4ae7a --- /dev/null +++ b/docs/zh/learn/architecture/hubs.md @@ -0,0 +1,61 @@ +# Hubs(中心节点) + +Hubs 是一个分布式服务器网络,用于存储和验证 Farcaster 数据。 + +计算机可以通过运行特定软件成为 Farcaster Hub。它会从以太坊区块链下载链上 Farcaster 数据,并从其他 Hubs 获取链下 Farcaster 数据。每个 Hub 都存储着所有 Farcaster 数据的副本,这些数据可以通过 API 访问。 + +Hubs 允许您读取和写入 Farcaster 数据,任何构建 Farcaster 应用的开发者都需要与 Hub 进行交互。任何人都可以在笔记本电脑或云服务器上运行 Hub。完整的 Hub 设置和运行指南可参见[此处](https://www.thehubble.xyz)。 + +## 设计架构 + +Hub 首先从 Optimism 区块链上的 Farcaster 合约同步数据。它会识别每个用户的账户及其账户密钥。 + +Farcaster 消息的生命周期如下: + +1. Alice 创建一条新消息"Hello World!"。 +2. Alice(或她的应用)使用账户密钥对消息进行签名。 +3. Alice(或她的应用)将消息上传至 Hub。 +4. Hub 检查消息的有效性。 +5. Hub 通过 gossip 协议将消息发送给对等节点。 + +![Hub](/assets/hub.png) + +### 验证机制 + +Alice 的消息会通过验证其是否包含有效的账户密钥签名。Hub 还会确保消息符合该消息类型的要求。例如,公开消息(或称"cast")必须小于 320 字节。消息类型的详细要求参见[协议规范](https://github.com/farcasterxyz/protocol/blob/main/docs/SPECIFICATION.md)。 + +### 存储机制 + +在存储 Alice 的消息前,Hub 会检查是否存在冲突。冲突可能由多种原因导致: + +1. Hub 已存在该消息的副本。 +2. Hub 已收到 Alice 删除该消息的后续指令。 +3. Alice 仅支付了存储 5000 条消息的费用,而这是她的第 5001 条消息。 + +系统使用 CRDTs(无冲突复制数据类型)以确定性和异步方式解决冲突。例如,如果 Alice 没有存储空间,系统将删除她最早的消息。 + +### 数据同步 + +Hubs 采用两阶段同步流程:gossip 和差异同步。当 Hub 接收并存储消息后,会立即通过 gossip 协议传播给对等节点。gossip 使用 libp2p 的 gossipsub 协议实现,可能存在丢包情况。随后 Hubs 会定期选择随机对等节点进行差异同步,以找回丢失的消息。差异同步过程通过比较消息哈希的默克尔树来高效定位丢失消息。 + +### 一致性保证 + +Hubs 具有强最终一致性。即使 Hub 断开连接,仍可接收写入操作,并在重新联网后安全恢复。这与区块链不同,区块链节点断开时无法确认交易。缺点是消息可能乱序到达,例如 Bob 对 Alice 的回复可能早于她的"Hello World!"消息出现。 + +### 节点评分 + +Hubs 会监控对等节点并评估其行为。如果节点拒绝有效消息、同步滞后或过度 gossip,可能会被其他节点忽略。 + +### 实现方案 + +- [Hubble](https://www.thehubble.xyz) - 基于 TypeScript 和 Rust 的 Hub 实现 + +## 常见问题 + +**1. 为什么需要运行 Hub?** + +如果您正在开发需要读写 Farcaster 数据的应用,就需要运行 Hub。 + +**2. 运行 Hub 有奖励吗?** + +Farcaster 不为运行 Hub 提供奖励,也没有相关计划。 diff --git a/docs/zh/learn/architecture/overview.md b/docs/zh/learn/architecture/overview.md new file mode 100644 index 0000000..c09235a --- /dev/null +++ b/docs/zh/learn/architecture/overview.md @@ -0,0 +1,30 @@ +# 架构 + +Farcaster 采用混合架构,将身份信息存储在链上,数据存储在链下。 + +![架构示意图](/assets/architecture.png) + +## 链上部分 + +Farcaster 的链上系统以 [OP 主网上的智能合约](./contracts.md) 形式实现。只有当安全性和一致性至关重要时,才会在链上执行操作。链上操作的使用被控制在最低限度,以降低成本并提升性能。 + +仅在少数情况下会执行链上操作,包括: + +- 创建 [账户](../what-is-farcaster/accounts.md)。 +- 支付租金以 [存储数据](../what-is-farcaster/messages.md#storage)。 +- 为 [已连接应用](../what-is-farcaster/apps.md#connected-apps) 添加账户密钥。 + +## 链下部分 + +Farcaster 的链下系统是由称为 [枢纽(Hubs)](./hubs.md) 的服务器组成的点对点网络,用于存储用户数据。大多数用户操作都在链下执行,包括: + +- 发布新的公开消息。 +- 关注其他用户。 +- 对帖子做出反应(点赞等)。 +- 更新个人资料图片。 + +当性能和成本是关键考量因素时,操作会在链下执行。在一致性并非严格要求的情况下,通常优先选择链下操作。链下系统通过依赖链上系统的数字签名来确保安全性。 + +:::tip +注意:文档中涉及的专有名词(如 Hubs)和技术术语(如 onchain/offchain)已按技术文档惯例处理,保持术语一致性。 +::: diff --git a/docs/zh/learn/contributing/fips.md b/docs/zh/learn/contributing/fips.md new file mode 100644 index 0000000..05a35bf --- /dev/null +++ b/docs/zh/learn/contributing/fips.md @@ -0,0 +1,26 @@ +# FIPs + +FIP(Farcaster 改进提案)是一个围绕协议变更建立共识的流程。FIP 的灵感来源于 [Ethereum 的 EIPs](https://eips.ethereum.org/EIPS/eip-1) 和 [Python 的 PEPs](https://peps.python.org/pep-0001/)。任何人都可以撰写 FIP 来提议以下变更: + +1. 流程类,例如协议的发布周期 +2. 标准类,例如链上资产的统一资源标识符(URI) +3. 实现类,例如新增协议功能 + +更多关于 FIP 的信息请参阅 [FIP-0:提案制定提案](https://github.com/farcasterxyz/protocol/discussions/82)。下方列出了已最终确定的提案列表。提案的提交和批准流程在 [讨论板](https://github.com/farcasterxyz/protocol/discussions/categories/fip-stage-4-finalized) 进行。 + +| FIP | 标题 | 作者 | +| --- | ------------------------------------------------------------------------------- | ---------------------------------------- | +| 0 | [提案制定提案](https://github.com/farcasterxyz/protocol/discussions/82) | @v | +| 1 | [规范 URI](https://github.com/farcasterxyz/protocol/discussions/72) | @pfh, @v | +| 2 | [消息的灵活目标](https://github.com/farcasterxyz/protocol/discussions/71) | @pfh | +| 3 | [链接](https://github.com/farcasterxyz/protocol/discussions/85) | @cassie, @v | +| 4 | [ENS 用户名](https://github.com/farcasterxyz/protocol/discussions/90) | @horsefacts, @sanjay, @sds, @v | +| 5 | [即时恢复](https://github.com/farcasterxyz/protocol/discussions/100) | @v | +| 6 | [灵活存储](https://github.com/farcasterxyz/protocol/discussions/98) | @cassie, @horsefacts, @v | +| 7 | [链上签名者](https://github.com/farcasterxyz/protocol/discussions/103) | @horsefacts, @sanjay, @v | +| 8 | [合约钱包验证](https://github.com/farcasterxyz/protocol/discussions/109) | @horsefacts, @sanjay, @eulerlagrange.eth | +| 9 | [全局唯一验证](https://github.com/farcasterxyz/protocol/discussions/114) | @sanjay, @v | +| 10 | [网关](https://github.com/farcasterxyz/protocol/discussions/133) | @horsefacts, @sanjay, @v | +| 11 | [使用 Farcaster 登录](https://github.com/farcasterxyz/protocol/discussions/110) | @deodad, @horsefacts, @sanjay, @v | +| 12 | [灵活存储的定价方案](https://github.com/farcasterxyz/protocol/discussions/126) | @v | +| 13 | [消息哈希的规范序列化](https://github.com/farcasterxyz/protocol/discussions/87) | @sanjay, @adityapk | diff --git a/docs/zh/learn/contributing/governance.md b/docs/zh/learn/contributing/governance.md new file mode 100644 index 0000000..4285e7a --- /dev/null +++ b/docs/zh/learn/contributing/governance.md @@ -0,0 +1,17 @@ +# 治理机制 + +Farcaster 采用[粗略共识与可运行代码](https://en.wikipedia.org/wiki/Rough_consensus)作为其治理模型。当有人提出提案、获得支持并交付可运行代码时,变更就会发生。根据变更类型的不同,需要说服不同的群体: + +1. **协议开发者**:决定是否将变更合并到 hub 节点和智能合约中。 +2. **Hub 节点运营者**:决定是否将这些变更部署到他们的 hub 节点上。 +3. **应用开发者**:决定从哪些 hub 节点读取数据。 +4. **用户**:决定使用哪些应用程序。 + +共识的形成源于人们接受或拒绝新代码。Farcaster 不会设立具有约束力的投票流程、官方角色或任何人的否决权。过多的结构会使系统僵化,助长政治博弈并拖慢进展。粗略共识倾向于行动,鼓励观点多样性,并最大限度地实现去中心化,这对一个长期存续的协议至关重要。大多数变更通过 [FIP](./fips.md) 流程实现。 + +:::tip +保留英文术语说明: + +- FIP (Farcaster Improvement Proposal) 未翻译,因其为专有流程名称 +- hub 作为技术组件名称保留不译 + ::: diff --git a/docs/zh/learn/contributing/overview.md b/docs/zh/learn/contributing/overview.md new file mode 100644 index 0000000..e9ce5bb --- /dev/null +++ b/docs/zh/learn/contributing/overview.md @@ -0,0 +1,27 @@ +# 贡献指南 + +Farcaster 欢迎社区成员参与各种规模的贡献。截至目前,协议的发展已受益于超过 100 位贡献者的帮助。 + +若想参与其中,您可以查看以下代码库中的开放议题,或加入开发者会议。 + +## 代码库 + +| 代码库 | 描述 | +| ---------------------------------------------------------------- | ------------------------------------------- | +| [Protocol](https://github.com/farcasterxyz/protocol) | 协议规范文档 | +| [Contracts](https://github.com/farcasterxyz/contracts) | 官方 Farcaster 智能合约 | +| [Hubble](https://github.com/farcasterxyz/hub-monorepo) | 基于 Rust + Typescript 开发的 Farcaster Hub | +| [FName Registry](https://github.com/farcasterxyz/fname-registry) | 用于注册 fname 的官方服务器 | +| [Docs](https://github.com/farcasterxyz/docs) | 上述所有项目的文档(即本网站) | + +## 文档 + +本网站是协议文档的中心枢纽。如有反馈意见,请在 [farcasterxyz/docs](https://github.com/farcasterxyz/docs) 提交议题或创建拉取请求。 + +## 开发者会议 + +我们每两周举办一次开发者会议,讨论协议的后续变更。会议向所有人开放,是参与项目的好机会。 + +- [日历邀请](https://calendar.google.com/calendar/u/0?cid=NjA5ZWM4Y2IwMmZiMWM2ZDYyMTkzNWM1YWNkZTRlNWExN2YxOWQ2NDU3NTA3MjQwMTk3YmJlZGFjYTQ3MjZlOEBncm91cC5jYWxlbmRhci5nb29nbGUuY29t) - + 加入后续会议的日历邀请。 +- [会议录像](https://www.youtube.com/watch?v=lmGXWP5m1_Y&list=PL0eq1PLf6eUeZnPtyKMS6uN9I5iRIlnvq) - 观看往期会议录像。 diff --git a/docs/zh/learn/index.md b/docs/zh/learn/index.md new file mode 100644 index 0000000..d54e390 --- /dev/null +++ b/docs/zh/learn/index.md @@ -0,0 +1,36 @@ +# 入门指南 + +Farcaster 是一个基于以太坊构建的[充分去中心化](https://www.varunsrinivasan.com/2022/01/11/sufficient-decentralization-for-social-networks)社交网络。 + +这是一个类似于 X(原 Twitter)和 Reddit 的公共社交网络。用户可以创建个人资料、发布"casts"(动态)并关注他人。他们拥有自己的账户和与其他用户的关系,并可以自由地在不同应用之间迁移。 + +:::tip 加入 Farcaster +如果您尚未使用 Farcaster,可以通过 [Warpcast](https://www.warpcast.com/) [创建账户](https://www.warpcast.com/)开始体验。 +::: + +## 学习资源 + +如果您想了解更多,可以从以下概念开始深入: + +- [Farcaster 101](https://www.youtube.com/playlist?list=PL0eq1PLf6eUdm35v_840EGLXkVJDhxhcF) - 通过简短的 5 分钟视频了解 Farcaster 协议。 +- [核心概念](/zh/learn/what-is-farcaster/accounts) - 从账户开始,学习 Farcaster 的基础构建模块。 +- [架构设计](/zh/learn/architecture/overview) - 解析 Farcaster 的链上和链下系统。 + +## 教程 + +- [构建您的第一个 mini app](/zh/developers/frames/getting-started) - 开发可在 Farcaster 内运行的 mini-app。 +- [使用 Farcaster 登录](/zh/auth-kit/installation) - 让用户通过 Farcaster 账户登录您的应用。 +- [编写您的第一个应用](/zh/developers/index) - 向 Farcaster 发布"Hello World"消息。 + +在[开发者](/zh/developers/)章节可以找到更多类似的指南和教程。 + +## 文档 + +- [Farcaster 规范](https://github.com/farcasterxyz/protocol) - Farcaster 的规范说明,包括其合约和枢纽(hubs)。 +- [Mini Apps 规范](/zh/developers/frames/spec) - 在 Farcaster 应用中编写和渲染 mini apps 的规范。 +- [API 文档](/zh/reference/) - 链上和链下系统的 API 和 ABI 文档。 + +## 贡献 + +要了解如何为协议(包括本文档站点)做出贡献,请查看 +[贡献指南](/zh/learn/contributing/overview)章节。 diff --git a/docs/zh/learn/what-is-farcaster/accounts.md b/docs/zh/learn/what-is-farcaster/accounts.md new file mode 100644 index 0000000..21a8143 --- /dev/null +++ b/docs/zh/learn/what-is-farcaster/accounts.md @@ -0,0 +1,48 @@ +# 账户 + +Farcaster 账户允许您设置用户名、个人资料图片,并发布称为 casts 的短文本消息。任何以太坊地址都可以通过发起链上交易来注册 Farcaster 账户。 + +## 创建账户 + +通过调用 IdGateway 合约即可创建 Farcaster 账户。该合约会为您的以太坊地址分配一个新的 Farcaster ID(即 fid)。 + +在使用账户前,您需要获取用户名、租赁存储空间并添加密钥。这些步骤需要多次签名和链上交易,使用常规以太坊钱包操作会较为繁琐。 + +我们推荐从 [Warpcast](https://www.warpcast.com/) 开始,这是一款专为 Farcaster 设计的客户端软件,能为您自动处理整个流程。它还会使用独立的以太坊账户来签署交易,从而保障您的主账户安全。 + +## 添加账户密钥 + +账户可以签发密钥,授权应用程序代其发布消息。用户通常会为他们使用的每个 Farcaster 应用签发一个密钥。 + +密钥由 KeyRegistry 合约管理。添加密钥时,您需要提交 EdDSA 密钥对的公钥以及请求者签名。请求者可以是账户本身,也可以是希望代其操作的应用程序。 + +## 恢复账户 + +用户常为社交应用配置独立钱包,容易丢失访问权限。Farcaster 允许任何账户设置恢复地址,用于找回账户。该地址可在创建账户时或之后任意时间配置。 + +用户可将恢复地址设为 Warpcast 等可信服务,也可使用常规以太坊钱包自行管理。 + +## 资源 + +### 规范文档 + +- [合约规范](https://github.com/farcasterxyz/protocol/blob/main/docs/SPECIFICATION.md#1-smart-contracts) - 管理 Farcaster 账户、账户密钥和恢复地址的链上合约。 + +### API 接口 + +- [IdRegistry](../../reference/contracts/reference/id-registry) - 链上查询账户数据 +- [IdGateway](../../reference/contracts/reference/id-gateway) - 链上创建账户 +- [KeyRegistry](../../reference/contracts/reference/key-registry) - 链上查询账户密钥数据 +- [KeyGateway](../../reference/contracts/reference/key-gateway) - 链上创建账户密钥 +- [获取 Farcaster ID 列表](../../reference/hubble/httpapi/fids) - 从 hub 获取所有已注册账户的 fid +- [获取账户密钥](../../reference/hubble/httpapi/onchain#onchainsignersbyfid) - 从 hub 获取账户的密钥(签名者) + +### 教程指南 + +- [创建账户](../../developers/guides/accounts/create-account.md) - 在 Farcaster 上新建账户 +- [创建账户密钥](../../developers/guides/accounts/create-account-key.md) - 为账户新建密钥 +- [通过用户名查找账户](../../developers/guides/accounts/find-by-name.md) - 根据用户名定位账户 +- [变更托管地址](../../developers/guides/accounts/change-custody.md) - 修改账户所属地址 +- [变更恢复地址](../../developers/guides/accounts/change-recovery.md) - 修改账户恢复地址 +- [查找密钥请求者](../../developers/guides/advanced/decode-key-metadata.md) - 追溯用户授权的应用密钥 +- [从 replicator 查询注册数据](../../developers/guides/advanced/query-signups.md) - 通过 replicator 查询注册数量 diff --git a/docs/zh/learn/what-is-farcaster/apps.md b/docs/zh/learn/what-is-farcaster/apps.md new file mode 100644 index 0000000..d29b6d7 --- /dev/null +++ b/docs/zh/learn/what-is-farcaster/apps.md @@ -0,0 +1,51 @@ +# 应用 + +使用 Farcaster 需要一个以太坊钱包来注册账户以及浏览网络的用户界面。如果你是新手,我们推荐从 [iOS](https://apps.apple.com/us/app/warpcast/id1600555445) 或 [Android](https://play.google.com/store/apps/details?id=com.farcaster.mobile&hl=en_US&gl=US) 上的 Warpcast 开始。 + +应用分为两种类型: + +1. **钱包应用** - 允许注册、添加关联应用、发布和浏览消息。 +2. **关联应用** - 仅允许发布和浏览消息。 + +## 钱包应用 + +用户必须安装一个钱包应用才能开始使用 Farcaster。它们可以执行链上和链下操作,例如注册、添加关联应用、发布消息和用户。 + +钱包应用控制着拥有账户的以太坊地址。它对账户拥有控制权,可以代表你执行任何操作,因此请仅使用你信任的钱包应用。 + +### Warpcast + +Warpcast 是由 Farcaster 团队开发的钱包应用。它提供网页版和移动版,但注册功能仅在移动端可用。 + +- 下载:[iOS](https://apps.apple.com/us/app/warpcast/id1600555445), [Android](https://play.google.com/store/apps/details?id=com.farcaster.mobile&hl=en_US&gl=US) + +## 关联应用 + +关联应用只能在用户通过钱包应用注册后添加。它们可以在 Farcaster 上执行链下操作,例如发布 casts、关注账户和浏览内容。 + +关联应用控制着由钱包应用授予的应用密钥。用户可以为其账户添加多个关联应用,并随时移除它们。恶意的关联应用无法控制你的账户,其执行的任何操作都可以由你的钱包应用撤销。 + +一些流行的关联应用包括: + +- [Supercast](https://supercast.xyz/) +- [Yup](https://yup.io/) +- [Farcord](https://farcord.com/) + +**Farcaster 不对关联应用进行审核,使用风险自负** + +## 资源 + +### 工具 + +- [Hubble](../../hubble/hubble.md) - 用于读写消息的 farcaster hub。 +- [Replicator](https://github.com/farcasterxyz/hub-monorepo/tree/main/apps/replicator) - 将 hub 同步到 postgres 数据库的工具。 + +### 教程 + +- [设置 hubble](../..//hubble/install#install-via-script) - 运行一个 farcaster hub。 +- [设置 replicator](../../developers/guides/apps/replicate) - 将 hub 同步到 postgres 以便轻松查询。 +- [复制模式](../../reference/replicator/schema) - replicator 的 postgres 表结构。 + +### 服务 + +- [Neynar](https://neynar.com/) - 用于构建 farcaster 应用的基础设施和服务。 diff --git a/docs/zh/learn/what-is-farcaster/channels.md b/docs/zh/learn/what-is-farcaster/channels.md new file mode 100644 index 0000000..c7a569e --- /dev/null +++ b/docs/zh/learn/what-is-farcaster/channels.md @@ -0,0 +1,73 @@ +# 频道 + +频道是供社区围绕某个主题展开对话的公共空间。 + +创建频道即为您的社区开启一个新的信息流。人们可以加入、发布动态(cast)并结识其他有趣的人。它能激发在首页信息流中不会发生的对话。 + +:::warning 实验性功能 +频道目前仅在 Warpcast 中进行原型测试,尚未得到 Farcaster 协议的完整支持。若该功能被验证成功,未来可能被整合至协议中;也可能被完全移除。 +::: + +## 托管频道 + +任何人都可以通过在 Warpcast 中支付费用并选择频道名称来创建频道托管。名称必须少于 16 个字符,且仅能包含小写字母和数字。频道的创建者被称为主持人(host),并可邀请其他联合主持人共同管理频道。主持人拥有特殊权限,例如: + +1. 制定"频道规范",所有加入者必须同意遵守。 +2. 在频道内置顶或隐藏动态。 +3. 禁止其他用户在该频道发布动态。 +4. 设置频道图片、描述和其他元数据。 + +在实验阶段,频道元数据不属于协议内容,而是存储在 Warpcast 中。 + +## 在频道中发布动态 + +任何人都可以通过 Warpcast 发布动态至频道,只需在创建动态时选择对应频道。Warpcast 会自动将动态的 `parentUrl` 设置为 `https://warpcast.com/~/channel/<名称>`。当动态的 `parentUrl` 是频道 URI 或另一个"位于频道中"的动态时,该动态即被视为"位于频道中"。 + +频道动态属于协议内容,并存储在枢纽(hubs)上。通过使用复制器(replicator),您可以通过筛选频道的 FIP-2 URL 对应的 `parentUrl` 字段来获取该频道中的所有动态。 + +## 关注频道 + +任何人都可以像关注用户一样关注频道。当使用 Warpcast 时,用户将在首页信息流中看到所关注频道发布的动态。 + +在实验阶段,频道关注功能不属于协议内容,而是存储在 Warpcast 中。 + +## 动态可见性 + +如果用户在频道中发布动态,Warpcast 会: + +1. 始终将该动态发送给所有关注该频道的用户的首页信息流。 +2. 通常也会将该动态发送给所有关注发布者的用户的首页信息流。 + +对于第(2)点的判定基于用户偏好、频道内容和其他社交图谱数据。该算法仍在微调中,待稳定后将进行文档化说明。 + +## 使用政策 + +若出现以下情况,Warpcast 可能会移除您的频道且不会退还已支付的 warps: + +1. 您的个人资料或频道冒充他人。 +2. 您占用了频道但未实际使用。 +3. 您违反了 Warpcast 的服务条款或应用商店规则。 + +## 常见问题 + +**为什么允许频道主持人隐藏内容和封禁用户?这不是审查制度吗?** + +频道并非完全开放的公共空间,它们由其创建者拥有并管理。您随时可以创建自己的频道并制定规则。 + +**为什么创建频道需要付费?** + +收费机制是为了防止人们占用简短名称却不使用频道。 + +**创建频道有什么好处?** + +创建频道也有助于扩大您的受众: + +1. Warpcast 会向您的关注者发送关于新频道的通知。 +2. 您的频道会被推荐给关注类似频道的用户。 +3. 关注您频道的用户将在首页信息流中看到频道动态。 + +## 资源 + +### API + +- [Warpcast 频道 API](../../reference/warpcast/api.md) - 获取所有已知频道的列表 diff --git a/docs/zh/learn/what-is-farcaster/messages.md b/docs/zh/learn/what-is-farcaster/messages.md new file mode 100644 index 0000000..7adfa7e --- /dev/null +++ b/docs/zh/learn/what-is-farcaster/messages.md @@ -0,0 +1,72 @@ +# 消息 + +Farcaster 账户通过签名和发布消息进行交互。Alice 可以创建一条内容为"_你好 @bob_"的消息,并用她的密钥进行签名。 + +消息存储在一个点对点的节点网络中。Farcaster 网络中的节点称为 Hub,每个 Hub 都存储着整个网络的副本。用户可以向一个 Hub 发布消息,几秒钟内该消息就会传播至整个网络。Farcaster 紧凑的消息格式和最终一致性模型使得这种架构能够扩展到数百万用户。 + +账户可以生成一个[密钥](./accounts.md#adding-account-keys)并授权给应用程序使用该密钥签署消息。用户可以在同一账户下使用多个应用程序,每个应用程序都可以拥有自己的密钥。将签名密钥与所有权密钥分离有助于保障账户安全。 + +## 消息类型 + +账户可以向网络发布五种不同类型的消息: + +| 类型 | 描述 | 示例 | +| ------------- | ---------------------------- | -------------------------- | +| Casts | 任何人都能看到的公开消息。 | "你好世界!" | +| Reactions | 账户与 Cast 之间的互动关系。 | Alice 点赞了 Bob 的 Cast。 | +| Links | 两个账户之间的关系。 | Alice 关注 Bob。 | +| Profile Data | 账户的元数据信息。 | 头像、显示名称。 | +| Verifications | 所有权证明。 | 以太坊地址。 | + +## 存储机制 + +账户需要支付租金才能将其消息保留在 Farcaster 网络上。收取租金可防止用户对网络进行垃圾信息攻击。 + +账户可以通过向存储注册表(Storage Registry)发起链上交易来租用存储单元。当前一个存储单元的价格为 7 美元,有效期为一年,允许每个账户存储一定数量的各类消息。当前各类型的存储限制为: + +- 5000 条 Casts +- 2500 条 Reactions +- 2500 条 Links +- 50 条 Profile Data +- 50 条 Verifications + +如果账户超出某类消息的存储限制,系统将自动清理最早的消息以腾出空间存储新消息。用户无需购买更多存储空间即可继续使用网络,同时 Hub 也能有效控制存储负载。账户随时可以通过购买更多存储空间来提高限制。 + +存储单元过期的账户可能会丢失所有消息。存储单元过期后有 30 天的宽限期,账户必须在此期间续费,否则将丢失消息。 + +每个存储单元的价格和容量会定期重新计算,以平衡网络的增长和质量。详见 [FIP-6](https://github.com/farcasterxyz/protocol/discussions/98)。 + +## 消息删除 + +账户随时可以通过发布对应的删除消息来删除原始消息。删除消息会清除原始消息的内容,并在其位置留下一个墓碑标记。被删除的消息仍会占用账户的存储配额,直到被新消息挤出存储空间为止。 + +## 时间戳 + +消息的时间戳从 Farcaster 纪元(2021 年 1 月 1 日 00:00:00 UTC)开始以秒计数。使用近期纪元可以使时间戳和消息体积更小,这对网络至关重要。 + +时间戳未经验证,用户可以回溯日期(类似于博客文章)。但时间戳不能设置为未来 15 分钟以上,否则网络将拒绝此类消息。 + +## 相关资源 + +### 技术规范 + +- [消息规范](https://github.com/farcasterxyz/protocol/blob/main/docs/SPECIFICATION.md#2-message-specifications) - Farcaster 的最小变更单元 +- [CRDTs](https://github.com/farcasterxyz/protocol/blob/main/docs/SPECIFICATION.md#31-crdts) - 保持网络消息同步的规则 +- [存储注册表](https://github.com/farcasterxyz/protocol/blob/main/docs/SPECIFICATION.md#13-storage-registry) - 获取存储单元的智能合约 + +### API 接口 + +- [获取 Casts](../../reference/hubble/httpapi/casts) - 从 Hub 获取账户的 Casts +- [获取 Reactions](../../reference/hubble/httpapi/reactions) - 从 Hub 获取账户的 Reactions +- [获取 Links](../../reference/hubble/httpapi/links) - 从 Hub 获取账户的 Links 或关注关系 +- [获取 UserData](../../reference/hubble/httpapi/userdata) - 从 Hub 获取账户的 Profile Data +- [提交消息](../../reference/hubble/httpapi/message#submitmessage) - 向 Hub 网络广播消息 +- [验证消息](../../reference/hubble/httpapi/message#validatemessage) - 通过 Hub 验证消息真实性 +- [存储注册表](../../reference/contracts/reference/storage-registry) - 为账户获取或检查存储单元 + +### 教程指南 + +- [获取 Casts](../../developers/guides/querying/fetch-casts) - 从 Hub 获取账户的 Casts +- [获取个人资料](../../developers/guides/querying/fetch-profile) - 从 Hub 获取账户的个人资料 +- [创建常见消息类型](../../developers/guides/writing/messages) - 创建 Casts、Links、Reactions 和 UserData +- [创建高级功能 Casts](../../developers/guides/writing/casts) - 创建带嵌入内容、表情和提及的 Casts diff --git a/docs/zh/learn/what-is-farcaster/usernames.md b/docs/zh/learn/what-is-farcaster/usernames.md new file mode 100644 index 0000000..ce74753 --- /dev/null +++ b/docs/zh/learn/what-is-farcaster/usernames.md @@ -0,0 +1,58 @@ +# 用户名 + +Farcaster 账户需要一个用户名,以便其他用户能够找到并提及它。Farcaster 使用 [Ethereum Name Service](https://ens.domains/) 来管理用户名。 + +ENS 用户名由以太坊地址拥有,就像 Farcaster 账户一样。区别在于一个地址可以拥有多个 ENS 名称,因此 Farcaster 账户必须指定它希望使用的名称。ENS 名称只有在长度不超过 16 个字符且仅包含小写字母、数字和连字符时才能在 Farcaster 上使用。 + +## 更改用户名 + +Farcaster 账户可以随时在不同用户名之间切换。更改名称不会影响您的历史记录或关注者。 + +每年更改几次名称是安全的。但更频繁地更改名称可能会导致用户或 mini app 对您的账户失去信任。如果您想更改公开标识,请考虑更改您的显示名称。 + +## 链下与链上名称 + +账户可以选择两种类型的用户名: + +- **链下 ENS 名称**:免费且由 Farcaster 控制。(例如 @alice) +- **链上 ENS 名称**:需要付费且由您的钱包控制。(例如 @alice.eth) + +如果您想快速开始且没有链上 ENS 名称,请选择链下 ENS 名称。账户以后随时可以升级为链上名称。建议使用像 Warpcast 这样的 mini app 来为您设置。 + +![用户名](/assets/usernames.png) + +### 链下 ENS 名称 + +- 链下 ENS 名称(也称为 fnames)是免费的,由 Farcaster 发行。 +- 任何以太坊账户都可以通过调用 [Fname Registry](/zh/learn/architecture/ens-names) 获得一个唯一的 fname。 +- Fnames 是免费的,但 Farcaster 可以随时撤销它们。 + +### 链上 ENS 名称 + +- 链上 ENS 名称(也称为 .eth 名称)是链上的,由 ENS 发行。 +- 任何以太坊账户都可以通过调用 [ENS Registry](https://docs.ens.domains/dapp-developer-guide/the-ens-registry) 获得一个 ENS 名称。 +- 名称不是免费的,但 Farcaster 无法撤销它们。 + +## 资源 + +### 规范 + +- [Farcaster 名称](https://github.com/farcasterxyz/protocol/blob/main/docs/SPECIFICATION.md#5-fname-specifications) - 一种 ENSIP-10 链下 ENS 名称,可在 Farcaster 内使用。 +- [UserData: 用户名](https://github.com/farcasterxyz/protocol/blob/main/docs/SPECIFICATION.md#23-user-data) - 将有效的用户名证明设置为当前用户名。 +- [用户名证明](https://github.com/farcasterxyz/protocol/blob/main/docs/SPECIFICATION.md#17-username-proof) - 证明链上或链下用户名的所有权。 +- [验证](https://github.com/farcasterxyz/protocol/blob/main/docs/SPECIFICATION.md#25-verifications) - 证明地址的所有权,链上用户名证明需要此验证。 + +### API + +- [UserData API](../../reference/hubble/httpapi/userdata) - 获取用户当前用户名的 UserData。 +- [用户名证明 API](../../reference/hubble/httpapi/usernameproof) - 从 hub 获取用户的名证明。 +- [验证证明 API](../../reference/hubble/httpapi/verification) - 从 hub 获取用户的验证信息。 +- [Fname Registry API](../../reference/fname/api.md) - 以编程方式注册和跟踪 fname 所有权。 + +### 教程 + +- [获取 UserData](../../developers/guides/querying/fetch-profile.md) - 从账户获取 UserData 消息。 +- [创建 UserData](../../developers/guides/writing/messages#user-data) - 创建 UserData 消息以选择有效的用户名。 +- [验证地址](../../developers/guides/writing/verify-address.md) - 验证以太坊账户的所有权。 +- [按用户名查找账户](../../developers/guides/accounts/find-by-name.md) - 按用户名查找账户。 +- [更改 Farcaster 名称](../../developers/guides/accounts/change-fname.md) - 更改 Farcaster 用户名。 diff --git a/docs/zh/reference/actions/spec.md b/docs/zh/reference/actions/spec.md new file mode 100644 index 0000000..8712330 --- /dev/null +++ b/docs/zh/reference/actions/spec.md @@ -0,0 +1,306 @@ +# 操作规范 + +操作(Actions)允许开发者在任何 Farcaster 应用中创建自定义按钮,用户可将其安装至操作栏。可将其视为浏览器扩展,但专用于 casts(广播消息)。 + +与 [Frames](/zh/developers/frames/spec) 类似,操作是一种开放标准,用于通过新型交互方式扩展 casts。操作与帧(frames)可组合使用,使开发者能创建跨 Farcaster 客户端工作的交互式认证应用。 + +目前,开发者已利用操作为 Farcaster 客户端添加了翻译、内容审核工具和小费等功能。 + +## 定义操作 + +操作是一个位于网络服务器 URL 上的 Web API。我们将该网络服务器称为"操作服务器"。 + +操作服务器必须定义两个路由: + +- 一个返回操作元数据的 GET 路由 +- 一个处理操作请求的 POST 路由 + +Farcaster 客户端加载 GET 路由以获取操作信息,当用户在信息流中点击操作按钮时,则向 POST 路由发起请求。 + +## 元数据路由 + +操作服务器必须响应对其元数据 URL 的 HTTP GET 请求,返回 200 OK 状态码及以下格式的 JSON 正文: + +```json +{ + "name": "10天后提醒我", + "icon": "lightbulb", + "description": "10天后自动接收来自 @remindbot 的提醒。", + "aboutUrl": "https://remindbot.example.com/remind/about", + "action": { + "type": "post", + "postUrl": "https://remindbot.example.com/actions/remind" + } +} +``` + +### 属性 + +| 键名 | 描述 | +| ---------------- | --------------------------------------------------------------------------------------------------------------- | +| `name` | 操作名称,最多 30 个字符。 | +| `icon` | 操作图标 ID。参见[有效图标](#valid-icons)获取支持的图标列表。 | +| `description` | 简短描述,最多 80 个字符。 | +| `aboutUrl` | 可选的"关于"页面外部链接。仅当无法通过描述字段完整说明操作功能时包含此项。必须为 `http://` 或 `https://` 协议。 | +| `action.type` | 操作类型。必须为 `'post'`。 | +| `action.postUrl` | 可选的操作处理程序 URL。若未提供,客户端将向操作元数据路由的相同 URL 发送 POST 请求。 | + +## 处理程序路由 + +当用户在信息流中点击 cast 操作按钮时,客户端会向操作处理程序发送带有签名消息的 POST 请求。操作使用与 Farcaster 帧相同的[帧签名消息](/zh/developers/frames/spec#frame-signature-packet)格式。 + +在此消息中: + +- `frame_url` 设置为 cast 操作的 `postUrl` +- `button_index` 设置为 `1` +- `cast_id` 是用户发起操作的 cast 的 ID + +操作服务器可响应简短消息、帧 URL 或错误。 + +### 消息响应类型 + +消息响应类型显示简短消息及可选外部链接,适用于简单的单步交互。 + +![消息操作](/assets/actions/message_type.png) + +操作服务器可返回 200 OK 及以下格式的 JSON 正文以显示消息: + +```json +{ + "type": "message", + "message": "提醒已保存!", + "link": "https://remindbot.example.com/reminders/1" +} +``` + +**属性** + +| 键名 | 描述 | +| --------- | ---------------------------------------------------------------------------------------------------- | +| `type` | 必须为 `message`。 | +| `message` | 简短消息,少于 80 个字符。 | +| `link` | 可选 URL。必须为 `http://` 或 `https://` 协议。若存在,客户端必须将消息显示为指向此 URL 的外部链接。 | + +### 帧响应类型 + +帧响应类型允许 cast 操作显示[帧](/zh/developers/frames/spec),适用于复杂的多步交互。 + +![帧操作](/assets/actions/frame_type.png) + +操作服务器可返回 200 OK 及以下格式的 JSON 正文以显示帧: + +```json +{ + "type": "frame", + "frameUrl": "https://remindbot.example.com/frame" +} +``` + +**属性** + +| 键名 | 描述 | +| ---------- | ---------------------------------------------------------------------------------------- | +| `type` | 必须为 `frame`。 | +| `frameUrl` | 要显示的帧 URL。256 字节字符串。客户端必须在特殊上下文(如模态框或底部表单)中显示该帧。 | + +### 错误响应类型 + +操作服务器可返回 4xx 响应及消息以显示错误: + +```json +{ + "message": "出错了。" +} +``` + +**属性** + +| 键名 | 描述 | +| --------- | ------------------------------ | +| `message` | 简短错误消息,少于 80 个字符。 | + +## 客户端实现 + +### 添加操作 + +客户端必须允许用户通过点击深度链接 URL 启用 cast 操作。例如在 Warpcast 中: + +``` +https://warpcast.com/~/add-cast-action?url=https%3A%2F%2Fremindbot.example.com%2Fremind +``` + +当用户启用 cast 操作时,客户端必须: + +- 加载 URL 并解析其查询参数以获取操作元数据路由 +- GET 操作元数据 URL 以检索操作元数据 +- 验证并清理所有数据 +- 代表用户保存发现的操作 +- 更新现有已保存操作的元数据 +- 允许用户移除不需要或未使用的操作 + +客户端应显示确认界面,向用户提供更多关于操作功能的上下文。若安装此操作将替换先前操作,该界面应警告用户。 + +### 显示操作 + +客户端必须在 casts 旁显示操作,并允许用户点击/轻触进行交互。 + +#### 处理点击 + +当用户点击操作时,客户端必须向操作的 `postUrl` 发送带有签名帧消息的 POST 请求。 + +在此消息中,客户端必须: + +- 将 `frame_url` 设置为 cast 操作的 `postUrl` +- 将 `button_index` 设置为 `1` +- 将 `cast_id` 设置为用户发起操作的 cast 的 ID +- 其余部分遵循现有的帧消息格式 + +#### 显示响应 + +操作服务器可能向客户端返回简短文本响应、帧或错误。 + +当客户端收到操作响应时,必须: + +1. 验证响应格式: + - 类型为 frame: + - 必须包含 `frameUrl` + - `frameUrl` 必须以 `https://` 开头 + - 类型为 message: + - 必须包含少于 80 字符的 `message` 字符串 +2. 若响应类型为 message 或 error,则显示它 +3. 若响应类型为 frame,则向返回的 `frameUrl` 发送帧消息 POST 请求: + - 将 `buttonIndex` 设置为 1 + - 将帧消息正文中的 `castId` 设置为被点击 cast 的 ID +4. 将响应解析为帧并在特殊上下文(如底部表单或模态框)中显示 + +客户端必须允许用户在交互完成后关闭帧。 + +## 参考 + +#### 有效图标 + +- number +- search +- image +- alert +- code +- meter +- ruby +- video +- filter +- stop +- plus +- info +- check +- book +- question +- mail +- home +- star +- inbox +- lock +- eye +- heart +- unlock +- play +- tag +- calendar +- database +- hourglass +- key +- gift +- sync +- archive +- bell +- bookmark +- briefcase +- bug +- clock +- credit-card +- globe +- infinity +- light-bulb +- location +- megaphone +- moon +- note +- pencil +- pin +- quote +- reply +- rocket +- shield +- stopwatch +- tools +- trash +- comment +- gear +- file +- hash +- square +- sun +- zap +- sign-out +- sign-in +- paste +- mortar-board +- history +- plug +- bell-slash +- diamond +- id-badge +- person +- smiley +- pulse +- beaker +- flame +- people +- person-add +- broadcast +- graph +- shield-check +- shield-lock +- telescope +- webhook +- accessibility +- report +- verified +- blocked +- bookmark-slash +- checklist +- circle-slash +- cross-reference +- dependabot +- device-camera +- device-camera-video +- device-desktop +- device-mobile +- dot +- eye-closed +- iterations +- key-asterisk +- law +- link-external +- list-ordered +- list-unordered +- log +- mention +- milestone +- mute +- no-entry +- north-star +- organization +- paintbrush +- paper-airplane +- project +- shield-x +- skip +- squirrel +- stack +- tasklist +- thumbsdown +- thumbsup +- typography +- unmute +- workflow +- versions diff --git a/docs/zh/reference/contracts/deployments.md b/docs/zh/reference/contracts/deployments.md new file mode 100644 index 0000000..b5a9976 --- /dev/null +++ b/docs/zh/reference/contracts/deployments.md @@ -0,0 +1,18 @@ +# 部署信息 + +## 合约地址 + +所有 Farcaster 合约均部署在 Optimism 主网,暂未部署测试网。 + +| 合约名称 | Optimism 地址 | Etherscan 链接 | +| ---------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------ | +| Id Registry | `0x00000000fc6c5f01fc30151999387bb99a9f489b` | [链接](https://optimistic.etherscan.io/address/0x00000000fc6c5f01fc30151999387bb99a9f489b) | +| Key Registry | `0x00000000Fc1237824fb747aBDE0FF18990E59b7e` | [链接](https://optimistic.etherscan.io/address/0x00000000Fc1237824fb747aBDE0FF18990E59b7e) | +| Storage Registry | `0x00000000fcCe7f938e7aE6D3c335bD6a1a7c593D` | [链接](https://optimistic.etherscan.io/address/0x00000000fcCe7f938e7aE6D3c335bD6a1a7c593D) | +| IdGateway | `0x00000000fc25870c6ed6b6c7e41fb078b7656f69` | [链接](https://optimistic.etherscan.io/address/0x00000000fc25870c6ed6b6c7e41fb078b7656f69) | +| KeyGateway | `0x00000000fc56947c7e7183f8ca4b62398caadf0b` | [链接](https://optimistic.etherscan.io/address/0x00000000fc56947c7e7183f8ca4b62398caadf0b) | +| Bundler | `0x00000000fc04c910a0b5fea33b03e0447ad0b0aa` | [链接](https://optimistic.etherscan.io/address/0x00000000fc04c910a0b5fea33b03e0447ad0b0aa) | + +## ABI 文件 + +合约的 ABI 可通过上方 Etherscan 链接获取,或从 [`@farcaster/core`](https://github.com/farcasterxyz/hub-monorepo/tree/main/packages/core/src/eth/contracts/abis) 包中获取。 diff --git a/docs/zh/reference/contracts/faq.md b/docs/zh/reference/contracts/faq.md new file mode 100644 index 0000000..d6793ff --- /dev/null +++ b/docs/zh/reference/contracts/faq.md @@ -0,0 +1,67 @@ +# 常见问题解答 + +## 签名相关 + +### 如何生成 EIP-712 签名? + +各 EIP-712 签名的文档说明请参阅合约参考文档,其中包含 TypeScript [代码示例](/zh/reference/contracts/reference/id-gateway#register-signature)。 + +若使用 TypeScript/JS,[`@farcaster/hub-web`](https://www.npmjs.com/package/@farcaster/hub-web) 包提供了生成和处理 EIP-712 签名的工具。为确保使用正确的地址和类型哈希,建议从 [合约模块](https://github.com/farcasterxyz/hub-monorepo/tree/main/packages/core/src/eth/contracts) 导入 ABI 和 EIP-712 类型,或使用提供的 [`Eip712Signer`](https://github.com/farcasterxyz/hub-monorepo/blob/main/packages/core/src/signers/eip712Signer.ts) 辅助工具。 + +参考 hub 单体仓库中的 [示例应用](https://github.com/farcasterxyz/hub-monorepo/tree/main/packages/hub-nodejs/examples/contract-signatures)"Working with EIP-712 signatures",该示例演示了各类签名和合约调用。 + +### 如何调试无效的 EIP-712 签名? + +为辅助调试 EIP-712 签名,所有使用 EIP-712 签名的合约都公开其 [域分隔符](https://optimistic.etherscan.io/address/0x00000000fc25870c6ed6b6c7e41fb078b7656f69#readContract#F3) 和类型哈希作为 [常量](https://optimistic.etherscan.io/address/0x00000000fc25870c6ed6b6c7e41fb078b7656f69#readContract#F1),并提供 [hashTypedDataV4](https://optimistic.etherscan.io/address/0x00000000fc25870c6ed6b6c7e41fb078b7656f69#readContract#F6) 辅助视图。若使用 Solidity 或其他底层语言构建签名,可利用这些工具进行调试。 + +## 参考信息 + +### 完整的合约源代码在哪里? + +合约仓库位于 GitHub [此处](https://github.com/farcasterxyz/contracts)。 + +### 如何获取合约 ABI? + +合约 ABI 和部署地址见 [此处](/zh/reference/contracts/deployments#abis)。 + +### 审计报告在哪里查看? + +历史审计报告链接见 [合约仓库](https://github.com/farcasterxyz/contracts/blob/1aceebe916de446f69b98ba1745a42f071785730/README.md#audits)。 + +### Farcaster 合约是否部署在测试网上? + +否。建议使用 [网络分叉](https://book.getfoundry.sh/tutorials/forking-mainnet-with-cast-anvil) 来测试或针对 OP 主网合约进行开发。 + +## 数据查询 + +### 如何查找用户的托管地址? + +调用 [IdRegistry](/zh/reference/contracts/reference/id-registry.md) 的 [`custodyOf`](https://optimistic.etherscan.io/address/0x00000000fc6c5f01fc30151999387bb99a9f489b#readContract#F5) 函数。 + +### 如何查找用户的恢复地址? + +调用 [IdRegistry](/zh/reference/contracts/reference/id-registry.md) 的 [`recoveryOf`](https://optimistic.etherscan.io/address/0x00000000fc6c5f01fc30151999387bb99a9f489b#readContract#F23) 函数。 + +### 如何查找账户的 fid? + +调用 [IdRegistry](/zh/reference/contracts/reference/id-registry.md) 的 [`idOf`](https://optimistic.etherscan.io/address/0x00000000fc6c5f01fc30151999387bb99a9f489b#readContract#F14) 函数。 + +### 如何查询我的 fid 对应的账户密钥? + +调用 [KeyRegistry](/zh/reference/contracts/reference/key-registry.md) 的 [`keysOf`](https://optimistic.etherscan.io/address/0x00000000fc1237824fb747abde0ff18990e59b7e#readContract#F16) 函数。 + +## 其他问题 + +### 什么是 app fid?如何获取? + +**什么是 FID?** + +FID (Farcaster ID) 是用于区分应用程序和用户的唯一标识符。通过 FID 可识别和区分不同的应用与用户。 + +**为什么需要 FID?** + +要在 Farcaster 平台上创建内容或发布信息,必须使用 FID 来标识您的应用或用户。 + +**如何获取?** + +您可以通过 [Bundler](/zh/reference/contracts/reference/bundler.md) 或 [IdGateway](/zh/reference/contracts/reference/id-gateway.md) 直接注册 app fid,也可使用 Farcaster 客户端为您的应用注册账户。由于需要通过拥有 app fid 的钱包签署 [密钥请求元数据](/zh/reference/contracts/reference/signed-key-request-validator.md),请妥善保管私钥。 diff --git a/docs/zh/reference/contracts/index.md b/docs/zh/reference/contracts/index.md new file mode 100644 index 0000000..53069d5 --- /dev/null +++ b/docs/zh/reference/contracts/index.md @@ -0,0 +1,34 @@ +# 概述 + +Farcaster 合约部署在以太坊二层网络 Optimism 上,包含三个核心合约: +ID 注册表(Id Registry)、密钥注册表(Key Registry)和存储注册表(Storage Registry)。对 ID 和密钥注册表的写入权限通过网关合约(Gateway)进行管控。此外还提供了 Bundler 辅助合约,支持在单笔交易中完成 fid 注册、密钥添加和存储租赁操作。 + +![合约架构图](/assets/contracts.png) + +## ID 注册表 + +ID 注册表合约用于管理 Farcaster ID,将 fid 映射到对应的以太坊地址。所有者还可设置"恢复地址",当注册地址访问权限丢失时可通过该地址恢复 fid。首次注册 fid 必须通过 [ID 网关](#idgateway) 完成。 + +## 密钥注册表 + +密钥注册表将 Farcaster ID 与零个或多个 ed2559 公钥关联。只有通过此处注册密钥签名的消息才会被 hub 节点视为有效。fid 所有者可撤销已注册密钥,但被撤销的密钥无法再次添加到该 fid。同一密钥可注册至多个 fid。添加密钥必须通过 [密钥网关](#keygateway) 完成。 + +## 存储注册表 + +存储注册表允许 fid 在 Farcaster 网络上租赁一个或多个存储"单元"。当前存储价格为每单元 7 美元/年,需使用 ETH 支付费用。该合约通过 ETH 价格预言机获取当前 ETH 计价金额,并提供查询接口。超额支付部分将退还调用方。 + +## ID 网关 + +ID 网关处理首次注册 fid 所需的附加逻辑。为防止垃圾注册,网关同时要求租赁 1 个存储单元。 + +## 密钥网关 + +类似地,密钥网关作为密钥注册表的接入点,所有密钥添加操作必须通过该网关完成。 + +## 聚合器 + +聚合器(Bundler)通过单次函数调用即可完成 fid 注册、密钥添加和存储租赁,大幅简化首次注册流程。 + +## 源代码 + +合约源代码仓库详见[此处](https://github.com/farcasterxyz/contracts),更底层的技术文档可参考[此文档](https://github.com/farcasterxyz/contracts/blob/main/docs/docs.md)。 diff --git a/docs/zh/reference/contracts/reference/bundler.md b/docs/zh/reference/contracts/reference/bundler.md new file mode 100644 index 0000000..0693384 --- /dev/null +++ b/docs/zh/reference/contracts/reference/bundler.md @@ -0,0 +1,69 @@ +--- +outline: [2, 3] +--- + +# Bundler(打包器) + +Bundler 允许用户通过单笔交易完成 fid 注册、密钥添加和存储空间租赁,从而简化首次注册流程。 + +若需通过单次交易创建新的 Farcaster 账户,请使用 Bundler。 + +## 读取操作 + +### 价格查询 + +获取注册 fid(包含 1 个存储单元)所需支付的 wei 金额。如需计算额外存储单元费用,请使用 `extraStorage` 参数。 + +| 参数 | 类型 | 描述 | +| ------------ | --------- | ---------------------------- | +| extraStorage | `uint256` | 需计入总价的额外存储单元数量 | + +## 写入操作 + +### 注册 + +通过单次操作完成 fid 注册、添加一个或多个密钥以及租赁存储空间。具体使用示例请参阅[注册演示应用](https://farcaster-signup-demo.vercel.app/bundler)。 + +| 参数 | 类型 | 描述 | +| -------------- | -------------------- | ------------------------ | +| `msg.value` | `wei` | 注册支付金额 | +| registerParams | `RegistrationParams` | 注册相关参数及签名 | +| signerParams | `SignerParams[]` | 密钥相关参数及签名 | +| extraStorage | `uint256` | 需租赁的额外存储单元数量 | + +**RegistrationParams 结构体** + +`RegistrationParams` 结构体包含注册参数以及来自 fid 接收者的 IdGateway [`Register`](/zh/reference/contracts/reference/id-gateway#register-signature) 签名。 + +| 参数 | 类型 | 描述 | +| -------- | --------- | ------------------------------------------------------------------------------------------------------- | +| to | `address` | 接收注册 fid 的地址 | +| recovery | `address` | 新 fid 的恢复地址 | +| deadline | `uint256` | 签名过期时间戳 | +| sig | `bytes` | 来自 `to` 地址的 EIP-712 [`Register`](/zh/reference/contracts/reference/key-gateway#add-signature) 签名 | + +**SignerParams 结构体** + +`SignerParams` 结构体包含签名者密钥参数以及来自 fid 接收者的 KeyGateway [`Add`](/zh/reference/contracts/reference/key-gateway#add-signature) 签名。调用者可提供多个 `SignerParams` 结构体以在注册时添加多个密钥。 + +| 参数 | 类型 | 描述 | +| ------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| keyType | `uint32` | 必须设为 `1`(当前唯一支持的 `keyType`) | +| key | `bytes` | 待添加的公钥 | +| metadataType | `uint8` | 必须设为 `1`(当前唯一支持的 `metadataType`) | +| metadata | `bytes` | 编码后的 [`SignedKeyRequestMetadata`](/zh/reference/contracts/reference/signed-key-request-validator#signedkeyrequestmetadata-struct) | +| deadline | `uint256` | 签名过期时间戳 | +| sig | `bytes` | 来自 `registrationParams.to` 地址的 EIP-712 [`Add`](/zh/reference/contracts/reference/key-gateway#add-signature) 签名 | + +## 错误类型 + +| 错误 | 选择器 | 描述 | +| ---------------- | ---------- | ---------------------------------------------------------- | +| InvalidPayment | `3c6b4b28` | 调用者支付金额不足 | +| InvalidMetadata | `bcecb64a` | 提供的密钥签名元数据无效 | +| InvalidSignature | `8baa579f` | 提供的签名无效(可能格式错误或由错误地址签署) | +| SignatureExpired | `0819bdcd` | 提供的签名已过期。请向签名者获取带有更晚截止时间戳的新签名 | + +## 源代码 + +[`Bundler.sol`](https://github.com/farcasterxyz/contracts/blob/1aceebe916de446f69b98ba1745a42f071785730/src/Bundler.sol) diff --git a/docs/zh/reference/contracts/reference/id-gateway.md b/docs/zh/reference/contracts/reference/id-gateway.md new file mode 100644 index 0000000..fc4e9ca --- /dev/null +++ b/docs/zh/reference/contracts/reference/id-gateway.md @@ -0,0 +1,140 @@ +--- +outline: [2, 3] +--- + +# ID 网关 + +ID 网关负责注册新的 Farcaster ID 并将其添加到 [ID 注册表](/zh/reference/contracts/reference/id-registry.md)。 + +如需创建新的 Farcaster ID,请使用 ID 网关。 + +## 读取操作 + +### price + +获取注册 fid 所需支付的 wei 金额。该金额包含 1 个存储单元的费用。使用 `extraStorage` 参数可在总价中包含额外存储单元。 + +| 参数 | 类型 | 描述 | +| ------------ | ---------------- | ------------------ | +| extraStorage | `uint256` (可选) | 额外存储单元的数量 | + +### nonces + +获取指定地址的下一个未使用 nonce 值。用于为 [registerFor](#registerfor) 生成 EIP-712 [`Register`](#register-signature) 签名。 + +| 参数 | 类型 | 描述 | +| ----- | --------- | --------------------- | +| owner | `address` | 需要获取 nonce 的地址 | + +## 写入操作 + +### register + +为调用者注册新 fid 并支付存储费用。调用者不得已拥有 fid。 + +| 参数 | 类型 | 描述 | +| ------------ | ---------------- | -------------------------- | +| `msg.value` | `wei` | 注册支付金额 | +| recovery | `address` | 新 fid 的恢复地址 | +| extraStorage | `uint256` (可选) | 需要租赁的额外存储单元数量 | + +### registerFor + +为指定地址注册新 fid 并支付存储费用。接收地址必须签署 EIP-712 [`Register`](#register-signature) 消息以批准注册。接收者不得已拥有 fid。 + +| 参数 | 类型 | 描述 | +| ------------ | ---------------- | ---------------------------------------- | +| `msg.value` | `wei` | 注册支付金额 | +| to | `address` | 接收 fid 的地址 | +| recovery | `address` | 新 fid 的恢复地址 | +| deadline | `uint256` | 签名过期时间戳 | +| sig | `bytes` | 来自 `to` 地址的 EIP-712 `Register` 签名 | +| extraStorage | `uint256` (可选) | 额外存储单元数量 | + +#### Register 签名 + +要代表其他账户注册 fid,必须提供接收地址的 EIP-712 类型签名,格式如下: + +`Register(address to,address recovery,uint256 nonce,uint256 deadline)` + +| 参数 | 类型 | 描述 | +| -------- | --------- | --------------------------------------------- | +| to | `address` | 接收 fid 的地址。类型化消息必须由此地址签名。 | +| recovery | `address` | 新 fid 的恢复地址 | +| nonce | `uint256` | `to` 地址的当前 nonce 值 | +| deadline | `uint256` | 签名过期时间戳 | + +::: code-group + +```ts [@farcaster/hub-web] +import { ViemWalletEip712Signer } from '@farcaster/hub-web'; +import { walletClient, account } from './clients.ts'; +import { readNonce, getDeadline } from './helpers.ts'; + +const nonce = await readNonce(); +const deadline = getDeadline(); + +const eip712Signer = new ViemWalletEip712Signer(walletClient); +const signature = await eip712signer.signRegister({ + to: account, + recovery: '0x00000000FcB080a4D6c39a9354dA9EB9bC104cd7', + nonce, + deadline, +}); +``` + +```ts [Viem] +import { ID_GATEWAY_EIP_712_TYPES } from '@farcaster/hub-web'; +import { walletClient, account } from './clients.ts'; +import { readNonce, getDeadline } from './helpers.ts'; + +const nonce = await readNonce(); +const deadline = getDeadline(); + +const signature = await walletClient.signTypedData({ + account, + ...ID_GATEWAY_EIP_712_TYPES, + primaryType: 'Register', + message: { + to: account, + recovery: '0x00000000FcB080a4D6c39a9354dA9EB9bC104cd7', + nonce, + deadline, + }, +}); +``` + +```ts [helpers.ts] +import { ID_GATEWAY_ADDRESS, idGatewayABI } from '@farcaster/hub-web'; +import { publicClient, account } from './clients.ts'; + +export const getDeadline = () => { + const now = Math.floor(Date.now() / 1000); + const oneHour = 60 * 60; + return now + oneHour; +}; + +export const readNonce = async () => { + return await publicClient.readContract({ + address: ID_GATEWAY_ADDRESS, + abi: idGatewayABI, + functionName: 'nonces', + args: [account], + }); +}; +``` + +<<< @/examples/contracts/clients.ts + +::: + +## 错误类型 + +| 错误类型 | 选择器 | 描述 | +| ---------------- | ---------- | -------------------------------------------------------------- | +| InvalidSignature | `8baa579f` | 提供的签名无效。可能是格式错误,或由错误的地址签署。 | +| SignatureExpired | `0819bdcd` | 提供的签名已过期。请从签名者处获取带有更晚截止时间戳的新签名。 | + +## 源代码 + +[`IdGateway.sol`](https://github.com/farcasterxyz/contracts/blob/1aceebe916de446f69b98ba1745a42f071785730/src/IdGateway.sol) diff --git a/docs/zh/reference/contracts/reference/id-registry.md b/docs/zh/reference/contracts/reference/id-registry.md new file mode 100644 index 0000000..cf9a9b4 --- /dev/null +++ b/docs/zh/reference/contracts/reference/id-registry.md @@ -0,0 +1,412 @@ +--- +outline: [2, 3] +--- + +# ID 注册表 + +ID 注册表记录了哪个 fid 与哪个以太坊地址相关联,并管理 fid 的转移和恢复。 + +如果您想读取 fid 的信息、转移或恢复 fid,或管理 fid 的恢复地址,请使用 ID 注册表。 + +如果您想注册一个新的 fid,请改用 [ID 网关](/zh/reference/contracts/reference/id-gateway.md)。 + +## 读取 + +### idOf + +返回地址拥有的 fid (`uint256`),如果该地址不拥有 fid 则返回零。 + +| 参数 | 类型 | 描述 | +| ----- | --------- | ------------------------- | +| owner | `address` | 要检查是否拥有 fid 的地址 | + +### custodyOf + +返回拥有特定 fid 的托管地址 (`address`)。如果 fid 不存在,则返回零地址。 + +| 参数 | 类型 | 描述 | +| ---- | --------- | ------------------ | +| fid | `uint256` | 要查找所有者的 fid | + +### recoveryOf + +返回 fid 的恢复地址 (`address`)。如果 fid 不存在,则返回零地址。 + +| 参数 | 类型 | 描述 | +| ---- | --------- | -------------------- | +| fid | `uint256` | 要查找恢复地址的 fid | + +### idCounter + +返回迄今为止注册的最高 fid (`uint256`)。 + +### verifyFidSignature + +检查消息是否由 fid 的当前托管地址签名。返回一个 `bool`。 + +| 参数 | 类型 | 描述 | +| -------------- | --------- | ---------------- | +| custodyAddress | `address` | 要检查签名的地址 | +| fid | `uint256` | 与签名关联的 fid | +| digest | `bytes32` | 已签名的哈希数据 | +| sig | `bytes` | 要检查的签名 | + +### nonces + +返回地址的下一个未使用的 nonce (`uint256`)。用于生成 EIP-712 签名。 + +| 参数 | 类型 | 描述 | +| ----- | --------- | ------------------- | +| owner | `address` | 要获取 nonce 的地址 | + +## 写入 + +### register + +如果直接调用将回滚。必须通过 [ID 网关](/zh/reference/contracts/reference/id-gateway.md) 调用。 + +### changeRecoveryAddress + +更改调用者 fid 的恢复地址。 + +| 参数 | 类型 | 描述 | +| -------- | --------- | ------------ | +| recovery | `address` | 新的恢复地址 | + +### transfer + +将调用者的 fid 转移到新地址。`to` 地址必须签署 EIP-712 [`Transfer`](#transfer-signature) 消息以接受转移。`to` 地址不能已拥有 fid。 + +| 参数 | 类型 | 描述 | +| -------- | --------- | --------------------------------------------------------------- | +| to | `address` | 要转移 fid 的地址 | +| deadline | `uint256` | 签名截止时间 | +| sig | `bytes` | 来自 `to` 地址的 EIP-712 [`Transfer`](#transfer-signature) 签名 | + +::: warning +转移 fid 不会重置其恢复地址。要转移 fid 并更新其恢复地址,请调用 [`transferAndChangeRecovery`](#transferandchangerecovery)。如果您从不信任的发送方接收 fid,请确保在转移时清除或更改其恢复地址。 +::: + +#### 转移签名 + +要将 fid 转移到另一个账户,调用者必须提供来自接收地址的 EIP-712 类型签名,格式如下: + +`Transfer(uint256 fid,address to,uint256 nonce,uint256 deadline)` + +| 参数 | 类型 | 描述 | +| -------- | --------- | ---------------------- | +| fid | `uint256` | 正在转移的 fid | +| to | `address` | 接收 fid 的地址 | +| nonce | `uint256` | 签名者地址的当前 nonce | +| deadline | `uint256` | 签名过期时间戳 | + +::: code-group + +```ts [@farcaster/hub-web] +import { ViemWalletEip712Signer } from '@farcaster/hub-web'; +import { walletClient, account } from './clients.ts'; +import { readNonce, getDeadline } from './helpers.ts'; + +const nonce = await readNonce(); +const deadline = getDeadline(); + +const eip712Signer = new ViemWalletEip712Signer(walletClient); +const signature = await eip712signer.signTransfer({ + fid: 1n, + to: account, + nonce, + deadline, +}); +``` + +```ts [Viem] +import { ID_REGISTRY_EIP_712_TYPES } from '@farcaster/hub-web'; +import { walletClient, account } from './clients.ts'; +import { readNonce, getDeadline } from './helpers.ts'; + +const nonce = await readNonce(); +const deadline = getDeadline(); + +const signature = await walletClient.signTypedData({ + account, + ...ID_REGISTRY_EIP_712_TYPES, + primaryType: 'Transfer', + message: { + fid: 1n, + to: account, + nonce, + deadline, + }, +}); +``` + +```ts [helpers.ts] +import { ID_REGISTRY_ADDRESS, idRegistryABI } from '@farcaster/hub-web'; +import { publicClient, account } from './clients.ts'; + +export const getDeadline = () => { + const now = Math.floor(Date.now() / 1000); + const oneHour = 60 * 60; + return now + oneHour; +}; + +export const readNonce = async () => { + return await publicClient.readContract({ + address: ID_REGISTRY_ADDRESS, + abi: idRegistryABI, + functionName: 'nonce', + args: [account], + }); +}; +``` + +<<< @/examples/contracts/clients.ts + +::: + +### transferAndChangeRecovery + +将调用者的 fid 转移到新地址 _并_ 更改 fid 的恢复地址。这可用于安全地从不信任的地址接收 fid 转移。 + +接收地址必须签署 EIP-712 [`TransferAndChangeRecovery`](#transferandchangerecovery-signature) 消息以接受转移。`to` 地址不能已拥有 fid。 + +| 参数 | 类型 | 描述 | +| -------- | --------- | --------------------------------------------------------------------------------------- | +| to | `address` | 要转移 fid 的地址 | +| recovery | `address` | 新的恢复地址 | +| deadline | `uint256` | 签名截止时间 | +| sig | `bytes` | 来自 `to` 地址的 EIP-712 [`TransferAndChangeRecovery`](#transferandchangerecovery) 签名 | + +#### TransferAndChangeRecovery 签名 + +要将 fid 转移到另一个账户并更改恢复地址,您必须提供来自 `to` 地址的 EIP-712 类型签名,格式如下: + +`TransferAndChangeRecovery(uint256 fid,address to,address recovery,uint256 nonce,uint256 deadline)` + +| 参数 | 类型 | 描述 | +| -------- | --------- | ---------------------- | +| fid | `uint256` | 正在转移的 fid | +| to | `address` | 接收 fid 的地址 | +| recovery | `address` | 新的恢复地址 | +| nonce | `uint256` | 签名者地址的当前 nonce | +| deadline | `uint256` | 签名过期时间戳 | + +::: code-group + +```ts [@farcaster/hub-web] +import { ViemWalletEip712Signer } from '@farcaster/hub-web'; +import { walletClient, account } from './clients.ts'; +import { readNonce, getDeadline } from './helpers.ts'; + +const nonce = await readNonce(); +const deadline = getDeadline(); + +const eip712Signer = new ViemWalletEip712Signer(walletClient); +const signature = await eip712signer.signTransferAndChangeRecovery({ + fid: 1n, + to: account, + recovery: '0x00000000FcB080a4D6c39a9354dA9EB9bC104cd7', + nonce, + deadline, +}); +``` + +```ts [Viem] +import { ID_REGISTRY_EIP_712_TYPES } from '@farcaster/hub-web'; +import { walletClient, account } from './clients.ts'; +import { readNonce, getDeadline } from './helpers.ts'; + +const nonce = await readNonce(); +const deadline = getDeadline(); + +const signature = await walletClient.signTypedData({ + account, + ...ID_REGISTRY_EIP_712_TYPES, + primaryType: 'TransferAndChangeRecovery', + message: { + fid: 1n, + to: account, + recovery: '0x00000000FcB080a4D6c39a9354dA9EB9bC104cd7', + nonce, + deadline, + }, +}); +``` + +```ts [helpers.ts] +import { ID_REGISTRY_ADDRESS, idGatewayABI } from '@farcaster/hub-web'; +import { publicClient, account } from './clients.ts'; + +export const getDeadline = () => { + const now = Math.floor(Date.now() / 1000); + const oneHour = 60 * 60; + return now + oneHour; +}; + +export const readNonce = async () => { + return await publicClient.readContract({ + address: ID_REGISTRY_ADDRESS, + abi: idRegistryABI, + functionName: 'nonce', + args: [account], + }); +}; +``` + +<<< @/examples/contracts/clients.ts + +::: + +### recover + +如果调用者是恢复地址,则将 fid 转移到新地址。`to` 地址必须签署 EIP-712 [`Transfer`](#transfer-signature) 消息以接受转移。 + +`to` 地址不能已拥有 fid。 + +| 参数 | 类型 | 描述 | +| -------- | --------- | --------------------------------------------------------------- | +| from | `address` | 要从中转移 fid 的地址 | +| to | `address` | 要转移 fid 的地址 | +| deadline | `uint256` | 签名截止时间 | +| sig | `bytes` | 来自 `to` 地址的 EIP-712 [`Transfer`](#transfer-signature) 签名 | + +### changeRecoveryAddressFor + +通过提供签名代表所有者更改 fid 的恢复地址。所有者必须签署 EIP-712 `ChangeRecoveryAddress` 签名以批准更改。 + +| 参数 | 类型 | 描述 | +| -------- | --------- | ---------------------------------------------------------------------------- | +| owner | `address` | fid 所有者的地址 | +| recovery | `address` | 新的恢复地址 | +| deadline | `uint256` | 签名截止时间 | +| sig | `bytes` | 来自 `to` 地址的 EIP-712 [`ChangeRecoveryAddress`](#transfer-signature) 签名 | + +### ChangeRecoveryAddress 签名 + +要代表 fid 所有者更改恢复地址,调用者必须提供来自 `owner` 地址的 EIP-712 类型签名,格式如下: + +`ChangeRecoveryAddress(uint256 fid,address from,address to,uint256 nonce,uint256 deadline)` + +| 参数 | 类型 | 描述 | +| -------- | --------- | ---------------------- | +| fid | `uint256` | 所有者的 fid | +| from | `address` | 先前的恢复地址 | +| to | `address` | 新的恢复地址 | +| nonce | `uint256` | 签名者地址的当前 nonce | +| deadline | `uint256` | 签名过期时间戳 | + +::: code-group + +```ts [@farcaster/hub-web] +import { ViemWalletEip712Signer } from '@farcaster/hub-web'; +import { walletClient, account } from './clients.ts'; +import { readNonce, getDeadline } from './helpers.ts'; + +const nonce = await readNonce(); +const deadline = getDeadline(); + +const eip712Signer = new ViemWalletEip712Signer(walletClient); +const signature = await eip712signer.signChangeRecoveryAddress({ + fid: 1n, + from: '0x00000000FcB080a4D6c39a9354dA9EB9bC104cd7', + to: '0xD7029BDEa1c17493893AAfE29AAD69EF892B8ff2', + nonce, + deadline, +}); +``` + +```ts [Viem] +import { ID_REGISTRY_EIP_712_TYPES } from '@farcaster/hub-web'; +import { walletClient, account } from './clients.ts'; +import { readNonce, getDeadline } from './helpers.ts'; + +const nonce = await readNonce(); +const deadline = getDeadline(); + +const signature = await walletClient.signTypedData({ + account, + ...ID_REGISTRY_EIP_712_TYPES, + primaryType: 'ChangeRecoveryAddress', + message: { + fid: 1n, + from: '0x00000000FcB080a4D6c39a9354dA9EB9bC104cd7', + to: '0xD7029BDEa1c17493893AAfE29AAD69EF892B8ff2', + nonce, + deadline, + }, +}); +``` + +```ts [helpers.ts] +import { ID_REGISTRY_ADDRESS, idGatewayABI } from '@farcaster/hub-web'; +import { publicClient, account } from './clients.ts'; + +export const getDeadline = () => { + const now = Math.floor(Date.now() / 1000); + const oneHour = 60 * 60; + return now + oneHour; +}; + +export const readNonce = async () => { + return await publicClient.readContract({ + address: ID_REGISTRY_ADDRESS, + abi: idRegistryABI, + functionName: 'nonces', + args: [account], + }); +}; +``` + +<<< @/examples/contracts/clients.ts + +::: + +### transferFor + +将 `from` 地址拥有的 fid 转移到 `to` 地址。调用者必须提供两个 EIP-712 [`Transfer`](#transfer-signature) 签名:一个来自 `from` 地址授权转出,一个来自 `to` 地址接受转入。这些消息具有 [相同格式](#transfer-signature)。`to` 地址不能已拥有 fid。 + +| 参数 | 类型 | 描述 | +| ------------ | --------- | ----------------------------------------------------------------- | +| from | `address` | 要从中转移 fid 的地址 | +| to | `address` | 要转移 fid 的地址 | +| fromDeadline | `uint256` | 签名截止时间 | +| fromSig | `bytes` | 来自 `from` 地址的 EIP-712 [`Transfer`](#transfer-signature) 签名 | +| toDeadline | `uint256` | 签名截止时间 | +| toSig | `bytes` | 来自 `to` 地址的 EIP-712 [`Transfer`](#transfer-signature) 签名 | + +### transferAndChangeRecoveryFor + +将 `from` 地址拥有的 fid 转移到 `to` 地址,并更改 fid 的恢复地址。这可用于安全地从不信任的地址接收 fid 转移。 + +调用者必须提供两个 EIP-712 [`TransferAndChangeRecovery`](#transferandchangerecovery-signature) 签名:一个来自 `from` 地址授权转出,一个来自 `to` 地址接受转入。这些消息具有 [相同格式](#transferandchangerecovery-signature)。`to` 地址不能已拥有 fid。 + +| 参数 | 类型 | 描述 | +| ------------ | --------- | --------------------------------------------------------------------------------------------------- | +| from | `address` | 要从中转移 fid 的地址 | +| to | `address` | 要转移 fid 的地址 | +| recovery | `address` | 新的恢复地址 | +| fromDeadline | `uint256` | 签名截止时间 | +| fromSig | `bytes` | 来自 `from` 地址的 EIP-712 [`TransferAndChangeRecovery`](#transferandchangerecovery-signature) 签名 | +| toDeadline | `uint256` | 签名截止时间 | +| toSig | `bytes` | 来自 `to` 地址的 EIP-712 [`TransferAndChangeRecovery`](#transferandchangerecovery-signature) 签名 | + +### recoverFor + +使用来自 fid 恢复地址的签名将 fid 转移到新地址。调用者必须提供两个 EIP-712 [`Transfer`](#transfer-signature) 签名:一个来自恢复地址授权转出,一个来自 `to` 地址接受转入。这些消息具有 [相同格式](#transfer-signature)。 + +`to` 地址不能已拥有 fid。 + +| 参数 | 类型 | 描述 | +| ---------------- | --------- | --------------------------------------------------------------- | +| from | `address` | 要从中转移 fid 的地址 | +| to | `address` | 要转移 fid 的地址 | +| recoveryDeadline | `uint256` | 恢复签名的截止时间 | +| recoverySig | `bytes` | 来自恢复地址的 EIP-712 [`Transfer`](#transfer-signature) 签名 | +| toDeadline | `uint256` | 接收者签名的截止时间 | +| toSig | `bytes` | 来自 `to` 地址的 EIP-712 [`Transfer`](#transfer-signature) 签名 | + +## 错误 + +| 错误 | Selector | 描述 | +| ----- | ---------- | --------- | +| HasId | `f90230a9` | `to` 地址 | diff --git a/docs/zh/reference/contracts/reference/key-gateway.md b/docs/zh/reference/contracts/reference/key-gateway.md new file mode 100644 index 0000000..860540e --- /dev/null +++ b/docs/zh/reference/contracts/reference/key-gateway.md @@ -0,0 +1,156 @@ +--- +outline: [2, 3] +--- + +# 密钥网关 + +密钥网关用于向[密钥注册表](/zh/reference/contracts/reference/key-registry.md)添加新密钥。 + +如需为 Farcaster 账户添加新的公钥,请使用密钥网关。 + +## 读取操作 + +### nonces + +返回指定地址的下一个可用 nonce 值。用于在 [addFor](#addFor) 操作中生成 EIP-712 签名。 + +| 参数 | 类型 | 描述 | +| ----- | --------- | ----------------------- | +| owner | `address` | 需要查询 nonce 值的地址 | + +## 写入操作 + +### add + +为调用者的 fid 添加新密钥,并将其状态设为 `Added`。如果密钥已注册则回滚操作。 + +| 参数 | 类型 | 描述 | +| ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| keyType | `uint32` | 必须设为 `1`。这是当前唯一支持的 `keyType` 值。 | +| key | `bytes` | 待添加的公钥 | +| metadataType | `uint8` | 必须设为 `1`。这是当前唯一支持的 `metadataType` 值。 | +| metadata | `bytes` | 编码后的 [`SignedKeyRequestMetadata`](/zh/reference/contracts/reference/signed-key-request-validator#signedkeyrequestmetadata-struct) | + +### addFor + +通过提供签名代表其他 fid 添加密钥。所有者必须签署 EIP-712 `Add` 消息来批准该密钥。如果密钥已注册则回滚操作。 + +| 参数 | 类型 | 描述 | +| ------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| fidOwner | `address` | fid 所有者的地址 | +| keyType | `uint32` | 必须设为 `1`。这是当前唯一支持的 `keyType` 值。 | +| key | `bytes` | 待添加的公钥 | +| metadataType | `uint8` | 必须设为 `1`。这是当前唯一支持的 `metadataType` 值。 | +| metadata | `bytes` | 编码后的 [`SignedKeyRequestMetadata`](/zh/reference/contracts/reference/signed-key-request-validator#signedkeyrequestmetadata-struct) | +| deadline | `uint256` | 签名过期时间戳 | +| sig | `bytes` | 来自 `fidOwner` 的 EIP-712 [`Add`](/zh/reference/contracts/reference/key-gateway#add-signature) 签名 | + +#### Add 签名 + +要代表其他账户添加密钥,必须提供符合以下格式的 EIP-712 类型签名: + +`Add(address owner,uint32 keyType,bytes key,uint8 metadataType,bytes metadata,uint256 nonce,uint256 deadline)` + +| 参数 | 类型 | 描述 | +| ------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| owner | `address` | 拥有 fid 的地址。该类型化消息必须由此地址签名。 | +| keyType | `uint32` | 必须设为 `1`。这是当前唯一支持的 `keyType` 值。 | +| key | `bytes` | 待添加的公钥 | +| metadataType | `uint8` | 必须设为 `1`。这是当前唯一支持的 `metadataType` 值。 | +| metadata | `bytes` | 编码后的 [`SignedKeyRequestMetadata`](/zh/reference/contracts/reference/signed-key-request-validator#signedkeyrequestmetadata-struct) | +| nonce | `uint256` | `owner` 地址的当前 nonce 值 | +| deadline | `uint256` | 签名过期时间戳 | + +::: code-group + +```ts [@farcaster/hub-web] +import { ViemWalletEip712Signer } from '@farcaster/hub-web'; +import { walletClient, account } from './clients.ts'; +import { getPublicKey } from './signer.ts'; +import { readNonce, getDeadline } from './helpers.ts'; + +const publicKey = await getPublicKey(); +const metadata = await getMetadata(); +const nonce = await readNonce(); +const deadline = getDeadline(); + +const eip712Signer = new ViemWalletEip712Signer(walletClient); +const signature = await eip712signer.signAdd({ + owner: account, + keyType: 1, + key: publicKey, + metadataType: 1, + metadata, + nonce, + deadline, +}); +``` + +```ts [Viem] +import { KEY_GATEWAY_EIP_712_TYPES } from '@farcaster/hub-web'; +import { bytesToHex } from 'viem'; +import { walletClient, account } from './clients.ts'; +import { getPublicKey } from './signer.ts'; +import { getMetadata } from './metadata.ts'; +import { readNonce, getDeadline } from './helpers.ts'; + +const publicKey = await getPublicKey(); +const metadata = await getMetadata(); +const nonce = await readNonce(); +const deadline = getDeadline(); + +const signature = await walletClient.signTypedData({ + account, + ...KEY_GATEWAY_EIP_712_TYPES, + primaryType: 'Add', + message: { + owner: account, + keyType: 1, + key: bytesToHex(publicKey), + metadataType: 1, + metadata, + nonce, + deadline, + }, +}); +``` + +```ts [helpers.ts] +import { KEY_GATEWAY_ADDRESS, keyGatewayABI } from '@farcaster/hub-web'; +import { publicClient, account } from './clients.ts'; + +export const getDeadline = () => { + const now = Math.floor(Date.now() / 1000); + const oneHour = 60 * 60; + return now + oneHour; +}; + +export const readNonce = async () => { + return await publicClient.readContract({ + address: KEY_GATEWAY_ADDRESS, + abi: keyGatewayABI, + functionName: 'nonces', + args: [account], + }); +}; +``` + +<<< @/examples/contracts/metadata.ts + +<<< @/examples/contracts/signer.ts + +<<< @/examples/contracts/clients.ts + +::: + +## 错误类型 + +| 错误类型 | 选择器 | 描述 | +| ---------------- | ---------- | ------------------------------------------------------------ | +| InvalidMetadata | `bcecb64a` | 提供的签名元数据无效。 | +| InvalidSignature | `8baa579f` | 提供的签名无效。可能是格式错误,或由错误的地址签署。 | +| SignatureExpired | `0819bdcd` | 提供的签名已过期。请向签署者获取带有更晚截止时间戳的新签名。 | + +## 源代码 + +[`KeyGateway.sol`](https://github.com/farcasterxyz/contracts/blob/1aceebe916de446f69b98ba1745a42f071785730/src/KeyGateway.sol) diff --git a/docs/zh/reference/contracts/reference/key-registry.md b/docs/zh/reference/contracts/reference/key-registry.md new file mode 100644 index 0000000..37a24d8 --- /dev/null +++ b/docs/zh/reference/contracts/reference/key-registry.md @@ -0,0 +1,176 @@ +--- +outline: [2, 3] +--- + +# 密钥注册表 + +密钥注册表存储与每个 Farcaster 账户关联的公钥。 + +如需读取 Farcaster 账户密钥信息或移除现有密钥,请使用密钥注册表。 + +如需添加新密钥,请改用 [密钥网关](/zh/reference/contracts/reference/key-gateway.md)。 + +## 读取 + +### totalKeys + +获取指定 fid 的活跃密钥数量 (`uint256`)。 + +| 参数 | 类型 | 描述 | +| ----- | ------------------------------------ | -------------------------- | +| fid | `uint256` | 要查询的 fid | +| state | `uint8` (1 表示已添加,2 表示已移除) | 密钥状态(已添加或已移除) | + +### keysOf + +列出指定 fid 的所有公钥 (`bytes[]`)。 + +| 参数 | 类型 | 描述 | +| --------- | ------------------------------------ | -------------------------- | +| fid | `uint256` | 要查询的 fid | +| state | `uint8` (1 表示已添加,2 表示已移除) | 密钥状态(已添加或已移除) | +| startIdx | `uint256` (可选) | 分页起始索引 | +| batchSize | `uint256` (可选) | 分页批量大小 | + +::: warning +请勿在链上调用!此函数 gas 消耗极高,仅适用于链下工具调用,不适用于其他合约调用。 +::: + +### keyDataOf + +返回指定 fid 特定密钥的状态 (`uint8`) 和 keyType (`uint32`)。 + +| 参数 | 类型 | 描述 | +| ---- | --------- | ------------ | +| fid | `uint256` | 要查询的 fid | +| key | `bytes` | 要检查的公钥 | + +## 写入 + +### add + +直接调用将回滚。必须通过 [密钥网关](/zh/reference/contracts/reference/key-gateway.md) 调用。 + +### remove + +从调用者的 fid 中移除公钥并将其标记为 `已移除`。 + +| 参数 | 类型 | 描述 | +| ---- | ------- | ------------ | +| key | `bytes` | 要移除的公钥 | + +::: warning +移除密钥将删除 Hubs 中与该密钥关联的所有链下消息。 +::: + +### removeFor + +通过提供签名代表其他 fid 移除密钥。fid 所有者必须签署 EIP-712 `Remove` 消息以授权移除。如果密钥不存在或已被移除,则回滚。 + +| 参数 | 类型 | 描述 | +| -------- | --------- | ------------------------------- | +| fidOwner | `address` | fid 所有者地址 | +| key | `bytes` | 要移除的公钥 | +| deadline | `uint256` | 签名截止时间 | +| sig | `bytes` | 来自 `fidOwner` 的 EIP-712 签名 | + +::: warning +移除密钥将删除 Hubs 中与该密钥关联的所有链下消息。 +::: + +#### 移除签名 + +要代表其他账户移除密钥,必须提供符合以下格式的 EIP-712 类型签名: + +`Remove(address owner,bytes key,uint256 nonce,uint256 deadline)` + +| 参数 | 类型 | 描述 | +| -------- | --------- | --------------------------------------------- | +| owner | `address` | 拥有 fid 的地址。类型化消息必须由此地址签名。 | +| key | `bytes` | 要移除的公钥 | +| nonce | `uint256` | `owner` 地址的当前 nonce 值 | +| deadline | `uint256` | 签名过期时间戳 | + +::: code-group + +```ts [@farcaster/hub-web] +import { ViemWalletEip712Signer } from '@farcaster/hub-web'; +import { walletClient, account } from './clients.ts'; +import { getPublicKey } from './signer.ts'; +import { readNonce, getDeadline } from './helpers.ts'; + +const publicKey = await getPublicKey(); +const nonce = await readNonce(); +const deadline = getDeadline(); + +const eip712Signer = new ViemWalletEip712Signer(walletClient); +const signature = await eip712signer.signRemove({ + owner: account, + key: publicKey, + nonce, + deadline, +}); +``` + +```ts [Viem] +import { KEY_REGISTRY_EIP_712_TYPES } from '@farcaster/hub-web'; +import { bytesToHex } from 'viem'; +import { walletClient, account } from './clients.ts'; +import { getPublicKey } from './signer.ts'; +import { readNonce, getDeadline } from './helpers.ts'; + +const publicKey = await getPublicKey(); +const nonce = await readNonce(); +const deadline = getDeadline(); + +const signature = await walletClient.signTypedData({ + account, + ...KEY_REGISTRY_EIP_712_TYPES, + primaryType: 'Remove', + message: { + owner: account, + key: bytesToHex(publicKey), + nonce, + deadline, + }, +}); +``` + +```ts [helpers.ts] +import { KEY_REGISTRY_ADDRESS, keyRegistryABI } from '@farcaster/hub-web'; +import { publicClient, account } from './clients.ts'; + +export const getDeadline = () => { + const now = Math.floor(Date.now() / 1000); + const oneHour = 60 * 60; + return now + oneHour; +}; + +export const readNonce = async () => { + return await publicClient.readContract({ + address: KEY_REGISTRY_ADDRESS, + abi: keyRegistryABI, + functionName: 'nonces', + args: [account], + }); +}; +``` + +<<< @/examples/contracts/signer.ts + +<<< @/examples/contracts/clients.ts + +::: + +## 错误 + +| 错误类型 | 选择器 | 描述 | +| ---------------- | ---------- | -------------------------------------------------------------------------------- | +| ExceedsMaximum | `29264042` | 添加密钥超出每个 fid 允许的最大密钥数(当前为 1000) | +| InvalidSignature | `8baa579f` | 提供的签名无效。可能格式错误或由错误地址签名。 | +| InvalidState | `baf3f0f7` | 操作违反状态转换规则。(添加已存在的密钥、移除不存在的密钥、添加已被移除的密钥) | +| SignatureExpired | `0819bdcd` | 提供的签名已过期。请从签名者处获取带有更晚截止时间戳的新签名。 | + +## 源代码 + +[`KeyRegistry.sol`](https://github.com/farcasterxyz/contracts/blob/1aceebe916de446f69b98ba1745a42f071785730/src/KeyRegistry.sol) diff --git a/docs/zh/reference/contracts/reference/signed-key-request-validator.md b/docs/zh/reference/contracts/reference/signed-key-request-validator.md new file mode 100644 index 0000000..6f4469f --- /dev/null +++ b/docs/zh/reference/contracts/reference/signed-key-request-validator.md @@ -0,0 +1,221 @@ +--- +outline: [2, 3] +--- + +# 签名密钥请求验证器 + +签名密钥请求验证器用于验证与密钥关联的签名元数据。密钥注册表在添加密钥前会调用该验证器,以检查提供的签名密钥请求是否有效。 + +如需构造或校验签名密钥请求,请使用签名密钥请求验证器。 + +::: info 什么是签名密钥请求? + +当用户向账户(主 fid)添加密钥时,必须包含请求者(请求 fid)的签名。这使得任何人都能识别特定密钥的请求者。 + +通常情况下,主 fid 是终端用户,而请求 fid 是用户希望连接的应用程序。 +::: + +## 读取 + +### encodeMetadata + +将 [`SignedKeyRequestMetadata`](#signedkeyrequestmetadata-结构体) 结构体转换为 `bytes`,以便传入 [add](/zh/reference/contracts/reference/key-gateway#add)、[register](/zh/reference/contracts/reference/bundler#register) 等合约函数。 + +| 参数 | 类型 | 描述 | +| -------- | -------------------------- | -------------------------------------------- | +| metadata | `SignedKeyRequestMetadata` | 需要编码的 `SignedKeyRequestMetadata` 结构体 | + +#### SignedKeyRequestMetadata 结构体 + +`SignedKeyRequestMetadata` 结构体包含验证签名密钥请求真实性所需的数据:请求 fid、请求 fid 所有者以及 EIP-712 [`SignedKeyRequest`](#signedkeyrequest-签名) 签名。 + +| 参数 | 类型 | 描述 | +| ------------- | --------- | ------------------------------------------------------------------------------------- | +| requestFid | `uint256` | 请求 fid | +| requestSigner | `address` | 请求 fid 的所有者地址 | +| signature | `bytes` | 来自 `requestSigner` 地址的 EIP-712 [`SignedKeyRequest`](#signedkeyrequest-签名) 签名 | +| deadline | `uint256` | 签名的过期时间戳 | + +以下代码示例展示了两种生成编码 `SignedKeyRequestMetadata` 的方法——使用 `@farcaster/hub-web` 库一步完成签名和编码,或使用 Viem 分别进行签名和编码。 + +::: code-group + +```ts [@farcaster/hub-web] +import { ViemLocalEip712Signer } from '@farcaster/hub-web'; +import { privateKeyToAccount } from 'viem/accounts'; +import { getDeadline } from './helpers.ts'; +import { getPublicKey } from './signer.ts'; + +export const appAccount = privateKeyToAccount('0x...'); + +const key = getPublicKey(); +const deadline = getDeadline(); + +// getSignedKeyRequestMetadata 辅助函数会生成 SignedKeyRequest +// 签名并返回 ABI 编码的 SignedKeyRequest 元数据结构体。 +const eip712Signer = new ViemLocalEip712Signer(appAccount); +const encodedData = await eip712Signer.getSignedKeyRequestMetadata({ + requestFid: 9152n, + key, + deadline, +}); +``` + +```ts [Viem] +import { bytesToHex, encodeAbiParameters } from 'viem'; +import { signature } from './signature.ts'; +import { getDeadline } from './helpers.ts'; + +const deadline = getDeadline(); + +// 分别收集签名和编码 SignedKeyRequest 元数据的示例。 +const encodedData = encodeAbiParameters( + [ + { + components: [ + { + name: 'requestFid', + type: 'uint256', + }, + { + name: 'requestSigner', + type: 'address', + }, + { + name: 'signature', + type: 'bytes', + }, + { + name: 'deadline', + type: 'uint256', + }, + ], + type: 'tuple', + }, + ], + [ + { + requestFid: 9152n, + requestSigner: '0x02ef790dd7993a35fd847c053eddae940d055596', + bytesToHex(signature), + deadline, + }, + ] +); +``` + +```ts [signature.ts] +import { ViemLocalEip712Signer } from '@farcaster/hub-web'; +import { privateKeyToAccount } from 'viem/accounts'; +import { getDeadline } from './helpers.ts'; +import { getPublicKey } from './signer.ts'; + +export const appAccount = privateKeyToAccount('0x...'); + +const key = getPublicKey(); +const deadline = getDeadline(); + +const eip712Signer = new ViemLocalEip712Signer(appAccount); +const signature = await eip712Signer.signKeyRequest({ + requestFid: 9152n, + key, + deadline, +}); +``` + +```ts [helpers.ts] +export const getDeadline = () => { + const now = Math.floor(Date.now() / 1000); + const oneHour = 60 * 60; + return now + oneHour; +}; +``` + +<<< @/examples/contracts/signer.ts + +::: + +### validate + +验证编码后的 [`SignedKeyRequestMetadata`](#signedkeyrequestmetadata-结构体)。密钥注册表在用户添加密钥时会内部调用此函数来验证签名密钥请求。如果您代表用户创建密钥,可以自行调用此函数来验证应用程序创建的签名密钥请求。 + +| 参数 | 类型 | 描述 | +| ---- | --------- | ---------------------------------------------------- | +| fid | `uint256` | 将与密钥关联的主 fid | +| key | `bytes` | 需要验证的公钥字节 | +| sig | `bytes` | 请求密钥的实体提供的 EIP-712 `SignedKeyRequest` 签名 | + +#### SignedKeyRequest 签名 + +`SignedKeyRequest` 消息是由请求 fid 所有者按照以下格式签名的 EIP-712 类型签名: + +`SignedKeyRequest(uint256 requestFid,bytes key,uint256 deadline)` + +::: info 为什么要签名元数据? +`SignedKeyRequest` 签名证明请求 fid 要求主 fid 授权密钥对。例如,当应用程序要求用户添加新密钥时,应用程序会创建签名密钥请求以证明其发起了请求,密钥注册表会在链上事件中发出该请求。这使得任何人都能将签名者归因于特定的请求者,这对于从了解正在使用的应用程序到基于生成内容的应用程序过滤内容等各种用途都非常有用。 +::: + +| 参数 | 类型 | 描述 | +| ---------- | --------- | ---------------- | +| requestFid | `uint256` | 实体的 fid | +| key | `bytes` | 公钥字节 | +| deadline | `uint256` | 签名的过期时间戳 | + +::: code-group + +```ts [@farcaster/hub-web] +import { ViemLocalEip712Signer } from '@farcaster/hub-web'; +import { privateKeyToAccount } from 'viem/accounts'; +import { getDeadline } from './helpers.ts'; +import { getPublicKey } from './signer.ts'; + +export const appAccount = privateKeyToAccount('0x...'); + +const key = getPublicKey(); +const deadline = getDeadline(); + +const eip712Signer = new ViemLocalEip712Signer(appAccount); +const signature = await eip712Signer.signKeyRequest({ + requestFid: 9152n, + key, + deadline, +}); +``` + +```ts [Viem] +import { SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_TYPES } from '@farcaster/hub-web'; +import { bytesToHex, privateKeyToAccount } from 'viem/accounts'; +import { getDeadline } from './helpers.ts'; +import { getPublicKey } from './signer.ts'; + +export const appAccount = privateKeyToAccount('0x...'); + +const key = getPublicKey(); +const deadline = getDeadline(); + +const signature = await appAccount.signTypedData({ + ...SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_TYPES, + primaryType: 'SignedKeyRequest', + message: { + requestFid: 9152n, + key: bytesToHex(key), + deadline, + }, +}); +``` + +```ts [helpers.ts] +export const getDeadline = () => { + const now = Math.floor(Date.now() / 1000); + const oneHour = 60 * 60; + return now + oneHour; +}; +``` + +<<< @/examples/contracts/signer.ts + +::: + +## 源代码 + +[`SignedKeyRequestValidator.sol`](https://github.com/farcasterxyz/contracts/blob/1aceebe916de446f69b98ba1745a42f071785730/src/validators/SignedKeyRequestValidator.sol) diff --git a/docs/zh/reference/contracts/reference/storage-registry.md b/docs/zh/reference/contracts/reference/storage-registry.md new file mode 100644 index 0000000..3f79b07 --- /dev/null +++ b/docs/zh/reference/contracts/reference/storage-registry.md @@ -0,0 +1,56 @@ +--- +outline: [2, 3] +--- + +# 存储注册表 + +存储注册表允许 Farcaster 账户在网络中租用一个或多个存储"单元"。 + +如果你想为 Farcaster 账户租用存储空间,请使用存储注册表。 + +## 读取 + +### unitPrice + +获取注册 1 个存储单元所需的价格(以 wei 为单位,`uint256` 类型)。 + +### price + +获取注册指定数量存储单元所需的价格(以 wei 为单位,`uint256` 类型)。 + +| 参数名称 | 类型 | 描述 | +| -------- | --------- | ---------------------- | +| units | `uint256` | 需要租用的存储单元数量 | + +## 写入 + +### rent + +为指定的 fid 租用特定数量的存储单元。多余的以太币将退还给调用者。租用的存储单元自注册之日起 1 年内有效。 + +| 参数名称 | 类型 | 描述 | +| ----------- | --------- | ---------------------- | +| `msg.value` | `wei` | 支付金额 | +| fid | `uint256` | 需要分配存储单元的 fid | +| units | `uint256` | 需要租用的存储单元数量 | + +### batchRent + +在一次交易中为多个 fid 租用存储空间。调用者必须发送足够的以太币以支付所有存储单元的总费用。与单次租用类似,多余的以太币将被退还,且存储单元有效期为 1 年。 + +| 参数名称 | 类型 | 描述 | +| ----------- | ----------- | ----------------------------------------------------- | +| `msg.value` | `wei` | 总支付金额 | +| fids | `uint256[]` | fid 数组 | +| units | `uint256[]` | 存储单元数量数组,与 `fids` 数组中的每个 fid 一一对应 | + +## 错误 + +| 错误类型 | 选择器 | 描述 | +| ----------------- | ---------- | ------------------------------------------------ | +| InvalidPayment | `3c6b4b28` | 调用者提供的以太币不足以支付请求的存储单元数量。 | +| InvalidBatchInput | `0a514b99` | 调用者提供的 `fids` 和 `units` 数组长度不匹配。 | + +## 源代码 + +[`StorageRegistry.sol`](https://github.com/farcasterxyz/contracts/blob/1aceebe916de446f69b98ba1745a42f071785730/src/validators/StorageRegistry.sol) diff --git a/docs/zh/reference/fname/api.md b/docs/zh/reference/fname/api.md new file mode 100644 index 0000000..3eb7a5c --- /dev/null +++ b/docs/zh/reference/fname/api.md @@ -0,0 +1,110 @@ +# FName 注册服务器 API 参考文档 + +[Fname 注册服务](https://github.com/farcasterxyz/fname-registry) 部署在 https://fnames.farcaster.xyz + +这是一个简单的 HTTP 服务,负责分配和追踪 fnames。所有 Fname 变更都以转移记录的形式保存。 +注册 fname 是从 FID 0 转移到用户 fid 的过程。转移 fname 是从用户 fid 转移到另一个 fid 的过程。注销 fname 是从用户 fid 转移回 fid 0 的过程。 + +::: warning 注册 fname 注意事项 + +请注意,注册新 fname 时,仅调用此 API 并不足够。这只会将名称预留到你的 fid 下。你还需要向 hub 提交 [UserDataAdd](/zh/reference/hubble/datatypes/messages#_2-userdata) 消息才能将此名称设为你的用户名。 + +::: + +### 获取转移历史记录 + +要获取所有转移记录,向 `/transfers` 发起 GET 请求: + +```bash +curl https://fnames.farcaster.xyz/transfers | jq +``` + +支持以下查询参数: + +- `from_id` - 分页起始的转移记录 ID +- `name` - 筛选特定 fname +- `fid` - 筛选涉及特定 fid(来源或目标)的记录 +- `from_ts` - 分页起始时间戳(秒) + +### 获取当前 fname 或 fid + +要获取 fid 或 fname 的最新转移事件,向 `/transfers/current` 发起 GET 请求 + +例如:要查询 `@farcaster` 对应的 fid,执行以下调用并读取返回值中 `to` 字段: + +```bash +curl https://fnames.farcaster.xyz/transfers?name=farcaster | jq +``` + +要查询 fid `1` 对应的 fname,执行以下调用并读取返回值中 `username` 字段: + +```bash +curl https://fnames.farcaster.xyz/transfers?fid=1 | jq +``` + +两者都会返回相同的转移对象: + +```json +{ + "transfers": [ + { + "id": 1, + "timestamp": 1628882891, + "username": "farcaster", + "owner": "0x8773442740c17c9d0f0b87022c722f9a136206ed", + "from": 0, + "to": 1, + "user_signature": "0xa6fdd2a69deab5633636f32a30a54b21b27dff123e6481532746eadca18cd84048488a98ca4aaf90f4d29b7e181c4540b360ba0721b928e50ffcd495734ef8471b", + "server_signature": "0xb7181760f14eda0028e0b647ff15f45235526ced3b4ae07fcce06141b73d32960d3253776e62f761363fb8137087192047763f4af838950a96f3885f3c2289c41b" + } + ] +} +``` + +### 注册或转移 fname + +要注册新 fid(例如 `hubble`),首先确认该 fname 未被注册。 + +然后向 `/transfers` 发起 POST 请求,包含以下请求体: + +```yaml +{ + "name": "hubble", // 待注册名称 + "from": 0, // 来源 fid(新注册时为 0) + "to": 123, // 目标 fid(注销时为 0) + "fid": 123, // 发起请求的 fid(必须匹配 from 或 to) + "owner": "0x...", // 发起请求 fid 的托管地址 + "timestamp": 1641234567, // 当前时间戳(秒) + "signature": "0x..." // 由 fid 托管地址签名的 EIP-712 签名 +} +``` + +生成 EIP-712 签名的代码示例: + +```js +import { makeUserNameProofClaim, EIP712Signer } from '@farcaster/hub-nodejs'; + +const accountKey: EIP712Signer = undefined; // 托管地址的账户密钥(使用 hub-nodejs 中适用于 ethers 或 viem 的相应子类) + +const claim = makeUserNameProofClaim({ + name: 'hubble', + owner: '0x...', + timestamp: Math.floor(Date.now() / 1000), +}); +const signature = ( + await accountKey.signUserNameProofClaim(claim) +)._unsafeUnwrap(); +``` + +这与向 hub 提供的 ENS UsernameProofs 中使用的签名类型完全相同。 + +调用示例: + +```bash +curl -X POST https://fnames.farcaster.xyz/transfers \ + -H "Content-Type: application/json" \ + -d \ +'{"name": "hubble", "owner": "0x...", "signature": "0x...", "from": 0, "to": 1000, "timestamp": 1641234567, fid: 1000}' +``` + +名称注册后,仍需向 hub 发送 [UserData](/zh/reference/hubble/datatypes/messages#_2-userdata) 消息才能实际设置用户名称。示例代码参见 [hub-nodejs](https://github.com/farcasterxyz/hub-monorepo/tree/main/packages/hub-nodejs/examples/hello-world) 仓库。 diff --git a/docs/zh/reference/frames-redirect.md b/docs/zh/reference/frames-redirect.md new file mode 100644 index 0000000..8401a11 --- /dev/null +++ b/docs/zh/reference/frames-redirect.md @@ -0,0 +1,13 @@ +--- +title: Frames v2 已更名为 Mini Apps +--- + +# Frames v2 已更名为 Mini Apps + + +自 2025 年初起,Frames v2 已更名为 Mini Apps。所有 Frames 文档和资源均已迁移至 [Mini Apps](https://miniapps.farcaster.xyz/){target="_self"} 生态系统。 + +Frames v2 已被弃用,其支持将持续至 2025 年 3 月底。我们强烈建议所有新项目使用 Mini Apps。 + + +[前往 Mini Apps 文档 →](https://miniapps.farcaster.xyz/){target="_self"} diff --git a/docs/zh/reference/hubble/architecture.md b/docs/zh/reference/hubble/architecture.md new file mode 100644 index 0000000..891caaf --- /dev/null +++ b/docs/zh/reference/hubble/architecture.md @@ -0,0 +1,28 @@ +# 架构 + +Hub 是一个单进程守护程序,负责接收来自客户端、其他 hub 和 farcaster 合约的数据。它包含四个主要组件: + +- **存储引擎** - 校验消息有效性,将其持久化存储到磁盘并触发事件。 +- **P2P 引擎** - 建立 gossipsub 网络以与其他 hub 交换消息。 +- **同步引擎** - 处理当 gossip 未能传递消息时的边缘情况。 +- **RPC 服务器** - 提供读写 hub 数据的 API 接口。 + +### 存储引擎 + +Hubble 接收到的消息会被转发至存储引擎,随后被分发到对应的 CRDT 集合。经过 CRDT 集合验证后,消息将被持久化存储至 [RocksDB](https://github.com/facebook/rocksdb),同时向监听器发送事件通知。 + +CRDT 集合的实现严格遵循 Farcaster 协议规范。该引擎还会追踪 Farcaster 合约的状态,这对于验证 Signer CRDT 集合至关重要。 + +### P2P 引擎 + +Hubble 通过基于 [LibP2P](https://github.com/libp2p/libp2p) 构建的 GossipSub 网络与其他节点建立连接。被存储引擎合并的消息会立即通过 gossip 协议广播给所有对等节点。 + +在测试阶段,Hubble 仅与可信节点建立连接,并采用简单的网络拓扑结构。启动时必须预先配置已知实例才能建立对等连接。在后续版本中,网络拓扑将进行调整以更接近去信任的网状结构。 + +### 同步引擎 + +Hubble 会定期与其他节点执行 [差异同步](https://github.com/farcasterxyz/protocol#41-synchronization),以发现可能在 gossip 过程中丢失的消息。该功能通过每个 Hub 实例暴露的 gRPC API 实现。 + +### RPC 服务器 + +Hubble 提供 gRPC 和 HTTP API 用于读写 hub 数据。向 farcaster 网络写入数据的主要方式是调用 hub 的 `submitMessage` 接口。hub 会验证消息的有效性,若符合协议规范,则将其存储到对应的 CRDT 集合并通过 gossip 协议传播给其他节点。hub 还提供其他 API 用于读取各集合的状态信息。 diff --git a/docs/zh/reference/hubble/datatypes/events.md b/docs/zh/reference/hubble/datatypes/events.md new file mode 100644 index 0000000..1c29bd1 --- /dev/null +++ b/docs/zh/reference/hubble/datatypes/events.md @@ -0,0 +1,162 @@ +# 事件 + +事件代表状态变化,例如新消息或合约事件。 + +当 Hubble 观察到状态变化时就会发出事件。由于不同 hub 可能以不同顺序看到消息,事件的顺序对每个 hub 是特定的。客户端可以使用[事件 API](/zh/reference/hubble/grpcapi/events)订阅 hub,获取 hub 变更的实时流。 + +Hubble 会将事件保留 3 天,之后为节省空间会删除它们。要获取更早的数据,请使用[GRPC](../grpcapi/grpcapi.md)或[HTTP](../httpapi/httpapi.md) API。 + +## HubEvent + +| 字段 | 类型 | 标签 | 描述 | +| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ---- | +| type | [HubEventType](#HubEventType) | | | +| id | [uint64](#uint64) | | | +| body | [MergeMessageBody](#mergemessagebody),
[PruneMessageBody](#prunemessagebody),
[RevokeMessageBody](#revokemessagebody),
[MergeUserNameProofBody](#mergeusernameproofbody),
[MergeOnChainEventBody](#mergeonchaineventbody) | oneOf | | + +## HubEventType + +| 名称 | 数值 | 描述 | +| ----------------------------------- | ---- | ---- | +| HUB_EVENT_TYPE_NONE | 0 | | +| HUB_EVENT_TYPE_MERGE_MESSAGE | 1 | | +| HUB_EVENT_TYPE_PRUNE_MESSAGE | 2 | | +| HUB_EVENT_TYPE_REVOKE_MESSAGE | 3 | | +| HUB_EVENT_TYPE_MERGE_USERNAME_PROOF | 6 | | +| HUB_EVENT_TYPE_MERGE_ON_CHAIN_EVENT | 9 | | + + + +## MergeMessageBody + +| 字段 | 类型 | 标签 | 描述 | +| ---------------- | ------------------- | -------- | ---- | +| message | [Message](#Message) | | | +| deleted_messages | [Message](#Message) | repeated | | + + + +## MergeUserNameProofBody + +| 字段 | 类型 | 标签 | 描述 | +| ------------------------------ | ------------------------------- | ---- | ---- | +| username_proof | [UserNameProof](#UserNameProof) | | | +| deleted_username_proof | [UserNameProof](#UserNameProof) | | | +| username_proof_message | [Message](#Message) | | | +| deleted_username_proof_message | [Message](#Message) | | | + + + +## PruneMessageBody + +| 字段 | 类型 | 标签 | 描述 | +| ------- | ------------------- | ---- | ---- | +| message | [Message](#Message) | | | + + + +## RevokeMessageBody + +| 字段 | 类型 | 标签 | 描述 | +| ------- | ------------------- | ---- | ---- | +| message | [Message](#Message) | | | + + + +## MergeOnChainEventBody + +| 字段 | 类型 | 标签 | 描述 | +| -------------- | ----------------------------- | ---- | ---- | +| on_chain_event | [OnChainEvent](#OnChainEvent) | | | + + + +## OnChainEvent + +| 字段 | 类型 | 标签 | 描述 | +| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ---------------- | +| type | [OnChainEventType](#OnChainEventType) | | 链上事件的类型 | +| chain_id | [uint32](#) | | 事件的链 ID | +| block_number | [uint32](#) | | 事件的区块号 | +| block_hash | [bytes](#) | | 事件的区块哈希 | +| block_timestamp | [uint64](#) | | 事件的区块时间戳 | +| transaction_hash | [bytes](#) | | 事件的交易哈希 | +| log_index | [uint32](#) | | 事件的日志索引 | +| fid | [uint64](#) | | 事件关联的 fid | +| body | [SignerEventBody](#signereventbody),
[SignerMigratedEventBody](#signermigratedeventbody),
[IdRegisterEventBody](#idregistereventbody),
[StorageRentEventBody](#storagerenteventbody) | oneOf | | +| tx_index | [uint32](#) | | 事件的交易索引 | + + + +## OnChainEventType + +| 名称 | 数值 | 描述 | +| -------------------------- | ---- | ---- | +| EVENT_TYPE_NONE | 0 | | +| EVENT_TYPE_SIGNER | 1 | | +| EVENT_TYPE_SIGNER_MIGRATED | 2 | | +| EVENT_TYPE_ID_REGISTER | 3 | | +| EVENT_TYPE_STORAGE_RENT | 4 | | + + + +## SignerEventBody + +| 字段 | 类型 | 标签 | 描述 | +| ------------- | ----------------------------------- | ---- | ---------------------------- | +| key | [bytes](#) | | 签名者公钥的字节 | +| key_type | [uint32](#) | | 密钥类型(当前仅设置为 1) | +| event_type | [SignerEventType](#SignerEventType) | | 签名者事件的类型 | +| metadata | [bytes](#) | | 与密钥关联的元数据 | +| metadata_type | [uint32](#) | | 元数据类型(当前仅设置为 1) | + + + +## SignerEventType + +| 名称 | 数值 | 描述 | +| ----------------------------- | ---- | ---- | +| SIGNER_EVENT_TYPE_NONE | 0 | | +| SIGNER_EVENT_TYPE_ADD | 1 | | +| SIGNER_EVENT_TYPE_REMOVE | 2 | | +| SIGNER_EVENT_TYPE_ADMIN_RESET | 3 | | + + + +## SignerMigratedEventBody + +| 字段 | 类型 | 标签 | 描述 | +| ----------- | ----------- | ---- | -------------------------- | +| migrated_at | [uint32](#) | | hub 迁移到 OP 主网的时间戳 | + + + +## SignerEventBody + +| 字段 | 类型 | 标签 | 描述 | +| --------------- | ------------------------------------------- | ---- | --------------------- | +| to | [bytes](#) | | fid 注册/转移到的地址 | +| event_type | [IdRegisterEventType](#IdRegisterEventType) | | ID 注册事件的类型 | +| from | [bytes](#) | | 转移发起地址 | +| recover_address | [bytes](#) | | fid 的恢复地址 | + + + +## IdRegisterEventType + +| 名称 | 数值 | 描述 | +| -------------------------------------- | ---- | ---- | +| ID_REGISTER_EVENT_TYPE_NONE | 0 | | +| ID_REGISTER_EVENT_TYPE_REGISTER | 0 | | +| ID_REGISTER_EVENT_TYPE_TRANSFER | 0 | | +| ID_REGISTER_EVENT_TYPE_CHANGE_RECOVERY | 0 | | + + + +## StorageRentEventBody + +| 字段 | 类型 | 标签 | 描述 | +| ------ | ----------- | ---- | -------------------------- | +| payer | [bytes](#) | | 支付者地址 | +| units | [uint32](#) | | 购买的存储单元数量 | +| expiry | [uint32](#) | | 这些存储单元将到期的时间戳 | diff --git a/docs/zh/reference/hubble/datatypes/messages.md b/docs/zh/reference/hubble/datatypes/messages.md new file mode 100644 index 0000000..667fa3c --- /dev/null +++ b/docs/zh/reference/hubble/datatypes/messages.md @@ -0,0 +1,229 @@ +# 消息 + +消息是 Farcaster 网络中的基础数据类型。 + +当账户执行诸如发布公开消息、更改个人资料或验证以太坊账户等操作时,都会生成新的消息。 + +## 1. 消息 + +消息是一个 protobuf 结构,包含数据、其哈希值以及作者的签名。 + +| 字段 | 类型 | 标签 | 描述 | +| ---------------- | ----------------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| data | [MessageData](#MessageData) | | 消息内容。也可以使用 data_bytes 字段来序列化 `MessageData` | +| hash | bytes | | 数据的哈希摘要 | +| hash_scheme | [HashScheme](#HashScheme) | | 生成哈希摘要的哈希方案 | +| signature | bytes | | 哈希摘要的签名 | +| signature_scheme | [SignatureScheme](#SignatureScheme) | | 生成签名的签名方案 | +| signer | bytes | | 生成签名的密钥对的公钥或地址 | +| data_bytes | bytes | | 替代 "data" 字段。如果使用非 Typescript 的编程语言构造 [MessageData](#MessageData),可以使用此字段序列化 `MessageData` 并基于这些字节计算 `hash` 和 `signature`。可选字段。 | + +### 1.1 MessageData + +MessageData 是一个通用信封,包含所有 Farcaster 消息中必须存在的类型、fid、时间戳和网络信息。它还包含一个 body,其类型由 MessageType 决定。 + +| 字段 | 类型 | 标签 | 描述 | +| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | --------------------------- | +| type | [MessageType](#MessageType) | | body 中包含的消息类型 | +| fid | uint64 | | 生成消息的用户 Farcaster ID | +| timestamp | uint32 | | Farcaster 时间戳(秒) | +| network | [FarcasterNetwork](#FarcasterNetwork) | | 消息目标 Farcaster 网络 | +| body | [CastAddBody](#CastAddBody),
[CastRemoveBody](#CastRemoveBody),
[ReactionBody](#ReactionBody),
[VerificationAddEthAddressBody](#VerificationAddEthAddressBody),
[VerificationRemoveBody](#VerificationRemoveBody),
[UserDataBody](#UserDataBody),
[LinkBody](#LinkBody),
[UserNameProof](#UserNameProof) | oneOf | 特定于 MessageType 的属性 | + +### 1.2 HashScheme + +用于生成 MessageData 摘要的哈希方案类型 + +| 名称 | 数值 | 描述 | +| ------------------ | ---- | --------------------------- | +| HASH_SCHEME_NONE | 0 | | +| HASH_SCHEME_BLAKE3 | 1 | 哈希 MessageData 的默认方案 | + +### 1.3 签名方案 + +用于签名消息哈希的签名方案类型 + +| 名称 | 数值 | 描述 | +| ------------------------ | ---- | ------------------------------ | +| SIGNATURE_SCHEME_NONE | 0 | | +| SIGNATURE_SCHEME_ED25519 | 1 | Ed25519 签名(默认) | +| SIGNATURE_SCHEME_EIP712 | 2 | 使用 EIP-712 方案的 ECDSA 签名 | + +### 1.4 消息类型 + +消息体的类型 + +| 名称 | 数值 | 描述 | +| ----------------------------------------- | ---- | --------------------- | +| MESSAGE_TYPE_NONE | 0 | 无效默认值 | +| MESSAGE_TYPE_CAST_ADD | 1 | 添加新 Cast | +| MESSAGE_TYPE_CAST_REMOVE | 2 | 移除现有 Cast | +| MESSAGE_TYPE_REACTION_ADD | 3 | 向 Cast 添加 Reaction | +| MESSAGE_TYPE_REACTION_REMOVE | 4 | 从 Cast 移除 Reaction | +| MESSAGE_TYPE_LINK_ADD | 5 | 向目标添加 Link | +| MESSAGE_TYPE_LINK_REMOVE | 6 | 从目标移除 Link | +| MESSAGE_TYPE_VERIFICATION_ADD_ETH_ADDRESS | 7 | 添加以太坊地址验证 | +| MESSAGE_TYPE_VERIFICATION_REMOVE | 8 | 移除验证 | +| MESSAGE_TYPE_USER_DATA_ADD | 11 | 添加用户元数据 | +| MESSAGE_TYPE_USERNAME_PROOF | 12 | 添加或替换用户名证明 | + +### 1.5 Farcaster 网络 + +消息目标 Farcaster 网络 + +| 名称 | 数值 | 描述 | +| ------------------------- | ---- | ---------- | +| FARCASTER_NETWORK_NONE | 0 | | +| FARCASTER_NETWORK_MAINNET | 1 | 公共主网 | +| FARCASTER_NETWORK_TESTNET | 2 | 公共测试网 | +| FARCASTER_NETWORK_DEVNET | 3 | 私有测试网 | + +## 2. 用户数据 + +UserData 消息表示用户元数据(例如个人资料图片 URL)。 + +### 2.1 UserDataBody + +UserData 消息体 + +| 字段 | 类型 | 标签 | 描述 | +| ----- | ----------------------------- | ---- | ---------- | +| type | [UserDataType](#UserDataType) | | 元数据类型 | +| value | string | | 元数据值 | + +### 2.2 UserDataType + +UserData 消息类型 + +| 名称 | 数值 | 描述 | +| ----------------------- | ---- | ----------------------- | +| USER_DATA_TYPE_NONE | 0 | 无效默认值 | +| USER_DATA_TYPE_PFP | 1 | 用户个人资料图片 | +| USER_DATA_TYPE_DISPLAY | 2 | 用户显示名称 | +| USER_DATA_TYPE_BIO | 3 | 用户简介 | +| USER_DATA_TYPE_URL | 5 | 用户 URL | +| USER_DATA_TYPE_USERNAME | 6 | 用户首选 Farcaster 名称 | +| USER_DATA_TYPE_LOCATION | 7 | 用户位置 | +| USER_DATA_TYPE_TWITTER | 8 | 用户 Twitter 用户名 | +| USER_DATA_TYPE_GITHUB | 9 | 用户 GitHub 用户名 | + +有关位置的更多信息,请参阅 [FIP-196](https://github.com/farcasterxyz/protocol/discussions/196)。 +有关 Twitter/X 和 Github 用户名的更多信息,请参阅 [FIP-19](https://github.com/farcasterxyz/protocol/discussions/199)。 + +## 3. Cast + +Cast 消息是用户的公开帖子。 + +### 3.1 CastAddBody + +添加新 Cast 消息。 + +| 字段 | 类型 | 标签 | 描述 | +| ------------------ | ----------------- | -------- | ---------------------------- | +| embeds_deprecated | string | repeated | 嵌入 Cast 的 URL | +| mentions | uint64 | repeated | Cast 中提及的 Fids | +| parent_cast_id | [CastId](#CastId) | | Cast 的父 Cast | +| parent_url | string | | Cast 的父 URL | +| text | string | | Cast 文本 | +| mentions_positions | uint32 | repeated | 文本中提及的位置 | +| embeds | [Embed](#Embed) | repeated | 嵌入 Cast 的 URL 或 cast ids | + +#### Embed + +| 字段 | 类型 | 标签 | 描述 | +| ------- | ----------------- | ---- | ---- | +| url | [string](#string) | | | +| cast_id | [CastId](#CastId) | | | + +### 3.2 CastRemoveBody + +移除现有 Cast 消息。 + +| 字段 | 类型 | 标签 | 描述 | +| ----------- | ----- | ---- | ------------------ | +| target_hash | bytes | | 要移除的 Cast 哈希 | + +### 3.3 CastId + +用于查找 Cast 的标识符 + +| 字段 | 类型 | 标签 | 描述 | +| ---- | ------ | ---- | -------------------- | +| fid | uint64 | | 创建 Cast 的用户 Fid | +| hash | bytes | | Cast 哈希 | + +## 4. Reaction + +Reaction 消息在账户和 Cast 之间创建关系(例如点赞)。 + +### 4.1 ReactionBody + +向 Cast 添加或移除 Reaction + +| 字段 | 类型 | 标签 | 描述 | +| -------------- | ----------------------------- | ---- | ----------------------- | +| type | [ReactionType](#ReactionType) | | Reaction 类型 | +| target_cast_id | [CastId](#CastId) | | 要反应的 Cast 的 CastId | +| target_url | [string](#string) | | 要反应的 URL | + +### 4.2 ReactionType + +Reaction 类型 + +| 名称 | 数值 | 描述 | +| -------------------- | ---- | ---------------------------- | +| REACTION_TYPE_NONE | 0 | 无效默认值 | +| REACTION_TYPE_LIKE | 1 | 点赞目标 Cast | +| REACTION_TYPE_RECAST | 2 | 将目标 Cast 分享到用户的受众 | + +## 5. Link + +Link 消息在两个用户之间创建关系(例如关注)。 + +### 5.1 LinkBody + +添加或移除 Link + +| 字段 | 类型 | 标签 | 描述 | +| ---------------- | ----------------- | -------- | ------------------------------------------------------------------------------ | +| type | [string](#string) | | Link 类型,≤ 8 个字符 | +| displayTimestamp | [uint32](#uint32) | optional | 用户定义的时间戳,当 message.data.timestamp 需要更新以进行压缩时保留原始时间戳 | +| target_fid | [uint64](#uint64) | | Link 关联的 fid | + +## 6. 验证 + +Verification 消息是某物所有权证明。 + +### 6.1 VerificationAddEthAddressBody + +添加双向签名,证明 fid 控制某个以太坊地址。 + +| 字段 | 类型 | 标签 | 描述 | +| ------------- | ----- | ---- | ------------------------------ | +| address | bytes | | 被验证的以太坊地址 | +| eth_signature | bytes | | 用户以太坊地址生成的签名 | +| block_hash | bytes | | 生成声明时的最新以太坊区块哈希 | + +### 6.2 VerificationRemoveBody + +移除任何类型的 Verification + +| 字段 | 类型 | 标签 | 描述 | +| ------- | ----- | ---- | -------------------------- | +| address | bytes | | 要移除的 Verification 地址 | + +## 7. Frame 操作 + +表示用户在 frame 上执行的操作。此消息不存储在 hubs 上且无法提交,仅用于验证。 + +### 7.1 FrameActionBody + +用户在 frame 上的操作 + +| 字段 | 类型 | 标签 | 描述 | +| ------------ | ----------------- | ---- | --------------------------------- | +| url | bytes | | 嵌入 Cast 中的 frame 原始 URL | +| button_index | uint32 | | 按下的按钮索引(从 1 开始) | +| cast_id | [CastId](#CastId) | | 托管 frame 的 cast id | +| input_text | bytes | | 用户作为操作一部分输入的任何文本 | +| state | bytes | | 从 frame 传递到服务器的序列化状态 | diff --git a/docs/zh/reference/hubble/grpcapi/casts.md b/docs/zh/reference/hubble/grpcapi/casts.md new file mode 100644 index 0000000..e3e871a --- /dev/null +++ b/docs/zh/reference/hubble/grpcapi/casts.md @@ -0,0 +1,23 @@ +# Casts API + +用于检索有效的 casts 或已删除 casts 的墓碑记录 + +## API + +| 方法名 | 请求类型 | 响应类型 | 描述 | +| ----------------------- | ---------- | ---------------- | ----------------------------------------- | +| GetCast | CastId | Message | 返回指定的 Cast | +| GetCastsByFid | FidRequest | MessagesResponse | 按时间倒序返回某个 Fid 的 CastAdds | +| GetCastsByParent | CastId | MessagesResponse | 按时间倒序返回对指定 Cast 的 CastAdd 回复 | +| GetCastsByMention | FidRequest | MessagesResponse | 按时间倒序返回提及某个 Fid 的 CastAdds | +| GetAllCastMessagesByFid | FidRequest | MessagesResponse | 按时间倒序返回某个 Fid 的所有 Casts | + +## CastsByParentRequest + +| 字段 | 类型 | 标签 | 描述 | +| -------------- | ----------------- | -------- | ---- | +| parent_cast_id | [CastId](#CastId) | | | +| parent_url | [string](#string) | | | +| page_size | [uint32](#uint32) | optional | | +| page_token | [bytes](#bytes) | optional | | +| reverse | [bool](#bool) | optional | | diff --git a/docs/zh/reference/hubble/grpcapi/events.md b/docs/zh/reference/hubble/grpcapi/events.md new file mode 100644 index 0000000..070a71f --- /dev/null +++ b/docs/zh/reference/hubble/grpcapi/events.md @@ -0,0 +1,23 @@ +# 事件 API + +用于订阅来自 Farcaster Hub 的实时事件更新 + +## API + +| 方法名称 | 请求类型 | 响应类型 | 描述 | +| --------- | ---------------- | --------------- | -------------------------- | +| Subscribe | SubscribeRequest | stream HubEvent | 当新事件发生时进行流式传输 | +| GetEvent | EventRequest | HubEvent | 当新事件发生时进行流式传输 | + +## SubscribeRequest + +| 字段 | 类型 | 标签 | 描述 | +| ----------- | -------------- | -------- | ---------------------------- | +| event_types | [EventType](#) | repeated | 需要订阅的事件类型 | +| from_id | uint64 | optional | 开始流式传输的事件 ID 起始点 | + +## EventRequest + +| 字段 | 类型 | 标签 | 描述 | +| ---- | ----------------- | ---- | ---- | +| id | [uint64](#uint64) | | | diff --git a/docs/zh/reference/hubble/grpcapi/fids.md b/docs/zh/reference/hubble/grpcapi/fids.md new file mode 100644 index 0000000..c82063e --- /dev/null +++ b/docs/zh/reference/hubble/grpcapi/fids.md @@ -0,0 +1,24 @@ +# Fids API + +用于获取所有 fids 的列表 + +## API + +| 方法名称 | 请求类型 | 响应类型 | 描述 | +| -------- | ----------- | ------------ | ------------------------ | +| GetFids | FidsRequest | FidsResponse | 返回所有 fids 的分页列表 | + +## FidsRequest + +| 字段 | 类型 | 标签 | 描述 | +| ---------- | ----------------- | -------- | ------------ | +| page_size | [uint32](#uint32) | optional | 每页大小 | +| page_token | [bytes](#bytes) | optional | 分页令牌 | +| reverse | [bool](#bool) | optional | 是否反向排序 | + +## Fids Response + +| 字段 | 类型 | 标签 | 描述 | +| --------------- | --------------- | -------- | ---------- | +| fids | [uint64](#) | repeated | fid 数组 | +| next_page_token | [bytes](#bytes) | optional | 下一页令牌 | diff --git a/docs/zh/reference/hubble/grpcapi/grpcapi.md b/docs/zh/reference/hubble/grpcapi/grpcapi.md new file mode 100644 index 0000000..9cb3ce0 --- /dev/null +++ b/docs/zh/reference/hubble/grpcapi/grpcapi.md @@ -0,0 +1,11 @@ +# GRPC API + +Hubble 默认在 2283 端口提供 gRPC API 服务。 + +## 使用 API + +我们推荐使用 [hub-nodejs](https://github.com/farcasterxyz/hub-monorepo/tree/main/packages/hub-nodejs) 这类库来与 Hubble 的 gRPC API 交互。具体使用方法请参阅其[文档](https://github.com/farcasterxyz/hub-monorepo/tree/main/packages/hub-nodejs/docs)。 + +## 其他语言支持 + +gRPC API 的 protobuf 文件存放在 [hub-monorepo](https://github.com/farcasterxyz/hub-monorepo/tree/main/protobufs) 中,可用于生成其他编程语言客户端的绑定。请注意,默认情况下,Hub 依赖 javascript 的 [ts-proto](https://www.npmjs.com/package/ts-proto) 库的序列化字节顺序来验证消息哈希。如果使用其他客户端,在调用 `SubmitMessage` 时可能需要通过 `data_bytes` 字段传入原始序列化字节,才能使消息被视为有效。更多细节请参考 [SubmitMessage HTTP API 文档](/zh/reference/hubble/httpapi/message#using-with-rust-go-or-other-programing-languages)。 diff --git a/docs/zh/reference/hubble/grpcapi/links.md b/docs/zh/reference/hubble/grpcapi/links.md new file mode 100644 index 0000000..ed6dc86 --- /dev/null +++ b/docs/zh/reference/hubble/grpcapi/links.md @@ -0,0 +1,40 @@ +# 链接 API + +Link(链接)表示两个用户之间的关系(例如关注) + +## API + +| 方法名称 | 请求类型 | 响应类型 | 描述 | +| ----------------------- | -------------------- | ---------------- | --------------------------------------------- | +| GetLink | LinkRequest | Message | 返回特定的 Link | +| GetLinksByFid | LinksByFidRequest | MessagesResponse | 按时间倒序返回某个 fid 创建的所有 Link | +| GetLinksByTarget | LinksByTargetRequest | MessagesResponse | 按时间倒序返回指向某个目标的所有 LinkAdd 消息 | +| GetAllLinkMessagesByFid | FidRequest | MessagesResponse | 按时间倒序返回某个 fid 创建的所有 Link | + +## Link 请求 + +| 字段 | 类型 | 标签 | 描述 | +| ---------- | ----------- | ---- | ------------------------------- | +| fid | [uint64](#) | | 生成该 Link 的用户 Farcaster ID | +| link_type | [string](#) | | 请求的 Link 类型 | +| target_fid | [uint64](#) | | 目标用户的 fid | + +## LinksByFid 请求 + +| 字段 | 类型 | 标签 | 描述 | +| ---------- | ----------- | ---- | ------------------------------- | +| fid | [uint64](#) | | 生成该 Link 的用户 Farcaster ID | +| link_type | string | | 请求的 Link 类型 | +| page_size | uint32 | | (可选) 请求的 Link 类型 | +| page_token | bytes | | (可选) 请求的 Link 类型 | +| reverse | boolean | | (可选) 控制返回结果的排序方式 | + +## LinksByTarget 请求 + +| 字段 | 类型 | 标签 | 描述 | +| ---------- | ----------- | ---- | ------------------------------- | +| target_fid | [uint64](#) | | 生成该 Link 的用户 Farcaster ID | +| link_type | string | | (可选) 请求的 Link 类型 | +| page_size | uint32 | | (可选) 请求的 Link 类型 | +| page_token | bytes | | (可选) 请求的 Link 类型 | +| reverse | boolean | | (可选) 控制返回结果的排序方式 | diff --git a/docs/zh/reference/hubble/grpcapi/message.md b/docs/zh/reference/hubble/grpcapi/message.md new file mode 100644 index 0000000..ddfcc64 --- /dev/null +++ b/docs/zh/reference/hubble/grpcapi/message.md @@ -0,0 +1,17 @@ +# 消息 API + +用于验证并向 Farcaster Hub 发送消息。有效消息会被接收并传播至网络中的其他 Hub。 + +## API + +| 方法名称 | 请求类型 | 响应类型 | 描述 | +| --------------- | -------- | ------------------ | ----------------------------------------- | +| SubmitMessage | Message | Message | 向 Hub 提交消息 | +| ValidateMessage | Message | ValidationResponse | 在 Hub 上验证消息(不执行合并和传播操作) | + +## 验证响应 + +| 字段 | 类型 | 标签 | 描述 | +| ------- | ------- | ---- | ------------------------------ | +| valid | boolean | | 消息是否有效 | +| message | Message | | 被验证的消息(与请求内容相同) | diff --git a/docs/zh/reference/hubble/grpcapi/onchain.md b/docs/zh/reference/hubble/grpcapi/onchain.md new file mode 100644 index 0000000..fc4d271 --- /dev/null +++ b/docs/zh/reference/hubble/grpcapi/onchain.md @@ -0,0 +1,42 @@ +# OnChainEvents API + +用于检索链上事件(ID 注册、密钥、存储租金) + +## API + +| 方法名 | 请求类型 | 响应类型 | 描述 | +| ---------------------------------- | ------------------------------- | -------------------- | ------------------------------------------------------------------------- | +| GetOnChainSigner | SignerRequest | OnChainEvent | 返回指定 Fid 的活跃签名者对应的链上事件 | +| GetOnChainSignersByFid | FidRequest | OnChainEventResponse | 返回指定 Fid 所有活跃账户密钥(签名者)的添加事件 | +| GetIdRegistryOnChainEvent | FidRequest | OnChainEvent | 返回指定 fid 最近一次的注册/转移链上事件 | +| GetIdRegistryOnChainEventByAddress | IdRegistryEventByAddressRequest | OnChainEvent | 根据地址返回注册/转移事件(如果存在),可用于通过地址查找 fid | +| GetOnChainEvents | OnChainEventRequest | OnChainEventResponse | 返回指定 Fid 按类型过滤的所有链上事件(包括非活跃密钥和已过期的租金事件) | + +## Signer Request + +| 字段 | 类型 | 标签 | 描述 | +| ------ | ----------- | ---- | ------------------------------- | +| fid | [uint64](#) | | 生成签名者的用户的 Farcaster ID | +| signer | [bytes](#) | | 签名者的公钥 | + +## Fid Request + +| 字段 | 类型 | 标签 | 描述 | +| ---------- | ----------- | ---- | ---------------------- | +| fid | [uint64](#) | | 用户的 Farcaster ID | +| page_size | uint32 | | (可选)请求的链接类型 | +| page_token | bytes | | (可选)请求的链接类型 | +| reverse | boolean | | (可选)响应排序方式 | + +#### IdRegistryEventByAddressRequest + +| 字段 | 类型 | 标签 | 描述 | +| ------- | --------------- | ---- | ---- | +| address | [bytes](#bytes) | | | + +#### OnChainEventResponse + +| 字段 | 类型 | 标签 | 描述 | +| --------------- | ----------------------------- | -------- | ---- | +| events | [OnChainEvent](#onchainevent) | repeated | | +| next_page_token | [bytes](#bytes) | optional | | diff --git a/docs/zh/reference/hubble/grpcapi/reactions.md b/docs/zh/reference/hubble/grpcapi/reactions.md new file mode 100644 index 0000000..1782fbb --- /dev/null +++ b/docs/zh/reference/hubble/grpcapi/reactions.md @@ -0,0 +1,53 @@ +# 反应(Reactions)API + +## API + +| 方法名称 | 请求类型 | 响应类型 | 描述 | +| --------------------------- | ------------------------ | ---------------- | -------------------------------------------- | +| GetReaction | ReactionRequest | Message | 返回指定的反应(Reaction) | +| GetReactionsByFid | ReactionsByFidRequest | MessagesResponse | 按时间倒序返回某个 Fid 用户创建的所有反应 | +| GetReactionsByCast | ReactionsByCastRequest | MessagesResponse | 按时间倒序返回针对某个 Cast 的所有反应 | +| GetReactionsByTarget | ReactionsByTargetRequest | MessagesResponse | 按时间倒序返回针对某个目标(Cast)的所有反应 | +| GetAllReactionMessagesByFid | FidRequest | MessagesResponse | 按时间倒序返回某个 Fid 用户创建的所有反应 | + +## 反应请求(Reaction Request) + +用于获取有效或已撤销的反应 + +| 字段 | 类型 | 标签 | 描述 | +| -------------- | ----------------- | ---- | -------------------------------- | +| fid | [uint64](#) | | 生成该反应的用户的 Farcaster ID | +| reaction_type | [ReactionType](#) | | 请求的反应类型 | +| target_cast_id | [CastId](#) | | (可选)请求其反应的 Cast 标识符 | +| target_url | [string](#) | | (可选)请求其反应的 URL 标识符 | + +## 按 Fid 获取反应请求(ReactionsByFid Request) + +| 字段 | 类型 | 标签 | 描述 | +| ------------- | ----------------- | ---- | ------------------------------- | +| fid | [uint64](#) | | 生成该反应的用户的 Farcaster ID | +| reaction_type | [ReactionType](#) | | 请求的反应类型 | +| page_size | uint32 | | (可选)请求的链接类型 | +| page_token | bytes | | (可选)请求的链接类型 | +| reverse | boolean | | (可选)响应结果的排序方式 | + +## 按 Cast 获取反应请求(ReactionsByCast Request) + +| 字段 | 类型 | 标签 | 描述 | +| ------------- | ----------------- | ---- | -------------------------- | +| cast_id | [CastId](#) | | 请求其反应的 Cast 标识符 | +| reaction_type | [ReactionType](#) | | 请求的反应类型 | +| page_size | uint32 | | (可选)请求的链接类型 | +| page_token | bytes | | (可选)请求的链接类型 | +| reverse | boolean | | (可选)响应结果的排序方式 | + +## 按目标获取反应请求(ReactionsByTargetRequest) + +| 字段 | 类型 | 标签 | 描述 | +| -------------- | ----------------------------- | -------- | ---- | +| target_cast_id | [CastId](#CastId) | | | +| target_url | [string](#string) | | | +| reaction_type | [ReactionType](#ReactionType) | optional | | +| page_size | [uint32](#uint32) | optional | | +| page_token | [bytes](#bytes) | optional | | +| reverse | [bool](#bool) | optional | | diff --git a/docs/zh/reference/hubble/grpcapi/storagelimits.md b/docs/zh/reference/hubble/grpcapi/storagelimits.md new file mode 100644 index 0000000..9eb70a6 --- /dev/null +++ b/docs/zh/reference/hubble/grpcapi/storagelimits.md @@ -0,0 +1,22 @@ +# 存储 API + +获取 FID 的存储限制。 + +## API + +| 方法名称 | 请求类型 | 响应类型 | 描述 | +| ---------------------------- | ---------- | --------------------- | --------------------------------------- | +| GetCurrentStorageLimitsByFid | FidRequest | StorageLimitsResponse | 返回指定 FID 所有存储类型的当前存储限制 | + +#### StorageLimitsResponse + +| 字段 | 类型 | 标签 | 描述 | +| ------ | ----------------- | -------- | ------------------------ | +| limits | [StorageLimit](#) | repeated | 按存储类型划分的存储限制 | + +#### StorageLimit + +| 字段 | 类型 | 标签 | 描述 | +| ---------- | -------------- | ---- | ---------------------------------------- | +| store_type | [StoreType](#) | | 存储管理的具体类型 | +| limit | [uint64](#) | | 存储类型的限制值(根据用户租金比例缩放) | diff --git a/docs/zh/reference/hubble/grpcapi/sync.md b/docs/zh/reference/hubble/grpcapi/sync.md new file mode 100644 index 0000000..f29962f --- /dev/null +++ b/docs/zh/reference/hubble/grpcapi/sync.md @@ -0,0 +1,96 @@ +# 同步 API + +这些 API 用于 Hubs 之间同步状态。不适用于外部应用程序使用。 + +## API + +| 方法名称 | 请求类型 | 响应类型 | 描述 | +| ----------------------- | ----------------- | ------------------------ | --------------------------- | +| GetInfo | HubInfoRequest | HubInfoResponse | 返回 Hub 状态的元数据。 | +| GetSyncStatus | SyncStatusRequest | SyncStatusResponse | 返回 Hub 的同步状态。 | +| GetAllSyncIdsByPrefix | TrieNodePrefix | SyncIds | 获取特定前缀下的所有 SyncId | +| GetAllMessagesBySyncIds | SyncIds | MessagesResponse | 根据同步 ID 获取所有消息 | +| GetSyncMetadataByPrefix | TrieNodePrefix | TrieNodeMetadataResponse | 获取特定前缀的同步元数据 | +| GetSyncSnapshotByPrefix | TrieNodePrefix | TrieNodeSnapshotResponse | 获取特定前缀的同步快照 | + +## HubInfoRequest + +| 字段 | 类型 | 标签 | 描述 | +| -------- | ------------- | ---- | ---- | +| db_stats | [bool](#bool) | | | + +## HubInfoResponse + +同步 RPC 方法的响应类型 + +| 字段 | 类型 | 标签 | 描述 | +| ---------- | ------------------- | ---- | ---- | +| version | [string](#string) | | | +| is_syncing | [bool](#bool) | | | +| nickname | [string](#string) | | | +| root_hash | [string](#string) | | | +| db_stats | [DbStats](#DbStats) | | | + +## SyncStatusRequest + +| 字段 | 类型 | 标签 | 描述 | +| ------ | ----------------- | -------- | ---- | +| peerId | [string](#string) | optional | | + +## SyncStatusResponse + +| 字段 | 类型 | 标签 | 描述 | +| ----------- | ------------------------- | -------- | ---- | +| is_syncing | [bool](#bool) | | | +| sync_status | [SyncStatus](#SyncStatus) | repeated | | + +## SyncStatus + +| 字段 | 类型 | 标签 | 描述 | +| -------------------- | ----------------- | ---- | ---- | +| peerId | [string](#string) | | | +| inSync | [string](#string) | | | +| shouldSync | [bool](#bool) | | | +| divergencePrefix | [string](#string) | | | +| divergenceSecondsAgo | [int32](#int32) | | | +| theirMessages | [uint64](#uint64) | | | +| ourMessages | [uint64](#uint64) | | | +| lastBadSync | [int64](#int64) | | | + +## TrieNodePrefix + +| 字段 | 类型 | 标签 | 描述 | +| ------ | --------------- | ---- | ---- | +| prefix | [bytes](#bytes) | | | + +## SyncIds + +| 字段 | 类型 | 标签 | 描述 | +| -------- | --------------- | -------- | ---- | +| sync_ids | [bytes](#bytes) | repeated | | + +## TrieNodeMetadataResponse + +| 字段 | 类型 | 标签 | 描述 | +| ------------ | ----------------------------------------------------- | -------- | ---- | +| prefix | [bytes](#bytes) | | | +| num_messages | [uint64](#uint64) | | | +| hash | [string](#string) | | | +| children | [TrieNodeMetadataResponse](#TrieNodeMetadataResponse) | repeated | | + +## TrieNodeSnapshotResponse + +| 字段 | 类型 | 标签 | 描述 | +| --------------- | ----------------- | -------- | ---- | +| prefix | [bytes](#bytes) | | | +| excluded_hashes | [string](#string) | repeated | | +| num_messages | [uint64](#uint64) | | | +| root_hash | [string](#string) | | | + +## DbStats + +| 字段 | 类型 | 标签 | 描述 | +| ---------------- | ----------------- | ---- | ---- | +| num_messages | [uint64](#uint64) | | | +| num_fid_events | [uint64](#uint64) | | | +| num_fname_events | [uint64](#uint64) | | | diff --git a/docs/zh/reference/hubble/grpcapi/userdata.md b/docs/zh/reference/hubble/grpcapi/userdata.md new file mode 100644 index 0000000..9a65a25 --- /dev/null +++ b/docs/zh/reference/hubble/grpcapi/userdata.md @@ -0,0 +1,25 @@ +# UserData API + +用于获取与用户关联的当前元数据 + +## API + +| 方法名 | 请求类型 | 响应类型 | 描述 | +| --------------------------- | --------------- | ---------------- | ---------------------------- | +| GetUserData | UserDataRequest | Message | 返回指定 Fid 的特定 UserData | +| GetUserDataByFid | FidRequest | MessagesResponse | 返回某个 Fid 的所有 UserData | +| GetAllUserDataMessagesByFid | FidRequest | MessagesResponse | 返回某个 Fid 的所有 UserData | + +## UserData 请求 + +| 字段 | 类型 | 标签 | 描述 | +| -------------- | ----------------- | ---- | --------------------------------- | +| fid | [uint64](#) | | 生成 UserData 用户的 Farcaster ID | +| user_data_type | [UserDataType](#) | | 请求的 UserData 类型 | + +## Messages 响应 + +| 字段 | 类型 | 标签 | 描述 | +| --------------- | --------------- | -------- | -------------- | +| messages | [Message](#) | repeated | Farcaster 消息 | +| next_page_token | [bytes](#bytes) | optional | | diff --git a/docs/zh/reference/hubble/grpcapi/usernameproof.md b/docs/zh/reference/hubble/grpcapi/usernameproof.md new file mode 100644 index 0000000..48ff820 --- /dev/null +++ b/docs/zh/reference/hubble/grpcapi/usernameproof.md @@ -0,0 +1,42 @@ +# 用户名证明 API + +## API + +| 方法名 | 请求类型 | 响应类型 | 描述 | +| ---------------------- | --------------------------------------------- | ------------------------------------------------- | ----------------------- | +| GetUsernameProof | [UsernameProofRequest](#UsernameProofRequest) | [UserNameProof](#UserNameProof) | 获取用户名证明 | +| GetUserNameProofsByFid | [FidRequest](#FidRequest) | [UsernameProofsResponse](#UsernameProofsResponse) | 通过 FID 获取用户名证明 | + +## UsernameProofRequest + +| 字段 | 类型 | 标签 | 描述 | +| ---- | --------------- | ---- | ------ | +| name | [bytes](#bytes) | | 用户名 | + +## UsernameProofsResponse + +| 字段 | 类型 | 标签 | 描述 | +| ------ | ------------------------------- | -------- | -------------- | +| proofs | [UserNameProof](#UserNameProof) | repeated | 用户名证明列表 | + +## UserNameProof + +| 字段 | 类型 | 标签 | 描述 | +| --------- | ----------------------------- | ---- | ------------ | +| timestamp | [uint64](#uint64) | | 时间戳 | +| name | [bytes](#bytes) | | 用户名 | +| owner | [bytes](#bytes) | | 所有者地址 | +| signature | [bytes](#bytes) | | 签名数据 | +| fid | [uint64](#uint64) | | Farcaster ID | +| type | [UserNameType](#UserNameType) | | 用户名类型 | + +## UserNameProof + +| 字段 | 类型 | 标签 | 描述 | +| --------- | ----------------------------- | ---- | ------------ | +| timestamp | [uint64](#uint64) | | 时间戳 | +| name | [bytes](#bytes) | | 用户名 | +| owner | [bytes](#bytes) | | 所有者地址 | +| signature | [bytes](#bytes) | | 签名数据 | +| fid | [uint64](#uint64) | | Farcaster ID | +| type | [UserNameType](#UserNameType) | | 用户名类型 | diff --git a/docs/zh/reference/hubble/grpcapi/verification.md b/docs/zh/reference/hubble/grpcapi/verification.md new file mode 100644 index 0000000..cefa89a --- /dev/null +++ b/docs/zh/reference/hubble/grpcapi/verification.md @@ -0,0 +1,18 @@ +# 验证 API + +用于检索以太坊地址有效或已撤销的所有权证明。 + +## API + +| 方法名称 | 请求类型 | 响应类型 | 描述 | +| ------------------------------- | ------------------- | ---------------- | ----------------------------------------------- | +| GetVerification | VerificationRequest | Message | 返回指定以太坊地址的 VerificationAdd 记录 | +| GetVerificationsByFid | FidRequest | MessagesResponse | 返回由指定 Fid 创建的所有 VerificationAdds 记录 | +| GetAllVerificationMessagesByFid | FidRequest | MessagesResponse | 返回由指定 Fid 创建的所有验证记录 | + +## 验证请求 + +| 字段 | 类型 | 标签 | 描述 | +| ------- | ----------- | ---- | ------------------------------- | +| fid | [uint64](#) | | 生成验证记录的用户 Farcaster ID | +| address | [bytes](#) | | 被验证的以太坊地址 | diff --git a/docs/zh/reference/hubble/httpapi/casts.md b/docs/zh/reference/hubble/httpapi/casts.md new file mode 100644 index 0000000..6b91e9c --- /dev/null +++ b/docs/zh/reference/hubble/httpapi/casts.md @@ -0,0 +1,195 @@ +# Casts API + +## castById + +通过 FID 和哈希值获取指定 cast。 + +**查询参数** +| 参数 | 描述 | 示例 | +| --------- | ----------- | ------- | +| fid | cast 创建者的 FID | `fid=6833` | +| hash | cast 的哈希值 | `hash=0xa48dd46161d8e57725f5e26e34ec19c13ff7f3b9` | + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/castById?fid=2&hash=0xd2b1ddc6c88e865a33cb1a565e0058d757042974 +``` + +**响应** + +```json +{ + "data": { + "type": "MESSAGE_TYPE_CAST_ADD", + "fid": 2, + "timestamp": 48994466, + "network": "FARCASTER_NETWORK_MAINNET", + "castAddBody": { + "embedsDeprecated": [], + "mentions": [], + "parentCastId": { + "fid": 226, + "hash": "0xa48dd46161d8e57725f5e26e34ec19c13ff7f3b9" + }, + "text": "Cast 文本", + "mentionsPositions": [], + "embeds": [] + } + }, + "hash": "0xd2b1ddc6c88e865a33cb1a565e0058d757042974", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "3msLXzxB4eEYe...dHrY1vkxcPAA==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x78ff9a...58c" +} +``` + +## castsByFid + +获取指定 FID 用户发布的所有 casts。 + +**查询参数** +| 参数 | 描述 | 示例 | +| --------- | ----------- | ------- | +| fid | cast 创建者的 FID | `fid=6833` | + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/castsByFid?fid=2 +``` + +**响应** + +```json +{ + "messages": [ + { + "data": { + "type": "MESSAGE_TYPE_CAST_ADD", + "fid": 2, + "timestamp": 48994466, + "network": "FARCASTER_NETWORK_MAINNET", + "castAddBody": {... }, + "text": "Cast 文本", + "mentionsPositions": [], + "embeds": [] + } + }, + "hash": "0xd2b1ddc6c88e865a33cb1a565e0058d757042974", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "3msLXzxB4eEYeF0Le...dHrY1vkxcPAA==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x78ff9a768cf1...2eca647b6d62558c" + } + ] + "nextPageToken": "" +} +``` + +## castsByParent + +通过父级 cast 的 FID 和哈希值或父级 URL 获取所有 casts。 + +**查询参数** +| 参数 | 描述 | 示例 | +| --------- | ----------- | ------- | +| fid | 父级 cast 的 FID | `fid=6833` | +| hash | 父级 cast 的哈希值 | `hash=0xa48dd46161d8e57725f5e26e34ec19c13ff7f3b9` | +| url | 父级 cast 的 URL | `url=chain://eip155:1/erc721:0x39d89b649ffa044383333d297e325d42d31329b2` | + +**注意** +可以使用 `?fid=...&hash=...` 或 `?url=...` 来查询此端点 + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/castsByParent?fid=226&hash=0xa48dd46161d8e57725f5e26e34ec19c13ff7f3b9 +``` + +**响应** + +```json +{ + "messages": [ + { + "data": { + "type": "MESSAGE_TYPE_CAST_ADD", + "fid": 226, + "timestamp": 48989255, + "network": "FARCASTER_NETWORK_MAINNET", + "castAddBody": { + "embedsDeprecated": [], + "mentions": [], + "parentCastId": { + "fid": 226, + "hash": "0xa48dd46161d8e57725f5e26e34ec19c13ff7f3b9" + }, + "text": "Cast 的文本", + "mentionsPositions": [], + "embeds": [] + } + }, + "hash": "0x0e501b359f88dcbcddac50a8f189260a9d02ad34", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "MjKnOQCTW42K8+A...tRbJfia2JJBg==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x6f1e8758...7f04a3b500ba" + } + ], + "nextPageToken": "" +} +``` + +## castsByMention + +获取所有提及指定 FID 的 casts。 + +**查询参数** +| 参数 | 描述 | 示例 | +| --------- | ----------- | ------- | +| fid | 在 cast 中被提及的 FID | `fid=6833` | + +**注意** +使用 `mentionsPositions` 可以提取 cast 文本中提及 FID 的位置偏移量 + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/castsByMention?fid=6833 +``` + +**响应** + +```json +{ + "messages": [ + { + "data": { + "type": "MESSAGE_TYPE_CAST_ADD", + "fid": 2, + "timestamp": 62298143, + "network": "FARCASTER_NETWORK_MAINNET", + "castAddBody": { + "embedsDeprecated": [], + "mentions": [15, 6833], + "parentCastId": { + "fid": 2, + "hash": "0xd5540928cd3daf2758e501a61663427e41dcc09a" + }, + "text": "cc 和 ", + "mentionsPositions": [3, 8], + "embeds": [] + } + }, + "hash": "0xc6d4607835197a8ee225e9218d41e38aafb12076", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "TOaWrSTmz+cyzPMFGvF...OeUznB0Ag==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x78ff9a768c...647b6d62558c" + } + ], + "nextPageToken": "" +} +``` diff --git a/docs/zh/reference/hubble/httpapi/events.md b/docs/zh/reference/hubble/httpapi/events.md new file mode 100644 index 0000000..5cd6f96 --- /dev/null +++ b/docs/zh/reference/hubble/httpapi/events.md @@ -0,0 +1,82 @@ +# 事件 API + +事件 API 返回合并到 Hub 的事件,可用于监听 Hub 活动。 + +## eventById + +通过事件 ID 获取单个事件 + +**查询参数** +| 参数 | 描述 | 示例 | +| --------- | ----------- | ------- | +| event_id | 事件的 Hub ID | `event_id=350909155450880` | + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/eventById?id=350909155450880 + +``` + +**响应** + +```json +{ + "type": "HUB_EVENT_TYPE_MERGE_USERNAME_PROOF", + "id": 350909155450880, + "mergeUsernameProofBody": { + "usernameProof": { + "timestamp": 1695049760, + "name": "nftonyp", + "owner": "0x23b3c29900762a70def5dc8890e09dc9019eb553", + "signature": "xp41PgeO...hJpNshw=", + "fid": 20114, + "type": "USERNAME_TYPE_FNAME" + } + } +} +``` + +## events + +获取 Hub 事件的分页数据 + +**查询参数** +| 参数 | 描述 | 示例 | +| --------- | ----------- | ------- | +| from_event_id | 可选参数,指定从哪个 Hub ID 开始获取事件。API 会返回 `nextPageEventId` 字段,可用于遍历所有 Hub 事件。设为 `0` 表示从第一个事件开始获取 | `from_event_id=350909155450880` | + +**注意** +Hub 会清理超过 3 天的事件,因此无法通过此 API 获取所有历史事件 + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/events?from_event_id=350909155450880 + +``` + +**响应** + +```json +{ + "nextPageEventId": 350909170294785, + "events": [ + { + "type": "HUB_EVENT_TYPE_MERGE_USERNAME_PROOF", + "id": 350909155450880, + "mergeUsernameProofBody": { + "usernameProof": { + "timestamp": 1695049760, + "name": "nftonyp", + "owner": "0x23b3c29900762a70def5dc8890e09dc9019eb553", + "signature": "xp41PgeOz...9Jw5vT/eLnGphJpNshw=", + "fid": 20114, + "type": "USERNAME_TYPE_FNAME" + } + } + }, + ... + ] +} +``` diff --git a/docs/zh/reference/hubble/httpapi/fids.md b/docs/zh/reference/hubble/httpapi/fids.md new file mode 100644 index 0000000..06e60b0 --- /dev/null +++ b/docs/zh/reference/hubble/httpapi/fids.md @@ -0,0 +1,25 @@ +# Fids API + +## fids + +获取所有 FID 的列表 + +**查询参数** +| 参数 | 描述 | 示例 | +| --------- | ----------- | ------- | +| | 此接口不接受任何参数 | | + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/fids +``` + +**响应** + +```json +{ + "fids": [1, 2, 3, 4, 5, 6], + "nextPageToken": "AAAnEA==" +} +``` diff --git a/docs/zh/reference/hubble/httpapi/httpapi.md b/docs/zh/reference/hubble/httpapi/httpapi.md new file mode 100644 index 0000000..230f56c --- /dev/null +++ b/docs/zh/reference/hubble/httpapi/httpapi.md @@ -0,0 +1,130 @@ +# HTTP API + +Hubble 默认在 2281 端口提供 HTTP API 服务。 + +## 使用 API + +该 API 可通过常规 HTTP 请求从任何编程语言或浏览器调用。 + +**在浏览器中查看 API 响应** + +直接在浏览器中打开 URL + +```url +http://127.0.0.1:2281/v1/castsByFid?fid=2 +``` + +**使用 curl 调用 API** + +```bash +curl http://127.0.0.1:2281/v1/castsByFid?fid=2 +``` + +**通过 Javascript 调用 API(使用 axios 库)** + +```Javascript +import axios from "axios"; + +const fid = 2; +const server = "http://127.0.0.1:2281"; + +try { + const response = await axios.get(`${server}/v1/castsByFid?fid=${fid}`); + + console.log(`API 返回 HTTP 状态码 ${response.status}`); + console.log(`第一条 Cast 的文本内容是 ${response.messages[0].data.castAddBody.text}`); +} catch (e) { + // 错误处理 + console.log(response); +} +``` + +**OpenAPI 规范** + +Hubble REST API 提供了 OpenAPI 规范,可[在此处](https://redocly.github.io/redoc/?url=https://raw.githubusercontent.com/farcasterxyz/hub-monorepo/main/packages/hub-nodejs/spec.yaml)查看。 + +## 响应编码 + +API 响应以 `application/json` 格式编码,可按常规 JSON 对象解析。 + +1. 哈希值、ETH 地址、密钥等均以 `0x` 开头的十六进制字符串编码 +2. 签名和其他二进制字段以 base64 编码 +3. 常量编码为其字符串类型。例如,`hashScheme` 编码为 `HASH_SCHEME_BLAKE3`,对应 protobuf 模式中的 `HASH_SCHEME_BLAKE3 = 1` + +## 时间戳 + +消息中的时间戳表示自 Farcaster 纪元(2021 年 1 月 1 日 00:00:00 UTC)以来的秒数。 + +## 分页 + +大多数端点支持分页以获取大量响应。 + +**分页查询参数** + +| 参数 | 描述 | 示例 | +| --------- | ------------------------------------------------------------------ | ----------------------------------- | +| pageSize | 单次响应返回的最大消息数量 | `pageSize=100` | +| reverse | 反转排序顺序,优先返回最新消息 | `reverse=1` | +| pageToken | 前一查询返回的分页令牌,用于获取下一页。若该参数为空,则获取第一页 | `pageToken=AuzO1V0Dta...fStlynsGWT` | + +若 `nextPageToken` 返回为空,则表示没有更多页面可返回。 + +分页查询参数可与端点支持的其他查询参数组合使用。例如 `/v1/casts?fid=2&pageSize=3`。 + +**示例** + +获取 FID `2` 的所有 Casts,每页最多获取 3 条 + +```bash +# 获取第一页 +http://127.0.0.1:2281/v1/castsByFid?fid=2&pageSize=3 + +# 获取下一页。pageToken 来自前一响应(`response.nextPageToken`) +http://127.0.0.1:2281/v1/castsByFid?fid=2&pageSize=3&pageToken=AuzO1V0DtaItCwwa10X6YsfStlynsGWT +``` + +**Javascript 示例** + +```Javascript +import axios from "axios"; + +const fid = 2; +const server = "http://127.0.0.1:2281"; + +let nextPageToken = ""; +do { + const response = await axios.get(`${server}/v1/castsByFid?fid=${fid}&pageSize=100&nextPageToken=${nextPageToken}`); + // 处理响应... + nextPageToken = response.nextPageToken; +} while (nextPageToken !== "") +``` + +## 错误处理 + +若 API 出错,HTTP 状态码将设为 `400` 或 `500`(视情况而定)。响应为包含 `detail`、`errCode` 和 `metadata` 字段的 JSON 对象,用于识别和调试错误。 + +**示例** + +```bash +$ curl "http://127.0.0.1:2281/v1/castById?fid=invalid" +{ + "errCode": "bad_request.validation_failure", + "presentable": false, + "name": "HubError", + "code": 3, + "details": "fid 必须为整数", + "metadata": { + "errcode": [ + "bad_request.validation_failure", + ], + }, +} +``` + +## CORS + +运行 Hubble 实例时,可通过 `--http-cors-origin` 参数设置自定义 CORS 头。设为 `*` 将允许来自任何源的请求。 + +## 限制 + +当前 HTTP API 不支持 gRPC 版本中可用的任何同步 API。当 Hub 之间相互同步时,将使用 gRPC API 而非 HTTP API。 diff --git a/docs/zh/reference/hubble/httpapi/info.md b/docs/zh/reference/hubble/httpapi/info.md new file mode 100644 index 0000000..52f464c --- /dev/null +++ b/docs/zh/reference/hubble/httpapi/info.md @@ -0,0 +1,35 @@ +# Info API + +## info + +获取 Hub 的信息 + +**查询参数** +| 参数 | 描述 | 示例 | +| --------- | ----------- | ------- | +| dbstats | 是否返回数据库统计信息 | `dbstats=1` | + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/info?dbstats=1 + +``` + +**响应** + +```json +{ + "version": "1.5.5", + "isSyncing": false, + "nickname": "Farcaster Hub", + "rootHash": "fa349603a6c29d27041225261891bc9bc846bccb", + "dbStats": { + "numMessages": 4191203, + "numFidEvents": 20287, + "numFnameEvents": 20179 + }, + "peerId": "12D3KooWNr294AH1fviDQxRmQ4K79iFSGoRCWzGspVxPprJUKN47", + "hubOperatorFid": 6833 +} +``` diff --git a/docs/zh/reference/hubble/httpapi/links.md b/docs/zh/reference/hubble/httpapi/links.md new file mode 100644 index 0000000..0bfae36 --- /dev/null +++ b/docs/zh/reference/hubble/httpapi/links.md @@ -0,0 +1,134 @@ +# 链接 API + +Link(链接)表示两个用户之间的关系(例如关注)。 + +Links API 接受以下 `link_type` 字段值: + +| 字符串 | 描述 | +| ------ | ------------------------------ | +| follow | 从源 FID 到目标 FID 的关注关系 | + +## linkById + +通过源 FID 和目标 FID 获取链接。 + +**查询参数** +| 参数 | 描述 | 示例 | +| --------- | ----------- | ------- | +| fid | 链接发起者的 FID | `fid=6833` | +| target_fid | 链接目标的 FID | `target_fid=2` | +| link_type | 链接类型,字符串值 | `link_type=follow` | + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/linkById?fid=6833&target_fid=2&link_type=follow +``` + +**响应** + +```json +{ + "data": { + "type": "MESSAGE_TYPE_LINK_ADD", + "fid": 6833, + "timestamp": 61144470, + "network": "FARCASTER_NETWORK_MAINNET", + "linkBody": { + "type": "follow", + "targetFid": 2 + } + }, + "hash": "0x58c23eaf4f6e597bf3af44303a041afe9732971b", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "sMypYEMqSyY...nfCA==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x0852c07b56...06e999cdd" +} +``` + +## linksByFid + +获取来自某个源 FID 的所有链接。 + +**查询参数** +| 参数 | 描述 | 示例 | +| --------- | ----------- | ------- | +| fid | 链接创建者的 FID | `fid=6833` | +| link_type | 链接类型,字符串值 | `link_type=follow` | + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/linksByFid?fid=6833 +``` + +**响应** + +```json +{ + "messages": [ + { + "data": { + "type": "MESSAGE_TYPE_LINK_ADD", + "fid": 6833, + "timestamp": 61144470, + "network": "FARCASTER_NETWORK_MAINNET", + "linkBody": { + "type": "follow", + "targetFid": 83 + } + }, + "hash": "0x094e35891519c0e04791a6ba4d2eb63d17462f02", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "qYsfX08mS...McYq6IYMl+ECw==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x0852c0...a06e999cdd" + } + ], + "nextPageToken": "" +} +``` + +## linksByTargetFid + +获取指向某个目标 FID 的所有链接。 + +**查询参数** +| 参数 | 描述 | 示例 | +| --------- | ----------- | ------- | +| target_fid | 链接目标的 FID | `fid=6833` | +| link_type | 链接类型,字符串值 | `link_type=follow` | + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/linksByTargetFid?target_fid=6833 +``` + +**响应** + +```json +{ + "messages": [ + { + "data": { + "type": "MESSAGE_TYPE_LINK_ADD", + "fid": 302, + "timestamp": 61144668, + "network": "FARCASTER_NETWORK_MAINNET", + "linkBody": { + "type": "follow", + "targetFid": 6833 + } + }, + "hash": "0x78c62531d96088f640ffe7e62088b49749efe286", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "frIZJGIizv...qQd9QJyCg==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x59a04...6860ddfab" + } + ], + "nextPageToken": "" +} +``` diff --git a/docs/zh/reference/hubble/httpapi/message.md b/docs/zh/reference/hubble/httpapi/message.md new file mode 100644 index 0000000..adb8502 --- /dev/null +++ b/docs/zh/reference/hubble/httpapi/message.md @@ -0,0 +1,235 @@ +# 消息 API + +消息 API 允许您验证并向 Hub 提交已签名的 Farcaster 协议消息。请注意,消息必须作为 protobuf 的编码字节流(在 TypeScript 中为 `Message.encode(msg).finish()`)通过 POST 数据发送到端点。 + +POST 数据的编码必须设置为 `application/octet-stream`。如果消息成功提交或验证,端点将返回 JSON 格式的 Message 对象。 + +## submitMessage + +向 Hub 提交已签名的 protobuf 序列化消息 + +**查询参数** +| 参数 | 描述 | 示例 | +| --------- | ----------- | ------- | +| | 此端点不接受任何参数 | | + +**示例** + +```bash +curl -X POST "http://127.0.0.1:2281/v1/submitMessage" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@message.encoded.protobuf" + +``` + +**响应** + +```json +{ + "data": { + "type": "MESSAGE_TYPE_CAST_ADD", + "fid": 2, + "timestamp": 48994466, + "network": "FARCASTER_NETWORK_MAINNET", + "castAddBody": { + "embedsDeprecated": [], + "mentions": [], + "parentCastId": { + "fid": 226, + "hash": "0xa48dd46161d8e57725f5e26e34ec19c13ff7f3b9" + }, + "text": "Cast Text", + "mentionsPositions": [], + "embeds": [] + } + }, + "hash": "0xd2b1ddc6c88e865a33cb1a565e0058d757042974", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "3msLXzxB4eEYe...dHrY1vkxcPAA==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x78ff9a...58c" +} +``` + +### 认证 + +如果服务器启用了 RPC 认证(使用 `--rpc-auth username:password`),在调用 `submitMessage` 时还需要通过 HTTP 基本认证传递用户名和密码。 + +**示例** + +```bash +curl -X POST "http://127.0.0.1:2281/v1/submitMessage" \ + -u "username:password" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@message.encoded.protobuf" +``` + +**JS 示例** + +```Javascript +import axios from "axios"; + +const url = `http://127.0.0.1:2281/v1/submitMessage`; + +const postConfig = { + headers: { "Content-Type": "application/octet-stream" }, + auth: { username: "username", password: "password" }, +}; + +// 将消息编码为字节 Buffer +const messageBytes = Buffer.from(Message.encode(castAdd).finish()); + +try { + const response = await axios.post(url, messageBytes, postConfig); +} catch (e) { + // 处理错误... +} +``` + +## validateMessage + +通过 Hub 验证已签名的 protobuf 序列化消息。这可用于验证 Hub 是否认为消息有效,或验证无法提交的消息(例如 Frame 操作)。 + +::: details +Hub 对所有消息进行以下验证: + +- FID 已注册 +- 签名者处于活跃状态并已注册到 FID +- 消息哈希正确 +- 签名有效且与签名者对应 +- 任何其他特定于消息的验证 + +对于 FrameAction 消息,请注意 Hub 不会验证 castId 是否实际存在,也不会验证 frame URL 是否与 cast 中嵌入的 URL 匹配。如果这对您的应用很重要,请确保进行检查。 + +::: + +**查询参数** +| 参数 | 描述 | 示例 | +| --------- | ----------- | ------- | +| | 此端点不接受任何参数 | | + +**示例** + +```bash +curl -X POST "http://127.0.0.1:2281/v1/validateMessage" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@message.encoded.protobuf" + +``` + +**响应** + +```json +{ + "valid": true, + "message": { + "data": { + "type": "MESSAGE_TYPE_FRAME_ACTION", + "fid": 2, + "timestamp": 48994466, + "network": "FARCASTER_NETWORK_MAINNET", + "frameActionBody": { + "url": "https://fcpolls.com/polls/1", + "buttonIndex": 2, + "inputText": "", + "castId": { + "fid": 226, + "hash": "0xa48dd46161d8e57725f5e26e34ec19c13ff7f3b9" + } + } + }, + "hash": "0xd2b1ddc6c88e865a33cb1a565e0058d757042974", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "3msLXzxB4eEYe...dHrY1vkxcPAA==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x78ff9a...58c" + } +} +``` + +## 在 Rust、Go 或其他编程语言中使用 + +消息需要使用属于 FID 的 Ed25519 账户密钥签名。如果您使用的编程语言不是 TypeScript,可以手动构造 `MessageData` 对象并将其序列化为消息的 `data_bytes` 字段。然后,使用 `data_bytes` 计算 `hash` 和 `signature`。更多详情请参阅 [`rust-submitmessage` 示例](https://github.com/farcasterxyz/hub-monorepo/tree/main/packages/hub-web/examples)。 + +```rust +use ed25519_dalek::{SecretKey, Signer, SigningKey}; +use hex::FromHex; +use reqwest::Client; + +use message::{CastAddBody, FarcasterNetwork, MessageData}; +use protobuf::Message; + + +#[tokio::main] +async fn main() { + let fid = 6833; // 提交消息的用户 FID + let network = FarcasterNetwork::FARCASTER_NETWORK_MAINNET; + + // 构造 cast add 消息 + let mut cast_add = CastAddBody::new(); + cast_add.set_text("Welcome to Rust!".to_string()); + + // 构造 cast add 消息数据对象 + let mut msg_data = MessageData::new(); + msg_data.set_field_type(message::MessageType::MESSAGE_TYPE_CAST_ADD); + msg_data.set_fid(fid); + msg_data.set_timestamp( + (std::time::SystemTime::now() + .duration_since(FARCASTER_EPOCH) + .unwrap() + .as_secs()) as u32, + ); + msg_data.set_network(network); + msg_data.set_cast_add_body(cast_add); + + let msg_data_bytes = msg_data.write_to_bytes().unwrap(); + + // 计算 blake3 哈希,截断为 20 字节 + let hash = blake3::hash(&msg_data_bytes).as_bytes()[0..20].to_vec(); + + // 构造实际消息 + let mut msg = message::Message::new(); + msg.set_hash_scheme(message::HashScheme::HASH_SCHEME_BLAKE3); + msg.set_hash(hash); + + // 签名消息。您需要使用与要添加的 FID 对应的签名密钥。 + // 请替换为您自己的私钥 + let private_key = SigningKey::from_bytes( + &SecretKey::from_hex("0x...").expect("请提供有效的私钥"), + ); + let signature = private_key.sign(&msg_data_bytes).to_bytes(); + + msg.set_signature_scheme(message::SignatureScheme::SIGNATURE_SCHEME_ED25519); + msg.set_signature(signature.to_vec()); + msg.set_signer(private_key.verifying_key().to_bytes().to_vec()); + + // 序列化消息 + msg.set_data_bytes(msg_data_bytes.to_vec()); + let msg_bytes = msg.write_to_bytes().unwrap(); + + // 最后,将消息提交到网络 + + // 创建 reqwest Client + let client = Client::new(); + + // 定义端点 URL + let url = "http://127.0.0.1:2281/v1/submitMessage"; + + // 发送 POST 请求 + let res = client + .post(url) + .header("Content-Type", "application/octet-stream") + .body(msg_bytes) + .send() + .await + .unwrap(); + + // 检查是否成功 + if res.status().is_success() { + println!("消息发送成功。"); + } else { + println!("消息发送失败。HTTP 状态码: {}", res.status()); + } +} + +``` diff --git a/docs/zh/reference/hubble/httpapi/onchain.md b/docs/zh/reference/hubble/httpapi/onchain.md new file mode 100644 index 0000000..db2b54a --- /dev/null +++ b/docs/zh/reference/hubble/httpapi/onchain.md @@ -0,0 +1,133 @@ +# 链上 API + +## onChainSignersByFid + +获取由 FID 提供的账户密钥(签名者)列表 + +**查询参数** +| 参数 | 描述 | 示例 | +| --------- | ----------- | ------- | +| fid | 请求的 FID | `fid=2` | +| signer | 可选的签名者密钥 | `signer=0x0852c07b5695ff94138b025e3f9b4788e06133f04e254f0ea0eb85a06e999cdd` | + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/onChainSignersByFid?fid=6833 +``` + +**响应** + +```json +{ + "events": [ + { + "type": "EVENT_TYPE_SIGNER", + "chainId": 10, + "blockNumber": 108875854, + "blockHash": "0xceb1cdc21ee319b06f0455f1cedc0cd4669b471d283a5b2550b65aba0e0c1af0", + "blockTimestamp": 1693350485, + "transactionHash": "0x76e20cf2f7c3db4b78f00f6bb9a7b78b0acfb1eca4348c1f4b5819da66eb2bee", + "logIndex": 2, + "fid": 6833, + "signerEventBody": { + "key": "0x0852c07b5695ff94138b025e3f9b4788e06133f04e254f0ea0eb85a06e999cdd", + "keyType": 1, + "eventType": "SIGNER_EVENT_TYPE_ADD", + "metadata": "AAAAAAAAAAAA...AAAAAAAA", + "metadataType": 1 + }, + "txIndex": 0 + } + ] +} +``` + +## onChainEventsByFid + +获取由 FID 提供的账户密钥列表 + +**查询参数** +| 参数 | 描述 | 示例 | +| --------- | ----------- | ------- | +| fid | 请求的 FID | `fid=2` | +| event_type | 请求的事件类型的数值或字符串值。此参数为必填项 | `event_type=1` 或 `event_type=EVENT_TYPE_STORAGE_RENT` | + +onChainEventsByFid API 接受以下 `event_type` 字段值: + +| 字符串 | 数值 | +| -------------------------- | ---- | +| EVENT_TYPE_SIGNER | 1 | +| EVENT_TYPE_SIGNER_MIGRATED | 2 | +| EVENT_TYPE_ID_REGISTER | 3 | +| EVENT_TYPE_STORAGE_RENT | 4 | + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/onChainEventsByFid?fid=3&event_type=1 +``` + +**响应** + +```json +{ + "events": [ + { + "type": "EVENT_TYPE_SIGNER", + "chainId": 10, + "blockNumber": 108875456, + "blockHash": "0x75fbbb8b2a4ede67ac350e1b0503c6a152c0091bd8e3ef4a6927d58e088eae28", + "blockTimestamp": 1693349689, + "transactionHash": "0x36ef79e6c460e6ae251908be13116ff0065960adb1ae032b4cc65a8352f28952", + "logIndex": 2, + "fid": 3, + "signerEventBody": { + "key": "0xc887f5bf385a4718eaee166481f1832198938cf33e98a82dc81a0b4b81ffe33d", + "keyType": 1, + "eventType": "SIGNER_EVENT_TYPE_ADD", + "metadata": "AAAAAAAAA...AAAAA", + "metadataType": 1 + }, + "txIndex": 0 + } + ] +} +``` + +## onChainIdRegistryEventByAddress + +获取给定地址的链上事件列表 + +**查询参数** +| 参数 | 描述 | 示例 | +| --------- | ----------- | ------- | +| address | 请求的 ETH 地址 | `address=0x74232bf61e994655592747e20bdf6fa9b9476f79` | + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/onChainIdRegistryEventByAddress?address=0x74232bf61e994655592747e20bdf6fa9b9476f79 +``` + +**响应** + +```json +{ + "type": "EVENT_TYPE_ID_REGISTER", + "chainId": 10, + "blockNumber": 108874508, + "blockHash": "0x20d83804a26247ad8c26d672f2212b28268d145b8c1cefaa4126f7768f46682e", + "blockTimestamp": 1693347793, + "transactionHash": "0xf3481fc32227fbd982b5f30a87be32a2de1fc5736293cae7c3f169da48c3e764", + "logIndex": 7, + "fid": 3, + "idRegisterEventBody": { + "to": "0x74232bf61e994655592747e20bdf6fa9b9476f79", + "eventType": "ID_REGISTER_EVENT_TYPE_REGISTER", + "from": "0x", + "recoveryAddress": "0x00000000fcd5a8e45785c8a4b9a718c9348e4f18" + }, + "txIndex": 0 +} +``` diff --git a/docs/zh/reference/hubble/httpapi/reactions.md b/docs/zh/reference/hubble/httpapi/reactions.md new file mode 100644 index 0000000..c4d4950 --- /dev/null +++ b/docs/zh/reference/hubble/httpapi/reactions.md @@ -0,0 +1,187 @@ +# 反应(Reactions)API + +反应 API 将接受以下 `reaction_type` 字段值(可以是字符串表示或数值)。 + +| 字符串 | 数值 | 描述 | +| -------------------- | ---- | ---------------------------- | +| REACTION_TYPE_LIKE | 1 | 点赞目标 cast | +| REACTION_TYPE_RECAST | 2 | 将目标 cast 分享给用户的受众 | + +## reactionById + +通过创建者 FID 和目标 Cast 获取反应。 + +**查询参数** +| 参数 | 描述 | 示例 | +| ---- | ---- | ---- | +| fid | 反应创建者的 FID | `fid=6833` | +| target_fid | cast 创建者的 FID | `target_fid=2` | +| target_hash | cast 的哈希值 | `target_hash=0xa48dd46161d8e57725f5e26e34ec19c13ff7f3b9` | +| reaction_type | 反应类型,可以是数值枚举值或字符串表示 | `reaction_type=1` 或 `reaction_type=REACTION_TYPE_LIKE` | + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/reactionById?fid=2&reaction_type=1&target_fid=1795&target_hash=0x7363f449bfb0e7f01c5a1cc0054768ed5146abc0 +``` + +**响应** + +```json +{ + "data": { + "type": "MESSAGE_TYPE_REACTION_ADD", + "fid": 2, + "timestamp": 72752656, + "network": "FARCASTER_NETWORK_MAINNET", + "reactionBody": { + "type": "REACTION_TYPE_LIKE", + "targetCastId": { + "fid": 1795, + "hash": "0x7363f449bfb0e7f01c5a1cc0054768ed5146abc0" + } + } + }, + "hash": "0x9fc9c51f6ea3acb84184efa88ba4f02e7d161766", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "F2OzKsn6Wj...gtyORbyCQ==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x78ff9a7...647b6d62558c" +} +``` + +## reactionsByFid + +获取某个 FID 的所有反应 + +**查询参数** +| 参数 | 描述 | 示例 | +| ---- | ---- | ---- | +| fid | 反应创建者的 FID | `fid=6833` | +| reaction_type | 反应类型,可以是数值枚举值或字符串表示 | `reaction_type=1` 或 `reaction_type=REACTION_TYPE_LIKE` | + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/reactionsByFid?fid=2&reaction_type=1 +``` + +**响应** + +```json +{ + "messages": [ + { + "data": { + "type": "MESSAGE_TYPE_REACTION_ADD", + "fid": 2, + "timestamp": 72752656, + "network": "FARCASTER_NETWORK_MAINNET", + "reactionBody": { + "type": "REACTION_TYPE_LIKE", + "targetCastId": { + "fid": 1795, + "hash": "0x7363f449bfb0e7f01c5a1cc0054768ed5146abc0" + } + } + }, + "hash": "0x9fc9c51f6ea3acb84184efa88ba4f02e7d161766", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "F2OzKsn6WjP8MTw...hqUbrAvp6mggtyORbyCQ==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x78ff9a768...62558c" + } + ], + "nextPageToken": "" +} +``` + +## reactionsByCast + +获取某个 cast 的所有反应 + +**查询参数** +| 参数 | 描述 | 示例 | +| ---- | ---- | ---- | +| target_fid | cast 创建者的 FID | `fid=6833` | +| target_hash | cast 的哈希值 | `target_hash=`0x7363f449bfb0e7f01c5a1cc0054768ed5146abc0`| +| reaction_type | 反应类型,可以是数值枚举值或字符串表示 |`reaction_type=1`或`reaction_type=REACTION_TYPE_LIKE` | + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/reactionsByCast?target_fid=2&reaction_type=1&target_hash=0x7363f449bfb0e7f01c5a1cc0054768ed5146abc0 +``` + +**响应** + +```json +{ + "messages": [ + { + "data": { + "type": "MESSAGE_TYPE_REACTION_ADD", + "fid": 426, + "timestamp": 72750141, + "network": "FARCASTER_NETWORK_MAINNET", + "reactionBody": { + "type": "REACTION_TYPE_LIKE", + "targetCastId": { + "fid": 1795, + "hash": "0x7363f449bfb0e7f01c5a1cc0054768ed5146abc0" + } + } + }, + "hash": "0x7662fba1be3166fc75acc0914a7b0e53468d5e7a", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "tmAUEYlt/+...R7IO3CA==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x13dd2...204e57bc2a" + } + ], + "nextPageToken": "" +} +``` + +## reactionsByTarget + +获取对 cast 目标 URL 的所有反应 + +**查询参数** +| 参数 | 描述 | 示例 | +| ---- | ---- | ---- | +| url | 父级 cast 的 URL | url=chain://eip155:1/erc721:0x39d89b649ffa044383333d297e325d42d31329b2 | +| reaction_type | 反应类型,可以是数值枚举值或字符串表示 | `reaction_type=1` 或 `reaction_type=REACTION_TYPE_LIKE` | + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/reactionsByTarget?url=chain://eip155:1/erc721:0x39d89b649ffa044383333d297e325d42d31329b2 +``` + +**响应** + +```json +{ + "messages": [ + { + "data": { + "type": "MESSAGE_TYPE_REACTION_ADD", + "fid": 1134, + "timestamp": 79752856, + "network": "FARCASTER_NETWORK_MAINNET", + "reactionBody": { + "type": "REACTION_TYPE_LIKE", + "targetUrl": "chain://eip155:1/erc721:0x39d89b649ffa044383333d297e325d42d31329b2" + } + }, + "hash": "0x94a0309cf11a07b95ace71c62837a8e61f17adfd", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "+f/+M...0Uqzd0Ag==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0xf6...3769198d4c" + } + ], + "nextPageToken": "" +} +``` diff --git a/docs/zh/reference/hubble/httpapi/storagelimits.md b/docs/zh/reference/hubble/httpapi/storagelimits.md new file mode 100644 index 0000000..af22e3c --- /dev/null +++ b/docs/zh/reference/hubble/httpapi/storagelimits.md @@ -0,0 +1,49 @@ +# 存储 API + +## storageLimitsByFid + +获取某个 FID 的存储限制。 + +**查询参数** +| 参数 | 描述 | 示例 | +| --------- | ----------- | ------- | +| fid | 请求查询的 FID | `fid=6833` | + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/storageLimitsByFid?fid=6833 +``` + +**响应** + +```json +{ + "limits": [ + { + "storeType": "STORE_TYPE_CASTS", + "limit": 10000 + }, + { + "storeType": "STORE_TYPE_LINKS", + "limit": 5000 + }, + { + "storeType": "STORE_TYPE_REACTIONS", + "limit": 5000 + }, + { + "storeType": "STORE_TYPE_USER_DATA", + "limit": 100 + }, + { + "storeType": "STORE_TYPE_USERNAME_PROOFS", + "limit": 10 + }, + { + "storeType": "STORE_TYPE_VERIFICATIONS", + "limit": 50 + } + ] +} +``` diff --git a/docs/zh/reference/hubble/httpapi/userdata.md b/docs/zh/reference/hubble/httpapi/userdata.md new file mode 100644 index 0000000..54aaab9 --- /dev/null +++ b/docs/zh/reference/hubble/httpapi/userdata.md @@ -0,0 +1,55 @@ +# UserData API + +UserData API 的 `user_data_type` 字段接受以下值。 + +| 字符串 | 数值 | 描述 | +| ----------------------- | ---- | --------------------- | +| USER_DATA_TYPE_PFP | 1 | 用户的个人资料图片 | +| USER_DATA_TYPE_DISPLAY | 2 | 用户的显示名称 | +| USER_DATA_TYPE_BIO | 3 | 用户的个人简介 | +| USER_DATA_TYPE_URL | 5 | 用户的 URL | +| USER_DATA_TYPE_USERNAME | 6 | 用户的偏好名称 | +| USER_DATA_TYPE_LOCATION | 7 | 用户的位置 | +| USER_DATA_TYPE_TWITTER | 8 | 用户的 Twitter 用户名 | +| USER_DATA_TYPE_GITHUB | 9 | 用户的 GitHub 用户名 | + +有关 Location 的更多信息,请参阅 [FIP-196](https://github.com/farcasterxyz/protocol/discussions/196)。 +有关 Twitter/X 和 Github 用户名的更多信息,请参阅 [FIP-19](https://github.com/farcasterxyz/protocol/discussions/199)。 + +## userDataByFid + +获取指定 FID 的 UserData。 + +**查询参数** +| 参数 | 描述 | 示例 | +| --------- | ----------- | ------- | +| fid | 请求的 FID | `fid=6833` | +| user_data_type | 用户数据类型,可以是数值或类型字符串。如果省略此参数,则返回该 FID 的所有用户数据 | `user_data_type=1` 或 `user_data_type=USER_DATA_TYPE_DISPLAY` | + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/userDataByFid?fid=6833&user_data_type=1 +``` + +**响应** + +```json +{ + "data": { + "type": "MESSAGE_TYPE_USER_DATA_ADD", + "fid": 6833, + "timestamp": 83433831, + "network": "FARCASTER_NETWORK_MAINNET", + "userDataBody": { + "type": "USER_DATA_TYPE_PFP", + "value": "https://i.imgur.com/HG54Hq6.png" + } + }, + "hash": "0x327b8f47218c369ae01cc453cc23efc79f10181f", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "XITQZD7q...LdAlJ9Cg==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x0852...6e999cdd" +} +``` diff --git a/docs/zh/reference/hubble/httpapi/usernameproof.md b/docs/zh/reference/hubble/httpapi/usernameproof.md new file mode 100644 index 0000000..e521f3f --- /dev/null +++ b/docs/zh/reference/hubble/httpapi/usernameproof.md @@ -0,0 +1,69 @@ +# 用户名证明 API + +## userNameProofByName + +通过 Farcaster 用户名获取用户名证明 + +**查询参数** +| 参数 | 描述 | 示例 | +| --------- | ----------- | ------- | +| name | Farcaster 用户名或 ENS 地址 | `name=adityapk` 或 `name=dwr.eth` | + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/userNameProofByName?name=adityapk +``` + +**响应** + +```json +{ + "timestamp": 1670603245, + "name": "adityapk", + "owner": "Oi7uUaECifDm+larm+rzl3qQhcM=", + "signature": "fo5OhBP/ud...3IoJdhs=", + "fid": 6833, + "type": "USERNAME_TYPE_FNAME" +} +``` + +## userNameProofsByFid + +获取由 FID 提供的一系列证明 + +**查询参数** +| 参数 | 描述 | 示例 | +| --------- | ----------- | ------- | +| fid | 请求的 FID | `fid=2` | + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/userNameProofsByFid?fid=2 +``` + +**响应** + +```json +{ + "proofs": [ + { + "timestamp": 1623910393, + "name": "v", + "owner": "0x4114e33eb831858649ea3702e1c9a2db3f626446", + "signature": "bANBae+Ub...kr3Bik4xs=", + "fid": 2, + "type": "USERNAME_TYPE_FNAME" + }, + { + "timestamp": 1690329118, + "name": "varunsrin.eth", + "owner": "0x182327170fc284caaa5b1bc3e3878233f529d741", + "signature": "zCEszPt...zqxTiFqVBs=", + "fid": 2, + "type": "USERNAME_TYPE_ENS_L1" + } + ] +} +``` diff --git a/docs/zh/reference/hubble/httpapi/verification.md b/docs/zh/reference/hubble/httpapi/verification.md new file mode 100644 index 0000000..0890692 --- /dev/null +++ b/docs/zh/reference/hubble/httpapi/verification.md @@ -0,0 +1,45 @@ +# 验证信息 API + +## verificationsByFid + +获取指定 FID 提供的验证信息列表 + +**查询参数** +| 参数 | 描述 | 示例 | +| --------- | ----------- | ------- | +| fid | 请求的 FID 编号 | `fid=2` | +| address | 可选的 ETH 地址过滤条件 | `address=0x91031dcfdea024b4d51e775486111d2b2a715871` | + +**示例** + +```bash +curl http://127.0.0.1:2281/v1/verificationsByFid?fid=2 +``` + +**响应** + +```json +{ + "messages": [ + { + "data": { + "type": "MESSAGE_TYPE_VERIFICATION_ADD_ETH_ADDRESS", + "fid": 2, + "timestamp": 73244540, + "network": "FARCASTER_NETWORK_MAINNET", + "verificationAddEthAddressBody": { + "address": "0x91031dcfdea024b4d51e775486111d2b2a715871", + "ethSignature": "tyxj1...x1cYzhyxw=", + "blockHash": "0xd74860c4bbf574d5ad60f03a478a30f990e05ac723e138a5c860cdb3095f4296" + } + }, + "hash": "0xa505331746ec8c5110a94bdb098cd964e43a8f2b", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "bln1zIZM.../4riB9IVBQ==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x78ff9...b6d62558c" + } + ], + "nextPageToken": "" +} +``` diff --git a/docs/zh/reference/index.md b/docs/zh/reference/index.md new file mode 100644 index 0000000..7435ab2 --- /dev/null +++ b/docs/zh/reference/index.md @@ -0,0 +1,12 @@ +# 概述 + +参考章节记录了 Farcaster 开发者常用的 API、标准和协议文档。 + + +- [Mini Apps](https://miniapps.farcaster.xyz){target="_self"} - 编写和渲染 mini app 的规范说明。 +- [Warpcast](/zh/reference/warpcast/api) - Warpcast 公开 API 的概览文档。 +- [Hubble](/zh/reference/hubble/architecture) - Farcaster Hubs 的设计概述与 API 参考。 +- [Replicator](/zh/reference/replicator/schema) - 复制器的概述与数据模式说明。 +- [Contracts](/zh/reference/contracts/index) - Farcaster 合约的设计概述与 ABI 参考。 +- [FName Registry](/zh/reference/fname/api) - Farcaster 名称服务的概述与 API 参考。 +- [Neynar](/zh/reference/third-party/neynar/index) - 帮助开发者快速上手 Farcaster 的 Neynar API 概览 diff --git a/docs/zh/reference/replicator/schema.md b/docs/zh/reference/replicator/schema.md new file mode 100644 index 0000000..25eeb70 --- /dev/null +++ b/docs/zh/reference/replicator/schema.md @@ -0,0 +1,219 @@ +# 数据表结构 + +以下表格创建于 Postgres 数据库中,用于存储来自 Hubs 的数据: + +## chain_events(链上事件表) + +存储从 hub 事件流接收的所有链上事件。这些事件代表任何链上操作,包括注册、转移、签名者添加/移除、存储租金等。事件永远不会被删除(即该表仅追加数据)。 + +| 列名 | 数据类型 | 描述 | +| ----------------- | -------------------------- | ------------------------------------------------------------------------------------- | +| id | `uuid` | 该数据库特有的通用标识符(又称[代理键](https://en.wikipedia.org/wiki/Surrogate_key)) | +| created_at | `timestamp with time zone` | 该行首次在数据库中创建的时间(与消息时间戳不同!) | +| block_timestamp | `timestamp with time zone` | 事件所在区块的时间戳(UTC)。 | +| fid | `bigint` | 签署消息的用户 FID。 | +| chain_id | `bigint` | 链 ID。 | +| block_number | `bigint` | 事件所在区块的区块号。 | +| transaction_index | `smallint` | 交易在区块中的索引。 | +| log_index | `smallint` | 日志事件在区块中的索引。 | +| type | `smallint` | 链事件类型。 | +| block_hash | `bytea` | 事件所在区块的哈希值。 | +| transaction_hash | `bytea` | 触发该事件的交易哈希值。 | +| body | `json` | 链事件主体的 JSON 表示(根据 `type` 不同而变化)。 | +| raw | `bytea` | 序列化 `OnChainEvent` [protobuf](https://protobuf.dev/) 的原始字节。 | + +## fids(FID 表) + +存储 Farcaster 网络上所有已注册的 FID。 + +| 列名 | 数据类型 | 描述 | +| ---------------- | -------------------------- | ------------------------------------------------ | +| fid | `bigint` | 用户 FID(主键) | +| created_at | `timestamp with time zone` | 该行首次在数据库中创建的时间(与注册时间不同!) | +| updated_at | `timestamp with time zone` | 该行最后更新时间。 | +| registered_at | `timestamp with time zone` | 用户注册所在区块的时间戳。 | +| chain_event_id | `uuid` | `chain_events` 表中对应此 FID 初始注册行的 ID。 | +| custody_address | `bytea` | 拥有该 FID 的地址。 | +| recovery_address | `bytea` | 可以为此 FID 发起恢复的地址。 | + +## signers(签名者表) + +存储所有已注册的账户密钥(签名者)。 + +| 列名 | 数据类型 | 描述 | +| --------------------- | -------------------------- | ------------------------------------------------------------------------------------- | +| id | `uuid` | 该数据库特有的通用标识符(又称[代理键](https://en.wikipedia.org/wiki/Surrogate_key)) | +| created_at | `timestamp with time zone` | 该行首次在数据库中创建的时间(与密钥在网络上的创建时间不同!) | +| updated_at | `timestamp with time zone` | 该行最后更新时间。 | +| added_at | `timestamp with time zone` | 该签名者被添加的区块时间戳。 | +| removed_at | `timestamp with time zone` | 该签名者被移除的区块时间戳。 | +| fid | `bigint` | 授权此签名者的用户 FID。 | +| requester_fid | `bigint` | 请求此签名者的用户/app 的 FID。 | +| add_chain_event_id | `uuid` | `chain_events` 表中对应此签名者添加事件的行的 ID。 | +| remove_chain_event_id | `uuid` | `chain_events` 表中对应此签名者移除事件的行的 ID。 | +| key_type | `smallint` | 密钥类型。 | +| metadata_type | `smallint` | 元数据类型。 | +| key | `bytea` | 公钥字节。 | +| metadata | `bytea` | 区块链上存储的元数据字节。 | + +## username_proofs(用户名证明表) + +存储所有已观察到的用户名证明。包括已失效的证明(通过 `deleted_at` 列软删除)。查询用户名时,建议直接查询 `fnames` 表而非此表。 + +| 列名 | 数据类型 | 描述 | +| ---------- | -------------------------- | ------------------------------------------------------------------------------------- | +| id | `uuid` | 该数据库特有的通用标识符(又称[代理键](https://en.wikipedia.org/wiki/Surrogate_key)) | +| created_at | `timestamp with time zone` | 该行首次在数据库中创建的时间(与密钥在网络上的创建时间不同!) | +| updated_at | `timestamp with time zone` | 该行最后更新时间。 | +| timestamp | `timestamp with time zone` | 证明消息的时间戳。 | +| deleted_at | `timestamp with time zone` | 此证明被撤销或失效的时间。 | +| fid | `bigint` | 证明中用户名所属的 FID。 | +| type | `smallint` | 证明类型(fname 或 ENS)。 | +| username | `text` | 用户名,例如 fname 为 `dwr`,ENS 名称为 `dwr.eth`。 | +| signature | `bytea` | 证明签名。 | +| owner | `bytea` | 拥有 ENS 名称的钱包地址,或提供证明签名的钱包地址。 | + +## fnames(用户名表) + +存储当前所有已注册的用户名。注意:当用户名被注销时,该行会被软删除(通过 `deleted_at` 列标记),直到为该 FID 注册新用户名。 + +| 列名 | 数据类型 | 描述 | +| ------------- | -------------------------- | ------------------------------------------------------------------------------------- | +| id | `uuid` | 该数据库特有的通用标识符(又称[代理键](https://en.wikipedia.org/wiki/Surrogate_key)) | +| created_at | `timestamp with time zone` | 该行首次在数据库中创建的时间(与密钥在网络上的创建时间不同!) | +| updated_at | `timestamp with time zone` | 该行最后更新时间。 | +| registered_at | `timestamp with time zone` | 用户名证明消息的时间戳。 | +| deleted_at | `timestamp with time zone` | 证明被撤销或用户名从该用户注销的时间。 | +| fid | `bigint` | 用户名所属的 FID。 | +| type | `smallint` | 用户名类型(fname 或 ENS)。 | +| username | `text` | 用户名,例如 fname 为 `dwr`,ENS 名称为 `dwr.eth`。 | + +## messages(消息表) + +存储从 hub 获取的所有 Farcaster 消息。消息永远不会被删除,只会被软删除(即标记为删除但不会从数据库中实际移除)。 + +| 列名 | 数据类型 | 描述 | +| ---------------- | -------------------------- | ------------------------------------------------------------------------------------- | +| id | `uuid` | 该数据库特有的通用标识符(又称[代理键](https://en.wikipedia.org/wiki/Surrogate_key)) | +| created_at | `timestamp with time zone` | 该行首次在数据库中创建的时间(与消息时间戳不同!) | +| updated_at | `timestamp with time zone` | 该行最后更新时间。 | +| timestamp | `timestamp with time zone` | 消息时间戳(UTC)。 | +| deleted_at | `timestamp with time zone` | hub 删除消息的时间(例如响应 `CastRemove` 消息等)。 | +| pruned_at | `timestamp with time zone` | hub 清理消息的时间。 | +| revoked_at | `timestamp with time zone` | 由于签署消息的签名者被撤销,hub 撤销消息的时间。 | +| fid | `bigint` | 签署消息的用户 FID。 | +| type | `smallint` | 消息类型。 | +| hash_scheme | `smallint` | 消息哈希方案。 | +| signature_scheme | `smallint` | 消息哈希方案。 | +| hash | `bytea` | 消息哈希值。 | +| signature | `bytea` | 消息签名。 | +| signer | `bytea` | 用于签署此消息的签名者。 | +| body | `json` | 消息主体的 JSON 表示。 | +| raw | `bytea` | 序列化消息 [protobuf](https://protobuf.dev/) 的原始字节。 | + +## casts(Cast 表) + +表示用户发布的 cast。 + +| 列名 | 数据类型 | 描述 | +| ------------------ | -------------------------- | ------------------------------------------------------------------------------------- | +| id | `uuid` | 该数据库特有的通用标识符(又称[代理键](https://en.wikipedia.org/wiki/Surrogate_key)) | +| created_at | `timestamp with time zone` | 该行首次在数据库中创建的时间(与消息时间戳不同!) | +| updated_at | `timestamp with time zone` | 该行最后更新时间。 | +| timestamp | `timestamp with time zone` | 消息时间戳(UTC)。 | +| deleted_at | `timestamp with time zone` | cast 被 hub 视为删除/撤销/清理的时间(例如响应 `CastRemove` 消息等)。 | +| fid | `bigint` | 签署消息的用户 FID。 | +| parent_fid | `bigint` | 如果此 cast 是回复,则为父 cast 作者的 FID。否则为 `null`。 | +| hash | `bytea` | 消息哈希值。 | +| root_parent_hash | `bytea` | 如果此 cast 是回复,则为回复链中原始 cast 的哈希值。否则为 `null`。 | +| parent_hash | `bytea` | 如果此 cast 是回复,则为父 cast 的哈希值。否则为 `null`。 | +| root_parent_url | `text` | 如果此 cast 是回复,则为回复链中原始 cast 所回复的 URL。 | +| parent_url | `text` | 如果此 cast 是回复 URL(例如 NFT、网页 URL 等),则为该 URL。否则为 `null`。 | +| text | `text` | 移除了提及内容的 cast 原始文本。 | +| embeds | `json` | 与此 cast 一起嵌入的 URL 或 cast ID 数组。 | +| mentions | `json` | cast 中提及的 FID 数组。 | +| mentions_positions | `json` | cast 中提及的 FID 的 UTF8 字节偏移量。 | + +## reactions(反应表) + +表示用户对内容的反应(点赞或转发)。 + +| 列名 | 数据类型 | 描述 | +| ---------------- | -------------------------- | ------------------------------------------------------------------------------------- | +| id | `uuid` | 该数据库特有的通用标识符(又称[代理键](https://en.wikipedia.org/wiki/Surrogate_key)) | +| created_at | `timestamp with time zone` | 该行首次在数据库中创建的时间(与消息时间戳不同!) | +| updated_at | `timestamp with time zone` | 该行最后更新时间。 | +| timestamp | `timestamp with time zone` | 消息时间戳(UTC)。 | +| deleted_at | `timestamp with time zone` | 反应被 hub 视为删除的时间(例如响应 `ReactionRemove` 消息等)。 | +| fid | `bigint` | 签署消息的用户 FID。 | +| target_cast_fid | `bigint` | 如果目标是 cast,则为 cast 作者的 FID。否则为 `null`。 | +| type | `smallint` | 反应类型。 | +| hash | `bytea` | 消息哈希值。 | +| target_cast_hash | `bytea` | 如果目标是 cast,则为 cast 的哈希值。否则为 `null`。 | +| target_url | `text` | 如果目标是 URL(例如 NFT、网页 URL 等),则为该 URL。否则为 `null`。 | + +## links(链接表) + +表示两个 FID 之间的链接(例如关注、订阅等)。 + +| 列名 | 数据类型 | 描述 | +| ----------------- | -------------------------- | ------------------------------------------------------------------------------------- | +| id | `uuid` | 该数据库特有的通用标识符(又称[代理键](https://en.wikipedia.org/wiki/Surrogate_key)) | +| created_at | `timestamp with time zone` | 该行首次在数据库中创建的时间(非链接在网络上的创建时间!) | +| updated_at | `timestamp with time zone` | 该行最后更新时间 | +| timestamp | `timestamp with time zone` | 消息时间戳(UTC)。 | +| deleted_at | `timestamp with time zone` | 链接被 hub 视为删除的时间(例如响应 `LinkRemoveMessage` 消息等)。 | +| fid | `bigint` | Farcaster ID(用户 ID)。 | +| target_fid | `bigint` | 目标用户的 Farcaster ID。 | +| display_timestamp | `timestamp with time zone` | 该行最后更新时间。 | +| type | `string` | 用户间连接类型,例如 `follow`。 | +| hash | `bytea` | 消息哈希值。 | + +## verifications(验证表) + +表示用户在网络上验证某些内容。目前唯一的验证是证明以太坊钱包地址的所有权。 + +| 列名 | 数据类型 | 描述 | +| -------------- | -------------------------- | ------------------------------------------------------------------------------------- | +| id | `uuid` | 该数据库特有的通用标识符(又称[代理键](https://en.wikipedia.org/wiki/Surrogate_key)) | +| created_at | `timestamp with time zone` | 该行首次在数据库中创建的时间(与消息时间戳不同!) | +| updated_at | `timestamp with time zone` | 该行最后更新时间。 | +| timestamp | `timestamp with time zone` | 消息时间戳(UTC)。 | +| deleted_at | `timestamp with time zone` | 验证被 hub 视为删除的时间(例如响应 `VerificationRemove` 消息等)。 | +| fid | `bigint` | 签署消息的用户 FID。 | +| hash | `bytea` | 消息哈希值。 | +| signer_address | `bytea` | 被验证的钱包地址。 | +| block_hash | `bytea` | 验证所有权时最新区块的区块哈希值。 | +| signature | `bytea` | 所有权证明签名。 | + +## user_data(用户数据表) + +表示与用户关联的数据(例如个人资料照片、简介、用户名等)。 + +| 列名 | 数据类型 | 描述 | +| ---------- | -------------------------- | ------------------------------------------------------------------------------------- | +| id | `uuid` | 该数据库特有的通用标识符(又称[代理键](https://en.wikipedia.org/wiki/Surrogate_key)) | +| created_at | `timestamp with time zone` | 该行首次在数据库中创建的时间(与消息时间戳不同!) | +| updated_at | `timestamp with time zone` | 该行最后更新时间。 | +| timestamp | `timestamp with time zone` | 消息时间戳(UTC)。 | +| deleted_at | `timestamp with time zone` | 数据被 hub 视为删除的时间 | +| fid | `bigint` | 签署消息的用户 FID。 | +| type | `smallint` | 用户数据类型(PFP、简介、用户名等)。 | +| hash | `bytea` | 消息哈希值。 | +| value | `text` | 字段的字符串值。 | + +## storage_allocations(存储分配表) + +存储每个 FID 购买的存储单元数量及其过期时间。 + +| 列名 | 数据类型 | 描述 | +| -------------- | -------------------------- | ------------------------------------------------------------------------------------- | +| id | `uuid` | 该数据库特有的通用标识符(又称[代理键](https://en.wikipedia.org/wiki/Surrogate_key)) | +| created_at | `timestamp with time zone` | 该行首次在数据库中创建的时间 | +| updated_at | `timestamp with time zone` | 该行最后更新时间。 | +| rented_at | `timestamp with time zone` | 消息时间戳(UTC)。 | +| expires_at | `timestamp with time zone` | 此存储分配的过期时间。 | +| chain_event_id | `uuid` | `chain_events` 表中表示存储分配的链上事件行的 ID。 | +| fid | `bigint` | 拥有存储的 FID。 | +| units | `smallint` | 分配的存储单元数量。 | +| payer | `bytea` | 支付存储费用的钱包地址。 | diff --git a/docs/zh/reference/third-party/neynar/index.md b/docs/zh/reference/third-party/neynar/index.md new file mode 100644 index 0000000..99b63d1 --- /dev/null +++ b/docs/zh/reference/third-party/neynar/index.md @@ -0,0 +1,12 @@ +# Neynar + +[Neynar](https://neynar.com) 是一家独立的第三方服务提供商,为 Farcaster 数据提供以下服务: + +- [托管式 hub 节点](https://docs.neynar.com/docs/create-a-stream-of-casts) +- [REST API 接口](https://docs.neynar.com/reference/quickstart) +- [签名器管理](https://docs.neynar.com/docs/which-signer-should-you-use-and-why) +- [新账户创建](https://docs.neynar.com/docs/how-to-create-a-new-farcaster-account-with-neynar) +- [Webhook 服务](https://docs.neynar.com/docs/how-to-create-webhooks-on-the-go-using-the-sdk) +- [数据摄取管道](https://docs.neynar.com/docs/how-to-choose-the-right-data-product-for-you) + +🪐 diff --git a/docs/zh/reference/warpcast/api.md b/docs/zh/reference/warpcast/api.md new file mode 100644 index 0000000..c7f6eca --- /dev/null +++ b/docs/zh/reference/warpcast/api.md @@ -0,0 +1,571 @@ +# Warpcast API 参考文档 + +本文档记录了 Warpcast 提供的公开 API,包含协议中未公开的信息。 + +主机名始终为 `https://api.warpcast.com`。 + +## 分页 + +分页端点会在 `result` 对象旁返回 `next.cursor` 属性。要获取下一页数据,请将该值作为 `cursor` 查询参数发送。可选参数 `limit` 可用于指定每页大小。 + +```json +{ + "result": { + ... + }, + "next": { + "cursor": "eyJwYWdlIjoxLCJsaW1pdCI6MTAwfQ" + } +} +``` + +## 认证 + +认证端点使用自签名令牌,作为 FID 的应用密钥进行签名: + +```tsx +import { NobleEd25519Signer } from "@farcaster/hub-nodejs"; + +// 你为某个 FID 持有的应用密钥的私钥/公钥 +const fid = 6841; //替换 +const privateKey = 'secret'; // 替换 +const publicKey = 'pubkey'; // 替换 +const signer = new NobleEd25519Signer(new Uint8Array(Buffer.from(privateKey))); + +const header = { + fid, + type: 'app_key', + key: publicKey +}; +const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url'); + +const payload = { exp: Math.floor(Date.now() / 1000) + 300 }; // 5 分钟 +const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url'); + +const signatureResult = await signer.signMessageHash(Buffer.from(`${encodedHeader}.${encodedPayload}`, 'utf-8')); +if (signatureResult.isErr()) { + throw new Error("Failed to sign message"); +} + +const encodedSignature = Buffer.from(signatureResult.value).toString("base64url"); + +const authToken = encodedHeader + "." + encodedPayload + "." + encodedSignature; + +await got.post( + "https://api.warpcast.com/fc/channel-follows", + { + body: { channelKey: 'evm' } + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + authToken; + } + } +) +``` + +## 概念 + +- 频道:Warpcast 基于 FIP-2(在 casts 上设置 `parentUrl`)构建了频道概念。你可以在[文档](https://www.notion.so/warpcast/Channels-4f249d22575348a5a0488b5d86f0dd1c?pvs=4)中了解更多关于频道的信息。 + +## 获取所有频道 + +`GET /v2/all-channels` + +列出所有频道。无参数。不分页。无需认证。 + +返回:包含以下属性的 `channels` 数组: + +- `id` - 不可更改的唯一频道 ID(创建频道时称为"名称") +- `url` - 用于频道主 casts 的 FIP-2 `parentUrl` +- `name` - 显示给用户的友好名称(编辑频道时称为"显示名称") +- `description` - 频道的描述(如果有) +- `descriptionMentions` - 描述中提到的用户 fid 数组。多次提及会导致多个条目。 +- `descriptionMentionsPositions` - 描述中被提及用户(来自 `descriptionMentions`)出现的索引位置。该数组始终与 `descriptionMentions` 大小相同。提及会插入到该索引处现有字符的左侧。 +- `leadFid` - 创建频道的用户 fid(如果有) +- `moderatorFids` - 版主的 fid(新频道成员方案下) +- `createdAt` - 频道创建的 UNIX 时间(秒) +- `followerCount` - 关注频道的用户数 +- `memberCount` - 频道成员数,包括所有者和版主 +- `pinnedCastHash` - 频道置顶 cast 的哈希值(如果有) +- `publicCasting` - `true`/`false` 表示频道是否允许任何人发布 cast,或仅限成员 +- `externalLink` - 出现在 Warpcast 频道头部的外部链接(如果有),包含 2 个属性: + - `title` - 频道头部显示的标题 + - `url` - 链接的 URL + +```json +{ + "result": { + "channels": [ + { + "id": "illustrations", + "url": "https://warpcast.com/~/channel/illustrations", + "name": "illustrations", + "description": "分享你的作品、草图、艺术、发布、GMs、你喜爱或收藏的艺术品——所有与插画相关的内容,标记 加入 ⊹ ࣪ ˖ 封面由 ", + "descriptionMentions": [ + 367850, + 335503 + ], + "descriptionMentionsPositions": [ + 122, + 151 + ], + "imageUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7721c951-b0ed-44ee-aa9c-c31507b69c00/original", + "headerImageUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/64efe955-c3ab-4aad-969d-1aed978a3e00/original", + "leadFid": 367850, + "moderatorFids": [ + 367850 + ], + "createdAt": 1709753166, + "followerCount": 2361, + "memberCount": 300, + "pinnedCastHash": "0x3ef52987ccacd89af096a753c07efcd55a93e143", + "publicCasting": false, + "externalLink": { + "title": "/creatorssupport", + "url": "https://warpcast.com/~/channel/creators-support" + } + }, + ... + ] + } +} +``` + +示例: + +```bash +curl 'https://api.warpcast.com/v2/all-channels' +``` + +## 获取单个频道 + +`GET /v1/channel` + +获取单个频道。无需认证。 + +查询参数: + +- `channelId` - 频道的 ID + +返回:单个频道对象,如"获取所有频道"端点所述。 + +```json +{ + "result": { + "channel": { + "id": "illustrations", + "url": "https://warpcast.com/~/channel/illustrations", + "name": "illustrations", + "description": "分享你的作品、草图、艺术、发布、GMs、你喜爱或收藏的艺术品——所有与插画相关的内容,标记 加入 ⊹ ࣪ ˖ 封面由 ", + "descriptionMentions": [367850, 335503], + "descriptionMentionsPositions": [122, 151], + "imageUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7721c951-b0ed-44ee-aa9c-c31507b69c00/original", + "headerImageUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/64efe955-c3ab-4aad-969d-1aed978a3e00/original", + "leadFid": 367850, + "moderatorFids": [367850], + "createdAt": 1709753166, + "followerCount": 2361, + "memberCount": 300, + "pinnedCastHash": "0x3ef52987ccacd89af096a753c07efcd55a93e143", + "publicCasting": false, + "externalLink": { + "title": "/creatorssupport", + "url": "https://warpcast.com/~/channel/creators-support" + } + } + } +} +``` + +```bash +curl 'https://api.warpcast.com/v1/channel?channelId=illustrations' +``` + +## 获取频道关注者 + +`GET /v1/channel-followers` + +列出频道的关注者。按关注时间降序排列。分页。无需认证。 + +查询参数: + +- `channelId` - 频道的 ID + +返回:包含以下属性的 `users` 数组: + +- `fid` - 用户的 fid +- `followedAt` - 关注频道的 UNIX 时间(秒) + +```json +{ + "result": { + "users": [ + { + "fid": 466624, + "followedAt": 1712685183 + }, + { + "fid": 469283, + "followedAt": 1712685067 + }, + ... + ], + }, + "next": { "cursor": "..." } +} +``` + +示例: + +```bash +curl 'https://api.warpcast.com/v1/channel-followers?channelId=books' +``` + +## 获取用户关注的频道 + +`GET /v1/user-following-channels` + +列出用户关注的所有频道。按关注时间降序排列。分页。无需认证。 + +参数: + +- `fid` - 用户的 fid + +返回:包含以下属性的 `channels` 数组: + +- 上述"获取所有频道"端点中记录的所有属性 +- `followedAt` - 关注频道的 UNIX 时间(秒) + +```json +{ + "result": { + "channels": [ + { + "id": "fc-updates", + "url": "https://warpcast.com/~/channel/fc-updates", + "name": "fc-updates", + "description": "关于 Farcaster 重要事件的更新", + "imageUrl": "https://i.imgur.com/YnnrPaH.png", + "leadFid": 2, + "moderatorFid": 5448, + "moderatorFids": [ + 5448, + 3 + ], + "createdAt": 1712162074, + "followerCount": 17034, + "followedAt": 1712162620 + }, + ... + ] + }, + "next": { "cursor": "..." } +} +``` + +示例: + +```bash +curl 'https://api.warpcast.com/v1/user-following-channels?fid=3' +``` + +## 获取用户关注频道状态 + +`GET /v1/user-channel` + +检查用户是否关注某个频道。 + +查询参数: + +- `fid` - 用户的 fid +- `channelId` - 频道的 ID + +返回:2 个属性: + +- `following` - 表示是否关注该频道 +- `followedAt` - 关注频道的 UNIX 时间(秒)(可选,仅在关注频道时存在) + +```json +{ + "result": { + "following": true, + "followedAt": 1687943747 + } +} +``` + +示例: + +```bash +curl 'https://api.warpcast.com/v1/user-channel?fid=3&channelId=books' +``` + +## 获取频道成员 + +`GET /fc/channel-members` + +列出频道的成员。按成为成员的时间降序排列。分页。无需认证。 + +查询参数: + +- `channelId` - 频道的 ID +- `fid` - (可选)用于筛选的用户 fid + +返回:`members` 数组: + +- `fid` - 成员的 fid +- `memberAt` - 成为成员的 UNIX 时间(秒) + +```json +{ + "result": { + "members": [ + { + "fid": 466624, + "memberAt": 1712685183 + }, + { + "fid": 469283, + "memberAt": 1712685067 + }, + ... + ] + }, + "next": { "cursor": "..." } +} +``` + +示例: + +```bash +curl 'https://api.warpcast.com/fc/channel-members?channelId=memes' +``` + +## 获取频道邀请 + +`GET /fc/channel-invites` + +列出未处理的频道邀请。按邀请时间降序排列。分页。无需认证。 + +每个用户(`invitedFid`)和频道(`channelId`)最多有一个未处理的邀请。 + +查询参数: + +- `channelId` - (可选)用于筛选的频道 ID +- `fid` - (可选)用于筛选的用户 fid + +返回:`invites` 数组: + +- `channelId` - 用户被邀请加入的频道 ID +- `invitedFid` - 被邀请用户的 fid +- `invitedAt` - 用户被邀请的 UNIX 时间(秒) +- `inviterFid` - 创建邀请的用户的 fid +- `role` - 用户被邀请的角色,`member` 或 `moderator` + +```json +{ + "result": { + "invites": [ + { + "channelId": "coke-zero", + "invitedFid": 194, + "invitedAt": 1726879628, + "inviterFid": 18949, + "role": "member" + }, + { + "channelId": "brain-teasers", + "invitedFid": 627785, + "invitedAt": 1726874566, + "inviterFid": 235128, + "role": "member" + }, + ... + ] + }, + "next": { "cursor": "..." } +} +``` + +示例: + +```bash +curl 'https://api.warpcast.com/fc/channel-invites?channelId=memes' +``` + +## 获取 Cast 审核操作 + +`GET /fc/moderated-casts` + +列出审核操作。按操作时间降序排列。分页。无需认证。 + +查询参数: + +- `channelId` - (可选)用于筛选的频道 ID + +返回:`moderationActions` 数组: + +- `castHash` - 被审核的 cast 哈希(包括 `0x` 前缀) +- `channelId` - cast 所在的频道 ID +- `action` - `hide` 或 `unhide` +- `moderatedAt` - 审核发生的 UNIX 时间(秒) + +```json +{ + "result": { + "moderationActions": [ + { + "castHash": "0x6b2253105ef8c1d1b984a5df87182b105a1f0b3a", + "channelId": "welcome", + "action": "hide", + "moderatedAt": 1727767637 + }, + ... + ] + }, + "next": { "cursor": "..." } +} +``` + +示例: + +```bash +curl 'https://api.warpcast.com/fc/moderated-casts?channelId=welcome' +``` + +## 获取频道受限用户 + +`GET /fc/channel-restricted-users` + +获取被限制通过邀请链接加入频道的用户(他们仍可被手动邀请)。用户在作为成员被移除后以及被解禁后会自动进入此状态。用户在受邀后(即使尚未接受/拒绝邀请)以及被禁后会自动解除此状态。无法直接限制或解除限制用户。按限制时间降序排列。分页。无需认证。 + +**注意:** + +- 受限用户的回复仍可在折叠下方查看 +- 此端点仅返回当前受限用户。如果用户曾被限制但随后被重新邀请或禁言,此端点将不再返回该条目。 + +查询参数: + +- `channelId` - (可选)用于筛选的频道 ID +- `fid` - (可选)用于筛选的用户 fid + +返回:`restrictedUsers` 数组: + +- `fid` - 受限用户的 fid +- `channelId` - 用户被限制加入的频道 ID +- `restrictedAt` - 限制开始的 UNIX 时间(秒) + +```json +{ + "result": { + "restrictedUsers": [ + { + "fid": 1234, + "channelId": "welcome", + "restrictedAt": 1727767637 + }, + ... + ] + }, + "next": { "cursor": "..." } +} +``` + +示例: + +```bash +curl 'https://api.warpcast.com/fc/channel-restricted-users?channelId=memes' +``` + +## 获取频道禁言用户 + +`GET /fc/channel-bans` + +获取被频道禁言的用户。按禁言时间降序排列。分页。无需认证。 + +**注意:** + +- 此端点仅返回当前禁言。如果用户被解禁,此端点将不再返回该条目。 + +查询参数: + +- `channelId` - (可选)用于筛选的频道 ID +- `fid` - (可选)用于筛选的用户 fid + +返回:`bannedUsers` 数组: + +- `fid` - 被禁言用户的 fid +- `channelId` - 用户被禁言的频道 ID +- `bannedAt` - 禁言开始的 UNIX 时间(秒) + +```json +{ + "result": { + "bannedUsers": [ + { + "fid": 1234, + "channelId": "welcome", + "bannedAt": 1727767637 + }, + ... + ] + }, + "next": { "cursor": "..." } +} +``` + +示例: + +```bash +curl 'https://api.warpcast.com/fc/channel-bans?channelId=memes' +``` + +## 禁言频道用户 + +`POST /fc/channel-bans` + +禁言频道用户。被禁言用户将无法再回复频道 casts,其所有现有回复将被隐藏。需认证。 + +调用者必须拥有或管理该频道。 + +请求体参数: + +- `channelId` - 禁言用户的频道 ID +- `banFid` - 被禁言用户的 fid + +返回: + +- `success: true` + +示例: + +```bash +curl -X POST \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Bearer ' \ + -d '{ "channelId": "memes", "banFid": 1234 }' \ + https://api.warpcast.com/fc/channel-bans +``` + +## 解禁频道用户 + +`DELETE /fc/channel-bans` + +解禁频道用户。用户的所有现有回复将重新可见并可再次回复(出现在折叠下方)。用户将进入受限状态。需认证。 + +调用者必须拥有或管理该频道。 + +请求体参数: + +- `channelId` - 解禁用户的频道 ID +- `unbanFid` - 被解禁用户的 fid + +返回: + +- `success: true` + +示例: + +```bash +curl - +``` diff --git a/docs/zh/reference/warpcast/cast-composer-intents.md b/docs/zh/reference/warpcast/cast-composer-intents.md new file mode 100644 index 0000000..8a1d48b --- /dev/null +++ b/docs/zh/reference/warpcast/cast-composer-intents.md @@ -0,0 +1,64 @@ +--- +title: Warpcast 意图 URL +--- + +# 意图 URL + +## 发布意图 URL + +Warpcast 意图允许开发者将已认证用户引导至预填充的发布编辑器。 + +> [!IMPORTANT] +> 如果您正在构建 mini app 并希望提示用户撰写发布内容,请使用 [composeCast 操作](https://miniapps.farcaster.xyz/docs/sdk/actions/compose-cast) +> 来自 Mini App SDK。 + +#### 包含发布文本 + +``` +https://warpcast.com/~/compose?text=Hello%20world! +``` + +#### 包含发布文本和一个嵌入内容 + +``` +https://warpcast.com/~/compose?text=Hello%20world!&embeds[]=https://farcaster.xyz +``` + +#### 包含提及内容和两个嵌入内容的发布文本 + +``` +https://warpcast.com/~/compose?text=Hello%20@farcaster!&embeds[]=https://farcaster.xyz&embeds[]=https://github.com/farcasterxyz/protocol +``` + +#### 在特定频道发布文本 + +``` +https://warpcast.com/~/compose?text=Hello%20world!&channelKey=farcaster +``` + +#### 回复带有哈希值的发布内容 + +``` +https://warpcast.com/~/compose?text=Looks%20good!&parentCastHash=0x09455067393562d3296bcbc2ec1c2d6bba8ac1f1 +``` + +#### 附加说明 + +- 嵌入内容可以是任何有效的 URL +- 以 `.png`、`.jpg` 或 `.gif` 结尾的 URL 将呈现为图片嵌入 +- 嵌入 Zora 铸造页面的 URL 将显示 NFT 并在下方附带铸造链接 +- 您可以在 https://warpcast.com/~/developers/embeds 查看嵌入内容在 Warpcast 中的呈现效果 + +## 资源 URL + +#### 通过 FID 查看个人资料 + +``` +https://warpcast.com/~/profiles/:fid +``` + +#### 通过哈希值查看发布内容 + +``` +https://warpcast.com/~/conversations/:hash +``` diff --git a/docs/zh/reference/warpcast/direct-casts.md b/docs/zh/reference/warpcast/direct-casts.md new file mode 100644 index 0000000..b7dd9a5 --- /dev/null +++ b/docs/zh/reference/warpcast/direct-casts.md @@ -0,0 +1,18 @@ +# 直接私信(Direct Casts) + +本文档记录了 Warpcast 为直接私信提供的公开 API。目前直接私信尚未纳入协议规范,计划将于今年晚些时候在协议中增加该功能。 + +#### 发送/写入直接私信的 API + +- [通过 API 发送直接私信](https://www.notion.so/warpcast/Public-Programmable-DCs-v1-50d9d99e34ac4d10add55bd26a91804f) +- 上述链接同时提供了如何获取直接私信 API 密钥的信息 + +#### 直接私信意图(Intents) + +通过意图功能,开发者可将认证用户引导至预填充内容的直接私信编写界面(通过 URL 实现)。 + +```bash +https://warpcast.com/~/inbox/create/[fid]?text=[message] + +https://warpcast.com/~/inbox/create/1?text=gm +``` diff --git a/docs/zh/reference/warpcast/embeds.md b/docs/zh/reference/warpcast/embeds.md new file mode 100644 index 0000000..de935da --- /dev/null +++ b/docs/zh/reference/warpcast/embeds.md @@ -0,0 +1,11 @@ +# Warpcast 嵌入功能参考 + +Warpcast 在渲染 URL 嵌入的富媒体预览时遵循 [Open Graph 协议](https://ogp.me)。 + +开发者可以通过 https://warpcast.com/~/developers/embeds 重置 Warpcast 上现有的嵌入缓存。 + +#### 补充说明 + +- 重置嵌入缓存不会重置 Open Graph 图片缓存。如果您遇到 Open Graph 图片未更新的问题,请更改作为 `og:image` 提供的图片文件路径。 +- 开发者必须登录 Warpcast 才能访问此页面。 +- 如需渲染 NFT 的富媒体预览,请遵循 [Farcaster Frames 规范](/zh/developers/frames/spec)。 diff --git a/docs/zh/reference/warpcast/signer-requests.md b/docs/zh/reference/warpcast/signer-requests.md new file mode 100644 index 0000000..c9c0a73 --- /dev/null +++ b/docs/zh/reference/warpcast/signer-requests.md @@ -0,0 +1,391 @@ +# 签名者请求 + +如果您的应用程序希望代表用户向 Farcaster 写入数据,则必须让用户为其应用程序添加签名密钥。 + +## 指南 + +### 前提条件 + +- 已注册的 FID + +### 1. 已认证用户在您的应用中点击"通过 Warpcast 连接" + +当您的应用能够识别并认证用户后,您可以向他们展示"通过 Warpcast 连接"的选项。 + +### 2. 为用户生成新的 Ed25519 密钥对和 SignedKeyRequest 签名 + +您的应用应生成并与该用户关联的 Ed25519 密钥对,并安全存储。在后续步骤中,您将提示用户批准此密钥对代表他们签署 Farcaster 消息。 + +需注意以下几点: + +- 私钥必须安全存储且永不暴露 +- 当用户返回时,可以检索密钥对并用于签署消息 + +除了生成新的密钥对外,您的应用还必须使用其 FID 的托管地址生成 ECDSA 签名。这允许将密钥归属于应用,对于了解正在使用的应用或基于生成内容的应用程序过滤内容等广泛用途非常有用。 + +**示例代码:** + +```ts +import * as ed from '@noble/ed25519'; +import { mnemonicToAccount, signTypedData } from 'viem/accounts'; + +/*** EIP-712 辅助代码 ***/ + +const SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN = { + name: 'Farcaster SignedKeyRequestValidator', + version: '1', + chainId: 10, + verifyingContract: '0x00000000fc700472606ed4fa22623acf62c60553', +} as const; + +const SIGNED_KEY_REQUEST_TYPE = [ + { name: 'requestFid', type: 'uint256' }, + { name: 'key', type: 'bytes' }, + { name: 'deadline', type: 'uint256' }, +] as const; + +/*** 生成密钥对 ***/ + +const privateKey = ed.utils.randomPrivateKey(); +const publicKeyBytes = await ed.getPublicKey(privateKey); +const key = '0x' + Buffer.from(publicKeyBytes).toString('hex'); + +/*** 生成 Signed Key Request 签名 ***/ + +const appFid = process.env.APP_FID; +const account = mnemonicToAccount(process.env.APP_MNENOMIC); + +const deadline = Math.floor(Date.now() / 1000) + 86400; // 签名有效期为1天 +const signature = await account.signTypedData({ + domain: SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN, + types: { + SignedKeyRequest: SIGNED_KEY_REQUEST_TYPE, + }, + primaryType: 'SignedKeyRequest', + message: { + requestFid: BigInt(appFid), + key, + deadline: BigInt(deadline), + }, +}); +``` + +**截止时间** + +此值控制 Signed Key Request 签名的有效期。它是一个以秒为单位的 Unix 时间戳(注意:JavaScript 使用毫秒)。我们建议将其设置为 24 小时。 + +### 3. 应用使用公钥 + SignedKeyRequest 签名通过 Warpcast API 发起 Signed Key Request + +应用调用 Warpcast 后端,返回一个深度链接和一个可用于检查请求状态的会话令牌。 + +```ts +/*** 创建 Signed Key Request ***/ + +const warpcastApi = 'https://api.warpcast.com'; +const { token, deeplinkUrl } = await axios + .post(`${warpcastApi}/v2/signed-key-requests`, { + key: publicKey, + requestFid: fid, + signature, + deadline, + }) + .then((response) => response.data.result.signedKeyRequest); + +// deeplinkUrl 应展示给用户 +// token 应用于轮询 +``` + +**重定向 URL** + +如果此请求是从可以打开深度链接的原生移动应用发出的,您可以包含一个 redirectUrl,用户在完成请求后应被带回该 URL。 + +注意:如果您的应用是 PWA 或 Web 应用,请不要包含此值,因为用户将被带回一个没有状态的会话。 + +**赞助** + +您可以为用户赞助链上费用。请参阅下面的[赞助签名者](#sponsoring-a-signer)。 + +### 4. 应用向用户展示响应中的深度链接 + +应用展示深度链接,将提示用户打开 Warpcast 应用并授权签名者请求(底部有截图)。应用应引导用户在已安装 Warpcast 的移动设备上打开链接: + +1. 在移动设备上时,直接触发深度链接 +2. 在 Web 上时,将深度链接显示为可扫描的二维码 + +**示例代码** + +```ts +import QRCode from 'react-qr-code'; + +const DeepLinkQRCode = (deepLinkUrl) => ; +``` + +### 5. 应用开始使用令牌轮询签名者请求端点 + +向用户展示深度链接后,应用必须等待用户完成签名者请求流程。应用可以轮询签名者请求资源,并查找表明用户已完成请求的数据: + +````ts +const poll = async (token: string) => { + while (true) { + // 休眠1秒 + await new Promise((r) => setTimeout(r, 2000)); + + console.log('轮询签名密钥请求'); + const signedKeyRequest = await axios + .get(`${warpcastApi}/v2/signed-key-request`, { + params: { + token, + }, + }) + .then((response) => response.data.result.signedKeyRequest); + + if (signedKeyRequest.state === 'completed') { + console.log('签名密钥请求完成:', signedKeyRequest); + + /** + * 此时签名者已在链上注册,您可以开始提交 + * 由其密钥签名的消息到 hubs: + * ``` + * const signer = Ed25519Signer.fromPrivateKey(privateKey)._unsafeUnwrap(); + * const message = makeCastAdd(..., signer) + * ``` + */ + break; + } + } +}; + +poll(token); +```` + +### 6. 用户打开链接并在 Warpcast 中完成签名者请求流程 + +当用户在 Warpcast 中批准请求时,将进行链上交易,授予该签名者写入权限。完成后,您的应用应显示成功,并可以开始使用新添加的密钥写入消息。 + +### 参考实现 + +````ts +import * as ed from '@noble/ed25519'; +import { Hex } from 'viem'; +import { mnemonicToAccount } from 'viem/accounts'; +import axios from 'axios'; +import * as qrcode from 'qrcode-terminal'; + +/*** EIP-712 辅助代码 ***/ + +const SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN = { + name: 'Farcaster SignedKeyRequestValidator', + version: '1', + chainId: 10, + verifyingContract: '0x00000000fc700472606ed4fa22623acf62c60553', +} as const; + +const SIGNED_KEY_REQUEST_TYPE = [ + { name: 'requestFid', type: 'uint256' }, + { name: 'key', type: 'bytes' }, + { name: 'deadline', type: 'uint256' }, +] as const; + +(async () => { + /*** 生成密钥对 ***/ + + const privateKey = ed.utils.randomPrivateKey(); + const publicKeyBytes = await ed.getPublicKey(privateKey); + const key = '0x' + Buffer.from(publicKeyBytes).toString('hex'); + + /*** 生成 Signed Key Request 签名 ***/ + + const appFid = process.env.APP_FID; + const account = mnemonicToAccount(process.env.APP_MNEMONIC); + + const deadline = Math.floor(Date.now() / 1000) + 86400; // 签名有效期为1天 + const signature = await account.signTypedData({ + domain: SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN, + types: { + SignedKeyRequest: SIGNED_KEY_REQUEST_TYPE, + }, + primaryType: 'SignedKeyRequest', + message: { + requestFid: BigInt(appFid), + key, + deadline: BigInt(deadline), + }, + }); + + /*** 创建 Signed Key Request ***/ + + const warpcastApi = 'https://api.warpcast.com'; + const { token, deeplinkUrl } = await axios + .post(`${warpcastApi}/v2/signed-key-requests`, { + key, + requestFid: appFid, + signature, + deadline, + }) + .then((response) => response.data.result.signedKeyRequest); + + qrcode.generate(deeplinkUrl, console.log); + console.log('用手机扫描此二维码'); + console.log('深度链接:', deeplinkUrl); + + const poll = async (token: string) => { + while (true) { + // 休眠1秒 + await new Promise((r) => setTimeout(r, 2000)); + + console.log('轮询签名密钥请求'); + const signedKeyRequest = await axios + .get(`${warpcastApi}/v2/signed-key-request`, { + params: { + token, + }, + }) + .then((response) => response.data.result.signedKeyRequest); + + if (signedKeyRequest.state === 'completed') { + console.log('签名密钥请求完成:', signedKeyRequest); + + /** + * 此时签名者已在链上注册,您可以开始提交 + * 由其密钥签名的消息到 hubs: + * ``` + * const signer = Ed25519Signer.fromPrivateKey(privateKey)._unsafeUnwrap(); + * const message = makeCastAdd(..., signer) + * ``` + */ + break; + } + } + }; + + await poll(token); +})(); +```` + +## API + +### POST /v2/signed-key-request + +创建签名密钥请求。 + +**请求体:** + +- `key` - Ed25519 公钥的十六进制字符串 +- `requestFid` - 请求应用的 fid +- `deadline` - 签名有效的 Unix 时间戳 +- `signature` - 来自请求应用的 [SignedKeyRequest](https://docs.farcaster.xyz/reference/contracts/reference/signed-key-request-validator#signed-key-request-validator) 签名 +- `redirectUrl` - 可选。批准签名者后应重定向到的 URL。注意:仅当从原生移动应用请求签名者时才应使用此值。 +- `sponsorship` - + +**示例响应:** + +```json +{ + "result": { + "signedKeyRequest": { + "token": "0xa241e6b1287a07f4d3f9c5bd", + "deeplinkUrl": "farcaster://signed-key-request?token=0xa241e6b1287a07f4d3f9c5bd" + "key": "0x48b0c7a6deff69bad7673357df43274f3a08163a6440b7a7e3b3cb6b6623faa7", + "state": "pending" + } + } +} +``` + +### GET /v2/signed-key-request + +获取签名密钥请求的状态。 + +**查询参数:** + +- `token` - 标识请求的令牌 + +**响应:** + +- `token` - 标识请求的令牌 +- `deeplinkUrl` - 用户可完成请求的 URL +- `key` - 请求添加的密钥 +- `state` - 请求状态:`pending` - 用户未采取任何操作,`approved` - 用户已批准但链上交易未确认,`completed` - 链上交易已确认 + +**待处理状态的示例响应:** + +```json +{ + "result": { + "signedKeyRequest": { + "token": "0xa241e6b1287a07f4d3f9c5bd", + "deeplinkUrl": "farcaster://signed-key-request?token=0xa241e6b1287a07f4d3f9c5bd" + "key": "0x48b0c7a6deff69bad7673357df43274f3a08163a6440b7a7e3b3cb6b6623faa7", + "state": "pending" + } + } +} +``` + +**批准后但交易未确认的示例响应:** + +```json +{ + "result": { + "signedKeyRequest": { + "token": "0xa241e6b1287a07f4d3f9c5bd", + "deeplinkUrl": "farcaster://signed-key-request?token=0xa241e6b1287a07f4d3f9c5bd" + "key": "0x48b0c7a6deff69bad7673357df43274f3a08163a6440b7a7e3b3cb6b6623faa7", + "state": "approved", + "userFid": 1, + } + } +} +``` + +**交易确认后的示例响应:** + +```json +{ + "result": { + "signedKeyRequest": { + "token": "0xa241e6b1287a07f4d3f9c5bd", + "deeplinkUrl": "farcaster://signed-key-request?token=0xa241e6b1287a07f4d3f9c5bd" + "key": "0x48b0c7a6deff69bad7673357df43274f3a08163a6440b7a7e3b3cb6b6623faa7", + "state": "completed", + "userFid": 1, + } + } +} +``` + +## 赞助签名者 + +应用可以赞助签名者,这样用户无需支付费用。应用必须在 Warpcast 上注册并拥有 warps ≥ 100。 + +生成签名密钥请求时,应用可以通过在请求体中包含额外的 `sponsorship` 字段来表明应被赞助。 + +```ts +type SignedKeyRequestSponsorship = { + sponsorFid: number; + signature: string; // 由 sponsorFid 签署的赞助签名 +}; + +type SignedKeyRequestBody = { + key: string; + requestFid: number; + deadline: number; + signature: string; // 由 requestFid 签署的密钥请求签名 + sponsorship?: SignedKeyRequestSponsorship; +}; +``` + +创建 `SignedKeyRequestSponsorship`: + +1. 创建密钥对并让请求 FID 生成签名 +2. 从赞助 FID 创建第二个签名,使用步骤 1 中生成的签名作为 EIP-191 消息的原始输入 + +```ts +// sponsoringAccount 是赞助 FID 托管地址的 Viem 账户实例 +// signedKeyRequestSignature 是由请求 FID 签署的 EIP-712 签名 +const sponsorSignature = sponsoringAccount.signMessage({ + message: { raw: signedKeyRequestSignature }, +}); +``` + +当用户在 Warpcast 中打开签名密钥请求时,他们将看到链上费用已由您的应用赞助。 diff --git a/docs/zh/reference/warpcast/signers.md b/docs/zh/reference/warpcast/signers.md new file mode 100644 index 0000000..cb0dae9 --- /dev/null +++ b/docs/zh/reference/warpcast/signers.md @@ -0,0 +1,391 @@ +# 签名者 + +如果您的应用程序希望代表用户向 Farcaster 写入数据,则必须让用户为其应用程序添加一个签名密钥。 + +## 指南 + +#### 前提条件 + +- 已注册的 FID + +#### 1. 已认证用户在您的应用中点击"通过 Warpcast 连接" + +您的应用在向用户展示"通过 Warpcast 连接"选项前,应能识别并认证用户身份。 + +#### 2. 为用户生成新的 Ed25519 密钥对和 SignedKeyRequest 签名 + +您的应用应生成并安全存储与此用户关联的 Ed25519 密钥对。在后续步骤中,您将提示用户批准此密钥对代表其签署消息。 + +由于此密钥对可以代表用户向协议写入数据,因此必须确保: + +- 私钥被安全存储且永不暴露 +- 当用户返回时,可以检索密钥对并用于签署消息 + +除了生成新的密钥对外,您的应用还必须使用其 FID 的托管地址生成 ECDSA 签名。这允许将密钥归属于应用程序,对于了解正在使用的应用程序到基于生成内容的应用程序过滤内容等各种用途都非常有用。 + +**示例代码:** + +```ts +import * as ed from '@noble/ed25519'; +import { mnemonicToAccount, signTypedData } from 'viem/accounts'; + +/*** EIP-712 辅助代码 ***/ + +const SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN = { + name: 'Farcaster SignedKeyRequestValidator', + version: '1', + chainId: 10, + verifyingContract: '0x00000000fc700472606ed4fa22623acf62c60553', +} as const; + +const SIGNED_KEY_REQUEST_TYPE = [ + { name: 'requestFid', type: 'uint256' }, + { name: 'key', type: 'bytes' }, + { name: 'deadline', type: 'uint256' }, +] as const; + +/*** 生成密钥对 ***/ + +const privateKey = ed.utils.randomPrivateKey(); +const publicKeyBytes = await ed.getPublicKey(privateKey); +const key = '0x' + Buffer.from(publicKeyBytes).toString('hex'); + +/*** 生成 Signed Key Request 签名 ***/ + +const appFid = process.env.APP_FID; +const account = mnemonicToAccount(process.env.APP_MNENOMIC); + +const deadline = Math.floor(Date.now() / 1000) + 86400; // 签名有效期为1天 +const signature = await account.signTypedData({ + domain: SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN, + types: { + SignedKeyRequest: SIGNED_KEY_REQUEST_TYPE, + }, + primaryType: 'SignedKeyRequest', + message: { + requestFid: BigInt(appFid), + key, + deadline: BigInt(deadline), + }, +}); +``` + +**截止时间** + +此值控制 Signed Key Request 签名的有效期。它是一个以秒为单位的 Unix 时间戳(注意:JavaScript 使用毫秒)。我们建议将其设置为 24 小时。 + +#### 3. 应用使用公钥 + SignedKeyRequest 签名通过 Warpcast API 发起 Signed Key Request + +应用调用 Warpcast 后端,返回一个深度链接和一个可用于检查请求状态的会话令牌。 + +```ts +/*** 创建 Signed Key Request ***/ + +const warpcastApi = 'https://api.warpcast.com'; +const { token, deeplinkUrl } = await axios + .post(`${warpcastApi}/v2/signed-key-requests`, { + key: publicKey, + requestFid: fid, + signature, + deadline, + }) + .then((response) => response.data.result.signedKeyRequest); + +// deeplinkUrl 应展示给用户 +// token 应用于轮询 +``` + +**重定向 URL** + +如果此请求是从可以打开深度链接的原生移动应用发出的,您可以包含一个 redirectUrl,用户在完成请求后应被带回该 URL。 + +注意:如果您的应用是 PWA 或 Web 应用,请不要包含此值,因为用户将被带回一个没有状态的会话。 + +**赞助** + +您可以为用户赞助链上费用。请参阅下面的[赞助签名者](#sponsoring-a-signer)。 + +#### 4. 应用向用户展示响应中的深度链接 + +应用展示深度链接,将提示用户打开 Warpcast 应用并授权签名者请求(底部有截图)。应用应引导用户在已安装 Warpcast 的移动设备上打开链接: + +1. 在移动设备上时,直接触发深度链接 +2. 在 Web 上时,将深度链接显示为可扫描的二维码 + +**示例代码** + +```ts +import QRCode from 'react-qr-code'; + +const DeepLinkQRCode = (deepLinkUrl) => ; +``` + +#### 5. 应用开始使用令牌轮询签名者请求端点 + +向用户展示深度链接后,应用必须等待用户完成签名者请求流程。应用可以轮询签名者请求资源,并查找表明用户已完成请求的数据: + +````ts +const poll = async (token: string) => { + while (true) { + // 休眠1秒 + await new Promise((r) => setTimeout(r, 2000)); + + console.log('轮询 signed key request'); + const signedKeyRequest = await axios + .get(`${warpcastApi}/v2/signed-key-request`, { + params: { + token, + }, + }) + .then((response) => response.data.result.signedKeyRequest); + + if (signedKeyRequest.state === 'completed') { + console.log('Signed Key Request 已完成:', signedKeyRequest); + + /** + * 此时签名者已在链上注册,您可以开始提交 + * 由其密钥签名的消息到 hubs: + * ``` + * const signer = Ed25519Signer.fromPrivateKey(privateKey)._unsafeUnwrap(); + * const message = makeCastAdd(..., signer) + * ``` + */ + break; + } + } +}; + +poll(token); +```` + +#### 6. 用户打开链接并在 Warpcast 中完成签名者请求流程 + +当用户在 Warpcast 中批准请求时,将进行一笔链上交易,授予该签名者写入权限。完成后,您的应用应显示成功,并可以开始使用新添加的密钥写入消息。 + +#### 参考实现 + +````ts +import * as ed from '@noble/ed25519'; +import { Hex } from 'viem'; +import { mnemonicToAccount } from 'viem/accounts'; +import axios from 'axios'; +import * as qrcode from 'qrcode-terminal'; + +/*** EIP-712 辅助代码 ***/ + +const SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN = { + name: 'Farcaster SignedKeyRequestValidator', + version: '1', + chainId: 10, + verifyingContract: '0x00000000fc700472606ed4fa22623acf62c60553', +} as const; + +const SIGNED_KEY_REQUEST_TYPE = [ + { name: 'requestFid', type: 'uint256' }, + { name: 'key', type: 'bytes' }, + { name: 'deadline', type: 'uint256' }, +] as const; + +(async () => { + /*** 生成密钥对 ***/ + + const privateKey = ed.utils.randomPrivateKey(); + const publicKeyBytes = await ed.getPublicKey(privateKey); + const key = '0x' + Buffer.from(publicKeyBytes).toString('hex'); + + /*** 生成 Signed Key Request 签名 ***/ + + const appFid = process.env.APP_FID; + const account = mnemonicToAccount(process.env.APP_MNEMONIC); + + const deadline = Math.floor(Date.now() / 1000) + 86400; // 签名有效期为1天 + const signature = await account.signTypedData({ + domain: SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN, + types: { + SignedKeyRequest: SIGNED_KEY_REQUEST_TYPE, + }, + primaryType: 'SignedKeyRequest', + message: { + requestFid: BigInt(appFid), + key, + deadline: BigInt(deadline), + }, + }); + + /*** 创建 Signed Key Request ***/ + + const warpcastApi = 'https://api.warpcast.com'; + const { token, deeplinkUrl } = await axios + .post(`${warpcastApi}/v2/signed-key-requests`, { + key, + requestFid: appFid, + signature, + deadline, + }) + .then((response) => response.data.result.signedKeyRequest); + + qrcode.generate(deeplinkUrl, console.log); + console.log('用手机扫描此二维码'); + console.log('深度链接:', deeplinkUrl); + + const poll = async (token: string) => { + while (true) { + // 休眠1秒 + await new Promise((r) => setTimeout(r, 2000)); + + console.log('轮询 signed key request'); + const signedKeyRequest = await axios + .get(`${warpcastApi}/v2/signed-key-request`, { + params: { + token, + }, + }) + .then((response) => response.data.result.signedKeyRequest); + + if (signedKeyRequest.state === 'completed') { + console.log('Signed Key Request 已完成:', signedKeyRequest); + + /** + * 此时签名者已在链上注册,您可以开始提交 + * 由其密钥签名的消息到 hubs: + * ``` + * const signer = Ed25519Signer.fromPrivateKey(privateKey)._unsafeUnwrap(); + * const message = makeCastAdd(..., signer) + * ``` + */ + break; + } + } + }; + + await poll(token); +})(); +```` + +## API + +#### POST /v2/signed-key-request + +创建签名密钥请求。 + +**请求体:** + +- `key` - Ed25519 公钥的十六进制字符串 +- `requestFid` - 请求应用的 fid +- `deadline` - 签名有效的 Unix 时间戳 +- `signature` - 来自请求应用的 [SignedKeyRequest](https://docs.farcaster.xyz/reference/contracts/reference/signed-key-request-validator#signed-key-request-validator) 签名 +- `redirectUrl` - 可选。批准签名者后应重定向到的 URL。注意:此值仅当从原生移动应用请求签名者时使用。 +- `sponsorship` - + +**示例响应:** + +```json +{ + "result": { + "signedKeyRequest": { + "token": "0xa241e6b1287a07f4d3f9c5bd", + "deeplinkUrl": "farcaster://signed-key-request?token=0xa241e6b1287a07f4d3f9c5bd" + "key": "0x48b0c7a6deff69bad7673357df43274f3a08163a6440b7a7e3b3cb6b6623faa7", + "state": "pending" + } + } +} +``` + +#### GET /v2/signed-key-request + +获取签名密钥请求的状态。 + +**查询参数:** + +- `token` - 标识请求的令牌 + +**响应:** + +- `token` - 标识请求的令牌 +- `deeplinkUrl` - 用户可完成请求的 URL +- `key` - 请求添加的密钥 +- `state` - 请求状态:`pending` - 用户未采取任何操作,`approved` - 用户已批准但链上交易未确认,`completed` - 链上交易已确认 + +**pending 状态的示例响应:** + +```json +{ + "result": { + "signedKeyRequest": { + "token": "0xa241e6b1287a07f4d3f9c5bd", + "deeplinkUrl": "farcaster://signed-key-request?token=0xa241e6b1287a07f4d3f9c5bd" + "key": "0x48b0c7a6deff69bad7673357df43274f3a08163a6440b7a7e3b3cb6b6623faa7", + "state": "pending" + } + } +} +``` + +**批准后但交易未确认前的示例响应:** + +```json +{ + "result": { + "signedKeyRequest": { + "token": "0xa241e6b1287a07f4d3f9c5bd", + "deeplinkUrl": "farcaster://signed-key-request?token=0xa241e6b1287a07f4d3f9c5bd" + "key": "0x48b0c7a6deff69bad7673357df43274f3a08163a6440b7a7e3b3cb6b6623faa7", + "state": "approved", + "userFid": 1, + } + } +} +``` + +**交易确认后的示例响应:** + +```json +{ + "result": { + "signedKeyRequest": { + "token": "0xa241e6b1287a07f4d3f9c5bd", + "deeplinkUrl": "farcaster://signed-key-request?token=0xa241e6b1287a07f4d3f9c5bd" + "key": "0x48b0c7a6deff69bad7673357df43274f3a08163a6440b7a7e3b3cb6b6623faa7", + "state": "completed", + "userFid": 1, + } + } +} +``` + +## 赞助签名者 + +应用程序可以赞助签名者,这样用户就不需要支付费用。应用程序必须在 Warpcast 上注册并拥有 warps ≥ 100。 + +生成签名密钥请求时,应用程序可以通过在请求体中包含额外的 `sponsorship` 字段来表明应被赞助。 + +```ts +type SignedKeyRequestSponsorship = { + sponsorFid: number; + signature: string; // 由 sponsorFid 签署的赞助签名 +}; + +type SignedKeyRequestBody = { + key: string; + requestFid: number; + deadline: number; + signature: string; // 由 requestFid 签署的密钥请求签名 + sponsorship?: SignedKeyRequestSponsorship; +}; +``` + +要创建 `SignedKeyRequestSponsorship`: + +1. 创建密钥对并让请求 FID 生成签名 +2. 从赞助 FID 创建第二个签名,使用步骤 1 中生成的签名作为 EIP-191 消息的原始输入 + +```ts +// sponsoringAccount 是赞助 FID 托管地址的 Viem 账户实例 +// signedKeyRequestSignature 是由请求 FID 签署的 EIP-712 签名 +const sponsorSignature = sponsoringAccount.signMessage({ + message: { raw: signedKeyRequestSignature }, +}); +``` + +当用户在 Warpcast 中打开签名密钥请求时,他们将看到链上费用已由您的应用程序赞助。 diff --git a/docs/zh/reference/warpcast/videos.md b/docs/zh/reference/warpcast/videos.md new file mode 100644 index 0000000..fc7c7bb --- /dev/null +++ b/docs/zh/reference/warpcast/videos.md @@ -0,0 +1,21 @@ +# 如何在 Warpcast 上展示你的视频 + +1. 确保将视频以可流式传输的 `.m3u8` 文件格式提供。这能确保客户端在观看时仅下载所需内容,提供高性能的观看体验。 + +2. 确保 `.m3u8` 清单文件暴露视频的分辨率信息。Warpcast 会使用这些信息来确定渲染时的正确宽高比。包含分辨率数据的清单文件示例如下: + +``` +#EXTM3U +#EXT-X-VERSION:3 + +#EXT-X-STREAM-INF:BANDWIDTH=2444200,CODECS="avc1.64001f,mp4a.40.2",RESOLUTION=474x842 +480p/video.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=4747600,CODECS="avc1.640020,mp4a.40.2",RESOLUTION=720x1280 +720p/video.m3u8 +``` + +3. 确保在发布 cast 时 `.m3u8` 文件是可访问的。Warpcast 会检查一次,如果没有有效数据,cast 将不会显示视频。 + +4. 当 URL 从以 `/my-video.m3u8` 结尾改为 `/thumbnail.jpg` 时,提供一个预览/缩略图,以便在用户与视频交互前显示。 + +5. 联系 Warpcast 团队,申请为你的域名启用视频功能。告诉我们你的视频 URL 格式,我们将配置爬虫程序以在信息流中正确显示它们。请注意,在申请加入白名单前,请确保已完成上述所有步骤。[在 Warpcast 上联系 @gt](https://warpcast.com/~/inbox/create/302?text=Completed%20video%20setup)。 diff --git a/package.json b/package.json index 93cd8fb..c726cac 100644 --- a/package.json +++ b/package.json @@ -19,5 +19,6 @@ }, "lint-staged": { "*.md": "prettier --config \"./.prettierrc.yml\" --write \"**/*.{json,md}\"" - } + }, + "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" }