第 8 章 · 01 Solana 账户与密钥模型 本节摘要:本章是全教程技术密度最高的一章,讲 AutoHedge 唯一真实动钱的代码。本节先打地基——讲清 Solana 的账户模型与密钥模型,这是理解后续 Jupiter Ultra Swap API 和签名流程的前提。核心概念:Solana 上一切皆账户(包括代币、程序、钱包),每个账户有个 32 字节的地址(公钥);钱包由一个 Keypair(公私钥对) 控制,私钥是 base58 编码的字符串,公钥可由私钥唯一派生;代币(如 SOL、USDC)在链上有唯一的 mint 地址;金额用 lamports 等最小单位记录(1 SOL = 10^9 lamports)。
本节摘要:本章是全教程技术密度最高的一章,讲 AutoHedge 唯一真实动钱的代码。本节先打地基——讲清 Solana 的账户模型与密钥模型,这是理解后续 Jupiter Ultra Swap API 和签名流程的前提。核心概念:Solana 上一切皆账户(包括代币、程序、钱包),每个账户有个 32 字节的地址(公钥);钱包由一个 Keypair(公私钥对) 控制,私钥是 base58 编码的字符串,公钥可由私钥唯一派生;代币(如 SOL、USDC)在链上有唯一的 mint 地址;金额用 lamports 等最小单位记录(1 SOL = 10^9 lamports)。本节会对照
ultra_tools.py里的_get_keypair、_get_wallet_pubkey,讲清这套模型怎么落到代码里,为后两节的交易与签名打底。
内容来源:原项目源码
autohedge/tools/ultra_tools.py的密钥相关函数 + Solana 官方文档,精读并套用体系化模板。
⚠️ 风险提示:本节涉及私钥。Solana 私钥一旦泄露,钱包资产立即清零,没有任何冻结/找回机制。务必用测试钱包、测试网,绝不在主网用大额资产试这套未经验证的代码。
阅读完本节,你应当能够:
_get_keypair / _get_wallet_pubkey 如何把私钥变成可签名的 Keypair。与以太坊「合约是一种特殊地址」不同,Solana 的设计更极端——链上一切实体都是账户(Account)。一个账户就是一条数据记录,包含:
BQ72nSv9f3PRyRKCBnHLVrerrv37CYTHm5h3s9VSGQDV)。lamports 字段存这个账户里有多少 SOL(最小单位);data 字段存任意字节(可以是代币余额、程序代码、状态等)。举几个例子帮助理解:
| 实体 | 在 Solana 上是什么 |
|---|---|
| 你的钱包 | 一个系统账户,owner 是系统程序,持有 SOL |
| 你持有的 USDC | 一个代币账户,owner 是 SPL Token 程序,data 里存余额 |
| USDC 这个代币本身 | 一个 mint 账户,owner 是 SPL Token 程序,定义代币的 decimals/supply |
| Jupiter 聚合器 | 一个 程序账户,data 里是可执行的字节码 |
💡 核心心法:Solana 没有独立的「合约账户」概念——程序本身也是个账户(可执行账户),代币、钱包也都是账户。统一模型降低了认知负担,但要记住:只有账户的 owner 程序能改它的数据。这就是为什么转 USDC 要调 SPL Token 程序(它的 owner),不能直接改你的代币账户余额。
控制一个钱包账户的,是一对密钥——Keypair。solders 库里 Keypair 对象同时持有公钥和私钥:
_get_keypair(ultra_tools.py 第 39-67 行)做的就是「从私钥恢复 Keypair」:
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()) except Exception as e: raise ValueError(f"Invalid SOLANA_PRIVATE_KEY in .env: {e}") from e
逐行:
SOLANA_PRIVATE_KEY 读私钥(注意这个变量名,下一节会点和 .env.example 的不一致)。Keypair.from_base58_string():把 base58 文本私钥解析成 Keypair 对象。这一步同时恢复了私钥和由它派生的公钥。💡 关键心法:Keypair 是「公私钥对」的封装——给一个 base58 私钥,它能在本地算出对应的公钥,不需要额外存公钥。所以
.env里只要存私钥,公钥/地址随时能派生。_get_wallet_pubkey就是这样做的。
Solana 的私钥/公钥都用 base58 编码成文本。base58 是比特币发明的编码方式,类似 base64,但去掉了 0、O、I、l 这几个容易混淆的字符,适合人眼辨识和手抄。特点:
对比以太坊:以太坊私钥是 64 位十六进制(0-9a-f),地址是 40 位 hex 加 0x 前缀。Solana 用 base58 是为了更短、更不易抄错。
⚠️ 风险提示:AutoHedge 把这串 base58 私钥明文存在
.env里。这意味着:一旦.env被提交到 git、被日志打印、被进程内存 dump,私钥就泄露了。生产环境绝不能这样存,要用 HSM、KMS、硬件钱包或至少系统钥匙串(第 10 章详讲)。
_get_wallet_pubkey(第 70-82 行)演示了「公钥从私钥派生、公钥即地址」:
def _get_wallet_pubkey() -> str: return str(_get_keypair().pubkey())
就这么一行:
_get_keypair() 拿到 Keypair(含私钥+公钥)。.pubkey() 取出公钥对象。str(...) 把公钥转成 base58 文本,就是钱包地址。这行的意义:钱包地址不是独立存的,而是从私钥算出来的。所以 get_order 要告诉 Jupiter「谁是付款方」时,不需要额外配置地址,直接从私钥派生即可(第 131 行 wallet_pubkey = _get_wallet_pubkey())。
第 7 章讲过,Solana 上代币用 mint 地址唯一标识。本节需要记住两个最常用的 mint:
| 代币 | mint 地址 | decimals | 说明 |
|---|---|---|---|
| Wrapped SOL(wSOL) | So11111111111111111111111111111111111111112 |
9 | 包装在 SPL Token 里的原生 SOL |
| USDC(Solana 版) | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v |
6 | Circle 发行的美元稳定币 |
几点说明:
So111...11112。Jupiter 通常自动处理 wrap/unwrap,所以代码里直接传 wrapped SOL 的 mint 即可。get_order 的参数必须是完整 mint 地址,不能写 "SOL" 或 "USDC"。第 7 章的 search 工具就是用来从 symbol 反查 mint 的。💡 记忆窍门:wrapped SOL 的 mint
So111...11112以So开头(像 SOL 的前两个字母)、中间全是 1、以 2 结尾,是 Solana 故意设计的「好看地址」;USDC 的 mint 没规律,只能硬记或查表。实际代码里会从 search 工具或常量拿,不用死记。
Solana 的金额用最小单位记录,不是人类单位:
1 SOL = 1,000,000,000 lamports(10^9)。账户里存的「SOL 余额」实际是 lamports 数。1 USDC = 1,000,000 个最小单位。一个 decimals=9 的代币,最小单位就是 10^-9。get_order 的 amount 参数(下一节详讲)传的就是最小单位数。例如要兑换 0.01 SOL:
0.01 SOL = 0.01 × 10^9 lamports = 10,000,000 lamports
所以代码里 amount 字符串传 "10000000",不是 "0.01"。ultra_tools.py 的 docstring 示例也是这么写的:
>>> get_order("So11111111111111111111111111111111111111112", ... "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", ... "10000000") # ← 0.01 SOL 的 lamports 表示
⚠️ 常见坑:把
0.01直接当 amount 传,链上会理解成「0.01 lamports」(几乎是 0),交易可能成功但成交量为零。务必先按 decimals 换算成最小单位。第 7 章查价时返回的decimals字段,就是用来做这个换算的。
理解了账户模型,就能看懂 Solana 交易为什么这么「重」。一笔交易(Transaction)的本质是:一条消息,告诉链上一组指令;每个指令是「让某个程序修改某些账户的数据」。
交易(Transaction) └─ 消息(Message) ├─ payer(付款方,付 gas 的账户) ├─ 最近 blockhash(防重放) └─ 指令列表(Instructions) └─ 每条指令: ├─ program_id(调哪个程序,如 SPL Token) ├─ accounts(这条指令要碰的所有账户地址) └─ data(给程序的参数,字节流)
关键点:每条指令要显式列出它要读写的所有账户。一次 Jupiter 兑换涉及 input mint、output mint、付款方、收款方、各代币账户、ATL 表......动辄几十个账户。这就是为什么 Solana 用 Versioned Transaction + Address Lookup Table——把常用账户放进查找表压缩,legacy 格式装不下。
💡 与以太坊对比:以太坊交易是「从 A 调合约 B 的方法 m,带参数 p」——简洁;Solana 交易是「显式列出所有要碰的账户 + 交给程序解释」——冗长但并行友好。这种差异决定了 Solana 交易签名前必须由程序(这里是 Jupiter)预先组装好完整账户列表,这正是 get_order 要做的事(下一节讲)。
把概念和 ultra_tools.py 的代码对应一遍,加深印象:
| 概念 | 代码位置 | 体现 |
|---|---|---|
| Keypair | _get_keypair |
Keypair.from_base58_string(raw) |
| 公钥即地址 | _get_wallet_pubkey |
str(keypair.pubkey()) |
| 私钥明文存 .env | _get_keypair |
os.getenv("SOLANA_PRIVATE_KEY") |
| mint 地址 | get_order 入参 |
input_mint / output_mint |
| 最小单位 amount | get_order 入参 |
amount(lamports for SOL) |
| 账户列表在交易里 | 第 03 节详讲 | VersionedTransaction.message |
Keypair.from_base58_string(私钥) 恢复出含公私钥的对象;公钥由私钥单向派生,Ed25519 无法反推。_get_wallet_pubkey 就是从私钥 str(keypair.pubkey()) 一行算出,无需额外存。.env 明文存储是生产隐患。So11111111111111111111111111111111111111112(decimals=9),USDC = EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v(decimals=6)。下一节,我们看这套账户模型之上的 Jupiter Ultra Swap API——get_order 怎么组装出一笔未签名交易,routePlan 怎么定路由,feeBps 是什么,以及一个本项目特有的「私钥变量名不一致」的坑。