如何在不交出资金保管权的前提下收加密货币

·8 分钟

多数加密支付网关会先拿到你的钱再转给你。正是这个设计让冻结、扣留、「账户审核中」成为可能。这篇讲清非托管方案端到端是怎么运作的。

本文目录
  1. 1非托管到底指什么
  2. 2一笔付款的完整流转
  3. 3团队最容易低估的部分
  4. 4正确验证 Webhook
  5. 5你放弃了什么
  6. 6从哪开始

如果你认真读过加密支付服务商的条款,大概会注意到宣传和机制之间的落差:宣传说钱是你的,机制说钱要先经过他们控制的账户,再按他们定的周期转给你。

冻结、扣留、滚动准备金、突然的账户审核,都住在这个落差里。倒不是服务商恶意——多数时候他们在管理自己的合规敞口——而是因为托管方按定义就能拦下一笔钱。如果你的生意依赖「付款不能被拦」,那真正该问的不是哪家政策最好,而是这条链路上到底有没有托管方。

非托管到底指什么

非托管的意思是:资金从不进入网关控制的账户。顾客把钱付到一个地址,结算规则由链上强制执行,钱最终到达一个只有你持有私钥的钱包。

判断标准很简单:这家网关如果想冻结你的资金,做得到吗?只要在任何情形下答案是「做得到」,那它就是托管的,不管首页怎么写。

一个好用的判断角度:托管网关要你信任一条政策;非托管网关让这条政策变得不必要,因为「冻结」这个能力根本不存在。

一笔付款的完整流转

完整顺序,不省略任何一步:

  1. 1你的后端创建一笔收款,指定金额、链、币种和你自己的订单号,拿回一个仅属于这笔订单的收款地址。
  2. 2把地址展示给顾客——文本、二维码或托管收银台页面都行。顾客用任何钱包普通转账,甚至直接从交易所提币过来。不用连钱包、不调合约、不弹签名。
  3. 3网关的节点在这笔转账落进区块的那一刻就检测到。
  4. 4结算在链上完成。如果你配了分账,所有收款方在同一笔原子交易里一起拿到钱——任何一腿失败整笔回滚。
  5. 5一条带签名的 webhook 打到你的服务器。你验签后发货。

值得多说一句的是第 1 步。每笔订单一个专属地址,让对账变得毫不费力:地址与订单一一对应,你永远不用猜某笔到账属于哪个顾客。这也是团队考虑自建时最容易低估的部分。

团队最容易低估的部分

接收一笔转账很容易;接收一笔「付款」不容易。差别在于能安全发货之前,必须做对的那些事:

这些都不高深,但每一条都是承重的,每一条也都是自建集成悄悄漏钱的地方。

正确验证 Webhook

这里是最多集成出错的地方,所以直接上代码。规则只有一条:对收到的原始字节签名,绝不要对「解析后再序列化」的结果签名。

Node.js —— 正确的验签写法
import crypto from "node:crypto";

// 用原始 body, 不是 parse 后再 stringify 的对象。
app.post("/webhooks/finality",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.header("X-Finality-Signature") ?? "";
    const expected = crypto
      .createHmac("sha256", process.env.FINALITY_WEBHOOK_SECRET)
      .update(req.body)          // 收到的确切字节
      .digest("hex");

    // 必须先比长度: timingSafeEqual 遇长度不等会抛异常, 不是返回 false。
    const a = Buffer.from(sig);
    const b = Buffer.from(expected);
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
      return res.sendStatus(401);
    }

    const evt = JSON.parse(req.body.toString());

    // 去重 —— 网络会重试, 你一定会收到重复事件。
    if (alreadyHandled(req.header("X-Finality-Event-Id"))) {
      return res.sendStatus(200);
    }

    // 只有已结算才算钱到账。绝不要在 detected 时发货。
    if (evt.event === "payment.settled") {
      fulfillOrder(evt.merchant_order_id);
    }

    res.sendStatus(200);   // 先返回, 再做耗时逻辑。
  });

这段代码里有三个细节最容易咬人:先解析再验签会让签名必然对不上;用普通相等比较会泄露时序信息;在错误的事件上发货等于货已出门钱还没落地。

你放弃了什么

一份诚实的指南必须写清代价,因为它们真实存在,而且会直接排除掉一部分生意。

你放弃的为什么谁会在意
法币结算换成法币需要货币转移牌照,而牌照要求托管。一家网关不可能既持有你的美元又「不碰你的钱」。必须当天以法币入账的生意
争议仲裁没有托管方能逆转一笔已结算的付款。这同时意味着没有拒付欺诈——但也没有仲裁。依赖服务商居中调解纠纷的商户
银行卡收款卡本身就是托管且可逆的。这是另一条通道,不是替代品。顾客用卡付款的生意

退款依然存在——只是由商户签名广播,因为平台根本没有可用来打款的钱包。实际操作就是多签一次名,而这正是你想要的那件事的直接结果:别人动不了你的钱。

从哪开始

最诚实也最快的路径,是先用沙盒密钥把整个流程演练一遍再碰真钱:建单、强制结算、确认你的 webhook 处理逻辑做对了——全程一分钱不动。

快速上手大约五分钟走完,包含沙盒演练。

阅读快速上手 →
相关文章