openapi: 3.1.0 info: title: WaaS 开放接口(B 端 /v1) version: "1.2" description: | 托管钱包 WaaS 面向交易所(租户)的 B 端开放接口。 **认证**:B 端 `/v1` 的每个请求都要带 `X-API-Key: ` 与 `Authorization: Bearer `。JWT 使用 Ed25519(`alg: EdDSA`) 以你方私钥签名,claims:`uri`(实际发送的 path+query,含 `/v1`)、`nonce`(唯一,≤64 字节)、`iat`、`exp` (`exp - iat ≤ 30`)、`sub`(= keyId)、`bodyHash`(原始请求体 SHA-256 hex;空请求体为 sha256(""))。 每个请求(含重试)都必须重新签名。详见文档“认证与签名”。 **控制台认证**:平台后台 `/admin/v1` 与租户后台 `/tenant-admin/v1` 的认证入口匿名(`security: []`);其余控制台路径 使用管理员会话 Bearer(`AdminBearer`,形如 `v1..`,非 JWT、非 API Key),由“本地 Ed25519 凭证签名 + 强制 TOTP”登录后签发。 **金额**:所有 `*Raw` 字段(及 `minDeposit` 等少数字段)为最小单位的十进制整数字符串。 **错误**:`{"error":{"code","message","retryable","requestId"}}`。`BAD_REQUEST` 默认返回 HTTP 422: 业务校验与手写校验(缺少/非法 `Idempotency-Key`、`expiresAt`、`keyId`、签名或请求体等)都返回 422; 仅路径/查询参数解析失败由框架返回 400。 **订阅到期**:租户订阅到期后,所有 `/v1` 接口(无 GET 例外)返回 `403 FORBIDDEN`,message 为 `tenant subscription expired`;平台暂停为 `tenant suspended`,两者独立,续期不会自动解除暂停。 **托管 C 端域名**:租户门户为 `https://{slug}.wallet.fablewallet.top`;平台自营(slug `platform`) 使用根域名 `https://wallet.fablewallet.top`。C 端自助注册已永久停用。 servers: - url: https://api.fablewallet.top description: 正式 B 端 API;账号与 API Key 由平台开通 security: - ApiKey: [] SignedJwt: [] tags: - name: Users - name: Wallets - name: Assets - name: Transactions - name: Webhooks - name: Admin description: 平台后台 `/admin/v1`(平台管理员,非 B 端交易所调用) - name: TenantAdmin description: 租户后台 `/tenant-admin/v1`(租户管理员,非 B 端交易所调用) - name: C-end (deprecated) description: 托管 C 端 `/app/v1`;自助注册已永久停用 paths: /v1/users: post: tags: [Users] operationId: createUser summary: 创建用户(已存在则返回已有用户) requestBody: required: true content: application/json: schema: type: object required: [customerRefId] properties: customerRefId: { type: string, minLength: 1, maxLength: 128, description: 交易所侧用户 ID } email: { type: string, format: email, description: 可选;仅新建时使用,租户内唯一 } password: type: string writeOnly: true minLength: 10 maxLength: 128 description: 可选;同时为托管 C 端开通登录密码(须含字母和数字),必须与 email 一起提供。仅写入新账号,重复 customerRefId 不会覆盖 nickname: { type: string, maxLength: 32, description: 可选;昵称,仅新建时使用 } responses: "200": description: 用户 content: { application/json: { schema: { $ref: "#/components/schemas/User" } } } "409": { $ref: "#/components/responses/Error" } "422": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } get: tags: [Users] operationId: listUsers summary: 用户列表(仅 B 端创建的用户) parameters: - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } } - { name: cursor, in: query, schema: { type: string, format: uuid }, description: 上一页的 nextCursor } - { name: status, in: query, schema: { type: string, enum: [ACTIVE, FROZEN, CLOSED] } } responses: "200": description: 用户列表 content: application/json: schema: type: object required: [items, nextCursor] properties: items: type: array items: { $ref: "#/components/schemas/UserListItem" } nextCursor: { type: [string, "null"], format: uuid } default: { $ref: "#/components/responses/Error" } /v1/users/{customerRefId}: parameters: - $ref: "#/components/parameters/CustomerRefId" get: tags: [Users] operationId: getUser summary: 查询用户及其钱包 responses: "200": description: 用户与钱包 content: application/json: schema: type: object required: [user, wallets] properties: user: { $ref: "#/components/schemas/User" } wallets: type: array items: { $ref: "#/components/schemas/Wallet" } "404": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } patch: tags: [Users] operationId: updateUser summary: 修改邮箱、冻结或解冻用户 requestBody: required: true content: application/json: schema: type: object properties: email: { type: string, format: email } status: { type: string, enum: [ACTIVE, FROZEN] } responses: "200": description: 修改后的用户 content: { application/json: { schema: { $ref: "#/components/schemas/User" } } } "404": { $ref: "#/components/responses/Error" } "409": { $ref: "#/components/responses/Error" } "422": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /v1/users/{customerRefId}/deposit_address: parameters: - $ref: "#/components/parameters/CustomerRefId" post: tags: [Users] operationId: getDepositAddress summary: 一次调用获取充值地址(自动创建用户与默认钱包) description: 幂等。所有 EVM 网络共用同一地址。地址未就绪时返回 status=PROVISIONING、address=null,请稍后重试。 requestBody: required: true content: application/json: schema: type: object required: [networkKey] properties: networkKey: { type: string } email: { type: string, format: email, description: 仅在本次新建用户时使用 } responses: "200": description: 充值地址 content: application/json: schema: type: object required: [customerRefId, uid, walletId, networkKey, chainFamily, address, status] properties: customerRefId: { type: string } uid: { type: string } walletId: { type: string, format: uuid } networkKey: { type: string } chainFamily: { $ref: "#/components/schemas/ChainFamily" } address: { type: [string, "null"] } status: { type: string, enum: [READY, PROVISIONING] } "403": { $ref: "#/components/responses/Error" } "409": { $ref: "#/components/responses/Error" } "422": { $ref: "#/components/responses/Error" } "429": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /v1/users/{customerRefId}/balances: parameters: - $ref: "#/components/parameters/CustomerRefId" get: tags: [Users] operationId: getUserBalances summary: 用户各资产余额(所有钱包汇总) responses: "200": description: 余额 content: application/json: schema: type: object required: [customerRefId, items] properties: customerRefId: { type: string } items: type: array items: type: object required: [assetId, availableRaw, frozenRaw] properties: assetId: { type: string } availableRaw: { $ref: "#/components/schemas/RawAmount" } frozenRaw: { $ref: "#/components/schemas/RawAmount" } "404": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /v1/wallets: post: tags: [Wallets] operationId: createWallet summary: 创建钱包(异步分配地址) parameters: - $ref: "#/components/parameters/IdempotencyKeyRequired" requestBody: required: true content: application/json: schema: type: object required: [customerRefId, chainFamily] properties: customerRefId: { type: string, minLength: 1, maxLength: 128 } chainFamily: { $ref: "#/components/schemas/ChainFamily" } name: { type: string, maxLength: 40 } responses: "202": description: 已创建,status=PROVISIONING content: { application/json: { schema: { $ref: "#/components/schemas/Wallet" } } } "403": { $ref: "#/components/responses/Error" } "409": { $ref: "#/components/responses/Error" } "422": { $ref: "#/components/responses/Error" } "429": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } get: tags: [Wallets] operationId: listWallets summary: 钱包列表 parameters: - { name: customerRefId, in: query, schema: { type: string } } - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } } - { name: before, in: query, schema: { type: string, format: uuid }, description: 上一页的 nextBefore } responses: "200": description: 钱包列表 content: application/json: schema: type: object required: [items, nextBefore] properties: items: type: array items: { $ref: "#/components/schemas/WalletListItem" } nextBefore: { type: [string, "null"], format: uuid } default: { $ref: "#/components/responses/Error" } /v1/wallets/{id}: parameters: - $ref: "#/components/parameters/WalletId" get: tags: [Wallets] operationId: getWallet summary: 查询钱包 responses: "200": description: 钱包 content: { application/json: { schema: { $ref: "#/components/schemas/Wallet" } } } "404": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /v1/wallets/{id}/assets: parameters: - $ref: "#/components/parameters/WalletId" get: tags: [Wallets] operationId: getWalletAssets summary: 钱包各资产余额 responses: "200": description: 余额(字段名不带 Raw 后缀,但同样是最小单位字符串) content: application/json: schema: type: array items: type: object required: [assetId, available, frozen] properties: assetId: { type: string } available: { $ref: "#/components/schemas/RawAmount" } frozen: { $ref: "#/components/schemas/RawAmount" } "404": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /v1/wallets/{id}/addresses: parameters: - $ref: "#/components/parameters/WalletId" get: tags: [Wallets] operationId: getWalletAddresses summary: 钱包在各网络上的充值地址与可充值资产 responses: "200": description: 每个网络一项 content: application/json: schema: type: array items: { $ref: "#/components/schemas/DepositInfo" } "404": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /v1/supported_assets: get: tags: [Assets] operationId: listSupportedAssets summary: 本租户可用资产与当前生效的充值费、提现费 responses: "200": description: 资产列表 content: application/json: schema: type: array items: { $ref: "#/components/schemas/SupportedAsset" } default: { $ref: "#/components/responses/Error" } /v1/networks: get: tags: [Assets] operationId: listNetworks summary: 本租户已开通的网络 responses: "200": description: 网络列表 content: application/json: schema: type: array items: { $ref: "#/components/schemas/Network" } default: { $ref: "#/components/responses/Error" } /v1/transactions/validate_address/{assetId}/{address}: get: tags: [Assets] operationId: validateAddress summary: 地址校验 parameters: - { name: assetId, in: path, required: true, schema: { type: string } } - { name: address, in: path, required: true, schema: { type: string } } responses: "200": description: 校验结果 content: application/json: schema: type: object required: [isValid, isInternal] properties: isValid: { type: boolean } normalized: { type: string, description: 仅 isValid=true 时返回 } isInternal: { type: boolean, description: 是否为本租户钱包的充值地址(站内结算) } "422": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /v1/transactions: post: tags: [Transactions] operationId: createTransaction summary: 发起链上提现或站内转账 description: | externalTxId 作为幂等键;未提供 externalTxId 时必须带 Idempotency-Key 头。 响应为提交回执(按 settlement 区分两种形状),不是交易对象。 parameters: - $ref: "#/components/parameters/IdempotencyKeyOptional" - name: X-User-2FA-Code in: header required: false schema: { type: string } description: 租户开启“API 出金需终端用户两步验证”时必填:付款用户当前的 TOTP 码 requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateTransactionRequest" } responses: "202": description: 已受理 content: application/json: schema: oneOf: - $ref: "#/components/schemas/WithdrawalReceipt" - $ref: "#/components/schemas/TransferReceipt" discriminator: propertyName: settlement mapping: ON_CHAIN: "#/components/schemas/WithdrawalReceipt" OFF_CHAIN: "#/components/schemas/TransferReceipt" "403": { $ref: "#/components/responses/Error" } "404": { $ref: "#/components/responses/Error" } "409": { $ref: "#/components/responses/Error" } "422": { $ref: "#/components/responses/Error" } "429": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } get: tags: [Transactions] operationId: listTransactions summary: 交易列表(对账) parameters: - { name: walletId, in: query, schema: { type: string, format: uuid }, description: 付款或收款钱包 } - { name: customerRefId, in: query, schema: { type: string }, description: 付款或收款用户 } - { name: kind, in: query, schema: { $ref: "#/components/schemas/TransactionKind" } } - { name: status, in: query, schema: { $ref: "#/components/schemas/TxStatus" } } - { name: assetId, in: query, schema: { type: string } } - { name: createdFrom, in: query, schema: { type: string, format: date-time }, description: 含 } - { name: createdTo, in: query, schema: { type: string, format: date-time }, description: 不含 } - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } } - { name: before, in: query, schema: { type: string, format: uuid }, description: 上一页的 nextBefore } responses: "200": description: 交易列表(倒序) content: application/json: schema: type: object required: [items, nextBefore] properties: items: type: array items: { $ref: "#/components/schemas/Transaction" } nextBefore: { type: [string, "null"], format: uuid } "422": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /v1/transactions/estimate_fee: post: tags: [Transactions] operationId: estimateFee summary: 估算手续费(不冻结资金) requestBody: required: true content: application/json: schema: type: object required: [assetId, sourceWalletId, amountRaw, destination] properties: assetId: { type: string } sourceWalletId: { type: string, format: uuid } amountRaw: { $ref: "#/components/schemas/RawAmount" } destination: { type: string, description: 收款地址 } responses: "200": description: 估算结果 content: application/json: schema: type: object required: [assetId, amountRaw, feeRaw, totalRaw, destination, isInternal, settlement, networkFeePaidBy, validUntil] properties: assetId: { type: string } amountRaw: { $ref: "#/components/schemas/RawAmount" } feeRaw: { $ref: "#/components/schemas/RawAmount" } totalRaw: { $ref: "#/components/schemas/RawAmount" } destination: { type: string, description: 规范化地址 } isInternal: { type: boolean } settlement: { $ref: "#/components/schemas/Settlement" } networkFeePaidBy: { type: string, enum: [PLATFORM] } validUntil: { type: string, format: date-time } "404": { $ref: "#/components/responses/Error" } "422": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /v1/transactions/{id}: get: tags: [Transactions] operationId: getTransaction summary: 查询交易 parameters: - $ref: "#/components/parameters/TransactionId" responses: "200": description: 交易对象 content: { application/json: { schema: { $ref: "#/components/schemas/Transaction" } } } "404": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /v1/transactions/external_tx_id/{externalTxId}: get: tags: [Transactions] operationId: getTransactionByExternalId summary: 按 externalTxId 查询交易 parameters: - { name: externalTxId, in: path, required: true, schema: { type: string, minLength: 1, maxLength: 128 } } responses: "200": description: 交易对象 content: { application/json: { schema: { $ref: "#/components/schemas/Transaction" } } } "404": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /v1/transactions/{id}/cancel: post: tags: [Transactions] operationId: cancelTransaction summary: 取消链上提现(签名前) parameters: - $ref: "#/components/parameters/TransactionId" responses: "200": description: 取消后的提现回执 content: { application/json: { schema: { $ref: "#/components/schemas/WithdrawalView" } } } "404": { $ref: "#/components/responses/Error" } "409": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /v1/webhooks: get: tags: [Webhooks] operationId: listWebhooks summary: 回调端点列表 responses: "200": description: 回调端点 content: application/json: schema: type: array items: { $ref: "#/components/schemas/WebhookEndpoint" } default: { $ref: "#/components/responses/Error" } post: tags: [Webhooks] operationId: createWebhook summary: 注册回调端点(不幂等) requestBody: required: true content: application/json: schema: type: object required: [url] properties: url: { type: string, maxLength: 2048 } events: type: array items: { $ref: "#/components/schemas/EventType" } description: 空或省略 = 全部事件 description: { type: string } responses: "201": description: 已创建 content: { application/json: { schema: { $ref: "#/components/schemas/WebhookEndpoint" } } } "422": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /v1/webhooks/{id}: parameters: - $ref: "#/components/parameters/WebhookId" patch: tags: [Webhooks] operationId: updateWebhook summary: 修改回调端点 requestBody: required: true content: application/json: schema: type: object properties: url: { type: string, maxLength: 2048 } events: type: array items: { $ref: "#/components/schemas/EventType" } description: { type: string } active: { type: boolean, description: true 启用 / false 停用 } responses: "200": description: 修改后的端点 content: { application/json: { schema: { $ref: "#/components/schemas/WebhookEndpoint" } } } "404": { $ref: "#/components/responses/Error" } "422": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } delete: tags: [Webhooks] operationId: deleteWebhook summary: 停用回调端点(状态改为 DISABLED) responses: "204": { description: 已停用 } "404": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /v1/webhooks/{id}/test: parameters: - $ref: "#/components/parameters/WebhookId" post: tags: [Webhooks] operationId: testWebhook summary: 发送 WEBHOOK_TEST 事件 responses: "202": description: 已排队 content: application/json: schema: type: object required: [eventId] properties: eventId: { type: string, format: uuid } "404": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /v1/webhooks/deliveries: get: tags: [Webhooks] operationId: listWebhookDeliveries summary: 投递记录 parameters: - { name: endpointId, in: query, schema: { type: string, format: uuid } } - { name: status, in: query, schema: { type: string, enum: [PENDING, DELIVERED, DEAD] } } - { name: before, in: query, schema: { type: integer, format: int64 }, description: 上一页最后一条的 id } - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } } responses: "200": description: 投递记录 content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: "#/components/schemas/WebhookDelivery" } default: { $ref: "#/components/responses/Error" } /v1/webhooks/events/{eventId}/resend: post: tags: [Webhooks] operationId: resendWebhookEvent summary: 重发事件 parameters: - { name: eventId, in: path, required: true, schema: { type: string, format: uuid } } responses: "200": description: 已重新排队 content: application/json: schema: type: object required: [eventId, requeued] properties: eventId: { type: string, format: uuid } requeued: { type: integer, description: 重新排队的投递数 } "404": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /v1/webhooks/public_key: get: tags: [Webhooks] operationId: getWebhookPublicKey summary: 平台回调签名公钥 responses: "200": description: 公钥 content: application/json: schema: type: object required: [keyId, publicKey] properties: keyId: { type: string, enum: [v1] } publicKey: { type: string, pattern: "^[0-9a-f]{64}$", description: 32 字节 Ed25519 公钥 hex } default: { $ref: "#/components/responses/Error" } /v1/webhooks/event_types: get: tags: [Webhooks] operationId: getWebhookEventTypes summary: 事件类型与签名说明(无需认证) security: [] responses: "200": description: 元数据 content: application/json: schema: type: object properties: eventTypes: type: array items: { $ref: "#/components/schemas/EventType" } signature: type: object properties: algorithm: { type: string, enum: [Ed25519] } keyId: { type: string } publicKey: { type: string } headers: { type: array, items: { type: string } } signedMessage: { type: string } signatureHeaderFormat: { type: string } retrySchedule: type: array items: { type: string } # ── 非 B 端:平台管理订阅有效期、租户后台内部开户与提款元数据 ── # 这些路径使用管理员 Bearer 会话而非 X-API-Key + EdDSA JWT。 /admin/v1/tenants: servers: - url: https://admin.fablewallet.top post: tags: [Admin] operationId: createTenant summary: 开通租户(平台 OWNER,走双人复核) description: | 平台后台接口。请求体 `{slug,name,ownerEmail,ownerPublicKey,expiresAt?}`;`ownerPublicKey` 是 owner 本地持有的 32 字节 Ed25519 公钥(64 hex),私钥与解锁口令不出本机。**不接收** `ownerPassword` 或任何密码字段; 未知字段(含私钥/密码/解锁口令)在入队前校验即返回 422,不会落成待复核变更单。 经平台双人复核(另一名 OWNER 批准)后才真正创建,届时返回租户对象。 security: [{ AdminBearer: [] }] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateTenantRequest" } responses: "202": description: 已进入平台双人复核队列(尚未创建) content: { application/json: { schema: { $ref: "#/components/schemas/PendingChange" } } } "403": { $ref: "#/components/responses/Error" } "409": { $ref: "#/components/responses/Error" } "422": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /admin/v1/tenants/{id}/subscription: servers: - url: https://admin.fablewallet.top parameters: - name: id in: path required: true schema: { type: string, format: uuid } put: tags: [Admin] operationId: updateTenantSubscription summary: 设置/清除租户订阅有效期(平台 OWNER,走双人复核) description: | 平台后台接口,仅 `OWNER` 可调用。请求体必须**恰好**包含 `expiresAt`(RFC 3339 字符串或 `null`=长期), 缺字段、多字段或格式错误返回 422 `BAD_REQUEST`。请求先进入平台双人复核队列:普通调用只得到 `202 PENDING` (`changeId`/`status`),**202 不等于生效**;另一名 OWNER 批准后才真正执行,200 返回的是复核发布后的业务结果 (最新租户对象)。设置过去时间即立即到期。`status`(ACTIVE/SUSPENDED/CLOSED)与订阅有效期相互独立, 续期不会自动解除暂停。 security: [{ AdminBearer: [] }] requestBody: required: true content: application/json: schema: type: object additionalProperties: false required: [expiresAt] properties: expiresAt: { type: [string, "null"], format: date-time, description: RFC 3339;null = 长期 } responses: "202": description: 已进入平台双人复核队列(尚未生效) content: { application/json: { schema: { $ref: "#/components/schemas/PendingChange" } } } "200": description: 复核发布后的业务结果(最新租户对象) content: { application/json: { schema: { $ref: "#/components/schemas/Tenant" } } } "403": { $ref: "#/components/responses/Error" } "422": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /admin/v1/tenants/{id}/admins: servers: - url: https://admin.fablewallet.top parameters: - name: id in: path required: true schema: { type: string, format: uuid } post: tags: [Admin] operationId: createTenantAdmin summary: 创建租户管理员(平台 CONFIG/OWNER,走双人复核) description: | 平台后台接口。请求体 `{email,publicKey,roles}`;`publicKey` 是管理员本地持有的 32 字节 Ed25519 公钥(64 hex)。 **不接收** `password` 或任何密码字段;未知字段(含密码/私钥)在入队前校验即返回 422,不会落成待复核变更单。 经平台双人复核后才真正创建。返回的管理员对象只含公开字段,不含密码哈希或任何私钥。 security: [{ AdminBearer: [] }] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateTenantAdminRequest" } responses: "202": description: 已进入平台双人复核队列(尚未创建) content: { application/json: { schema: { $ref: "#/components/schemas/PendingChange" } } } "403": { $ref: "#/components/responses/Error" } "409": { $ref: "#/components/responses/Error" } "422": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } # ── 管理员认证(控制台,匿名自助;不属于 B 端 /v1 对接范围)── /admin/v1/auth/challenge: servers: - url: https://admin.fablewallet.top post: tags: [Admin] operationId: platformAdminChallenge summary: 平台管理员凭证挑战 description: | 匿名自助认证入口。`keyId` 必须是平台范围内**已知且有效**的凭证(未吊销、owner ACTIVE);否则不返回可用挑战。 签发一次性挑战,120 秒有效,绑定 scope;签名原文为 `waas-admin-login:v1:`。 security: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/AdminChallengeRequest" } responses: "200": description: 挑战 content: { application/json: { schema: { $ref: "#/components/schemas/AdminChallenge" } } } "401": { $ref: "#/components/responses/Error" } "403": { $ref: "#/components/responses/Error" } "422": { $ref: "#/components/responses/Error" } "429": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /admin/v1/auth/key-login: servers: - url: https://admin.fablewallet.top post: tags: [Admin] operationId: platformAdminKeyLogin summary: 平台管理员凭证登录(签名 + 强制 TOTP) description: | 校验对挑战的 Ed25519 签名(一次性,120 秒)。**未绑定 TOTP** 的凭证只返回 `enrollTotp`(含一次性 enrollmentToken 与 TOTP 密钥/otpauth URI),**不创建会话**;完成 `POST /admin/v1/auth/enroll` 绑定后才发会话。**已绑定**的必须提供有效 `totp`(未提供 403 `TWO_FACTOR_REQUIRED`,错误或同码二次使用 403 `TWO_FACTOR_INVALID`)。固定密码登录已永久停用。 吊销凭证会阻断未完成的 enrollment,并注销该管理员所有会话;其他设备仍可用“凭证 + TOTP”重新登录。 security: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/AdminKeyLoginRequest" } responses: "200": description: 会话或首次绑定步骤 content: { application/json: { schema: { $ref: "#/components/schemas/AdminLogin" } } } "401": { $ref: "#/components/responses/Error" } "403": { $ref: "#/components/responses/Error" } "422": { $ref: "#/components/responses/Error" } "429": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /admin/v1/auth/enroll: servers: - url: https://admin.fablewallet.top post: tags: [Admin] operationId: platformAdminEnrollTotp summary: 平台管理员绑定 TOTP(发放会话) description: 用 `key-login` 返回的一次性 `enrollmentToken` 与 6 位 `code` 完成绑定;成功后才创建会话。令牌一次性、10 分钟有效,且绑定到仍存活的凭证。 security: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/AdminEnrollRequest" } responses: "200": description: 绑定成功并发放会话 content: { application/json: { schema: { $ref: "#/components/schemas/AdminLogin" } } } "401": { $ref: "#/components/responses/Error" } "403": { $ref: "#/components/responses/Error" } "422": { $ref: "#/components/responses/Error" } "429": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /admin/v1/auth/login: servers: - url: https://admin.fablewallet.top post: tags: [Admin] operationId: platformAdminLoginDeprecated deprecated: true summary: 平台管理员固定密码登录(已永久停用) description: | 固定密码管理员登录**永久停用**:不解析请求体,无论请求体(含空体或非法 JSON)如何都返回 403 `FORBIDDEN`。 没有固定密码登录回退。管理员只能通过“本地 Ed25519 凭证签名 + 强制 TOTP”登录。 security: [] responses: "403": { $ref: "#/components/responses/Error" } # ── 租户管理员认证(控制台,匿名自助;不属于 B 端 /v1 对接范围)── /tenant-admin/v1/auth/challenge: servers: - url: https://admin.fablewallet.top post: tags: [TenantAdmin] operationId: tenantAdminChallenge summary: 租户管理员凭证挑战 description: | 匿名自助认证入口。需带 `tenantSlug` 以确定租户范围;`keyId` 必须是该租户范围内**已知且有效**的凭证, 租户被暂停(SUSPENDED)时拒绝。签发一次性挑战,120 秒有效,绑定 scope/租户;签名原文为 `waas-admin-login:v1:`。 security: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/TenantAdminChallengeRequest" } responses: "200": description: 挑战 content: { application/json: { schema: { $ref: "#/components/schemas/AdminChallenge" } } } "401": { $ref: "#/components/responses/Error" } "403": { $ref: "#/components/responses/Error" } "404": { $ref: "#/components/responses/Error" } "422": { $ref: "#/components/responses/Error" } "429": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /tenant-admin/v1/auth/key-login: servers: - url: https://admin.fablewallet.top post: tags: [TenantAdmin] operationId: tenantAdminKeyLogin summary: 租户管理员凭证登录(签名 + 强制 TOTP) description: | 校验对挑战的 Ed25519 签名(一次性,120 秒,租户绑定)。**未绑定 TOTP** 的凭证只返回 `enrollTotp`(不创建会话), 完成 `POST /tenant-admin/v1/auth/enroll` 绑定后才发会话;**已绑定**必须提供有效 `totp`(未提供 403 `TWO_FACTOR_REQUIRED`, 错误或同码二次使用 403 `TWO_FACTOR_INVALID`)。暂停租户拒绝;**订阅到期**的租户仍可凭证登录(仅放行白名单路径)。 固定密码登录已永久停用。吊销凭证会阻断未完成的 enrollment 并注销该管理员所有会话;其他设备可用“凭证 + TOTP”重新登录。 security: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/TenantAdminKeyLoginRequest" } responses: "200": description: 会话或首次绑定步骤 content: { application/json: { schema: { $ref: "#/components/schemas/AdminLogin" } } } "401": { $ref: "#/components/responses/Error" } "403": { $ref: "#/components/responses/Error" } "422": { $ref: "#/components/responses/Error" } "429": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /tenant-admin/v1/auth/enroll: servers: - url: https://admin.fablewallet.top post: tags: [TenantAdmin] operationId: tenantAdminEnrollTotp summary: 租户管理员绑定 TOTP(发放会话) description: 用 `key-login` 返回的一次性 `enrollmentToken` 与 6 位 `code` 完成绑定;成功后才创建会话。令牌一次性、10 分钟有效,且绑定到仍存活的凭证。 security: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/TenantAdminEnrollRequest" } responses: "200": description: 绑定成功并发放会话 content: { application/json: { schema: { $ref: "#/components/schemas/AdminLogin" } } } "401": { $ref: "#/components/responses/Error" } "403": { $ref: "#/components/responses/Error" } "422": { $ref: "#/components/responses/Error" } "429": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /tenant-admin/v1/auth/login: servers: - url: https://admin.fablewallet.top post: tags: [TenantAdmin] operationId: tenantAdminLoginDeprecated deprecated: true summary: 租户管理员固定密码登录(已永久停用) description: | 固定密码管理员登录**永久停用**:不解析请求体,无论请求体(含空体或非法 JSON)如何都返回 403 `FORBIDDEN`。 没有固定密码登录回退。管理员只能通过“本地 Ed25519 凭证签名 + 强制 TOTP”登录。 security: [] responses: "403": { $ref: "#/components/responses/Error" } /tenant-admin/v1/subscription: servers: - url: https://admin.fablewallet.top get: tags: [TenantAdmin] operationId: getTenantSubscription summary: 当前租户的订阅状态 description: 返回 `{expiresAt, isExpired, status}`;`expires_at <= now` 时 `isExpired=true`,`null` 恒为 false。到期租户仍可访问。 security: [{ AdminBearer: [] }] responses: "200": description: 订阅状态 content: { application/json: { schema: { $ref: "#/components/schemas/Subscription" } } } "403": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /tenant-admin/v1/users: servers: - url: https://admin.fablewallet.top post: tags: [TenantAdmin] operationId: createInternalUser summary: 租户后台内部开户(OWNER/OPS,step-up 2FA) description: 按 customerRefId 幂等;重复返回原账号,不覆盖邮箱/密码/昵称。新建账号 source=B_API。与 B 端 `POST /v1/users` 对同一 customerRefId 幂等(B 端成功为 200,本接口成功为 201)。 security: [{ AdminBearer: [] }] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/InternalUserRequest" } responses: "201": description: 用户 content: { application/json: { schema: { $ref: "#/components/schemas/User" } } } "403": { $ref: "#/components/responses/Error" } "409": { $ref: "#/components/responses/Error" } "422": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /tenant-admin/v1/withdrawal-assets: servers: - url: https://admin.fablewallet.top get: tags: [TenantAdmin] operationId: listWithdrawalAssets summary: 本租户已开放网络的可提款资产元数据 description: 返回本租户**已开放网络**的可提款资产元数据(`{items:[{assetId,symbol,name,decimals,networkKey,kind}]}`),**不含费率/限额**,也不会回退到 `GET /tenant-admin/v1/asset-settings`。订阅到期后仍可访问,用于资金库提款选择资产。 security: [{ AdminBearer: [] }] responses: "200": description: 可提款资产 content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: "#/components/schemas/WithdrawalAsset" } "403": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /tenant-admin/v1/vault-payouts: servers: - url: https://admin.fablewallet.top post: tags: [TenantAdmin] operationId: createVaultPayout summary: 租户资金池提款(OPS/OWNER + step-up 2FA,另一名管理员批准) description: | `Idempotency-Key` 必填:缺少/非法时返回 `422 BAD_REQUEST`;同一 (租户, 管理员, 接口, key) 与同一请求体返回原单; 同一 key 携带不同请求体返回 `409 IDEMPOTENCY_CONFLICT`。角色不足或 step-up 2FA 未通过返回 `403`;发起人不能批准自己发起的提款。 请求体 JSON 无法解析/字段类型错误由框架返回 `400` 或 `422`(两者都会出现)。订阅到期后仍可发起,只收窄可访问范围,金额/资产/角色/账本规则不变。 security: [{ AdminBearer: [] }] parameters: - $ref: "#/components/parameters/IdempotencyKeyRequired" requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/VaultPayoutRequest" } responses: "200": description: 提款单 content: { application/json: { schema: { $ref: "#/components/schemas/VaultPayout" } } } "400": { $ref: "#/components/responses/Error" } "403": { $ref: "#/components/responses/Error" } "409": { $ref: "#/components/responses/Error" } "422": { $ref: "#/components/responses/Error" } default: { $ref: "#/components/responses/Error" } /app/v1/auth/register/code: servers: - url: https://wallet.fablewallet.top post: tags: [C-end (deprecated)] operationId: sendRegisterCodeDeprecated deprecated: true summary: 发送注册验证码(已永久停用) description: 自助注册停用;无论请求体如何都返回 403 FORBIDDEN(Self registration is disabled; contact your tenant)。 security: [] responses: "403": { $ref: "#/components/responses/Error" } /app/v1/auth/register: servers: - url: https://wallet.fablewallet.top post: tags: [C-end (deprecated)] operationId: registerDeprecated deprecated: true summary: C 端自助注册(已永久停用) description: 自助注册停用;无论请求体如何都返回 403 FORBIDDEN。用户改由 POST /v1/users 或租户后台内部开户创建。 security: [] responses: "403": { $ref: "#/components/responses/Error" } webhooks: waasEvent: post: summary: 平台推送到你方回调地址的事件 description: | 验签:Ed25519(publicKey, X-WaaS-Timestamp + "." + 原始请求体),签名在 X-WaaS-Signature 的 sig 中(hex)。 10 秒内返回 2xx 视为成功;失败按 1m/5m/30m/2h/12h 重试。按 X-WaaS-Event-Id 去重。 parameters: - { name: X-WaaS-Event-Id, in: header, required: true, schema: { type: string, format: uuid } } - { name: X-WaaS-Event-Type, in: header, required: true, schema: { $ref: "#/components/schemas/EventType" } } - { name: X-WaaS-Timestamp, in: header, required: true, schema: { type: string, description: Unix 秒 } } - { name: X-WaaS-Signature, in: header, required: true, schema: { type: string, example: "keyId=v1,sig=" } } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/WebhookEvent" } responses: "200": { description: 已接收(任意 2xx) } components: securitySchemes: ApiKey: type: apiKey in: header name: X-API-Key description: API Key 的 keyId(如 ak_0123456789abcdef01234567) SignedJwt: type: http scheme: bearer bearerFormat: JWT (EdDSA / Ed25519) description: | 每个请求单独生成:header {"alg":"EdDSA","typ":"JWT"};claims uri、nonce、iat、exp(exp-iat≤30)、sub、bodyHash。 base64url 无填充。 AdminBearer: type: http scheme: bearer description: | 平台后台 `/admin/v1` 与租户后台 `/tenant-admin/v1` 的管理员会话令牌,形如 `v1..`; 由“本地 Ed25519 凭证签名 + 强制 TOTP”登录后签发,服务端可即时吊销。它不是 B 端 API Key / SignedJwt, 也不是 JWT。 parameters: CustomerRefId: name: customerRefId in: path required: true description: 交易所侧用户 ID(需 URL 编码) schema: { type: string, minLength: 1, maxLength: 128 } WalletId: name: id in: path required: true schema: { type: string, format: uuid } TransactionId: name: id in: path required: true schema: { type: string, format: uuid } WebhookId: name: id in: path required: true schema: { type: string, format: uuid } IdempotencyKeyRequired: name: Idempotency-Key in: header required: true schema: { type: string, minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9_\\-:.]+$" } IdempotencyKeyOptional: name: Idempotency-Key in: header required: false description: 请求体没有 externalTxId 时必填 schema: { type: string, minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9_\\-:.]+$" } responses: Error: description: 错误 headers: X-Request-Id: schema: { type: string } content: application/json: schema: { $ref: "#/components/schemas/ErrorResponse" } schemas: RawAmount: type: string pattern: "^[0-9]+$" description: 最小单位的十进制整数字符串 examples: ["1500000"] ChainFamily: type: string enum: [EVM, TRON, BTC, SOL] Settlement: type: string enum: [ON_CHAIN, OFF_CHAIN] TransactionKind: type: string enum: [DEPOSIT, WITHDRAWAL, INTERNAL_TRANSFER] TxStatus: type: string enum: [SUBMITTED, PENDING_AML_SCREENING, PENDING_AUTHORIZATION, QUEUED, PENDING_SIGNATURE, BROADCASTING, CONFIRMING, PENDING_REVIEW, COMPLETED, CANCELLED, REJECTED, BLOCKED, FAILED, REVERSED] EventType: type: string enum: [TRANSACTION_CREATED, TRANSACTION_STATUS_UPDATED, WALLET_CREATED, DEPOSIT_REVERSED, MANUAL_DEPOSIT_CREDITED, VAULT_GAS_LOW, WEBHOOK_TEST] ErrorCode: type: string enum: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - RATE_LIMITED - IDEMPOTENCY_CONFLICT - INVALID_STATE - INTERNAL - INVALID_ADDRESS - ASSET_NOT_SUPPORTED - ASSET_DISABLED - NETWORK_DISABLED - ASSET_MISMATCH - WALLET_LIMIT_REACHED - WALLET_NOT_ACTIVE - RECIPIENT_NOT_FOUND - RECIPIENT_NO_WALLET - SAME_WALLET_TRANSFER - SAME_USER_USE_WALLET_TRANSFER - BELOW_MIN_WITHDRAWAL - QUOTE_EXPIRED - INSUFFICIENT_BALANCE - LIMIT_EXCEEDED - TWO_FACTOR_REQUIRED - TWO_FACTOR_INVALID - COOLDOWN_ACTIVE - BLOCKED_BY_POLICY - EMAIL_TAKEN - INVALID_CREDENTIALS - INTERNAL_DESTINATION_NOT_ALLOWED - PROVIDER_UNAVAILABLE - MANUAL_REVIEW_REQUIRED - SIGNER_UNAVAILABLE ErrorResponse: type: object required: [error] properties: error: type: object required: [code, message, retryable, requestId] properties: code: { $ref: "#/components/schemas/ErrorCode" } message: { type: string } retryable: { type: boolean } requestId: { type: string } Tenant: type: object required: [id, tenantIndex, slug, name, status, branding, expiresAt] properties: id: { type: string, format: uuid } tenantIndex: { type: integer } slug: { type: string } name: { type: string } status: { type: string, enum: [ACTIVE, SUSPENDED, CLOSED], description: 与订阅有效期独立;续期不会自动解除 SUSPENDED } branding: { type: object } expiresAt: { type: [string, "null"], format: date-time, description: 订阅截止时间;null = 长期 } Subscription: type: object required: [expiresAt, isExpired, status] properties: expiresAt: { type: [string, "null"], format: date-time } isExpired: { type: boolean, description: expiresAt <= now 时 true;null 恒为 false } status: { type: string, enum: [ACTIVE, SUSPENDED, CLOSED] } InternalUserRequest: type: object required: [customerRefId] properties: customerRefId: { type: string, minLength: 1, maxLength: 128, description: 租户内唯一、幂等键 } email: { type: string, format: email } password: { type: string, format: password, writeOnly: true, minLength: 10, maxLength: 128, description: 必须与 email 一起提供;仅写入新账号 } nickname: { type: string, maxLength: 32 } WithdrawalAsset: type: object required: [assetId, symbol, name, decimals, networkKey, kind] properties: assetId: { type: string } symbol: { type: string } name: { type: string } decimals: { type: integer } networkKey: { type: string } kind: { type: string, enum: [NATIVE, TOKEN] } VaultPayoutRequest: type: object required: [vaultRole, assetId, toAddress, amountRaw] properties: vaultRole: { type: string, enum: [HOT, SWEEP_TARGET, GAS], description: 来源金库角色 } assetId: { type: string } toAddress: { type: string, description: 外部目的地址;不得是本租户自有地址 } amountRaw: { $ref: "#/components/schemas/RawAmount" } note: { type: [string, "null"] } VaultPayout: type: object required: [id, tenantId, networkKey, assetId, vaultRole, toAddress, amountRaw, state, exceedsOwnFunds, initiatedBy] properties: id: { type: string, format: uuid } tenantId: { type: string, format: uuid } networkKey: { type: string } assetId: { type: string } vaultRole: { type: string, enum: [HOT, SWEEP_TARGET, GAS] } toAddress: { type: string } amountRaw: { $ref: "#/components/schemas/RawAmount" } state: { type: string, enum: [PENDING_APPROVAL, APPROVED, BROADCASTING, COMPLETED, FAILED, REJECTED, CANCELLED] } exceedsOwnFunds: { type: boolean, description: 是否超出该租户自有资金(页面红字警示) } initiatedBy: { type: string, format: uuid } txHash: { type: [string, "null"] } PendingChange: type: object required: [changeId, status] properties: changeId: { type: string, format: uuid } status: { type: string, enum: [PENDING], description: 已入队待复核;不等于已生效 } domain: { type: string, description: 变更领域标签(平台回执附带) } CreateTenantRequest: type: object additionalProperties: false required: [slug, name, ownerEmail, ownerPublicKey] properties: slug: { type: string, minLength: 2, maxLength: 63, description: 小写字母/数字/连字符,不以连字符开头 } name: { type: string } ownerEmail: { type: string, format: email } ownerPublicKey: { type: string, pattern: "^[0-9a-f]{64}$", description: owner 本地持有的 32 字节 Ed25519 公钥(64 hex);私钥与解锁口令不出本机 } expiresAt: { type: [string, "null"], format: date-time, description: 可选;RFC 3339 或 null=长期 } CreateTenantAdminRequest: type: object additionalProperties: false required: [email, publicKey, roles] properties: email: { type: string, format: email } publicKey: { type: string, pattern: "^[0-9a-f]{64}$", description: 管理员本地持有的 32 字节 Ed25519 公钥(64 hex);私钥与解锁口令不出本机 } roles: { type: array, items: { type: string, enum: [OWNER, APPROVER, CONFIG, RISK, OPS, AUDITOR] } } AdminChallengeRequest: type: object required: [keyId] properties: keyId: { type: string, description: 已登记的凭证 keyId } AdminKeyLoginRequest: type: object required: [keyId, signature] properties: keyId: { type: string } signature: { type: string, pattern: "^[0-9a-f]{128}$", description: Ed25519 对挑战 message 的签名(64 字节 hex) } totp: { type: string, pattern: "^[0-9]{6}$", description: 已绑定 TOTP 时必填 } AdminEnrollRequest: type: object required: [enrollmentToken, code] properties: enrollmentToken: { type: string, description: key-login 返回的一次性绑定令牌 } code: { type: string, pattern: "^[0-9]{6}$", description: 当前 TOTP 6 位验证码 } TenantAdminChallengeRequest: type: object required: [tenantSlug, keyId] properties: tenantSlug: { type: string } keyId: { type: string } TenantAdminKeyLoginRequest: type: object required: [tenantSlug, keyId, signature] properties: tenantSlug: { type: string } keyId: { type: string } signature: { type: string, pattern: "^[0-9a-f]{128}$" } totp: { type: string, pattern: "^[0-9]{6}$" } TenantAdminEnrollRequest: type: object required: [tenantSlug, enrollmentToken, code] properties: tenantSlug: { type: string } enrollmentToken: { type: string } code: { type: string, pattern: "^[0-9]{6}$" } AdminChallenge: type: object required: [keyId, challenge, message, expiresIn] properties: keyId: { type: string } challenge: { type: string, description: 一次性随机挑战,120 秒有效 } message: { type: string, description: "需签名的原文 `waas-admin-login:v1:`" } expiresIn: { type: integer, description: 有效期(秒) } Admin: type: object additionalProperties: false required: [id, scope, tenantId, email, username, roles, totpEnabled, status] description: 管理员公开对象;不含密码哈希或任何私钥。 properties: id: { type: string, format: uuid } scope: { type: string, enum: [PLATFORM, TENANT] } tenantId: { type: [string, "null"], format: uuid } email: { type: [string, "null"] } username: { type: [string, "null"] } roles: { type: array, items: { type: string } } totpEnabled: { type: boolean } status: { type: string, enum: [ACTIVE, DISABLED] } AdminLogin: oneOf: - $ref: "#/components/schemas/AdminLoginEnrollTotp" - $ref: "#/components/schemas/AdminLoginAuthenticated" discriminator: propertyName: step mapping: enrollTotp: "#/components/schemas/AdminLoginEnrollTotp" authenticated: "#/components/schemas/AdminLoginAuthenticated" AdminLoginEnrollTotp: type: object required: [step, enrollmentToken, secret, otpauthUrl] description: 首次登录:只返回 TOTP 绑定材料,**不创建会话**。 properties: step: { type: string, enum: [enrollTotp] } enrollmentToken: { type: string, description: 一次性绑定令牌(10 分钟) } secret: { type: string, description: TOTP 密钥(Base32);仅用于本机绑定,服务端不返回固定值 } otpauthUrl: { type: string, description: 供本机二维码使用的 otpauth URI } AdminLoginAuthenticated: type: object required: [step, accessToken, expiresIn, admin] properties: step: { type: string, enum: [authenticated] } accessToken: { type: string, description: 控制台会话令牌;后续请求在 `Authorization` 头以 `Bearer ` 发送 } expiresIn: { type: integer, description: 会话有效期(秒,8 小时) } admin: { $ref: "#/components/schemas/Admin" } User: type: object required: [id, tenantId, uid, email, nickname, totpEnabled, source, customerRefId, status, cooldownUntil] properties: id: { type: string, format: uuid } tenantId: { type: string, format: uuid } uid: { type: string, pattern: "^[1-9][0-9]{7}$" } email: { type: [string, "null"] } nickname: { type: [string, "null"] } totpEnabled: { type: boolean } source: { type: string, enum: [B_API, C_DIRECT] } customerRefId: { type: [string, "null"] } status: { type: string, enum: [ACTIVE, FROZEN, CLOSED] } cooldownUntil: { type: [string, "null"], format: date-time } UserListItem: type: object required: [uid, customerRefId, email, status, createdAt] properties: uid: { type: string } customerRefId: { type: [string, "null"] } email: { type: [string, "null"] } status: { type: string, enum: [ACTIVE, FROZEN, CLOSED] } createdAt: { type: string, format: date-time } WalletStatus: type: string enum: [PROVISIONING, ACTIVE, HIDDEN, FROZEN, CLOSED] Wallet: type: object required: [id, tenantId, userId, chainFamily, name, seq, isDefault, status, address, createdAt] properties: id: { type: string, format: uuid } tenantId: { type: string, format: uuid } userId: { type: string, format: uuid } chainFamily: { $ref: "#/components/schemas/ChainFamily" } name: { type: string } seq: { type: integer } isDefault: { type: boolean } status: { $ref: "#/components/schemas/WalletStatus" } address: { type: [string, "null"] } createdAt: { type: string, format: date-time } WalletListItem: type: object required: [id, userId, customerRefId, chainFamily, name, seq, isDefault, status, address, createdAt] properties: id: { type: string, format: uuid } userId: { type: string, format: uuid } customerRefId: { type: [string, "null"] } chainFamily: { $ref: "#/components/schemas/ChainFamily" } name: { type: string } seq: { type: integer } isDefault: { type: boolean } status: { $ref: "#/components/schemas/WalletStatus" } address: { type: [string, "null"] } createdAt: { type: string, format: date-time } DepositInfo: type: object required: [walletId, networkKey, networkName, address, watchStatus, confirmations, supportedAssets] properties: walletId: { type: string, format: uuid } networkKey: { type: string } networkName: { type: string } address: { type: [string, "null"], description: 仅钱包 ACTIVE 且监听 ACTIVE/DEGRADED 时返回 } watchStatus: { type: [string, "null"], enum: [ALLOCATED, REGISTERING, ACTIVE, DEGRADED, FROZEN, null] } confirmations: type: object description: '{"mode":"confirmations","n":12} | {"mode":"finalized"} | {"mode":"solidified"}' properties: mode: { type: string } n: { type: integer } supportedAssets: type: array items: type: object required: [assetId, symbol, name, decimals, minDepositRaw, depositFeeRaw, depositPaused, iconUrl] properties: assetId: { type: string } symbol: { type: string } name: { type: string } decimals: { type: integer } minDepositRaw: { $ref: "#/components/schemas/RawAmount" } depositFeeRaw: { $ref: "#/components/schemas/RawAmount" } depositPaused: { type: boolean } iconUrl: { type: [string, "null"] } Network: type: object required: [networkKey, chainFamily, displayName, isTestnet, enabled] properties: networkKey: { type: string } chainFamily: { $ref: "#/components/schemas/ChainFamily" } displayName: { type: string } isTestnet: { type: boolean } enabled: { type: boolean } SupportedAsset: type: object required: [assetId, networkKey, chainFamily, kind, symbol, name, decimals, status, withdrawEnabled, minDeposit, minWithdraw, withdrawFeeFloor, depositFeeFloor, networkWithdrawEnabled, networkDepositEnabled, depositFeeRaw, withdrawFeeRaw] properties: assetId: { type: string } networkKey: { type: string } chainFamily: { $ref: "#/components/schemas/ChainFamily" } kind: { type: string, enum: [NATIVE, TOKEN] } contract: { type: [string, "null"] } tokenProgram: { type: [string, "null"] } symbol: { type: string } name: { type: string } decimals: { type: integer } status: { type: string, enum: [ENABLED, DEPOSIT_PAUSED] } withdrawEnabled: { type: boolean } minDeposit: { $ref: "#/components/schemas/RawAmount" } minWithdraw: { $ref: "#/components/schemas/RawAmount" } withdrawFeeFloor: { $ref: "#/components/schemas/RawAmount", description: "平台默认提现费(历史字段名);租户可覆盖,动态成本下限仍生效" } depositFeeFloor: { $ref: "#/components/schemas/RawAmount", description: "平台默认充值费(历史字段名);租户可覆盖,动态成本下限仍生效" } singleLimitCap: { type: [string, "null"], description: "平台默认单笔限额;不限制租户覆盖,租户显式 null 表示不限额" } dailyLimitCap: { type: [string, "null"], description: "平台默认每日限额;不限制租户覆盖,租户显式 null 表示不限额" } iconUrl: { type: [string, "null"] } networkWithdrawEnabled: { type: boolean } networkDepositEnabled: { type: boolean } depositFeeRaw: { $ref: "#/components/schemas/RawAmount" } withdrawFeeRaw: { $ref: "#/components/schemas/RawAmount" } Party: type: object required: [walletId, address, customerRefId, uid] properties: walletId: { type: [string, "null"], format: uuid } address: { type: [string, "null"] } customerRefId: { type: [string, "null"] } uid: { type: [string, "null"] } Transaction: type: object required: [id, kind, settlement, status, subStatus, assetId, networkKey, decimals, amountRaw, feeRaw, networkFeeRaw, source, destination, txHash, externalTxId, note, createdAt, updatedAt] properties: id: { type: string, format: uuid } kind: { $ref: "#/components/schemas/TransactionKind" } settlement: { $ref: "#/components/schemas/Settlement" } status: { $ref: "#/components/schemas/TxStatus" } subStatus: { type: [string, "null"] } assetId: { type: string } networkKey: { type: string } decimals: { type: integer } amountRaw: { type: [string, "null"], pattern: "^[0-9]+$" } feeRaw: { type: [string, "null"], pattern: "^[0-9]+$" } netAmountRaw: type: string pattern: "^[0-9]+$" description: 仅充值且已确定手续费时出现:amountRaw - feeRaw networkFeeRaw: type: [string, "null"] description: 链上提现实际网络费,单位为网络原生币最小单位 networkFeeAssetId: type: [string, "null"] description: networkFeeRaw 的币种(该网络原生币的资产 ID) source: { $ref: "#/components/schemas/Party" } destination: { $ref: "#/components/schemas/Party" } txHash: { type: [string, "null"] } externalTxId: { type: [string, "null"] } note: { type: [string, "null"] } createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } CreateTransactionRequest: type: object required: [assetId, source, destination, amountRaw] properties: assetId: { type: string } source: type: object required: [type, id] properties: type: { type: string, enum: [WALLET] } id: { type: string, format: uuid } destination: oneOf: - $ref: "#/components/schemas/DestinationWallet" - $ref: "#/components/schemas/DestinationEndUser" - $ref: "#/components/schemas/DestinationOneTimeAddress" discriminator: propertyName: type mapping: WALLET: "#/components/schemas/DestinationWallet" END_USER: "#/components/schemas/DestinationEndUser" ONE_TIME_ADDRESS: "#/components/schemas/DestinationOneTimeAddress" amountRaw: { $ref: "#/components/schemas/RawAmount" } externalTxId: { type: string, minLength: 1, maxLength: 128, description: 你方唯一单号,同时作为幂等键 } note: { type: string } DestinationWallet: type: object required: [type, id] properties: type: { type: string, enum: [WALLET] } id: { type: string, format: uuid } DestinationEndUser: type: object required: [type] description: uid 与 customerRefId 必须且只能提供一个 properties: type: { type: string, enum: [END_USER] } uid: { type: string } customerRefId: { type: string } DestinationOneTimeAddress: type: object required: [type, oneTimeAddress] properties: type: { type: string, enum: [ONE_TIME_ADDRESS] } oneTimeAddress: type: object required: [address] properties: address: { type: string } WithdrawalView: type: object required: [withdrawalId, transactionId, status, subStatus, state, assetId, amountRaw, feeRaw, reservedRaw, destination] properties: withdrawalId: { type: string, format: uuid } transactionId: { type: string, format: uuid } status: { $ref: "#/components/schemas/TxStatus" } subStatus: { type: [string, "null"] } state: { type: string, description: 平台内部提现状态,仅供排查 } assetId: { type: string } amountRaw: { $ref: "#/components/schemas/RawAmount" } feeRaw: { $ref: "#/components/schemas/RawAmount" } reservedRaw: { $ref: "#/components/schemas/RawAmount" } destination: { type: string } WithdrawalReceipt: allOf: - $ref: "#/components/schemas/WithdrawalView" - type: object required: [settlement] properties: settlement: { type: string, enum: [ON_CHAIN] } TransferReceipt: type: object required: [settlement, transferId, transactionId, status, fromWalletId, toWalletId, assetId, amount, fee] properties: settlement: { type: string, enum: [OFF_CHAIN] } transferId: { type: string, format: uuid } transactionId: { type: string, format: uuid } status: { type: string, enum: [COMPLETED, PENDING_AUTHORIZATION] } fromWalletId: { type: string, format: uuid } toWalletId: { type: string, format: uuid } assetId: { type: string } amount: { $ref: "#/components/schemas/RawAmount" } fee: { $ref: "#/components/schemas/RawAmount" } WebhookEndpoint: type: object required: [id, url, events, description, status, createdAt] properties: id: { type: string, format: uuid } url: { type: string } events: type: array items: { $ref: "#/components/schemas/EventType" } description: { type: [string, "null"] } status: { type: string, enum: [ACTIVE, DISABLED] } createdAt: { type: string, format: date-time } WebhookDelivery: type: object required: [id, eventId, eventType, endpointId, url, status, attempt, responseStatus, lastError, durationMs, lastAttemptAt, nextAt] properties: id: { type: integer, format: int64 } eventId: { type: string, format: uuid } eventType: { $ref: "#/components/schemas/EventType" } endpointId: { type: string, format: uuid } url: { type: string } status: { type: string, enum: [PENDING, DELIVERED, DEAD] } attempt: { type: integer } responseStatus: { type: [integer, "null"] } lastError: { type: [string, "null"] } durationMs: { type: [integer, "null"] } lastAttemptAt: { type: [string, "null"], format: date-time } nextAt: { type: string, format: date-time } WebhookEvent: type: object required: [eventId, type, tenantId, createdAt, data] properties: eventId: { type: string, format: uuid } type: { $ref: "#/components/schemas/EventType" } tenantId: { type: string, format: uuid } createdAt: { type: string, format: date-time } data: description: TRANSACTION_* 为投递时的交易对象;其他事件见对应结构 oneOf: - $ref: "#/components/schemas/Transaction" - type: object title: WALLET_CREATED properties: walletId: { type: string, format: uuid } chainFamily: { $ref: "#/components/schemas/ChainFamily" } address: { type: string } - type: object title: DEPOSIT_REVERSED properties: depositId: { type: string, format: uuid } - type: object title: MANUAL_DEPOSIT_CREDITED properties: caseId: { type: string, format: uuid } walletId: { type: string, format: uuid } assetId: { type: string } amountRaw: { $ref: "#/components/schemas/RawAmount" } - type: object title: VAULT_GAS_LOW properties: networkKey: { type: string } address: { type: string } assetId: { type: string } balanceRaw: { $ref: "#/components/schemas/RawAmount" } recommendedMinRaw: { $ref: "#/components/schemas/RawAmount" } - type: object title: WEBHOOK_TEST properties: message: { type: string } endpointId: { type: string, format: uuid }