Solana 开发者培训营

Made by Ziyang CHEN, Rocky, 陈子阳


Solana是一个快速、可扩展且具有环保特性的区块链网络,在生态发展、开发情况、环保特性和安全性等方面取得了显著的进展。它的生态系统不断扩大,吸引了越来越多的开发者和项目加入,并提供丰富的开发工具和文档支持。Solana的POS和POH共识机制使其在环保方面具有优势,而S7协议保证了链的安全性。

本课程内容主要介绍了 Solana 区块链的基础知识、Rust合约开发、RPC功能和NFT解决方案等相关内容。课程从基础入手,逐步讲解,包括与索拉纳交互、RPC提供的功能和如何构建应用等方面。同时,还介绍了如何在前端与 Solana 合约进行交互,以便更好地使用其生态系统中的资源。最后,通过实际案例展示了如何使用这些知识点解决实际问题。


📅 课程时间:2023年7月30日 - 2023年10月

📖 课件内容:solanazh.com

YouTube频道【Solana Bootcamp


00. Using Solana CLI

获取公钥

1
2
$ solana-keygen pubkey 
// Return <PUBKEY>

airdrop SOL/ Lamports

1
2
$ solana airdrop 1
// Return "1 SOL"

获得 balance

1
2
$ solana balance
// Return "3.00050001 SOL"

Confirm transaction

1
2
$ solana confirm <TX_SIGNATURE>
// Return "Confirmed" / "Not found" / "Transaction failed with error <ERR>"

Deploy program

1
2
$ solana program deploy <PATH>
// Return <PROGRAM_ID>

solana-

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
103
104
105
106
107
108
109
110
111
112
113
114
solana-cli 1.16.8 (src:b19d7219; feat:3712769919, client:SolanaLabs)
Blockchain, Rebuilt for Scale

USAGE:
solana [FLAGS] [OPTIONS] <SUBCOMMAND>

FLAGS:
-h, --help Prints help information
--no-address-labels Do not use address labels in the output
--skip-seed-phrase-validation Skip validation of seed phrases. Use this if your phrase does not use the BIP39
official English word list
--use-quic Use QUIC when sending transactions.
--use-udp Use UDP when sending transactions.
-V, --version Prints version information
-v, --verbose Show additional information

OPTIONS:
--commitment <COMMITMENT_LEVEL> Return information at the selected commitment level [possible values:
processed, confirmed, finalized]
-C, --config <FILEPATH> Configuration file to use [default: ~/.config/solana/cli/config.yml]
-u, --url <URL_OR_MONIKER> URL for Solana's JSON RPC or moniker (or their first letter): [mainnet-beta,
testnet, devnet, localhost]
-k, --keypair <KEYPAIR> Filepath or URL to a keypair
--output <FORMAT> Return information in specified output format [possible values: json, json-
compact]
--ws <URL> WebSocket URL for the solana cluster

SUBCOMMANDS:
account Show the contents of an account
address Get your public key
address-lookup-table Address lookup table management
airdrop Request SOL from a faucet
authorize-nonce-account Assign account authority to a new entity
balance Get your balance
block Get a confirmed block
block-height Get current block height
block-production Show information about block production
block-time Get estimated production time of a block
catchup Wait for a validator to catch up to the cluster
close-vote-account Close a vote account and withdraw all funds remaining
cluster-date Get current cluster date, computed from genesis creation time and network
time
cluster-version Get the version of the cluster entrypoint
completion Generate completion scripts for various shells
config Solana command-line tool configuration settings
confirm Confirm transaction by signature
create-address-with-seed Generate a derived account address with a seed. For program derived
addresses (PDAs), use the find-program-derived-address command instead
create-nonce-account Create a nonce account
create-stake-account Create a stake account
create-stake-account-checked Create a stake account, checking the withdraw authority as a signer
create-vote-account Create a vote account
deactivate-stake Deactivate the delegated stake from the stake account
decode-transaction Decode a serialized transaction
delegate-stake Delegate stake to a vote account
epoch Get current epoch
epoch-info Get information about the current epoch
feature Runtime feature management
fees Display current cluster fees (Deprecated in v1.8.0)
find-program-derived-address Generate a program derived account address with a seed
first-available-block Get the first available block in the storage
genesis-hash Get the genesis hash
gossip Show the current gossip network nodes
help Prints this message or the help of the given subcommand(s)
inflation Show inflation information
largest-accounts Get addresses of largest cluster accounts
leader-schedule Display leader schedule
live-slots Show information about the current slot progression
logs Stream transaction logs
merge-stake Merges one stake account into another
new-nonce Generate a new nonce, rendering the existing nonce useless
nonce Get the current nonce value
nonce-account Show the contents of a nonce account
ping Submit transactions sequentially
program Program management
redelegate-stake Redelegate active stake to another vote account
rent Calculate rent-exempt-minimum value for a given account data field length.
resolve-signer Checks that a signer is valid, and returns its specific path; useful for
signers that may be specified generally, eg. usb://ledger
sign-offchain-message Sign off-chain message
slot Get current slot
split-stake Duplicate a stake account, splitting the tokens between the two
stake-account Show the contents of a stake account
stake-authorize Authorize a new signing keypair for the given stake account
stake-authorize-checked Authorize a new signing keypair for the given stake account, checking the
authority as a signer
stake-history Show the stake history
stake-minimum-delegation Get the stake minimum delegation amount
stake-set-lockup Set Lockup for the stake account
stake-set-lockup-checked Set Lockup for the stake account, checking the new authority as a signer
stakes Show stake account information
supply Get information about the cluster supply of SOL
transaction-count Get current transaction count
transaction-history Show historical transactions affecting the given address from newest to
oldest
transfer Transfer funds between system accounts
upgrade-nonce-account One-time idempotent upgrade of legacy nonce versions in order to bump them
out of chain blockhash domain.
validator-info Publish/get Validator info on Solana
validators Show summary information about the current validators
verify-offchain-signature Verify off-chain message signature
vote-account Show the contents of a vote account
vote-authorize-voter Authorize a new vote signing keypair for the given vote account
vote-authorize-voter-checked Authorize a new vote signing keypair for the given vote account, checking
the new authority as a signer
vote-authorize-withdrawer Authorize a new withdraw signing keypair for the given vote account
vote-authorize-withdrawer-checked Authorize a new withdraw signing keypair for the given vote account,
checking the new authority as a signer
vote-update-commission Update the vote account's commission
vote-update-validator Update the vote account's validator identity
wait-for-max-stake Wait for the max stake of any one node to drop below a percentage of total.
withdraw-from-nonce-account Withdraw SOL from the nonce account
withdraw-from-vote-account Withdraw lamports from a vote account into a specified account
withdraw-stake Withdraw the unstaked SOL from the stake account

01. Solana 基础知识

1.1 Solana开发流程

任何人都可以通过 Solana 支付费用来存储和执行代码。部署的代码被称为程序 = 智能合约。要与程序交互,您需要从客户端在区块链上发送一笔交易。

Solana开发者工作流程是程序-客户 (program-client) 模型。这个模型由两个主要的工作流程组成:程序开发和客户端开发。

  1. 程序开发工作流程 (后端):
    • 编程语言:Rust、C 或 C++ 等。这些程序可以包含智能合约、验证器或其他自定义功能。
    • 一旦您完成了程序的开发,可以将其部署到 Solana 区块链上。
    • 部署完成后,任何了解如何与这些程序进行通信的人都可以使用它们。
  2. 客户端开发工作流程 (前端):
    • 在客户端开发工作流程中,您可以编写与已部署程序进行交互的去中心化应用程序(dApp)。
    • 您可以使用客户端 SDK 或命令行界面(CLI)来编写与部署的程序进行通信的 dApp。
    • 这些客户端 SDK 在底层使用 JSON RPC API 来与程序进行通信。
    • 您可以创建各种应用程序,如钱包、交易所等,通过客户端 SDK 向已部署的程序提交交易指令。
    • 常见的应用程序类型包括浏览器扩展钱包和 Web 应用程序,但您也可以构建移动应用程序、桌面应用程序或任何能够与 JSON RPC API 进行通信的应用程序。

这两个工作流程共同创建了一个由 dApp 和程序组成的网络。它们可以相互通信以更新状态和查询区块链。这种模型涵盖了前端开发和合约开发两个方向。

Account = 文件

在Solana中,”Everythin is an Account” 类似Linux世界里面把所有的资源都抽象成 “文件” 一样。

Solana作为一个分布式区块链系统,所有的信息都存储在Account对象中,如合约(Solana叫Onchain Program), 账号信息,合约中存储的内容等都是存储在一个个Account对象中。

1
2
3
4
5
6
7
8
9
10
11
12
13
pub struct Account {
/// lamports in the account
pub lamports: u64,
/// data held in this account
#[serde(with = "serde_bytes")]
pub data: Vec<u8>,
/// the program that owns this account. If executable, the program that loads this account.
pub owner: Pubkey,
/// this account's data contains a loaded program (and is now read-only)
pub executable: bool,
/// the epoch at which this account will next owe rent
pub rent_epoch: Epoch,
}

其中的lamports表示账号余额,data表示存储的内容,owner表示这个Account可以被谁来操作,类似文件所有者。 如果是合约账号,这里data的内容就是合约编译后的代码,同时executable为true。

Account and signature

Solana的签名系统使用的是 Ed25519 ,说人话就是: Ed25519是一种计算快,安全性高,且生成的签名内容小的一种不对称加密算法。新一代公链几乎都支持这个算法。

所以Solana的,我们用户理解的账号,就是一串Ed25519的私钥,各种钱包里面的助记词,会被转换成随机数种子, 再用随机数种子来生成一个私钥,所以助记词最终也是换算成私钥。所以用户账号的本质就是私钥,而用户账号的地址 则是这私钥对应的公钥,优于公钥是二进制的,为了可读性,将其进行Base58编码后的值,就是这个账号的地址。 如:HawRVHh7t4d3H3bitWHFt25WhhoDmbJMCfWdESQQoYEy

把这里的公钥和私钥放一起,就是所谓的Keypair,或者叫公私钥对。假设这里把私钥进行加密,并由用户来设置密码, 公钥作为这个私钥的索引。就实现了一个简单的钱包系统了。

通过用户选择的公钥,加上密码,得到对应的私钥,再用私钥去操作的他的账号

Transaction

交易就是链外数据和链上数据产生的一次交互。比如发起一笔转账,在StepN里面发起一次Claim动作。 交易是对多个交易指令的打包,所以起内容主要就是各个交易指令,以及相应指令对应的发起人和签名。Transaction 的定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pub struct Message {
pub header: MessageHeader,
#[serde(with = "short_vec")]
pub account_keys: Vec<Pubkey>,
pub recent_blockhash: Hash,
#[serde(with = "short_vec")]
pub instructions: Vec<CompiledInstruction>,
#[serde(with = "short_vec")]
pub address_table_lookups: Vec<MessageAddressTableLookup>,
}

pub enum VersionedMessage {
Legacy(LegacyMessage),
V0(v0::Message),
}

pub struct VersionedTransaction {
/// List of signatures
#[serde(with = "short_vec")]
pub signatures: Vec<Signature>,
/// Message to sign.
pub message: VersionedMessage,
}

从中可以简单理解为,交易就是一连串的交易指令,以及需要签名的指令的签名内容。

交易指令

上面说到的交易指令又是什么呢?先来看下定义:

1
2
3
4
5
6
7
8
9
10
pub struct CompiledInstruction {
/// Index into the transaction keys array indicating the program account that executes this instruction.
pub program_id_index: u8,
/// Ordered indices into the transaction keys array indicating which accounts to pass to the program.
#[serde(with = "short_vec")]
pub accounts: Vec<u8>,
/// The program input data.
#[serde(with = "short_vec")]
pub data: Vec<u8>,
}

从这些成员变量名就可以猜到。交易指令就是 执行哪个合约(program_id_index),输入为数据data,执行过程 中需要用到哪些Account: accounts

类似函数调用一样,program_id_index是函数名,因为合约都是用地址标识的,所以这里指在accounts数组中 的第几个地址。传入的参数包含两部分,二进制数据data和需要使用到的Account资源:accounts。

Contract

合约分为两类,一类是普通合约一类是系统合约,前者在Solana中称为”On Chain Program” 后者称为”Native Program” 其实本质都是类似其他公链上所说的合约。

System contract

系统合约是由节点在部署的时候生成的,普通用户无法更新,他们像普通合约一样,可以被其他合约或者RPC进行调用

系统合约有

  • System Program: 创建账号,转账等作用
  • BPF Loader Program: 部署和更新合约
  • Vote program: 创建并管理用户POS代理投票的状态和奖励

General contract

一般我们说的合约都是普通合约,或者叫 “On Chain Program”。普通合约是由用户开发并部署,Solana官方也有 一些官方开发的合约,如Token、ATA账号等合约。

当用户通过”BPF Loader Program”部署一个新合约的时候,新合约Account中的被标记为true,表示他是一个可以 被执行的合约账号。不同于有些公链,Solana上的合约是可以被更新的,也可以被销毁。并且当销毁的时候,用于存储 代码的账号所消耗的资源也会归还给部署者。

Contract与Account

在上面的Account介绍中,我们有个owner的成员,这个就表示这个Account是被哪个合约管理的,或者说哪个合约可以对这个Account进行读写,类似Linux操作系统中,文件属于哪个用户。

比如一般合约,他的Owner都是BPF Loader:

而存放我们代币余额的内容的ower都是Token合约:

对应的代币为:

  • 租约:Solana的资金模型中,每个 Solana 账户在区块链上存储数据的费用称为“租金”。 这种基于时间和空间的费用来保持账户及其数据在区块链上的活动为节点提供相应的收入。

    • 所有 Solana 账户(以及计划)都需要保持足够高的 LAMPORT 余额,才能免除租金并保留在 Solana 区块链上。

    • 当帐户不再有足够的 LAMPORTS 来支付租金时,它将通过称为垃圾收集的过程从网络中删除。

    • 注意:租金与交易费用不同。 支付租金(或保存在账户中)以将数据存储在 Solana 区块链上。 而交易费用是为了处理网络上的指令而支付的。

  • 租金率:Solana 租金率是在网络范围内设置的,主要基于每年每字节设置的 LAMPORTS。 目前,租金率为静态金额并存储在 Rent 系统变量中。

  • 免租:保持最低 LAMPORT 余额超过 2 年租金的账户被视为“免租金”,不会产生租金。每次账户余额减少时,都会检查该账户是否仍免租金。 导致账户余额低于租金豁免阈值的交易将会失败。

  • 垃圾收集:未保持租金豁免状态或余额不足以支付租金的帐户将通过称为垃圾收集的过程从网络中删除。 完成此过程是为了帮助减少不再使用/维护的数据的网络范围存储。

1.2 SPL 代币

在以太坊中,普通代币被一个叫ERC20的提案定了规范,可以认为普通代币合约统一叫做ERC20代币。

那么Solana世界里的ERC20代币是什么呢?答案就是SPL代币。

The Solana Program Library (SPL) is a collection of on-chain programs targeting the Sealevel parallel runtime.

SPL Token是 “ Solana Program Library”中的一个组成部分,叫做”Token Program”,简称为SPL Token。

所有的代币都有这个合约来管理,该合约代码在 https://github.com/solana-labs/solana-program-library/tree/master/token

代币信息

不同于以太坊中,一个代币就是一个合约。

SPL Token中,一个代币,仅仅是一个归Token合约管理的普通的Account对象,这个对象里面的二进制数据定义了 这个代币的基本属性。其结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pub struct Mint {
/// Optional authority used to mint new tokens. The mint authority may only be provided during
/// mint creation. If no mint authority is present then the mint has a fixed supply and no
/// further tokens may be minted.
pub mint_authority: COption<Pubkey>,
/// Total supply of tokens.
pub supply: u64,
/// Number of base 10 digits to the right of the decimal place.
pub decimals: u8,
/// Is `true` if this structure has been initialized
pub is_initialized: bool,
/// Optional authority to freeze token accounts.
pub freeze_authority: COption<Pubkey>,
}

相对有意义的就是supply表示总共的供应量,decimals表示代币的精度信息。

img

SPL Token Account

那么每个用户的拥有的代币数量信息存在哪里呢?

这个合约又定义了一个账号结构,来表示某个地址含有某个代币的数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pub struct Account {
/// The mint associated with this account
pub mint: Pubkey,
/// The owner of this account.
pub owner: Pubkey,
/// The amount of tokens this account holds.
pub amount: u64,
/// If `delegate` is `Some` then `delegated_amount` represents
/// the amount authorized by the delegate
pub delegate: COption<Pubkey>,
/// The account's state
pub state: AccountState,
/// If is_native.is_some, this is a native token, and the value logs the rent-exempt reserve. An
/// Account is required to be rent-exempt, so the value is used by the Processor to ensure that
/// wrapped SOL accounts do not drop below this threshold.
pub is_native: COption<u64>,
/// The amount delegated
pub delegated_amount: u64,
/// Optional authority to close the account.
pub close_authority: COption<Pubkey>,
}

这里owner表示谁的代币,amount表示代币的数量。

img

Account关系

所以整体结构是这样的:

img

这两个结构体都是SPL Token Program管理的Account对象,其自身所携带的数据,分别为代币信息,和 存储哪个代币的信息。

这样当需要进行代币的交易时,只需要相应用户的相应代币账号里面的amount即可。

1.3 Solana command line

接下来我们来开始体验Solana,Solana为我们提供了一套命令行工具来实现对Solana的操作。 这里注意,这个命令行工具,是除了节点外,官方提供的唯一工具。什么钱包,scan浏览器等还 都是第三方的,所以我们从这里开始。

这里建议开发工具平台使用Mac/Linux(Ubuntu),Windows不建议折腾,虽然Solana也是支持 的,下面我们以Mac作为演示平台进行讲演。

接下来几步将演示通过命令行,发行一个代币。并给自己账号mint一定数量的代币。 并通过插件钱包或者命令行的方式给其他同学空投该代币

1. 安装

打开命令行,输入:

1
sh -c "$(curl -sSfL https://release.solana.com/stable/install)"

自动加载:

1
2
3
4
5
6
7
8
downloading stable installer
✨ stable commit cd1c6d0 initialized
Adding
export PATH="/Users/you/.local/share/solana/install/active_release/bin:$PATH" to /Users/you/.profile

Close and reopen your terminal to apply the PATH changes or run the following in your existing shell:

export PATH="/Users/you/.local/share/solana/install/active_release/bin:$PATH"

再次输入加载出来的最后一行:

1
export PATH="/Users/you/.local/share/solana/install/active_release/bin:$PATH"

输入solana –version查看是否安装完成:

1
2
~ % solana --version
solana-cli 1.14.20 (src:cd1c6d0d; feat:1879391783)

这里打印出来cli的版本号。

查看spl-token命令的帮助文档

1
1solana config set --url https://api.devnet.solana.com

2. 设置网络环境

Solana的网络环境分成开发网、测试网、主网三类,开发网为Solana节点开发使用,更新频繁,测试网主要 给到DApp开发者使用,相对稳定。主网则是正式环境,里面的是真金白银。

官方RPC地址分别是:

这里我们使用测试网,测试网可以申请空投测试币。

1
solana config set --url https://api.testnet.solana.com
1
2
3
4
5
Config File: /Users/you/.config/solana/cli/config.yml
RPC URL: https://api.testnet.solana.com
WebSocket URL: wss://api.testnet.solana.com/ (computed)
Keypair Path: /Users/you/.config/solana/id.json
Commitment: confirmed

3. 创建账号

1
solana-keygen new --force

自动加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Generating a new keypair

For added security, enter a BIP39 passphrase

NOTE! This passphrase improves security of the recovery seed phrase NOT the
keypair file itself, which is stored as insecure plain text

BIP39 Passphrase (empty for none):

Wrote new keypair to /Users/you/.config/solana/id.json
// 这里设置好密码后,提示keypair被加密存在存在"/Users/you/.config/solana/id.json"

===========================================================================
pubkey: Czorr4y9oFvE3VdfCLVFuKDYxaNUG1iyQomR7kMZUuzi // public key: 账户公钥
===========================================================================
Save this seed phrase and your BIP39 passphrase to recover your new keypair:
tail ... despair // 创建完成后其对应的BIP39的助记词,这里助记词要记住,后续使用钱包的时候,可以通过助记词来恢复账号。
===========================================================================

通过如下命令可以查看当前账号的地址,也就是上面的Keypair文件的中的公钥:

1
solana-keygen pubkey

4. 申请水龙头

只有开发网和测试网可以申请水龙头币,这里可以通过命令行:

1
solana airdrop 1 // Request airdrop of 1 SOL

提示申请1个SOL成功。通过命令:

1
solana balance

可以查看当前账号的余额。当前账号也就是”/Users/you/.config/solana/id.json”中存储的keypair对应的账号。

5. 用 mint account 创建 Token

创建Token时,会先创建一个 “mint account”铸造 (mint) 新的 Token,**”mint account”** 创建完成,就可以 使用其私钥来铸造新的Token,并将其 发送给其他Solana账户

1
spl-token create-token
1
2
3
4
5
6
Creating token 7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9 under program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA

Address: 7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9 // mint account, Token地址
Decimals: 9 // 精度: 9

Signature: 5QRdzn59ig3j3qjEazteDR2zoCLUWoCWdbFc7iQTd68esfdV9je3fE2We3Ms7NUGfBt6kapCj7oBAr1kbiTskSmz

交易浏览器:5QRdzn59ig3j3qjEazteDR2zoCLUWoCWdbFc7iQTd68esfdV9je3fE2We3Ms7NUGfBt6kapCj7oBAr1kbiTskSmz

6. 创建 Token Account

在Solana上创建Token后,需要创建 Token Account 来存储和管理该Token的余额和相关信息。Token账户是Solana区块链上特定Token的持有者账户,类似于银行账户,用于记录Token的所有权和交易历史。

1
2
3
4
5
spl-token create-account 7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9

Creating account EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK // 调用了ATA合约,并创建了ATA账号:EZhhUANUMK...

Signature: 59yBhJzC2HDkF61AhgaXcvVGiw5CjdnNpFyxvCzbqQrCjGCVKotNvCMaRQooJkxmu6ypJ9P7AZDiKxYex7pvBZKq

交易浏览器:59yBhJzC2HDkF61AhgaXcvVGiw5CjdnNpFyxvCzbqQrCjGCVKotNvCMaRQooJkxmu6ypJ9P7AZDiKxYex7pvBZKq

7. 给自己的 Token Account 发送 mint

创建完 Token Account 后,要给我们的账号去 “mint” 一些token(下面mint了100个):

1
2
3
4
5
6
7
spl-token mint  7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9 100 EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK
// mint account mint 100 token to //我们刚刚创建的这个 token account 的账号来存这100个
Minting 100 tokens
Token: 7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9
Recipient: EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK

Signature: 5eE21U9ukZLP7Uvck5mzBbKRcXjxEYZYxCTnJX6qoS9kdXzfhPuN8k2Ko6BBekBdP2mhLmPMHAWNJW6bqyo6mqQe

交易记录:5eE21U9ukZLP7Uvck5mzBbKRcXjxEYZYxCTnJX6qoS9kdXzfhPuN8k2Ko6BBekBdP2mhLmPMHAWNJW6bqyo6mqQe

1
2
3
4
5
Token Program -TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA // owner program
#1 - Account -EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AKicon
#2 - Mint -7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9icon // token address
#3 - MintAuthority -Czorr4y9oFvE3VdfCLVFuKDYxaNUG1iyQomR7kMZUuziicon // Authority (也就是说是谁发的,这是他的公钥)
#4 - TokenAmount - 100

查询余额:

1
2
spl-token balance 7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9
100

因为这里是求取ATA账号,所以只需要提供Token Mint地址即刻。

8. 转账

这里通过命令行给另一个账号转账,如果这个账号之前不存在,需要使用–allow-unfunded-recipient来进行创建。

1
solana transfer --allow-unfunded-recipient {your wallet} 0.01

eg.

1
2
3
4
5
6
7
8
9
spl-token transfer --fund-recipient  7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9 1 BBy1K96Y3bohNeiZTHuQyB53LcfZv6NWCSWqQp89TiVu
// 也就是说转 1个 {7vtXvye2ECB1T5S..的所属类型代币} 到 {BBy1K96Y3bohNeiZT..}
Transfer 1 tokens
Sender: EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK
Recipient: BBy1K96Y3bohNeiZTHuQyB53LcfZv6NWCSWqQp89TiVu
Recipient associated token account: H1jfKknnnyfFGYPVRd4ZHwUbXLF4PbFSWSH6wMJq6EK9
Funding recipient: H1jfKknnnyfFGYPVRd4ZHwUbXLF4PbFSWSH6wMJq6EK9 // --fund-recipient, 帮这个用户创建了ATA账号

Signature: 5VqeT7ctVtGdcJDvTmLzL4Pbti8PzM3mSrRpdE8GNG4ghF3svSJMkTn4AfNRQDSeYqCotEQuzDY9KLgdSJbKEjXt

交易为: 5VqeT7ctVtGdcJDvTmLzL4Pbti8PzM3mSrRpdE8GNG4ghF3svSJMkTn4AfNRQDSeYqCotEQuzDY9KLgdSJbKEjXt

查询下这个 账号,余额为1.

02. 通过RPC与Solana交互

2.1 Solana的RPC介绍

RPC (Remote Procedure Call, 远程过程调用): 通过网络从远程计算机上请求服务。就是用一台 loacl 电脑远程调用 procedure (subroutine) 共享网络上的另一台计算机上 (不同的 address space ) 上面正在运行的函数。eg:

local: 2+3 == RPC (可能是使用的HTTP、gRPC、Thrift…) ==> Server: int func(a+b)
Server: 5 == RPC (可能是使用的HTTP、gRPC、Thrift…) ==> Local: 5

RPC 框架:是一种技术思想和方法而非一种规范和协议(实现RPC的方式有很多,使用RPC框架思想实现的例子有 gRPC、Thrift、mina…, HTTP是标准化的协议,也是RPC框架的一种实现方法),RPC框架的主要功能是把RPC调用的过程封装起来,使开发者不用考虑模块间的兼容性问题,简单配置就可调用。

RPC 跟区块链关系

  • 账户:用于管理和交易代币的虚拟数字资产;
  • 合约:一种智能合约,可以自动化地完成各种操作,例如转移代币或支付费用;
  • RPC:分布式系统中的一种通信协议,可以通过不同的节点进行交互。

RPC 跟区块链是什么关系呢?引用 Polkadot 的一个架构图:

1
2
3
"Networking:" 每个节点之间要进行通讯,这个网络一般就是我们所谓的P2P网络。
"Storage:" 区块需要存储,我们需要一个存储的组件。
"Consensus:" 存储完了以后互通信息的时候要有一个共识,比如说POW,POS这种的共识算法?
1
2
"Runtime call executor:" 相应的,智能合约,它要怎么去执行,执行什么语言,需要一个runtime
"Application logic and state WebAssembly (Wasm):" 这个runtime去执行我们合约编译出来的字节码。

RPC: 作为区块链系统中客户端与节点之间的通信接口,被普通用户直接使用,通过RPC可以实现与 区块链 node 的交互操作,同时节点的操作可能会影响到网络和存储等方面。

  • 因为程序员帮忙将中间的过程都通过代码来串联起来了,所以普通用户感知不到 RPC 的存在,只知道钱包,拉起、确定 => 币没了。所以 RPC 又是用户界面和区块链之间的桥梁。

  • Solana 提供的 RPC 分为主动请求的 HTTP 接口和消息推送的 Websocket 接口。只是单次查询一般使用 HTTP 接口,如发送交易,查询用户余额。而对于链上数据的监控则通过 Websocket 接口,如监控合约执行的日志。

    • HTTP 接口是通过 JSON RPC 的格式对外提供服务,JSON RPC 是一种以 JSSON 作为序列化工具,HTTP 作为传输协议的 RPC 模式,其有多个版本,当前使用的是 v2 版本。其请求格式为(对应的请求结果多了result等):

      1
      2
      3
      4
      5
      6
      7
      {"jsonrpc": "2.0",
      "id": 1,
      "method": "getBalance",...} // result 表示请求的结果。id 和请求里面的 id 对应,表示的是哪个请求的结果。
      /* 在请求查询的时候,对查询的结果有三种状态选择:
      - 'finalized' - 节点将查询由超过集群中超多数确认为达到最大封锁期的最新区块,表示集群已将此区块确认为已完成。
      - 'confirmed' - 节点将查询由集群的超多数投票的最新区块。
      - 'processed' - 节点将查询最新的区块。注意,该区块可能被集群跳过。*/
    • Websocket 是 HTTP 为了补充长链接,而增加一个特性,概括来说就可以认为这个是一条 TCP 长链接,可以实现实时数据传输。Solana 通过这条长连接来给客户端推送消息。只是这里的消息的内容也是采用了 JSONRPC 的格式,如:

      1
      2
      3
      4
      {"jsonrpc": "2.0",
      "id": 1,
      "method": "accountSubscribe",
      "params": [...]}

2.2 RPC API

Solana RPC API 格式

https://api.devnet.solana.com 来发送一个 POST 的请求,然后 POST 请求带了一个 Header 表示内容是 json

1
2
3
4
5
6
curl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d '
{// 下面是里面的内容
"jsonrpc": "2.0", "id": 1,
"method": "" // 添加方法, eg. getClusterNodes, getBlockHeight, getEpochInfo
}
'
  • getClusterNodes:可以获得当前网络内,集群节点的相关信息,比如验证者的key,节点IP,节点版本等。

block 相关 API

  • getBlockHeight:可以获取当前的区块高度

  • getLatestBlockhash:可以获得连上最近的一个Block的Hash值和高度,看到最近的一个区块的slot, hash以及block height

  • getBlock:获取指定高度block的信息,如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    curl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d '
    {
    "jsonrpc": "2.0","id":1,
    "method":"getBlock",
    "params": [
    174302734,
    {
    "encoding": "jsonParsed", // 将结果按照json的格式进行展示
    "maxSupportedTransactionVersion":0, // 和带版本号的交易有关, 表示返回最大的版本号, 默认是0
    "transactionDetails":"full", // 设置返回的交易信息的内容复杂等级, 默认是 full
    "rewards":false // rewards 表示是否携带 rewards 信息
    }
    ]
    }
    '

获取指定block的确认状态

有时候在上面获得当前最高区块,但是查询区块信息的时候却又查询不到,这里可以通过getBlockCommitment查看下对应区块的状态。

1
2
3
4
5
6
7
curl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d '
{
"jsonrpc": "2.0", "id": 1,
"method": "getBlockCommitment",
"params":[174302734]
}
'

得到结果:

1
2
3
4
5
6
7
8
{
"jsonrpc": "2.0",
"result": {
"commitment": null, // 如果commitment不为null的时候,将是一个数组表示各个集群中Stake的数量分布。
"totalStake": 144333782793465543 // totalStake表示提交确认的节点总共Stake的SOL数目,也就是POS的权重
},
"id": 1
}

一次性获取多个Block的信息

前面的getBlock获得了单个Block的信息,还可以通过getBlocks一次性获得多个Block的信息。

1
2
3
4
5
6
7
8
9
curl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d '
{
"jsonrpc": "2.0", "id": 1,
"method": "getBlocks",
"params": [
174302734, 174302735 // 其余参数都是一样的,这里参数中,前面的部分是block number的数组
]
}
'

分页获取Block

前面两个获取Block信息的方法,分别可以获得单个Block和多个指定Block号的信息。因为Block Number是递增且不一定连续的,因此 还Solana还提供了一个分页查询的方式getBlocksWithLimit,从起始号查询多少个。

1
2
3
4
5
6
7
8
curl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d '
{
"jsonrpc": "2.0",
"id":1,
"method":"getBlocksWithLimit",
"params":[174302734, 3] // 从 174302734 节点高度开始,往后请求三个,然后在我们的浏览器里面显示三个
}
'

得到:

1
2
3
4
5
6
7
8
9
{
"jsonrpc": "2.0",
"result": [
174302734,
174302735,
174302736
],
"id": 1
}

三个BlockNumber,接着我们可以前面的GetBlocks来获得这三个Block的详细信息。

Slot & Epoch 相关 API

Epoch:在一般POS中比较常见,表示这个周期内,一些参与验证的节点信息是固定的,如果有新 节点或者节点权重变更,将在下一个epoch中生效。

在POS(权益证明)中,”epoch”可以理解为在一段时间内选择多少个节点进行投票。在这个周期内,这些节点被选为投票者。提到了”Solanah”中的POH(Proof of History)概念,它选择一个主节点,并且其他节点以此主节点的信息进行同步。

  • getEpochInfo:获取当前Epoch信息。输出里面有当前周期的区块高度,slot数目,以及transaction的数目。
  • getEpochSchedule:获取Epoch的调度信息。
  • getSlot:获取最新Slot,和Epoch类似,可以获得当前的Slot

Account 相关 API

第一章有介绍,Solana上存储的内容,都是一个Account对象,有基础的源数据信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
pub struct Account {
/// lamports in the account
pub lamports: u64,
/// data held in this account
#[serde(with = "serde_bytes")]
pub data: Vec<u8>,
/// the program that owns this account. If executable, the program that loads this account.
pub owner: Pubkey,
/// this account's data contains a loaded program (and is now read-only)
pub executable: bool,
/// the epoch at which this account will next owe rent
pub rent_epoch: Epoch,
}

获取 Account Info

我们可以通过 getAccountInfo RPC请求来查看,比如查看我们前面的测试账号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
curl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d '
{
"jsonrpc": "2.0",
"id": 1,
"method": "getAccountInfo",
"params": [
"5pWae6RxD3zrYzBmPTMYo1LZ5vef3vfWH6iV3s8n6ZRG",
{
"encoding": "base58",
"commitment": "finalized"
}
]
}
'

这里我们通过curl来直接发起HTTP请求,最直观的看发生什么。请求中我们指定了测试网的RPC地址。 https://api.devnet.solana.com 得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"jsonrpc": "2.0",
"result": { // 在result里面可以看到value里面的值项目,和Rust的结构体是一一对应的
"context": {
"apiVersion": "1.16.1",
"slot": 206885329
},
"value": {
"data": [ // data表示数据内容, 这里我们的普通账号不是合约账号,因此其为空
"",
"base58" // 表示如果这里有值,那么他将是二进制内容的base58格式编码
],
"executable": false, // 表示是否为可执行合约
"lamports": 59597675320, // 表示余额,这里精度*10^9。
"owner": "11111111111111111111111111111111", // 所有普通账号的Owner都是系统根账号
"rentEpoch": 349,
"space": 0
}
},
"id": 1
}

获取账号 Balance

在上面的Account信息里面,我们已经可以知道 账号余额 (lamports) 了,同时RPC还提供了getBalance 可以更简洁的得到余额信息:

1
2
3
4
5
6
7
8
9
curl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d '
{
"jsonrpc": "2.0", "id": 1,
"method": "getBalance",
"params": [
"5pWae6RxD3zrYzBmPTMYo1LZ5vef3vfWH6iV3s8n6ZRG"
]
}
'

输出结果里,比如 value = 989995000,SOL的精度是10^9,所以也就是 0.989995 个SOL。

获取某个合约管理所有Account

类似Linux查询某个用户所有的文件。Solana提供了一个查询owener为某个合约的RPC方法。

getProgramAccounts:该方法的作用就是罗列出某个合约管理的Account,比如SPL Token合约记录的所有用户的余额信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
curl  https://api.devnet.solana.com  -X POST -H "Content-Type: application/json" -d '
{
"jsonrpc": "2.0",
"id": 1,
"method": "getProgramAccounts",
"params": [
"namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX",
{
"encoding": "jsonParsed",
"filters": [
{
"dataSize": 128 // 获取所有NameService服务管理的名字且记录空间大小为128字节的记录
}
]
}
]
}
'

SPL-Token 相关 API

我们知道SPL Token的结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1pub struct Account {
2 /// The mint associated with this account
3 pub mint: Pubkey,
4 /// The owner of this account.
5 pub owner: Pubkey,
6 /// The amount of tokens this account holds.
7 pub amount: u64,
8 /// If `delegate` is `Some` then `delegated_amount` represents
9 /// the amount authorized by the delegate
10 pub delegate: COption<Pubkey>,
11 /// The account's state
12 pub state: AccountState,
13 /// If is_native.is_some, this is a native token, and the value logs the rent-exempt reserve. An
14 /// Account is required to be rent-exempt, so the value is used by the Processor to ensure that
15 /// wrapped SOL accounts do not drop below this threshold.
16 pub is_native: COption<u64>,
17 /// The amount delegated
18 pub delegated_amount: u64,
19 /// Optional authority to close the account.
20 pub close_authority: COption<Pubkey>,
21 }

按照需求查询账号

getTokenAccountsByOwner:我们可以查询某个Token下,所有owner为某人的Token账号,或者delegate为某人的所有账号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
curl  https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d '
{
"jsonrpc": "2.0",
"id": 1,
"method": "getTokenAccountsByOwner",
"params": [
"Czorr4y9oFvE3VdfCLVFuKDYxaNUG1iyQomR7kMZUuzi",
{
"mint": "7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9"
},
{
"encoding": "jsonParsed"
}
]
}
'
// 这里查询到这个token:7vtXvye2ECB1T5Se8E... ower 为 CnjrCefFBHmWnKcw.. 所有账号

获取某个Token Account账号的余额

查询SPL Token的余额,有个ATA账号需要了解。本质上就是对应Token的账号:

1
2
3
4
5
6
7
8
9
curl  https://api.devnet.solana.com  -X POST -H "Content-Type: application/json" -d '
{
"jsonrpc": "2.0", "id": 1,
"method": "getTokenAccountBalance",
"params": [
"EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK"
]
}
'

返回的值,会列出数量,uiAmount是可以显示的数量,做了精度转换的

Transaction 相关 API

获取交易手续费

针对某个交易,需要预估其手续费时,可以借助节点的预计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
curl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d '
{
"id":1,
"jsonrpc":"2.0",
"method":"getFeeForMessage",
"params":[
"AQABAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQAA",
{
"commitment":"processed"
}
]
}
'

输出结果参数中的字符串,是Transaction打包后的结果。也就是RawTransaction的序列化结果。

获取交易详细信息

getTransaction:查询某个交易的详细信息(输出结果跟浏览器中的结果基本是对应的):

1
2
3
4
5
6
7
8
9
10
11
curl  https://api.devnet.solana.com  -X POST -H "Content-Type: application/json" -d '
{
"jsonrpc": "2.0",
"id": 1,
"method": "getTransaction",
"params": [
"2o9qCEhwKi8w7hTFQJTLwMBZPFH8qM3iNd9rprtdY6XShyrpsqkWt4Df3Zgsxv6y4nbRe4SDgU8KMvuMfs7HxVhp",
"jsonParsed"
]
}
'

2.3 RPC notification

推送RPC,其实是RPC节点允许连接的一个WebSocket长连接。通过在该长连接上发送订阅请求, RPC节点会将相关事件在长连接上推送过来。当前订阅主要分为:

  • accountSubscribe: 订阅 Account 的变化,比如lamports
  • logsSubscribe: 订阅交易的 日志 log
  • programSubscribe:订阅 合约Account 的变化
  • signatureSubscribe : 订阅签名状态变化
  • slotSubscribe : 订阅slot的变化

每个事件,还有对应的 取消订阅 (Unsubscribe) 动作。将上面的 Subscribe 替换成 Unsubscribe 即可。

这里我们通过wscat命令行工具来模拟wss客户端。首先安装工具:

1
npm install -g ws wscat

然后建立连接:

1
wscat -c wss://api.devnet.solana.com

下面举例说明:

订阅 Account 变化

accountSubscribe:这里的Account就是每个地址的Account元数据。主要变化的就是data部分和lamports部分。 比如我们要订阅我们的账号余额的变化。

这里订阅对账号的变化的事件,我们通过wscat来模拟:

1
2
3
4
wscat -c wss://api.testnet.solana.com
Connected (press CTRL+C to quit)
> {"jsonrpc":"2.0","id":1,"method":"accountSubscribe","params":["CnjrCefFBHmWnKcwH5T8DFUQuVEmUJwfBL3Goqj6YhKw",{"encoding":"jsonParsed","commitment":"finalized"}]}
< {"jsonrpc":"2.0","result":3283925,"id":1}

然后我们在另外一个终端里面进行转账:

1
solana transfer --allow-unfunded-recipient CZmVK1DymrSVWHiQCGXx6VG5zgHVrh5J1P514jHKRDxA 0.01

接着我们注意观察上面的wscat:

1
2
3
4
Connected (press CTRL+C to quit)
> {"jsonrpc":"2.0","id":1,"method":"accountSubscribe","params":["CnjrCefFBHmWnKcwH5T8DFUQuVEmUJwfBL3Goqj6YhKw",{"encoding":"jsonParsed","commitment":"finalized"}]}
< {"jsonrpc":"2.0","result":3283925,"id":1}
< {"jsonrpc":"2.0","method":"accountNotification","params":{"result":{"context":{"slot":209127027},"value":{"lamports":989995000,"data":["","base64"],"owner":"11111111111111111111111111111111","executable":false,"rentEpoch":0,"space":0}},"subscription":3283925}}

会发现,一段时间后,也就是到达了 “finalized”状态后,就会将修改过后的Account信息推送过来:

1
2
3
4
5
6
7
8
9
10
11
{
"lamports": 989995000,
"data": [
"",
"base64"
],
"owner": "11111111111111111111111111111111",
"executable": false,
"rentEpoch": 0,
"space": 0
}

可以看到这里余额发生了变化

订阅 日志 log

订阅日志可能是做应用最常见到的,任何在log里面打印了相关事件的交易都会被通知

比如这里我们订阅我们的一个ATA的账号:

1
2
3
4
wscat -c wss://api.testnet.solana.com
Connected (press CTRL+C to quit)
> {"jsonrpc":"2.0","id":1,"method":"logsSubscribe","params":[{"mentions":["CdJp6W7S8muM85UXq7u2P42ryytDacqEo8JgoHENSiUi"]},{"commitment":"finalized"}]}
< {"jsonrpc":"2.0","result":610540,"id":1}

然后我们给这个地址做mint增加他的余额:

1
2
3
4
5
6
spl-token mint 7dyTPp6Jd1nWWyz3y7CXqdSG86yFpVF7u45ARKnqDhRF 1000000000
Minting 1000000000 tokens
Token: 7dyTPp6Jd1nWWyz3y7CXqdSG86yFpVF7u45ARKnqDhRF
Recipient: CdJp6W7S8muM85UXq7u2P42ryytDacqEo8JgoHENSiUi

Signature: 5NVHNccPo4ADxnHZjVSYZzxk3fuZfZvuLP6MwkhSNBbQRNcGfC2gwScz24XYictZuqaMKFEcmsXuHV4WZDiFUD3r

可以在事件通知中看到:

1
2
3
4
5
wscat -c wss://api.testnet.solana.com
Connected (press CTRL+C to quit)
> {"jsonrpc":"2.0","id":1,"method":"logsSubscribe","params":[{"mentions":["CdJp6W7S8muM85UXq7u2P42ryytDacqEo8JgoHENSiUi"]},{"commitment":"finalized"}]}
< {"jsonrpc":"2.0","result":610540,"id":1}
< {"jsonrpc":"2.0","method":"logsNotification","params":{"result":{"context":{"slot":209131722},"value":{"signature":"5NVHNccPo4ADxnHZjVSYZzxk3fuZfZvuLP6MwkhSNBbQRNcGfC2gwScz24XYictZuqaMKFEcmsXuHV4WZDiFUD3r","err":null,"logs":["Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]","Program log: Instruction: MintToChecked","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4498 of 200000 compute units","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success"]}},"subscription":610540}}

这里有个”MintToChecked”指令。

订阅合约所属于Account事件

比如我们希望知道所有Token合约管理的账号的余额变化是,我们可以通过订阅合约管理的账号事件来发现:

1
2
3
4
5
6
7
8
9
10
11
{
"jsonrpc": "2.0",
"id": 1,
"method": "programSubscribe",
"params": [
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
{
"encoding": "jsonParsed"
}
]
}

对应的命令里面可以看到有很多的SPL Token账号都在变化。并且因为我们加了”jsonParsed”,所以这里SPL Token的内容也展示出来了。

订阅交易状态

比如我们希望在我们发起交易后,第一时间知道交易的确定状态,我们可以通过订阅该事件来实现:

1
2
3
4
5
6
7
8
9
10
11
12
{
"jsonrpc": "2.0",
"id": 1,
"method": "signatureSubscribe",
"params": [
"BfQAbgqQZMfsxFHwh6Hve8yGb843QfZcYtD2j2nN3K1hLHZrQjzdwG9uWgNkGXs4tBNVLE3JAzvNLtwJBt3zDsN",
{
"commitment": "finalized",
"enableReceivedNotification": false
}
]
}

这里。我们再次发起一笔转账交易:

1
2
3
solana transfer --allow-unfunded-recipient CZmVK1DymrSVWHiQCGXx6VG5zgHVrh5J1P514jHKRDxA 0.01

Signature: BfQAbgqQZMfsxFHwh6Hve8yGb843QfZcYtD2j2nN3K1hLHZrQjzdwG9uWgNkGXs4tBNVLE3JAzvNLtwJBt3zDsN

然后在另外一个终端,迅速建立wscat连接,并订阅该事件:

1
2
3
4
5
wscat -c wss://api.testnet.solana.com
Connected (press CTRL+C to quit)
> {"jsonrpc":"2.0","id":1,"method":"signatureSubscribe","params":["BfQAbgqQZMfsxFHwh6Hve8yGb843QfZcYtD2j2nN3K1hLHZrQjzdwG9uWgNkGXs4tBNVLE3JAzvNLtwJBt3zDsN",{"commitment":"finalized","enableReceivedNotification":false}]}
< {"jsonrpc":"2.0","result":3285176,"id":1}
< {"jsonrpc":"2.0","method":"signatureNotification","params":{"result":{"context":{"slot":209127740},"value":{"err":null}},"subscription":3285176}}

可以看到。当到达”finalized”状态时,通知我们,该交易已经成功,没有出错。

2.4 课后练习

通过curl和wscat命令行来模拟一个监视钱包动作。提示:

  • 创建一个新账号
  • 实时展示余额变化
  • 列出已知SPL-Token的余额
  • 实时展示SPL-Token余额变化

1. 创建账号

  • SOL账号: Czorr4y9oFvE3VdfCLVFuKDYxaNUG1iyQomR7kMZUuzi
  • SPL-Token (Mint Account): 7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9
  • Token Account: EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK
  1. 创建一个新账号:
1
solana-keygen new --force // 得到SOL账号公钥:Czorr4y9oFvE3VdfCLVFuKDYxaNUG1iyQomR7kMZUuzi
  1. 用 mint account 创建 Token
1
spl-token create-token
1
2
3
4
5
6
Creating token 7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9 under program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA

Address: 7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9 // 得到 SPL-Token (Mint Account), Token地址
Decimals: 9 // 精度: 9

Signature: 5QRdzn59ig3j3qjEazteDR2zoCLUWoCWdbFc7iQTd68esfdV9je3fE2We3Ms7NUGfBt6kapCj7oBAr1kbiTskSmz
  1. 创建 Token Account
1
spl-token create-account 7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9
1
2
3
Creating account EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK // 调用ATA合约,并创建ATA账号, Token Account.

Signature: 59yBhJzC2HDkF61AhgaXcvVGiw5CjdnNpFyxvCzbqQrCjGCVKotNvCMaRQooJkxmu6ypJ9P7AZDiKxYex7pvBZKq

2. 订阅SOL余额变化

这里的Account就是每个地址的Account元数据。主要变化的就是data部分和lamports部分。 比如我们要订阅我们的账号余额的变化。

这里订阅对账号的变化的事件,我们通过wscat来模拟:

1
2
3
4
wscat -c wss://api.devnet.solana.com
Connected (press CTRL+C to quit)
> {"jsonrpc":"2.0","id":1,"method":"accountSubscribe","params":["EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK",{"encoding":"jsonParsed","commitment":"finalized"}]}
< {"jsonrpc":"2.0","result":3283925,"id":1}

然后我们在另外一个终端里面进行转账:

1
solana transfer --allow-unfunded-recipient CZmVK1DymrSVWHiQCGXx6VG5zgHVrh5J1P514jHKRDxA 0.01

接着我们注意观察上面的wscat:

1
2
3
4
Connected (press CTRL+C to quit)
> {"jsonrpc":"2.0","id":1,"method":"accountSubscribe","params":["CnjrCefFBHmWnKcwH5T8DFUQuVEmUJwfBL3Goqj6YhKw",{"encoding":"jsonParsed","commitment":"finalized"}]}
< {"jsonrpc":"2.0","result":3283925,"id":1}
< {"jsonrpc":"2.0","method":"accountNotification","params":{"result":{"context":{"slot":209127027},"value":{"lamports":989995000,"data":["","base64"],"owner":"11111111111111111111111111111111","executable":false,"rentEpoch":0,"space":0}},"subscription":3283925}}

会发现,一段时间后,也就是到达了 “finalized”状态后,就会将修改过后的Account信息推送过来:

1
2
3
4
5
6
7
8
9
10
11
{
"lamports": 989995000,
"data": [
"",
"base64"
],
"owner": "11111111111111111111111111111111",
"executable": false,
"rentEpoch": 0,
"space": 0
}

可以看到这里余额发生了变化

3. 展示SPL-Token变化

早期的钱包是通过官方的 token-list 来获得 已知的SPL-Token,现在则通过Metaplex的FT标准查询。除此之外还可以通过订阅Token合约管理的账户变化 来判断是否有Owner为自己的 Token Account被创建。

这里我们假设第一种情况,钱包只维护知名token或者用户自己添加的Token,比如上面的 “7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9”

我们首先获取这个SPL-Token下我们有多少 Token Account:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
curl  https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d '
{
"jsonrpc": "2.0",
"id": 1,
"method": "getTokenAccountsByOwner",
"params": [
"Czorr4y9oFvE3VdfCLVFuKDYxaNUG1iyQomR7kMZUuzi",
{
"mint": "7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9"
},
{
"encoding": "jsonParsed"
}
]
}
'

这里根据结果发现只有一个 “EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK”

那么我们只需要按照教程里面,订阅这个Account的变化就可以了。如果有多个,那么就订阅多个。 在重复对其他 SPL-Token做同样操作,既可以完成SPL-Token钱包的功能。

首先建立websocket链接,并发起对这个Account的订阅:

1
2
3
wscat -c wscat -c wss://api.devnet.solana.com  --proxy=http://127.0.0.1:1087
Connected (press CTRL+C to quit)
> {"jsonrpc":"2.0","id":1,"method":"accountSubscribe","params":["EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK",{"encoding":"jsonParsed","commitment":"finalized"}]}

然后再另一个终端,用”spl-token”命令行来进行转账:

1
2
3
4
5
6
7
spl-token transfer --fund-recipient  7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9 1 BBy1K96Y3bohNeiZTHuQyB53LcfZv6NWCSWqQp89TiVu
Transfer 1 tokens
Sender: EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK
Recipient: BBy1K96Y3bohNeiZTHuQyB53LcfZv6NWCSWqQp89TiVu
Recipient associated token account: H1jfKknnnyfFGYPVRd4ZHwUbXLF4PbFSWSH6wMJq6EK9

Signature: 3paamDSKFk5depKufcDjmJ8wc3eXte3qcgtitFu4TyDi8z9GTXMrLGEgPHgQMnAzFBXYoWxyF5JFzA54Fjvi2ZUK

接着我们就可以在前面的监听中收到:

1
< {"jsonrpc":"2.0","method":"accountNotification","params":{"result":{"context":{"slot":236334118},"value":{"lamports":2039280,"data":{"program":"spl-token","parsed":{"info":{"isNative":false,"mint":"7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9","owner":"Czorr4y9oFvE3VdfCLVFuKDYxaNUG1iyQomR7kMZUuzi","state":"initialized","tokenAmount":{"amount":"92000000000","decimals":9,"uiAmount":92.0,"uiAmountString":"92"}},"type":"account"},"space":165},"owner":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","executable":false,"rentEpoch":0,"space":165}},"subscription":18067841}}

可以看到当前余额为92了。我们在用”balance”确认下

1
2
spl-token balance 7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9
92

03. 与Solana合约交互

3.1 如何使用Solana的Web3.js

对于 ETH 的 web3.js 是一个JavaScript API库,主要提供了通过 JavaScript 与 ETH 上合约进行交互。

而 Solana 也提供了与 Solana 的 JSON RPC 接口交互的 solana-web3.js 。通过这个库,开发人员可以实现 在 dapp 中用 JavaScritp 和 Solana 上的 区块链(智能合约) 进行交互。Web3.js 库主要分为三部分:

  • RPC 访问
  • keypair 管理
  • 交易发送
1
2
sudo npm i
npm run start

1. 安装Web3.js

首先,你需要在你的项目中安装Web3.js库。你可以使用npm进行安装:

1
npm install @solana/web3.js

2. Connection

这个js库给我们提供了哪些内容呢?

安装solana/web3.js后,可以使用它来连接到Solana的RPC服务:创建一个新的Connection对象,然后用它来调用RPC方法。

1
2
let url = 'https://api.devnet.solana.com';
rpcConnection = new Connection(url);

通过指定 RPC 的地址这样来创建。这个对象包含了所有的 RPC 方法:

所有的 RPC 方法

可以查看每个方法的文档,来查看使用方法。这里举几个简单的例子。比如获取当前区块高度。

1
2
let latestBlockhash = await this.connection.getLatestBlockhash('finalized');
console.log(" ✅ - Fetched latest blockhash. Last Valid Height:", latestBlockhash.lastValidBlockHeight);

这里指定了区块状态为 finalized,在 console 中可以看到:

1
✅ - Fetched latest blockhash. Last Valid Height: 175332530

3. 账号

你可以使用solana-web3.js来创建一个新的账户。早期版本的web3.js提供了一个Account对象,但现在已经被Keypair对象所取代。

  1. 钱包本质上就是一个Keypair,一个私钥和公钥的组合,因此 keypair 可以用一段私钥来进行初始化:

    1
    constructor(keypair?: Ed25519Keypair) // 私钥初始化 keypair
  2. 通过命令行创建了私钥后,在文件”~/.config/solana/id.json”中,没有加密的情况下可以直接取出来

    1
    2
    3
    let secretKey = Uint8Array.from(JSON.parse('[24,xxx,119]')); // demo 里把 id.json 里用数组表示的私钥来转换成二进制
    const keypair = Keypair.fromSecretKey(secretKey);
    console.log("address:", keypair.publicKey.toString())
  3. 私钥 -> 地址,可以看到:

    1
    address: 5pWae6RxD3zrYzBmPTMYo1LZ5vef3vfWH6iV3s8n6ZRG

    和我们命令行中的地址是一样的。这里 publicKey 就是对应的账号地址,keypair 就是 Signer。

4. 发送交易

在介绍 Solana 核心概念的时候,我们有介绍到 Instruction 和 Transaction 以及 Message。所以发送交易,就是构建 Instructions 数组,然后构造 Message,再放到 Transaction 里面,做签名并进行发送。

如果是普通应用合约,需要自己封装 Instruction。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Transaction Instruction class
*/
export class TransactionInstruction {
/**
* Public keys to include in this transaction
* Boolean represents whether this pubkey needs to sign the transaction
*/
keys: Array<AccountMeta>;
/**
* Program Id to execute
*/
programId: PublicKey;
/**
* Program input
*/
data: Buffer;
constructor(opts: TransactionInstructionCtorFields);
}

其中 programId 表示调用合约的地址。key 是合约中需要使用到的 Account, data 则是所有的输入序列化后的二进制。

因为合约的入口是:

1
2
3
4
5
6
7
8
declare_process_instruction!(
process_instruction,
DEFAULT_COMPUTE_UNITS,
|invoke_context| {
let transaction_context = &invoke_context.transaction_context;
let instruction_context = transaction_context.get_current_instruction_context()?;
let instruction_data = instruction_context.get_instruction_data();
let instruction = limited_deserialize(instruction_data)?;

可以简化为:

1
2
3
4
5
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {

具体的方法是从 data 里面解析出来,然后再解析出来参数。

而 Solana 的系统合约,或者说 Native Program。 Web3.js 已经为我们封装好了一些 Instruction。比如转账:

1
2
3
4
5
SystemProgram.transfer({
fromPubkey:new PublicKey(this.state.publicKey), //this.publicKey,
toPubkey: new PublicKey(this.state.toPublicKey),//destination,
lamports: this.state.toCount,//amount,
})

这里表示从 fromPubkey 地址转 lamports 的 SOL 到 toPubkey 的地址。他实际上会调用”11111111111111111111111111111111”合约的 transfer方法。该方法接受三个参数,其中 fromPubkey 需要是签名对象。

1
2
3
4
5
6
7
8
fn transfer(
from_account_index: IndexOfAccount,
to_account_index: IndexOfAccount,
lamports: u64,
invoke_context: &InvokeContext,
transaction_context: &TransactionContext,
instruction_context: &InstructionContext,
) -> Result<(), InstructionError> {

因为转账只需要用到一个 Instruction,所以用这个 Instrcuton 构造 Message:

1
2
3
4
5
const messageV0 = new TransactionMessage({
payerKey: this.keypair.publicKey,
recentBlockhash: latestBlockhash.blockhash,
instructions: txInstructions
}).compileToV0Message();

这里 instructions 是 Array一个数组。

payerKey 则是发送这个消息的 gas 付费者,其也需要提供签名。 recentBlockhash 通过我们前面的 RPC 可以获取到。这里 recentBlockhash 不能隔的太远。这样就限制了消息的签名时间。最后调用 compileToV0Message 构造 Message 对象。

有了 Message,还有构造 VersionedTransaction, 早期的 Transaction 已经废弃。

1
2
3
4
5
6
7
8
9
10
export class VersionedTransaction {
signatures: Array<Uint8Array>;
message: VersionedMessage;
get version(): TransactionVersion;
constructor(message: VersionedMessage, signatures?: Array<Uint8Array>);
serialize(): Uint8Array;
static deserialize(serializedTransaction: Uint8Array): VersionedTransaction;
sign(signers: Array<Signer>): void;
addSignature(publicKey: PublicKey, signature: Uint8Array): void;
}

新的 VersionedTransaction 对象,通过传入 VersionedMessage 来构造:

1
constructor(message: VersionedMessage, signatures?: Array<Uint8Array>);

这里我们上面构造的 V0 就是 VersionedMessage 的对象。

这里可以传入 signatures,比如通过硬件钱包签名的内容。或者不传入也可以,调用:

1
sign(signers: Array<Signer>): void;

传入我们上面的 keypair。也可以对 VersionedTransaction 进行签名。

构造结束后,通过 connection 的 sendTransaction 方法发送即可:

1
sendTransaction(transaction: VersionedTransaction, options?: SendOptions): Promise<TransactionSignature>;

这里返回的 TransactionSignature 即为,交易的 hash,可以通过浏览器进行查询。

3.2 通过 WalletAdatper 与钱包交互

为了给 DApp 提供一套统一的兼容钱包的接口。Solana 设计了一套 Wallet Adapter。 Solana 要求,钱包方需要按照该套接口设计,提供实现。这样 DApp 使用方,只需要按照一套接口,就可以轻松支持多个钱包。接口包含了

  • 网络选择;
  • 账号选择;
  • 账号签名等

除了统一的接口,Adapter 还设计了一套基础 UI,其包括了弹出钱包的选择列表,以及链接钱包后的的账号地址显示。

安装

在你的工程总安装 Wallet_Adapter 依赖:

1
2
3
4
5
6
npm install --save \
@solana/wallet-adapter-base \
@solana/wallet-adapter-react \
@solana/wallet-adapter-react-ui \
@solana/wallet-adapter-wallets \
@solana/web3.js

这里我们还会用到一些 web3.js 里面的变量,所以也将其 install 上。

在使用地方先 import 相关 SDK

1
2
3
4
5
6
7
8
9
import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
import { WalletAdapterNetwork } from '@solana/wallet-adapter-base';

import {
WalletModalProvider,
WalletDisconnectButton,
WalletMultiButton
} from '@solana/wallet-adapter-react-ui';
import { clusterApiUrl } from '@solana/web3.js';

这里因为我们的示例 demo 是 react 的,所以使用了 react-ui,Wallet-adapter 同时也提供了 Material UI 和 Ant Design 的组件。

已经实现了 Adapter 的钱包参见列表

这里我们使用:

1
2
import {SolongWalletAdapter} from '@solana/wallet-adapter-solong'
import {PhantomWalletAdapter} from '@solana/wallet-adapter-phantom';

链接钱包

链接钱包的步骤,是在用户界面设置一个”Connect”的按钮,当点击时,弹出一个钱包选择 list 界面。可使用钱包,通过数组参数参数。

1
2
3
4
5
6
7
8
9
this.network = WalletAdapterNetwork.Testnet;

// You can also provide a custom RPC endpoint.
this.endpoint = clusterApiUrl(this.network);

this.wallets =[
new SolongWalletAdapter(),
new PhantomWalletAdapter(),
];

然后再弹出 UI 将钱包罗列出来

1
2
3
4
5
6
7
8
<ConnectionProvider endpoint={endpoint}>
<WalletProvider wallets={wallets} autoConnect>
<WalletModalProvider>
<WalletMultiButton />
<WalletDisconnectButton />
</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>

这里主要使用了 ConnectionProvider 来指定相应的网络。endpoint 参数为使用的 RPC 环境。通过 WalletProvider 来选择实现了 Adapter 的插件钱包,示例中我们设置了 Phantom。

最后在 WalletModalProvider 通过相应的按钮触发对钱包的选择。也就是我们上面传递的 Solong 和 Phantom。

当用户点击 WalletMultiButton 的时候,会自动弹出钱包选择界面。选择钱包后,会弹出钱包的链接界面。当用户点击链接后,这里的 ModalProvider 会得到选择的账号的信息,并将地址显示在按钮上。

当点击 WalletDisconnectButton 后,会断开链接。

发送请求

前面介绍了 web3.js 的使用,在发送请求的时候,我们需要用账号的私钥对交易内容做签名。那么在使用钱包的情况下该如何操作呢?

首先 import 相关库

1
2
3
4
import { WalletNotConnectedError } from '@solana/wallet-adapter-base';
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import { Keypair, SystemProgram, Transaction } from '@solana/web3.js';
import React, { FC, useCallback } from 'react';

然后先取出链接和公钥:

1
2
const { connection } = useConnection();
const { publicKey, sendTransaction } = useWallet();

这里通过 useConnection 可以得到我们前面钱包里面选择的 RPC 链接,useWallet 返回的结果为选中的钱包的地址,以及使用该钱包发送交易的方法。

1
2
3
4
5
6
const {
context: { slot: minContextSlot },
value: { blockhash, lastValidBlockHeight }
} = await connection.getLatestBlockhashAndContext();

const signature = await sendTransaction(transaction, connection, { minContextSlot });

通过 connection.getLatestBlockhashAndContext 可以得到 minContextSlot 信息,然后再调用 sendTransaction 方法,就可以出发钱包弹出 UI,并提示用户确认,当用户点击确认后,既完成请求的发送。

切换账号

如果用户需要切换账号,那么通过 UI 提供的 Disconnect 入口,先取消当前账号的链接。然后再通过链接界面,选择其他的钱包账号。所以切换账号就是先断开,再重新链接的过程。

取消链接,只需要删除当前记录的用户即刻。

而切换账号则可以直接在此使用”连接” 的流程。

3.3 合约调用 – 真实的合约交互

在前面的例子中,我们通过 web3.js 提供的 SystemProgram 来帮助我们实现了转账的功能。

但是对于一个陌生的合约,我们要怎么来发起调用请求呢?

合约的入口

这里我们以 SPL Token 合约来举例。SPL Token 合约类似 web3.js 一样,其实已经封装好了一套 JS 库给我们来直接使用。这里我们不使用库,而以一个前端的身份,来看这样的一个合约,我们要怎么来交互。

我们以 transfer 函数作为例子。

首先要理解合约的作用和参数,这个可以跟合约开发去沟通。比如我们从注释了解到 transfer 为

3.4 课后练习

实现一个 DApp 页面,实现代币创建,并按白名单发送空投

提示:

  1. 通过与 Token 合约交互,创建代币
  2. 通过与 Token 合约交互,给白名单中的地址,发送 SPL Token 代币
  3. 建议使用 SPL-Token 提供的库来构建 instruction

1. 创建代币

首先要构造一个 MintAccount:

1
mintKeypair = Keypair.generate();

然后创建一个创建这个 MintAccount 的指令,我们借助系统指令库

1
2
3
4
5
6
7
SystemProgram.createAccount({
fromPubkey: publicKey,
newAccountPubkey: mintKeypair.publicKey,
space: MINT_SIZE,
lamports:lamports,
programId: TOKEN_PROGRAM_ID,
}),

这里 from 是付费人,new 是要创建的地址,space 是 data 的大小,因为我们要放 MintAccount 其为 spl-token 库里的定义

lamports 我们通过 库提供的const lamports = await getMinimumBalanceForRentExemptMint(connection);来获得一个 Account 对应这个大小的存储空间的最小的 rent 花费。

接着创建 创建 token 的指令:

1
2
3
4
5
createInitializeMint2Instruction(mintKeypair.publicKey,
9,
publicKey,
publicKey,
TOKEN_PROGRAM_ID)

这里依次是创建的 MintAccount,精度,mintAuthority,freezeAuthority 以及 Token 合约地址。

最后就是按照我们前面课程中的方式构造交易并发送。

这里要注意,因为我们创建了新的 MintAccount,其内容修改需要签名,因此在发送交易的时候,带上:

1
2
3
4
const signature = await sendTransaction(trx, connection, {
minContextSlot,
signers:[mintKeypair],
});

2. Mint Token

要 mint token,先要生成 ata 账号地址,spl token 的库里面有函数。

Copy

1
2
3
4
5
6
7
ataAccount = await getAssociatedTokenAddressSync(
mintKeypair.publicKey,
owner,
false,
TOKEN_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
);

依次传入 MintAccount,为谁创建,后三个参数固定这样传即可。

然后判断这个 account 是否存在,可以用我们前面 rpc 的 getaccount,这里库也封装了函数

1
await getAccount(connection, ataAccount);

没有的时候,会抛异常,我们要创建这个 ATA 账号

1
2
3
4
5
6
7
8
9
10
txInstructions.push(
createAssociatedTokenAccountInstruction(
publicKey,
ataAccount,
owner,
mintKeypair.publicKey,
TOKEN_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
)
);

调用库里的 createAssociatedTokenAccountInstruction 创建指令,依次传入付费的人,上面的 ata 账号,为谁创建以及 MintAccount。

如果存在则跳过

最后执行 Mint:

1
2
3
4
5
6
7
8
txInstructions.push(
createMintToInstruction(
mintKeypair.publicKey,
ataAccount,
publicKey,
BigInt(toCount)
)
);

调用库函数 createMintToInstruction 传入 MintAccount,ata 账号以及 MintAuthority,因为只有他才有权限 mint,和数量。

然后就是跟前面一样,构造交易并发送。注意这次没有额外的 singer 要求了。


Solana 开发者培训营
http://example.com/2023/08/11/Solana/
Author
Rocky CHEN
Posted on
August 11, 2023
Licensed under