第 8 章 · 03 VersionedTransaction 签名流程(ultratools 精读) 本节摘要:本节是 AutoHedge 全教程技术密度最高的部分,逐行精读 里的 函数——这是全项目唯一真实动钱的代码。它完成 Solana 链上交易的最后一步:把 Jupiter 返回的未签名交易,用你本地私钥签名,再广播上链。核心是用 solders 库操作 VersionedTransaction(版本化交易):从 base64 解码 → 还原交易 → 取待签消息 → 签名 → 填入签名重新组装 → base64 编码 → POST 给 Jupiter /execute。读完本节,你彻底理解 Solana 交易签名的工程实现,以及「本地签名、远程广播」这一 DeFi 安全模式。
本节摘要:本节是 AutoHedge 全教程技术密度最高的部分,逐行精读
autohedge/tools/ultra_tools.py里的execute_trade函数——这是全项目唯一真实动钱的代码。它完成 Solana 链上交易的最后一步:把 Jupiter 返回的未签名交易,用你本地私钥签名,再广播上链。核心是用 solders 库操作 VersionedTransaction(版本化交易):从 base64 解码 →from_bytes还原交易 →to_bytes_versioned取待签消息 →sign_message签名 →populate填入签名重新组装 → base64 编码 → POST 给 Jupiter /execute。读完本节,你彻底理解 Solana 交易签名的工程实现,以及「本地签名、远程广播」这一 DeFi 安全模式。
内容来源:原项目源码
autohedge/tools/ultra_tools.py,逐行精读并套用体系化模板。
⚠️ 风险提示:本节涉及私钥与真实链上交易。务必用测试钱包 + 测试网。私钥(
SOLANA_PRIVATE_KEY)一旦泄露,钱包资产即刻清零。AutoHedge 的风控极不成熟,切勿用主资产钱包跑这套代码。
阅读完本节,你应当能够:
execute_trade 函数的全部代码。Keypair、from_bytes、to_bytes_versioned、sign_message、populate 各自作用。先把 execute_trade(ultra_tools.py 第 158-238 行)完整呈现:
def execute_trade(unsigned_transaction: str, request_id: str) -> str: if not unsigned_transaction or not unsigned_transaction.strip(): raise ValueError("unsigned_transaction is required") if not request_id or not request_id.strip(): raise ValueError("request_id is required") keypair = _get_keypair() # ① 从 .env 私钥恢复 Keypair tx_b64 = unsigned_transaction.strip() try: tx_bytes = base64.b64decode(tx_b64) # ② base64 解码为原始字节 except Exception as e: raise ValueError(f"unsigned_transaction is not valid base64: {e}") from e try: raw_tx = VersionedTransaction.from_bytes(tx_bytes) # ③ 还原为交易对象 except Exception as e: raise ValueError(f"unsigned_transaction is not a valid versioned transaction: {e}") from e message_bytes = to_bytes_versioned(raw_tx.message) # ④ 取待签名的消息字节 signature = keypair.sign_message(message_bytes) # ⑤ 用私钥签名 signed_tx = VersionedTransaction.populate( # ⑥ 用签名重组交易 raw_tx.message, [signature] ) signed_b64 = base64.b64encode(bytes(signed_tx)).decode("ascii") # ⑦ 重新 base64 编码 url = f"{JUPITER_ULTRA_BASE}/execute" payload = {"signedTransaction": signed_b64, "requestId": request_id.strip()} try: with httpx.Client(timeout=30) as client: resp = client.post(url, json=payload, headers=_headers() or None) # ⑧ POST 广播 resp.raise_for_status() return json.dumps(resp.json()) except httpx.HTTPError as e: logger.error(f"Jupiter Ultra execute request failed: {e}") raise
整个函数的逻辑链:未签名交易字符串 + request_id → 签名后的交易 → POST 广播 → 返回结果 JSON。下面把关键步骤拆开讲。
签名前,要先从 .env 的私钥恢复出 Keypair 对象。这是 _get_keypair() 做的(第 39-67 行):
def _get_keypair() -> Keypair: raw = os.getenv("SOLANA_PRIVATE_KEY") if not raw or not raw.strip(): raise ValueError("SOLANA_PRIVATE_KEY is required in .env to sign transactions") try: return Keypair.from_base58_string(raw.strip()) # base58 私钥 → Keypair except Exception as e: raise ValueError(f"Invalid SOLANA_PRIVATE_KEY in .env: {e}") from e
要点:
Keypair.from_base58_string() 把它转成 Keypair 对象,这个对象同时持有公钥与私钥,能用于签名。keypair.pubkey() 派生(get_wallet_pubkey 就是这么做的)。💡 核心心法:Keypair 是「公私钥对」的封装——公钥用于验签和地址,私钥用于签名。Solana 的设计是「公钥即地址」,所以知道公钥就知道钱包地址,但只有持有私钥才能动用里面的资产。
tx_bytes = base64.b64decode(tx_b64)
Jupiter 的 /order 端点返回的交易是 base64 编码的字节串(便于在 JSON/HTTP 里传输)。第一步先把它解码回原始字节。base64 不是加密,只是编码——任何拿到这串的人都能解码,它此时还未签名,所以不含你的授权。
raw_tx = VersionedTransaction.from_bytes(tx_bytes)
把字节还原成 solders 的 VersionedTransaction 对象。这里的关键概念是 Versioned Transaction(版本化交易)——Solana 的新版交易格式,支持 Address Lookup Table(地址查找表),比 legacy 格式更省空间、能装更多账户。
💡 为什么是 VersionedTransaction 而不是 Transaction:Jupiter 这类聚合器返回的几乎都是版本化交易(因为路由涉及大量账户,legacy 格式装不下)。所以代码用
VersionedTransaction而非旧的Transaction,这是对的选择。
message_bytes = to_bytes_versioned(raw_tx.message)
签名时,你只签「消息」部分,不签整个交易。raw_tx.message 是交易的「内容」(付款方、收款方、金额、指令等),to_bytes_versioned() 把它序列化成用于签名的规范字节。
这一步至关重要:它确保签名覆盖的内容是确定的——链上验签时,节点会用同样的序列化方式还原消息,再用你的公钥验签。任何对消息的篡改都会让签名失效。
signature = keypair.sign_message(message_bytes)
用 Keypair 里的私钥,对消息字节做Ed25519 签名(Solana 用的签名算法)。产出 signature 对象。注意:
signed_tx = VersionedTransaction.populate(raw_tx.message, [signature])
把「消息 + 签名」重新组装成一个完整的已签名交易对象。populate 接受消息和一个签名列表(因为交易可能需要多个签名者,这里只有一个 payer)。
至此,signed_tx 就是一笔合法的、可广播的交易——它有内容、有你的授权签名。
signed_b64 = base64.b64encode(bytes(signed_tx)).decode("ascii")
把已签名交易序列化回 base64 字符串,准备发给 Jupiter。
url = f"{JUPITER_ULTRA_BASE}/execute" # https://api.jup.ag/ultra/v1/execute payload = {"signedTransaction": signed_b64, "requestId": request_id.strip()} with httpx.Client(timeout=30) as client: resp = client.post(url, json=payload, headers=_headers() or None) resp.raise_for_status() return json.dumps(resp.json())
注意几个细节:
status(Success/Failed)、signature(链上交易哈希)、slot、totalInputAmount/totalOutputAmount 等,可用于核对成交。这套「Jupiter 给未签名交易 → 本地签名 → 交给 Jupiter 广播」的模式,是 DeFi 的安全范式:
| 传统 CEX 模式 | AutoHedge/Jupiter 模式 |
|---|---|
| 把 API key 交给服务器,服务器代你下单 | 私钥永不离开本地,只把签名后的交易交出去 |
| 服务器被攻破=资产丢失 | 服务器(Jupiter)被攻破,没有你的私钥,无法伪造你的新交易 |
| 信任服务器不乱下单 | 信任只限于「这笔已签名的交易」,你签什么它才能发什么 |
💡 核心心法:私钥签名是不可伪造的授权。只要私钥不出本地,任何中间方都无法伪造你的交易——这是加密签名给 DeFi 带来的根本安全保证。代价是:私钥一旦本地泄露,安全保证立刻失效。所以密钥管理是这套模式的生命线。
把 get_order 与 execute_trade 串起来,完整流程:
关键安全点:从 get_order 拿到的未签名交易,在被你签名之前,对链上无效;只有你用私钥签了名,它才成为你的授权。所以即使 get_order 返回的交易被篡改,你的签名也只覆盖你看到的(实际上篡改会让签名失效,广播会被拒)。
.strip(),但格式错仍报错)。SOL 或 USDC,必须写完整 mint 地址(如 SOL 是 So111...11112,USDC 是 EPjF...Dt1v)。⚠️ 现实澄清:这套签名代码本身是正确且可用的——这是 AutoHedge 项目里最货真价实的部分。但请注意:它没有任何前置风控(不检查金额上限、不检查目标合约是否安全、不防夹子)。Agent 一旦决定交易,直接就签了。生产环境必须在这之前加风控闸门。
Keypair.from_base58_string(SOLANA_PRIVATE_KEY),Keypair 同时持有公钥(地址)与私钥(签名能力)。VersionedTransaction.from_bytes 还原 → to_bytes_versioned 取消息字节 → sign_message 签名 → populate 重组。signedTransaction 与 requestId(须配对),超时 30 秒,返回含链上 signature 与成交金额。下一节,我们把 get_order + execute_trade + get_holdings 串成完整实盘链路,看 Agent 如何触发一次真实交易。