Node.js SDK
Node.js SDK 为单文件 anony_v2.js,零第三方依赖,仅使用 Node 内置 crypto 模块。协议细节(RSA-OAEP SHA-1 分段加解密、SHA256withRSA 签名、canonical 串、时间戳/nonce 防重放)均已内置,无需自行实现。协议原理见加密与签名协议。
环境要求
- Node ≥ 18:
getToken()与本页示例均依赖原生fetch。Node < 18 会报ReferenceError: fetch is not defined。 - 无需安装任何 npm 包,将
anony_v2.js放入项目目录即可(文件随对接资料提供,见 SDK 总览)。
引入
const { AnonyV2Client } = require('./anony_v2');// anony_v2.js 是 CommonJS 模块。若 package.json 声明了 "type": "module",
// 请先将文件改名为 anony_v2.cjs,再按下面方式引入:
import { AnonyV2Client } from './anony_v2.cjs';初始化
const c = new AnonyV2Client(appId, serverPubB64, merchantPrivB64);
// 可选第 4 个参数 baseUrl,默认 'https://api.anonypay.io/api/merchant/'| 参数 | 说明 |
|---|---|
appId | 商户号(ANONY-APP-ID) |
serverPubB64 | 服务端公钥 public_t,格式为 base64(PEM),用于加密上行请求、验证下行签名 |
merchantPrivB64 | 商户私钥 private_u,格式为 base64(PEM),用于签名上行请求、解密下行数据 |
baseUrl | 可选,接口基址,默认 https://api.anonypay.io/api/merchant/(尾部斜杠 SDK 会自动规整) |
密钥格式为
base64(PEM)(对整段 PEM 文本再做一次 Base64),SDK 内部会自动解码还原,直接传入即可。4 项凭据的获取与保管见凭据准备。
获取 token:getToken(secret)(全自动)
Node SDK 提供开箱即用的 getToken(),内部自动完成:Authorization: auth + md5(APP-ID & secret) 鉴权 → POST getToken → 对加密响应信封验签 + 解密 → 取出 token 返回。
const token = await c.getToken(secret); // Node ≥18getToken请求体不加密、不签名,仅 md5 鉴权;但响应仍是加密 + 签名信封,SDK 已自动处理,你拿到的直接就是 token 字符串。- 失败时抛出
Error('getToken failed: <message>')。
token 有效期(实现细节)
token 采用滑动过期 8 小时、绝对上限 24 小时。业务请求收到 tokenExpired 时重新调用 getToken() 换新即可,不建议按固定周期盲刷。错误码含义见错误码。
发起业务请求:buildRequest(data, token)
buildRequest 完成一次上行请求所需的全部工作:JSON 序列化 → OAEP 加密为 Base64 密文 → 生成秒级时间戳与 16 字节 CSPRNG nonce → 对 canonical 串做 SHA256withRSA 签名,返回 { body, headers },直接交给 fetch 发送。
完整调用示例(与对接文档 §5.3 一致):
const { AnonyV2Client } = require('./anony_v2');
const c = new AnonyV2Client(appId, serverPubB64, merchantPrivB64);
const token = await c.getToken(secret); // Node ≥18:自动 md5 鉴权 + 验签解密取出 token
const req = c.buildRequest({ userOrder: 'A1001', addressType: 'trx' }, token);
const r = await fetch(c.baseUrl + 'createAddress', { method: 'POST', body: req.body, headers: req.headers });
const body = await r.json();
if (body.code === 10000) {
const data = c.verifyResponse(body.data, r.headers.get('anony-timestamp'),
r.headers.get('anony-nonce'), r.headers.get('anony-sign'));
}TRON 链 USDC 充值已关闭
向 trx 类型地址转入 TRON 链 USDC,到账不入账、不回调,资金无法自动记账。TRON 链充值请仅使用 USDT(trx.usdt)或 TRX。支持的币种枚举以接口参考为准。
返回值说明:
| 字段 | 说明 |
|---|---|
req.body | 业务 JSON 经 OAEP 加密后的 Base64 密文(即 HTTP body) |
req.headers | 全套 5 个请求头:ANONY-APP-ID / ANONY-TOKEN / ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN |
所有业务接口均为 POST,把示例中的 'createAddress' 换成对应路径即可(createWithdrawOrder / merchantInfo / getDeposits / checkWithdrawOrder,参数见接口参考)。金额类字段建议以字符串传递,避免精度问题。
处理响应与回调:verifyResponse(...)
verifyResponse(encryptedBody, timestamp, nonce, signB64, isFreshNonce?)一个方法覆盖两个场景:同步响应(body.data + 响应头)与平台回调(请求体密文 + 请求头),二者使用同一套 canonical 验签 + OAEP 解密逻辑。内部依次执行:
- 校验
timestamp为纯数字且在 ±5 分钟时间窗内,否则抛timestamp out of window; - 若传入了
isFreshNonce,调用它判重,返回假值即抛duplicate nonce (replay); - 用平台公钥对
canonical(ts, nonce, body)验签,失败抛signature verify failed; - 用商户私钥 OAEP 解密并
JSON.parse,返回业务数据对象。
处理同步响应
见上一节示例:先用标准 JSON 库解析外层 { code, data, message },code === 10000 时把 data 与三个响应头交给 verifyResponse。同步响应场景 isFreshNonce 可省略。
处理平台回调(isFreshNonce 必须传)
平台在充值到账 / 提现状态变更时回调你的 callback_url:HTTP 请求体就是 Base64 密文本身(不是 JSON 包裹),请求头带 ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN。回调方向必须做 nonce 去重——isFreshNonce 省略时 SDK 会跳过去重,回调将可被重放。
const express = require('express');
const Redis = require('ioredis');
const { AnonyV2Client } = require('./anony_v2');
const app = express();
const redis = new Redis();
const c = new AnonyV2Client(appId, serverPubB64, merchantPrivB64);
// 回调体是 Base64 密文原文,用 express.text 按原始文本读取(不要用 express.json)
app.post('/anony/callback', express.text({ type: '*/*' }), async (req, res) => {
// Node 中请求头键一律为小写
const ts = req.headers['anony-timestamp'];
const nonce = req.headers['anony-nonce'];
const sig = req.headers['anony-sign'];
try {
// 先用 Redis SET NX 做原子去重(过期 ≥ 15 分钟),再把结果以同步闭包传入
const fresh = await redis.set(`anony:cb:nonce:${nonce}`, 1, 'EX', 900, 'NX');
const data = c.verifyResponse(req.body, ts, nonce, sig, () => fresh === 'OK');
if (data.businessType === 'deposit') {
// 充值到账:按 userOrder / apiOrder 幂等入账
} else if (data.businessType === 'withdraw') {
// 提现状态变更:按 orderStatus 更新订单
}
res.send('SUCCESS'); // 处理成功后按与平台约定的应答返回
} catch (e) {
res.status(400).send('fail'); // 验签/去重/解密失败:拒绝,等待平台重试
}
});isFreshNonce 必须是同步函数
SDK 内部是同步调用:if (isFreshNonce && !isFreshNonce(nonce)) throw ...。若直接传 async 函数,其返回值是 Promise(恒为真值),去重会被静默跳过。正确做法如上:先 await Redis 原子去重,再把结果通过同步闭包 () => fresh === 'OK' 传入。
回调应答口径(实现细节)
文档示例应答为纯文本 success;当前服务端实现严格匹配大写 SUCCESS:充值回调返回 SUCCESS 才算成功;提现回调 SUCCESS = 确认推进、FAIL = 拒绝并退款、其他应答会触发重试(退避最多 5 次)。请按 SUCCESS/FAIL 应答。回调字段与重试机制详见回调说明。
上线自检(必做)
正式启用前,用真实密钥调用只读的 v2/selfcheck 打通上下行链路(可反复调用,不产生任何订单/地址):
const req = c.buildRequest({ ping: 1 }, token);
const r = await fetch(c.baseUrl + 'v2/selfcheck', { method: 'POST', body: req.body, headers: req.headers });
const body = await r.json();
const data = c.verifyResponse(body.data, r.headers.get('anony-timestamp'),
r.headers.get('anony-nonce'), r.headers.get('anony-sign'));
// data => { pong: true, apiVersion: 2, serverTime: ..., echo: { ping: 1 } }能成功验签、解密并取回 echo,即代表上下行均就绪,通知平台完成正式启用。详见上线自检。
常见坑
| 坑 | 说明 |
|---|---|
响应头小写 anony-timestamp | 原生 fetch 的 r.headers.get() 不区分大小写,照抄示例即可;但 axios(r.headers['anony-timestamp'])与 Node 原生 http(req.headers)的头键一律是全小写,统一按小写读取最稳妥 |
fetch is not defined | Node 版本 < 18。升级 Node,或自行改用其他 HTTP 客户端(加解密/签名仍用 SDK 函数) |
isFreshNonce 传了 async 函数 | Promise 恒为真值,去重被静默跳过。先 await 去重再传同步闭包(见上文) |
| 回调解析失败 / 400 | 回调体是 Base64 密文原文,不是 JSON。Express 用 express.text({ type: '*/*' }) 读原始文本,不要挂 express.json() |
"type": "module" 项目引入报错 | anony_v2.js 是 CommonJS,改名为 anony_v2.cjs 后再 import |
| 外层响应手工切串出错 | 响应体里 / 可能被转义为 \/,务必用 r.json() / JSON.parse 等标准 JSON 解析,禁止手写字符串切割 |
timestamp out of window / signatureFailed | 服务器时钟漂移超 ±5 分钟,确保 NTP 同步 |
| 自行改动加密参数 | OAEP 哈希必须 SHA-1、分段 214/256 字节、canonical 三段用 \n 连接——SDK 已逐字节对齐,请勿改动。更多排查见故障排查 |
方法参考
实例方法(AnonyV2Client):
| 方法 | 返回 | 说明 |
|---|---|---|
new AnonyV2Client(appId, serverPubB64, merchantPrivB64, baseUrl?) | 实例 | 初始化客户端 |
getToken(secret) | Promise<string> | 全自动获取 token(含验签 + 解密),需 Node ≥ 18 |
buildRequest(data, token) | { body, headers } | 加密业务 JSON 并生成全套签名请求头 |
verifyResponse(encryptedBody, timestamp, nonce, signB64, isFreshNonce?) | Object | 校验时间窗 / 去重 / 验签 / 解密下行(响应与回调通用),失败抛异常 |
底层函数(module.exports 同时导出,供自定义封装 / 单测使用):
| 函数 | 说明 |
|---|---|
encrypt(plaintext, serverPublicKeyB64) | OAEP-SHA1 分段加密,返回 Base64 密文 |
decrypt(cipherB64, merchantPrivateKeyB64) | 分段解密,返回 Buffer |
sign(canonicalStr, merchantPrivateKeyB64) | SHA256withRSA 签名,返回 Base64 |
verify(canonicalStr, serverPublicKeyB64, signB64) | 验签,返回 boolean |
canonical(ts, nonce, payload) | 拼接 canonical 签名串(\n 连接) |
newNonce() | 16 字节 CSPRNG,返回 32 位 hex 串 |
authHeader(appId, secret) | 计算 auth + md5(APP-ID & secret)(分隔符是 &) |