一 SOL简介
名称:
精度:
确认数:xxx块后交易安全不易会滚
是否支持token代币:是
是否支持质押:是
出块时间:
地址:Base58
交易签名算法
二 离线地址生成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| func GenerateOfflineAddress() types.Account { entropy, _ := bip39.NewEntropy(256) mnemonic, _ := bip39.NewMnemonic(entropy)
logrus.Infof("mnemonic: %s", mnemonic) seed := bip39.NewSeed(mnemonic, "")
path := `m/44'/501'/0'/0'` derivedKey, _ := hdwallet.Derived(path, seed) accountFromSeed, err := types.AccountFromSeed(derivedKey.PrivateKey) if err != nil { fmt.Printf("err=%v", err) } publicKey := accountFromSeed.PublicKey.ToBase58() fmt.Println("Solana Wallet Address:", publicKey) return accountFromSeed }
|
三 离线签名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| NewTx := types.NewTransactionParam{ Message: types.NewMessage(types.NewMessageParam{ FeePayer: Account.PublicKey, RecentBlockhash: block.Blockhash, Instructions: []types.Instruction{system.Transfer(system.TransferParam{ From: Account.PublicKey, To: common.PublicKeyFromString("6cPnfGr9Y4bZK7ykNpxe2hkKfaPPgsy6Tu5ahyGhzQLt"), Amount: 123000, })}, }), Signers: []types.Account{Account}, }
tx, err := types.NewTransaction(NewTx) if err != nil { logrus.Warnf("new transaction error: %s", err) }
hash, err := solClient.SendTransaction(bg, tx) if err != nil { logrus.Warnf("new transaction error: %s", err) } logrus.Infof("hash: %v", hash)
|
三、交易理解
sol转账交易
交易包括一个或多个 指令,每个代表要处理的特定操作。指令的执行逻辑存储在部署到Solana网络的程序上,每个程序都存储自己的指令集。
为简单起见,可以将交易视为处理一个或多个说明的请求

注意:会按照顺序执行,要么全部通过,要么全部不通过
Instruction 指令,下面是单个指令的示意图

指令中包含:
- System Program
- Accounts:帐户信息:Sender 地址 和 Receiver 地址
- Transfer Amount 转账金额,注意:给一个新地址转账的时候,必须要大于等于租金费用890880 也就是0.00089088,满足租金后就可以随便转账了,可以通过GetMinimumBalanceForRentExemption接口拿到这个金额
看代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
|
func SendSol() { DEV := "https://solana-devnet.g.alchemy.com/v2/wqZxT7UnY6AgrzV42CtGgGQ7ZGM-UrTq" c := client.NewClient(DEV) piv := "46M2pAp4z3mNPuTh7jS8XHSn69TC4FAnX33Avjx7wqy3W1zzZKiSoBmNTH5PEBDKu7xR2rPa9ocSyzGWYFK7VRF2" alice, err := types.AccountFromBase58(piv) if err != nil { fmt.Printf("err=%v", err) } bob := "6cPnfGr9Y4bZK7ykNpxe2hkKfaPPgsy6Tu5ahyGhzQLt"
balances, err := c.GetBalance(context.Background(), alice.PublicKey.String()) if err != nil { fmt.Printf("get balances err = %v", err) } fmt.Printf("balances = %v\n", balances) recentBlockhashResponse, err := c.GetLatestBlockhash(context.Background()) if err != nil { log.Fatalf("failed to get recent blockhash, err: %v", err) } minimumBalanceForRentExemption, err := c.GetMinimumBalanceForRentExemption(context.Background(), 0) fmt.Printf("minimumBalanceForRentExemption = %v\n", minimumBalanceForRentExemption) ins := make([]types.Instruction, 0, 2) ins = append(ins, system.Transfer( system.TransferParam{ From: common.PublicKeyFromString(alice.PublicKey.String()), To: common.PublicKeyFromString(bob), Amount: minimumBalanceForRentExemption, })) message := types.NewMessage( types.NewMessageParam{ FeePayer: common.PublicKeyFromString(alice.PublicKey.String()), Instructions: ins, RecentBlockhash: recentBlockhashResponse.Blockhash, }) tx, err := types.NewTransaction(types.NewTransactionParam{ Message: message, Signers: []types.Account{alice}, }) if err != nil { log.Fatalf("failed to new transaction, err: %v", err) } txhash, err := c.SendTransaction(context.Background(), tx) if err != nil { log.Fatalf("failed to SendTransaction, err: %v", err) } fmt.Println("tx hash", txhash) }
|
让我们来看看转账指令发生了什么

可以看到,内部是由发送者Sender 地址的Lamprots帐户转给了 和 Receiver 地址的Lamprots帐户,因为sol一切皆为帐户
继续往下看,交易由什么组成,由 签名Signatures + 消息Message组成

消息Message里包含了什么?4个东西

Token的转账
token转账的关键就是拿到发送者和接收者在这个代币合约上的地址
还有要确定接收者的子帐户(在这个token上的地址)是否上链,如果没有上过链(新地址)则需要为其创建对应的子帐户地址用于接收token
看代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
| func SendToken() {
tokenAddress := "58g1sdTgzazjR4ApXxPP63JRHj4ZDXePX251An7fxEhG" DEV := "https://solana-devnet.g.alchemy.com/v2/wqZxT7UnY6AgrzV42CtGgGQ7ZGM-UrTq" c := client.NewClient(DEV) piv := "46M2pAp4z3mNPuTh7jS8XHSn69TC4FAnX33Avjx7wqy3W1zzZKiSoBmNTH5PEBDKu7xR2rPa9ocSyzGWYFK7VRF2" alice, err := types.AccountFromBase58(piv) if err != nil { fmt.Printf("err=%v", err) } bob := "6cPnfGr9Y4bZK7ykNpxe2hkKfaPPgsy6Tu5ahyGhzQLt" fromTokenATA, _, err := common.FindAssociatedTokenAddress(alice.PublicKey, common.PublicKeyFromString(tokenAddress)) if err != nil { log.Fatalf("failed to FindAssociatedTokenAddress, err: %v", err) } toTokenATA, _, err := common.FindAssociatedTokenAddress(common.PublicKeyFromString(bob), common.PublicKeyFromString(tokenAddress)) if err != nil { log.Fatalf("failed to FindAssociatedTokenAddress, err: %v", err) } ins := make([]types.Instruction, 0, 2) recentBlockhashResponse, err := c.GetLatestBlockhash(context.Background()) if err != nil { log.Fatalf("failed to get recent blockhash, err: %v", err) } info, err := c.GetTokenAccount(context.Background(), toTokenATA.ToBase58()) if err != nil { if errors.Is(err, token.ErrInvalidAccountOwner) { fmt.Println("test") ins = append(ins, associated_token_account.Create( associated_token_account.CreateParam{ Funder: common.PublicKeyFromString(alice.PublicKey.String()), Owner: common.PublicKeyFromString(bob), Mint: common.PublicKeyFromString(tokenAddress), AssociatedTokenAccount: toTokenATA, }))
} else { log.Fatalf("failed to GetTokenAccount, err: %v", err) } } else { if info.Owner.ToBase58() != bob { log.Fatalf("failed to GetTokenAccount,info!=bob err: %v", err) } }
ComputerUnitPrice := uint64(110000) ComputerUnitLimit := uint32(200000) ins = append(ins, compute_budget.SetComputeUnitPrice(compute_budget.SetComputeUnitPriceParam{ MicroLamports: ComputerUnitPrice, }))
ins = append(ins, compute_budget.SetComputeUnitLimit(compute_budget.SetComputeUnitLimitParam{ Units: ComputerUnitLimit, })) ins = append(ins, token.TransferChecked( token.TransferCheckedParam{ From: fromTokenATA, To: toTokenATA, Mint: common.PublicKeyFromString(tokenAddress), Auth: alice.PublicKey, Signers: []common.PublicKey{alice.PublicKey}, Amount: 40000000000, Decimals: 9,
}), )
message := types.NewMessage( types.NewMessageParam{ FeePayer: common.PublicKeyFromString(alice.PublicKey.String()), Instructions: ins, RecentBlockhash: recentBlockhashResponse.Blockhash, }) tx, err := types.NewTransaction(types.NewTransactionParam{ Message: message, Signers: []types.Account{alice}, })
if err != nil { log.Fatalf("failed to new transaction, err: %v", err) } txhash, err := c.SendTransaction(context.Background(), tx) if err != nil { log.Fatalf("failed to SendTransaction, err: %v", err) } fmt.Println("tx hash", txhash) }
|
四 SOL钱包开发中的API
1.获取账户信息
2.获取 recentBlochHash, 直接签名的话 recentBlochHash 相当于 nonce
3. 获取准备 nonce 账户的 Minimum Balance For Rent 数据
4. 获取最新块高 (Slot)
5. 根据块高获取交易
6. 根据交易 Hash 获取交易详情
7.发送交易到区块链网络
五 中心化钱包开发
六、去中心化钱包开发 HD钱包
七、总结
八、附件
资料;
节点:https://dashboard.alchemy.com/ 第三方节点
浏览器:https://solscan.io/
第三方SDK:https://github.com/blocto/solana-go-sdk
文档:https://solana.com/zh/docs
API/Swager 文档:https://solana.com/zh/docs/rpc
额外笔记
交易
我们发送到 Solana 网络中的一笔交易包括四个部分:
- 一个或多个指令 (
instructions
)
- 一个要读取或写入的账户数组 (
account_keys
)
- 一个或多个签名 (
signatures
)
- 最近的区块哈希 (
recent_blockhash
)
一个指令是 Solana 上最小的执行逻辑。指令指定了执行程序、涉及的所有账户和操作数据。指令调用程序更新状态(例如,调用代币程序将代币从你的帐户转移到另一个帐户),程序解释指令中的数据,并对指定账户进行操作
指令—>类似于以太坊智能合约上的函数调用
交易费—基础手续费 和 优先费
(lamports_per_signature
),基础手续费目前设定为每个签名 0.000005 SOL(5k lamports),这是一次性的费用,以便获得使用网络资源的权利,无论实际使用多少资源来执行交易(或者交易是否执行),都需要预先支付给网络。50% 的费用支付给产生区块的验证节点,剩余的 50% 则被销毁。
如果想提高其交易的优先级【可选的费用】,它可以设置一个 “计算单元价格”。 这个价格与 计算单元限制 结合使用,用来确定交易的优先级费用。
默认的计算单元限制为 每个指令 20 万 CU, 如果计算量比较大,可以设置最大为 140 万 Cu。 Solana 交易会预先请求指定数量的计算单元(CUs),如果超出这个数量,交易将失败。
另外,Solana 交易还会收交易包大小的限制,Solana 网络遵循最大传输单元 (MTU) 大小为 1280 字节,这与 IPv6 MTU 大小约束一致,以确保通过 UDP 传输集群信息的快速和可靠性。在计算必要的标头(IPv6 的 40 字节和 8 字节的片段头)后,1232 字节仍然可用于数据包, 签名和消息的组合不能超过此限制。
以下官方视频介绍
Accounts
sol Accounts 模型
Solana 上的宗旨是一切皆为帐户,类似Linux 的一切皆文件一样,
1 2 3 4 5 6 7
| { "key":number,// 视为区块链公共地址 "lamports":number, // 实际总余额 本地soltoken 的最小单位,1 lamports=0.0000000001sol "data":Unit8Array,// 无符号整数存储的原始字节 "is_executable":boolean, // 是否为程序,如果为true 就是可执行程序,flase就是普通数据帐户 "ower":Publickey // 所有者,只有该帐户的所有者才可以更新该帐户内数据 }
|
Programs
Programs 程序就是solana 上的智能合约Smart contracts on Solana
程序是无状态的,只能读取和写入别人的帐户数据
必须是帐户所有者才能修改
程序运行指令
程序能发送指令给其他程序
总结
- 一切皆程序
- 所有帐户都持有SOL
- 帐户能存储任意数据
- 帐户也能存储可执行程序
- 帐户也能床底到程序中并行执行
Programs Instructions
程序指令
原始模型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| { "program_id":number, "keys":Array<{ "key":Publcky, "is_mutable":boolean, "is_signer":boolean, }>, "data":Unit8Array }
type Instruction struct { ProgramID common.PublicKey Accounts []AccountMeta Data []byte }
type AccountMeta struct { PubKey common.PublicKey IsSigner bool IsWritable bool }
|
简单指令
多个指令捆绑在一起,就可以组成一个交易
Transactions 交易
1 2 3 4 5 6 7 8 9 10
| message := types.NewMessage( types.NewMessageParam{ FeePayer: common.PublicKeyFromString(alice.PublicKey.String()), Instructions: ins, RecentBlockhash: recentBlockhashResponse.Blockhash, }) tx, err := types.NewTransaction(types.NewTransactionParam{ Message: message, Signers: []types.Account{alice}, })
|
创建帐户的指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
| func CreateAccountSimpleTx() { c := NewClient() alice, err := types.AccountFromBase58(Piv) if err != nil { fmt.Printf("err=%v", err) } fmt.Printf("alice=%v\n", alice.PublicKey.String()) balances, err := c.GetBalance(context.Background(), alice.PublicKey.String()) if err != nil { fmt.Printf("get balances err = %v", err) } fmt.Printf("balances = %v\n", balances) recentBlockhashResponse, err := c.GetLatestBlockhash(context.Background()) if err != nil { log.Fatalf("failed to get recent blockhash, err: %v", err) } fmt.Printf("recentBlockhashResponse = %v\n", recentBlockhashResponse)
minimumBalanceForRentExemption, err := c.GetMinimumBalanceForRentExemption(context.Background(), 0) fmt.Printf("minimumBalanceForRentExemption = %v\n", minimumBalanceForRentExemption) ins := make([]types.Instruction, 0, 2) ComputerUnitPrice := uint64(200000) ComputerUnitLimit := uint32(200000) ins = append(ins, compute_budget.SetComputeUnitPrice(compute_budget.SetComputeUnitPriceParam{ MicroLamports: ComputerUnitPrice, }))
ins = append(ins, compute_budget.SetComputeUnitLimit(compute_budget.SetComputeUnitLimitParam{ Units: ComputerUnitLimit, }))
bob := types.NewAccount() fmt.Printf("bob address=%s\n", bob.PublicKey.String()) Accounts := []types.AccountMeta{ {PubKey: common.PublicKeyFromString(alice.PublicKey.String()), IsSigner: true, IsWritable: true}, {PubKey: common.PublicKeyFromString(bob.PublicKey.String()), IsSigner: true, IsWritable: true}, } data, err := bincode.SerializeData(struct { Instruction system.Instruction Lamports uint64 Space uint64 Owner common.PublicKey }{ Instruction: system.InstructionCreateAccount, Lamports: minimumBalanceForRentExemption + 1, Space: 0, Owner: common.SystemProgramID, }) createAccountIns := types.Instruction{ ProgramID: common.SystemProgramID, Accounts: Accounts, Data: data, } ins = append(ins, createAccountIns) message := types.NewMessage( types.NewMessageParam{ FeePayer: alice.PublicKey, Instructions: ins, RecentBlockhash: recentBlockhashResponse.Blockhash, }) tx, err := types.NewTransaction(types.NewTransactionParam{ Message: message, Signers: []types.Account{alice, bob}, }) if err != nil { log.Fatalf("failed to new transaction, err: %v", err) } txhash, err := c.SendTransaction(context.Background(), tx) if err != nil { log.Fatalf("failed to SendTransaction, err: %v", err) } fmt.Println("tx hash", txhash) }
bob := types.NewAccount() fmt.Printf("bob address=%s\n", bob.PublicKey.String())
createAccountIns2 := system.CreateAccount(system.CreateAccountParam{ From: alice.PublicKey, New: bob.PublicKey, Owner: common.SystemProgramID, Lamports: minimumBalanceForRentExemption + 1, Space: 0, }) ins = append(ins, createAccountIns2)
|
总结
- 程序调用指令
- 指令通过交易发送广播出去
- 交易需要是原子的
- 所有交易都必须签名
Solana 交易的生命周期—->不单单是solana 很多题目都会出现
- 指令包含什么:程序id、 帐户key数组 、data
- 交易包含什么:指令、RecentBlockhash、签名消息、fee支付者
- 去中心化应用DAPP 客户端 组装交易(指令),
- 交易被发送到RPC 客户端
- RPC客户端把所有交易转发给投票验证者
- 验证者将实际使用solana运行时来启用,运行时实际执行每个交易内的每个指令
- 每个指令将调用一个特定程序来实际执行该程序代码试图做的事情,这个程序只是增加一个计数器并递增1
复杂指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
| func CreateComplexTx() {
c := NewClient() alice, err := types.AccountFromBase58(Piv) if err != nil { fmt.Printf("err=%v", err) } fmt.Printf("alice=%v\n", alice.PublicKey.String()) balances, err := c.GetBalance(context.Background(), alice.PublicKey.String()) if err != nil { fmt.Printf("get balances err = %v", err) } fmt.Printf("balances = %v\n", balances) recentBlockhashResponse, err := c.GetLatestBlockhash(context.Background()) if err != nil { log.Fatalf("failed to get recent blockhash, err: %v", err) }
minimumBalanceForRentExemption, err := c.GetMinimumBalanceForRentExemption(context.Background(), 0) fmt.Printf("minimumBalanceForRentExemption = %v\n", minimumBalanceForRentExemption) ins := make([]types.Instruction, 0, 4) ComputerUnitPrice := uint64(2000000) ComputerUnitLimit := uint32(2000000) ins = append(ins, compute_budget.SetComputeUnitPrice(compute_budget.SetComputeUnitPriceParam{ MicroLamports: ComputerUnitPrice, }))
ins = append(ins, compute_budget.SetComputeUnitLimit(compute_budget.SetComputeUnitLimitParam{ Units: ComputerUnitLimit, }))
bob := types.NewAccount() fmt.Printf("bob address=%s\n", bob.PublicKey.String()) createAccountIns2 := system.CreateAccount(system.CreateAccountParam{ From: alice.PublicKey, New: bob.PublicKey, Owner: common.SystemProgramID, Lamports: minimumBalanceForRentExemption + 1, Space: 0, }) ins = append(ins, createAccountIns2) Cindy := common.PublicKeyFromString("2e7MJy7rh3mr7myjEtyD6Bjyyc9fYcYVwwhGhgVDbx5U") ins = append(ins, system.Transfer(system.TransferParam{ From: alice.PublicKey, To: Cindy, Amount: 100000000, })) ins = append(ins, system.Transfer(system.TransferParam{ From: alice.PublicKey, To: bob.PublicKey, Amount: 200000000, })) ins = append(ins, system.Transfer(system.TransferParam{ From: alice.PublicKey, To: Cindy, Amount: 300000000, })) message := types.NewMessage( types.NewMessageParam{ FeePayer: alice.PublicKey, Instructions: ins, RecentBlockhash: recentBlockhashResponse.Blockhash, }) tx, err := types.NewTransaction(types.NewTransactionParam{ Message: message, Signers: []types.Account{alice, bob}, }) if err != nil { log.Fatalf("failed to new transaction, err: %v", err) } txhash, err := c.SendTransaction(context.Background(), tx) if err != nil { log.Fatalf("failed to SendTransaction, err: %v", err) } fmt.Println("tx hash", txhash) }
|
TOKEN 指令–>创建Token和元数据
createTokenWithMetadata
分成4个步骤
- Create a new account 创建一个新帐户,
- Initialize that account as a mint 初始化这个帐户为铸造方
- Create an associated token account 创建关联代币帐户 ATA
- Mint new tokens to that associated token account 将新代币铸造到关联的代币账户
我们先完成前面3个步骤来创建token和元数据,3个指令
createMintAccountInstruction,
initializeMintInstruction,
createMetadataInstruction,
见代码
第四步就是mint to 铸造一定数量的token发送给某个接收者地址
见代码
更改代币元数据
见代码
总结铸造流程:
- 创建一个帐户,
system.CreateAccount()方法
- 为帐户初始化为一个铸币帐户Mint
token.InitializeMint()方法
- 为该帐户初始名称并创建元数据帐户地址(PDA派生那一步),
common.FindProgramAddress()加上token_metadata.CreateMetadataAccountV3()方法
- create an associated token account for the user’s wallet为用户的钱包创建关联的代币账户 ·
associated_token_account.Create()方法
- mint a token to the user’s associated token account向用户关联的代币账户铸造代币
token.MintTo()方法
NFT
NFT就是一个splat Token 是一个具有独特属性的非同质化代币
特性
- Are SPL Tokens 是token
- 小数精度为0
- 总供应量为1
- 高度可定制的元数据如图像等各种属性
还有两个概念,主板和集合
调用合约
参考交易https://solscan.io/tx/4kEqM9WYQnud3G2UjiNgRktQKGkRu3B4PSuLepSrqYubU3Ha5J88PGmWNnt85MKwDuCyrc2q5odU8nWRNnUFVtNw?cluster=devnet
创建池子的交易:
https://solscan.io/tx/4kEqM9WYQnud3G2UjiNgRktQKGkRu3B4PSuLepSrqYubU3Ha5J88PGmWNnt85MKwDuCyrc2q5odU8nWRNnUFVtNw?cluster=devnet
3aPmA4hTkMQMFfF5cpktFSK6rniJHoL2DdmqVi1q3SoLTNXzisK4FiW6XJcD1tftygq9n1vMyjBLBrP6gymWapeK
AMM
CLMM
CPMM