引言:当你决定在桌面应用里做交易,噩梦就开始了你的监控软件发现了代币异动,所有数据展示得清清楚楚——然后呢?用户需要复制合约地址,打开浏览器,跳转到外部DEX,断开钱包重连,手动粘贴地址,完成交易,再切回你的软件。这个流程不仅割裂,而且危险——在跳转过程中,用户已经失去了对风险的判断力。于是你
<!--StartFragment-->
你的监控软件发现了代币异动,所有数据展示得清清楚楚——然后呢?用户需要复制合约地址,打开浏览器,跳转到外部DEX,断开钱包重连,手动粘贴地址,完成交易,再切回你的软件。这个流程不仅割裂,而且危险——在跳转过程中,用户已经失去了对风险的判断力。
于是你决定:在我的软件里面直接内建一个SWAP功能吧。
这个决定,意味着你要开始直面以下四个技术噩梦:
web3.py + solana 这两个库打包后增加近20MB体积,且封装了大量项目根本不需要的功能。pywebview WebView 无法安装 MetaMask/Phantom 插件——钱包连接、余额查询、交易签名全部需要跨进程桥接。这篇文章将完整拆解这四个噩梦的底层实现——不是调调API,而是手写ABI编码、自建RPC引擎、手写HMAC签名、手写Base58解码、用Promise构造异步流控。每一步都是普通开发者从未触及的底层细节。

<!--StartFragment-->
0x 覆盖了主流代币,但新上线的代币往往不在其路由中。ParaSwap 覆盖了更多链,但某些小众代币仍然没有流动性。单独的链上合约查询能查到 DEX 的瞬时报价,但需要手动遍历多个交易对路径。
所以必须设计一条降级链路:0x → ParaSwap → OKX → Swap API → 链上合约轮询。前面的失败了,后面的自动顶上。
但难题同时出现了:这四个聚合器返回的数据结构完全不同。
0x 的 allowance-holder 端点,返回的交易执行数据不在顶层,而在嵌套的 transaction 对象里。大多数开发者拿到 API 文档后会自然写 ox_data.get("to"),但这里返回的永远是 None。不是你调用错了,而是这个端点把"报价"和"交易构造"分了层。你需要的 to 和 data 在 transaction.to 和 transaction.data 里,不在根对象上。
这种隐式分层在文档里不会特别强调,只有实际调用才会暴露。解决方案是一个薄薄的双层校验:
tx_obj = ox_data.get("transaction", {}) tx_to = tx_obj.get("to") if isinstance(tx_obj, dict) else None tx_data_field = tx_obj.get("data") if isinstance(tx_obj, dict) else None
<!--StartFragment-->
不是防御性编程,是对 API 设计意图的逆向理解。
OKX DEX 的报价分两步。第一步 /quote 返回一个路由字符串 router,第二步 /swap 用这个 router 换取可执行的交易数据。而且每个请求都需要 HMAC 签名。
第一个陷阱是 code 字段的类型。文档没有标注,自然写 if code == 0,但永远不进分支。实际上返回的是字符串 "0"——在 JSON 中 0 和 "0" 肉眼无法区分,只能运行时发现。
第二个陷阱是 HMAC 签名的规范。签名消息的格式是 时间戳 + 请求方法 + 请求路径 + 查询字符串,然后用 sha256 + base64 编码。这个签名函数是从 OKX API 文档推导出来,不是调任何 SDK:
def _okx_sign(secret, ts, method, path, qs): message = ts + method + path + "?" + qs return base64.b64encode( hmac.new(secret.encode('utf-8'), message.encode('utf-8'), hashlib.sha256).digest() ).decode('utf-8')
<!--StartFragment-->
第三个陷阱是 Solana 链上 /quote 不接受 EVM 的零地址 0x000... 作为 userWalletAddress,必须换成合法的 Solana 公钥。这个限制同样没有在文档中明确说明。
当 /swap 成功返回后,交易数据藏在 swap_data["data"][0]["tx"] 的深层路径中,完全不同于 EVM 链的数据结构。对于 Solana 链,tx.data 是 Base58 编码,而不是 EVM 的十六进制。这意味着前后端都需要额外的兼容处理。
当所有聚合器都失败时,最后一层防线是直接查询链上 DEX 合约。这需要手写 eth_call 构造——不依赖 web3.py,不依赖任何 ABI 编码库。
V2 Router 的 getAmountsOut 接受一个动态数组 address[] 作为参数。在以太坊 ABI 编码标准中,动态数组需要先写一个偏移量(0x20),再写数组长度,最后才是每个元素的固定 32 字节编码:
path_encoded = format(0x20, '064x') # 数组偏移量 path_encoded += format(len(path), '064x') # 数组长度 for addr in path: path_encoded += '0'*24 + addr.lower()[2:] # 每个地址 32 字节
<!--StartFragment-->
V3 Quoter 的 quoteExactInputSingle 严格排列五个参数:address + address + uint24 + uint256 + uint160。这里有一个极易写错的细节:uint24 是定长类型,编码时如果用 format(value, '064x') 会多出填充字节导致调用失败。正确做法是按六位十六进制处理,再补齐到 32 字节:
data = '' # 函数选择器 data += '0'*24 + token_in.lower()[2:] # address data += '0'*24 + token_out.lower()[2:] # address data += format(fee, '06x').zfill(64) # uint24 data += format(amount_in_wei, '064x') # uint256 data += format(0, '064x') # uint160
<!--StartFragment-->
这五行代码背后,是对 Solidity ABI 编码标准的完整理解——任何一位偏了,eth_call 就会 revert,而且不会告诉你原因。
盘点 web3.py 在 SWAP 功能中的所有实际使用场景:连接 RPC(测试 web3_clientVersion)、查原生币余额(eth_getBalance)、查 ERC20 余额(eth_call + balanceOf)、查代币精度(eth_call + decimals)、V2 Router 报价(getAmountsOut)、V3 Quoter 报价(quoteExactInputSingle)。
全是 JSON-RPC 请求。 但 web3.py 打包后增加约 15-20MB 体积。
以太坊的函数选择器是 keccak256("函数签名") 的前 4 字节。hashlib 已内置 sha3_256——只是大多数人不知道 Keccak-256 和 SHA3-256 在标准参数上的微妙区别。Python 3.9+ 的 hashlib.sha3_256 使用的就是 Keccak-256:
def _function_selector(signature: str) -> str: return "0x" + hashlib.sha3_256(signature.encode()).hexdigest()[:8]
<!--StartFragment-->
有了函数选择器,还需要 ABI 编码函数来构造参数:
<!--EndFragment-->
def _abi_encode_address(addr: str) -> str: return "0" * 24 + addr.lower().replace("0x", "")
def _abi_encode_uint256(value: int) -> str: return format(value, '064x')
<!--StartFragment-->
前者的 24 个零填充对应地址的 uint256 编码,后者的 64 位十六进制左补零是 Solidity 的定长整型编码标准。有了这些,任何 ERC20 的 balanceOf 调用就可以手动构造了。
RPC 节点返回的 eth_call 结果有时是空字符串 "0x"——只有前缀,没有实际数据。直接 int("0x", 16) 会抛出 ValueError,导致整个报价链路崩溃。不是网络错误,不是合约错误,就是 RPC 在特定情况下返回的空响应。这个坑不存在于任何 API 文档中,只有大量测试中总结出来:
def _safe_hex_to_int(val, default=0): try: if val and isinstance(val, str) and val.startswith("0x") and len(val) > 2: return int(val, 16) return default except (ValueError, TypeError): return default
<!--StartFragment-->
Solana 余额查询本质上也是一次 RPC 调用——getBalance 方法,参数是 Base58 地址字符串。solders 和 solana 库只是封装了一层 HTTP 客户端:
<!--EndFragment-->
payload = {"jsonrpc": "2.0", "method": "getBalance", "params": [user_addr], "id": 1} resp = requests.post(RPC_URL, json=payload, timeout=5).json() balance_sol = resp.get("result", {}).get("value", 0) / 1e9
<!--StartFragment-->
三行代码,两个依赖库直接删除。
pywebview 用的是系统级 Edge WebView2,不像独立 Chrome 浏览器可以安装扩展。所以 MetaMask 和 Phantom 在主界面里是不存在的。唯一的出路是创建一个独立的 de x.html 页面,在用户的系统默认浏览器中打开。然后通过本地 HTTP 服务器(127.0.0.1:19999)完成数据桥接:浏览器连接钱包后把地址回调给 Python 后端,Python 再用 window.evaluate_js 注入主窗口。这三个环节构成了跨进程钱包通信的完整闭环。
用户先在 Solana 链连接钱包,再切换到 EVM 链点击 SWAP——这时如果 EVM 模态框拿了 Solana 地址去查 EVM 余额,w3.to_checksum_address(solana_base58_address) 直接抛异常。解决方案是后端分别存储两个链的地址,前端增加严格隔离:
if chain == 'solana': self.current_solana_address = address else: self.current_user_address = address
if chain === 'solana' && walletChain !== 'solana': showMessage('请连接 Solana 钱包') return
<!--StartFragment-->
Jupiter 返回的交易是 Base64 编码,前端用 atob 直接解码。但 OKX Solana 返回的是 Base58 编码——一种完全不同的字符集(去掉了容易混淆的字符),且前导零 '1' 对应实际字节零,不能直接丢弃。JavaScript 原生不支持 Base58,需要手写完整解码函数:
<!--EndFragment-->
function base58Decode(str) { let num = 0n; for (let i = 0; i < str.length; i++) { num = num * 58n + BigInt(base58Map[str[i]]); } let hex = num.toString(16); // 补齐偶数位 if (hex.length % 2) hex = '0' + hex; const arr = new Uint8Array(hex.length / 2); for (let i = 0; i < arr.length; i++) { arr[i] = parseInt(hex.substr(i * 2, 2), 16); } // 处理前导零 let zeroCount = 0; while (str[zeroCount] === '1') zeroCount++; if (zeroCount > 0) { const padded = new Uint8Array(zeroCount + arr.length); padded.set(arr, zeroCount); return padded; } return arr; }
<!--StartFragment-->
这一步处理不好,反序列化交易时会得到 Reached end of buffer unexpectedly 错误——不是交易问题,是解码不匹配。
<!--EndFragment-->
<!--StartFragment--
<!--EndFragment-->



<!--StartFragment-->
手动检测按钮的存在,本质上是把安全责任推给了用户。但用户发现新代币后的行为模式是冲动下单——看到绿色涨跌幅就直接点 SWAP,不会去点那个需要多一步的检测按钮。
在 signBtn 点击事件中,先调用 GoPlus API(后端已完整实现了 EVM 和 Solana 地址安全 + 代币合约深度检测),根据返回的 risk_level 决定走哪个分支:低风险静默放行,中风险弹出黄色警告框允许继续或放弃,高风险弹出红色强制拦截框默认按钮是放弃交易,网络错误时提示用户自行判断。
模态框是 UI 阻塞操作,但在异步函数 async 中需要等待用户点击按钮才能决定是否继续。解决方案是用 new Promise 包装整个弹窗——用户在弹窗中的点击行为通过 resolve(true) 或 resolve(false) 来通知外层逻辑是否继续:
var proceed = await new Promise(function(resolve) {
// 创建弹窗(高风险红色拦截框)
var overlay = document.createElement('div');
overlay.id = 'goplusHighRiskOverlay';
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:10004;display:flex;align-items:center;justify-content:center;';
overlay.innerHTML = <div style="background:#1e293b;border-radius:20px;padding:24px;width:440px;max-width:90%;border:2px solid #ef4444;color:#cbd5e1;max-height:80vh;overflow-y:auto;"> <h3 style="color:#ef4444;">🚨 高风险警告!</h3> <div id="goplusRiskDetails" style="margin:12px 0;font-size:12px;">${details}</div> <p style="color:#fbbf24;">强烈建议放弃交易!</p> <div style="display:flex;gap:10px;"> <button id="goplusRejectBtn" style="flex:1;background:#475569;border:none;color:white;padding:12px;border-radius:30px;cursor:pointer;">放弃交易</button> <button id="goplusProceedBtn" style="flex:1;background:#ef4444;border:none;color:white;padding:12px;border-radius:30px;cursor:pointer;">仍然继续(我已知晓风险)</button> </div> </div>;
document.body.appendChild(overlay);
overlay.querySelector('#goplusRejectBtn').onclick = function() { overlay.remove(); resolve(false); };
overlay.querySelector('#goplusProceedBtn').onclick = function() { overlay.remove(); resolve(true); };
});
if (!proceed) { statusDiv.innerText = '交易已取消(高风险代币)'; signBtn.disabled = false; return; }
<!--StartFragment-->
这种将 DOM 事件转化为 Promise 的 resolve 的模式,使得原本难以在同步逻辑中控制的用户交互,变得简洁、清晰且易于理解。
这套 SWAP 功能,不是"调 API"的实现,而是一场底层攻坚战:四个聚合器的异构数据兼容、两套重型依赖的完全替换、EVM 和 Solana 双链的钱包桥接、以及交易前最后一道安全防线的建立。
每个模块背后都藏着一堆实际踩过的坑:数据类型不匹配、编码标准不兼容、WebView 的安全限制、API 文档外的隐性规则。解决这些问题,靠的不是任何一篇现成的技术文章或开源项目,而是几十次打包测试、几十个代币的逐一验证、以及一个坚定的想法——要做就做到最好。
在 Web3 这个圈子里,最难的从来不是"使用工具",而是"创造工具"。为后来者铺路的人,注定是最孤独的。
<!--StartFragment-->
原创作者:潇楠Web3哨兵 <!--StartFragment--> GITHUB:github.com/pingdj/Web3
<!--EndFragment-->
<!--EndFragment-->
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!
作者暂未设置收款二维码