Skip to content

Node.js SDK

Node.js SDK 为单文件 anony_v2.js零第三方依赖,仅使用 Node 内置 crypto 模块。协议细节(RSA-OAEP SHA-1 分段加解密、SHA256withRSA 签名、canonical 串、时间戳/nonce 防重放)均已内置,无需自行实现。协议原理见加密与签名协议

环境要求

  • Node ≥ 18getToken() 与本页示例均依赖原生 fetch。Node < 18 会报 ReferenceError: fetch is not defined
  • 无需安装任何 npm 包,将 anony_v2.js 放入项目目录即可(文件随对接资料提供,见 SDK 总览)。

引入

js
const { AnonyV2Client } = require('./anony_v2');
js
// anony_v2.js 是 CommonJS 模块。若 package.json 声明了 "type": "module",
// 请先将文件改名为 anony_v2.cjs,再按下面方式引入:
import { AnonyV2Client } from './anony_v2.cjs';

初始化

js
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 返回。

js
const token = await c.getToken(secret); // Node ≥18
  • getToken 请求体不加密、不签名,仅 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 一致):

js
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(...)

js
verifyResponse(encryptedBody, timestamp, nonce, signB64, isFreshNonce?)

一个方法覆盖两个场景:同步响应body.data + 响应头)与平台回调(请求体密文 + 请求头),二者使用同一套 canonical 验签 + OAEP 解密逻辑。内部依次执行:

  1. 校验 timestamp 为纯数字且在 ±5 分钟时间窗内,否则抛 timestamp out of window
  2. 若传入了 isFreshNonce,调用它判重,返回假值即抛 duplicate nonce (replay)
  3. 用平台公钥对 canonical(ts, nonce, body) 验签,失败抛 signature verify failed
  4. 用商户私钥 OAEP 解密并 JSON.parse,返回业务数据对象。

处理同步响应

见上一节示例:先用标准 JSON 库解析外层 { code, data, message }code === 10000 时把 data 与三个响应头交给 verifyResponse。同步响应场景 isFreshNonce 可省略。

处理平台回调(isFreshNonce 必须传)

平台在充值到账 / 提现状态变更时回调你的 callback_urlHTTP 请求体就是 Base64 密文本身(不是 JSON 包裹),请求头带 ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN。回调方向必须做 nonce 去重——isFreshNonce 省略时 SDK 会跳过去重,回调将可被重放。

js
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 打通上下行链路(可反复调用,不产生任何订单/地址):

js
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原生 fetchr.headers.get() 不区分大小写,照抄示例即可;但 axios(r.headers['anony-timestamp'])与 Node 原生 httpreq.headers)的头键一律是全小写,统一按小写读取最稳妥
fetch is not definedNode 版本 < 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)(分隔符是 &