URI、剪贴板、二维码:一个地址在转手时会怎么坏掉
地址到你钱包之间要过三个编码器:URI、剪贴板、二维码。我们逐个实测它们怎么弄坏地址,以及发送前该检查什么。
地址不是一个「东西」,它是一串在你按下发送之前已经被编码、复制、渲染、截图、再解码过的字符。在兑换订单页上,这串字符只有三条出口:复制按钮、200 像素的二维码、以及可选中文本。三条出口各有各的坏法,而且坏了看不出来——因为坏掉的版本依然长得像地址。我们在线上把这三种通道都测了一遍:剪贴板到底留下了什么、二维码到底编码了什么、九种地址格式对十二种变形的反应。代价很具体:今天 18:06 UTC,100 USDT-TRC20 报 0.13937044 XMR,而少一个字符的收款地址不会让订单失败,只会让一笔已付款的订单打进空气。同一分钟目录里 434 个资产、62 条网络,其中 6 个还需要 Memo。 (XMR 换 USDT 退出路线)
先看 URI,因为它是唯一能携带金额的通道。BIP-21 的语法是 bitcoin:<地址>?amount=<十进制 BTC>;金额必须是十进制 BTC、最多八位小数,绝不能用聪,客户端遇到不认识的参数必须忽略而不是拒付。EIP-681 是 ethereum:<地址>@<链 ID>/transfer?address=<收款方>&uint256=<原子单位>;这里的金额是代币最小单位的整数——1 USDT 是 1000000,不是 1——而且规范明确允许 value=2.014e18 这种科学计数法。门罗用 monero:<地址>?tx_amount=<十进制 XMR>&tx_payment_id=<64 位十六进制>,而 wallet2.cpp 里的解析器会把不认识的参数塞进 unknown_parameters 然后照样返回成功。最后这句就是整个失效模式:URI 里把 tx_amount 写成 amount,不会被拒,而是被当成「没有金额」收下,然后把金额交回给人手输。 (XMR 换 USDT 退出路线)
我们拿站内校验器跑了九种地址格式 × 十二种变形,共 90 格,全部记录。凡是带 URI 前缀的,在每条链上都被拒,理由也很直白:「not a Bitcoin address (expect bc1…, 1… or 3…)」。对一个粘贴框来说这是正确选择:兑换站绝不该执行一个它没生成过的字符串里的金额,因为 URI 里的金额是指令,不是目的地。代价是这条路只有单程——钱包或区块浏览器给你 bitcoin:bc1q…?amount=0.01,这个框不会收;把 scheme 和查询串全部去掉,只留路径。而本站自己的二维码走的是反方向:只编码裸地址,所以扫出来的码不可能带错金额——也不可能带对金额。
再说剪贴板,三条通道里最不可靠、却用得最多的一条。复制按钮调用 navigator.clipboard.writeText,外面套了一个会吞掉错误的 try/catch:在非安全上下文或权限被拒时,什么都没复制,页面也不会告诉你。校验器在检查前会 trim,这一下悄悄救回了四种极常见的残留:行尾换行、行尾空格、两侧的不换行空格(U+00A0)、以及开头的 BOM(U+FEFF)——这四种在九种格式上全部通过。另外三种不行:零宽空格(U+200B)、从右到左标记(U+200F)、软连字符(U+00AD),无论插在中间还是缀在末尾,全部被拒——而这三种恰恰是从 PDF、聊天软件、或者做过两端对齐排版的网页复制时最容易带出来的东西。第二个坑是大小写:bech32 全大写合法、能通过;混合大小写校验和必挂;门罗地址全大写直接拒;0X 的大写 X 不是 EVM 地址。
二维码值得单开一节,因为它的几何参数在悄悄决定成败。订单页用 react-qr-code,size=200 且没有传 level,也就是库默认的 L 档纠错,约 7% 可恢复。这个组件只画模块、viewBox 直接等于模块数,所以码本身没有静区,白边全靠容器的 16 像素内边距。在 200 CSS 像素下,这圈白边折合 2.32 个模块(29 模块的码)、2.64(33)、2.96(37)、3.28(41)——而规范要求 4 个。模块尺寸则从 6.90 像素一路降到 4.88。106 个字符也不是随便一个长度:它正好是版本 5 在 L 档的字节容量,所以 95 字符的门罗地址和 106 字符的集成地址/统一地址都落在 37 模块,而多一个字符、或者把档位提到 M,就会跳到 41。
然后我们真的去解码,而不是只看几何。按 200 像素的实际布局四倍渲染、再做退化,用 jsQR 1.4.0 每格跑 20 次随机抖动,得到一条很干净的曲线。缩到 116 像素宽——也就是渲染尺寸的一半——二十四组地址与档位的组合每次都读对,即使叠加质量 40 的 JPEG、2 度旋转和亚像素偏移。缩到 93 像素、加 0.8 像素模糊、JPEG 40、3 度旋转之后,短码守住了:29 模块 20/20、33 模块 19–20/20;长码没守住:37 模块掉到 9/20 和 8/20,41 模块掉到 2/20 和 0/20。把纠错从 L 提到 M,这些长码反而更差——因为 200 像素里要塞进多四个模块。至于遮挡,直径占宽度 18% 的白圆放在偏心位置还能读:短码 16–19/20,长码 20/20,冗余面积大的反而更抗压。
最后一个缺口是结构性的,不是几何的。二维码只编码入金地址,因为传给组件的只有这一个值;Memo 单独放在下面一行。目录里 434 个资产中有 6 个必须带 Memo——XRP、XLM、HBAR、ATOM、STX、NEIRO-eth——而共用入金地址把这个缺口放大:在按 Tag 路由的链上,多笔订单共用一个地址,扫码即付的人等于把钱打进了没有标签的公共池。看代码就是这个形状:二维码渲染的是 depositAddress,Memo 渲染的是 depositExtraId,两个不同元素,只有其中一个能被扫。
所以实操清单很短。复制地址、粘贴之后,发送前用眼睛核对前四位和后四位——这一个动作能同时抓出截断和字形替换。不要把 URI 粘进只认地址的框,也不要接受任何不是你生成的 URI 里的金额。扫码就把金额当零,从订单页手输,八位小数全对上:0.13937044 不是 0.1394,在 100 USDT 的单子上这差不多是到账金额的 0.3%。需要 Memo 的资产干脆别扫,地址和 Memo 分开复制,Memo 核对两遍,因为它是唯一没有校验和的字段。最后,把二维码截图丢进聊天软件再让对方扫,就是 93 像素那一档变成现实的地方:图还在,地址不一定还在。 (XMR 换 USDT 退出路线)
这些都不算冷门。三个编码器、三种坏法、中间夹着一个根本看不见坏在哪的人:URI 静默丢掉金额、剪贴板夹着一个没有宽度的字符、二维码在 200 像素的框里铺了 37 个模块。解法不只是换更好的软件,而是知道这串字符是从哪扇门出来的,从而知道该检查那扇门会弄坏的哪一部分。从自己控制的钱包发出,退款地址留在同一条链上,如果只记一条规则,就记这条:屏幕上的地址和钱包里的地址,在你核对之前是两个不同的对象。 (转错网络如何找回)