Solana最佳实践
# 区块链概念
# Coin (原生货币) 与 Token (代币)
- 所有区块链协议都有代表价值且可交易的 Coin。在 Solana 上,这种 Coin 被称为 SOL。
- Token 是由各个项目在基础区块链 (base blockchain) 上构建的协议。所有区块链项目都是 “代币化的”。
# smart contract - 智能合约
智能合约是写入区块链代码并在满足条件时自动执行的合约。Solana 和某些其他协议可以做到这一点,但其他一些区块链(尤其是比特币)却无法做到。
# dApp
dApp 是一种去中心化的应用程序 (decentralized application) ,或者说是使用智能合约的应用。任何加密程序 —— 例如市场、区块链游戏或用于 DAO 治理的工具 —— 都属于 dApp。
# Web3
- Web 1.0 是静态网站和超链接 (如 AOL 或 GeoCities)
- Web 2 是社交媒体(如 Twitter、Facebook 和 等等)
- Web 3 是围绕去中心化的思想构建的
# NFT
NFT 的全称是 “非同质化代币”。
# Solana 概念
# 特点
- 非常快的确认时间:一个交易只需 400 毫秒就可以被整个网络验证,远快于其他区块链。
- 低交易费用
# Solana Accounts - Solana 账户
# Sending Transactions - 发送交易
# Program Derived Addresses (PDAs) - 程序派生地址
# Cross-Program Invocations (CPIs) - 跨程序调用
# Solana Playground (Solpg) - 开发环境
无需安装任何东西,只需在浏览器访问 https://beta.solpg.io/ (opens new window),即可快速开发、部署和测试 Solana 程序。
# 连接 Playground
点击屏幕左下方的 Not connected
按钮。
# 创建或导入钱包密钥
- 首次使用,点击
Save keypair
保存新创建的钱包 - 下次时候时,可点击
import keypair
导入此前创建的钱包
操作后,窗口底部可看到钱包地址、SOL 余额和连接的集群(默认为 devnet)。
- 钱包地址 (wallet address) :通常为 Ed25519 密钥对的 32 字节公钥 显示为 base-58 编码的字符串(例如,
7MNj7pL1y7XpPnN7ZeuaE4ctwg3WeufbX5o85sA91J1
)。相应的私钥会从此地址签署交易。在 Solana 上,地址是用户钱包、程序(智能合约)或网络上任何其他账户的唯一标识符。 - 公共集群 (Common clusters):
devnet
:用于开发人员实验的开发网络testnet
:为验证器测试保留的网络(请勿用作应用程序开发人员)mainnet-beta
:用于实时交易的主要 Solana 网络
# 获取 Devnet SOL
在开始开发之前,需要获取一些 devnet SOL
。
# 主要用途
- 创建新帐户以在网络上存储数据或部署程序
- 与 Solana 网络交互时支付交易费
# 获取方法
方法一:使用 Playground 终端
# 给自己空投5个sol
solana airdrop 5
方法二:使用 Devnet Faucet
- 访问 https://faucet.solana.com/ (opens new window)
- 输入 Playground 屏幕下方显示的钱包地址,填写金额
- 点击 “Confirm Airdrop” 即可收到 devnet SOL
# 网络接口
在 Solana 上,所有数据都存在于 “account” 中。可理解为所有数据都存储在一个名为 account
的 Map, key
为钱包地址, value
为账号类型。
pub struct Account {
/// lamports in the account
pub lamports: u64,
/// data held in this account
#[cfg_attr(feature = "serde", 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,
}
Solana 帐户包含以下任一内容:
- 状态:用于读写数据。这包括有关令牌、用户数据或程序中定义的其他数据的信息。
- 可执行程序:包含 Solana 程序实际代码的账户。这些账户存储网络执行的指令。
# 获取账户 - 钱包信息
钱包为 系统程序 (opens new window) ,Solana 的原生程序之一
点击此链接 (opens new window)在 Solana Playground 中打开示例
// 获取你的 Playground 钱包地址
const address = pg.wallet.publicKey;
// 获取该地址的账户的 `AccountInfo`
const accountInfo = await pg.connection.getAccountInfo(address);
console.log(JSON.stringify(accountInfo, null, 2));
- 在 Playground 终端中,输入
run
命令并按回车键,运行代码,得到以下输出
Running client...
client.ts:
{
"data": {
"type": "Buffer",
"data": []
},
"executable": false,
"lamports": 16000000000,
"owner": "11111111111111111111111111111111",
"rentEpoch": 18446744073709552000,
"space": 0
}
data
- 包含账户 “数据” 的字段。对于钱包来说,此字段为空(0 字节),但其他账户使用此字段以序列化字节缓冲区的形式存储任意数据。executable
- 指示帐户是否可作为可执行程序运行的标志。对于钱包和存储状态的帐户,此标志显示false
。owner
- 显示哪个程序控制该帐户的字段。对于钱包, 系统程序所有者地址为11111111111111111111111111111111
.lamports
- 账户中的 lampors 余额(1 SOL = 1,000,000,000 lampors)。rentEpoch
与 Solana 已弃用的租金收取机制相关的遗留字段(目前未使用)。space
-data
字段(不是Account
类型中的字段)的字节容量(长度)
# 获取账户 - Token Extensions Program (代币扩展程序)
点击此链接 (opens new window)在 Solana Playground 中打开示例。您将看到以下代码:
import { PublicKey } from "@solana/web3.js";
const address = new PublicKey("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb");
const accountInfo = await pg.connection.getAccountInfo(address);
console.log(JSON.stringify(accountInfo, null, 2));
run
输出:
Running client...
client.ts:
{
"data": {
"type": "Buffer",
"data": [
2,
0,
//... additional bytes
86,
51
]
},
"executable": true,
"lamports": 1141440,
"owner": "BPFLoaderUpgradeab1e11111111111111111111111",
"rentEpoch": 18446744073709552000,
"space": 36
}
代币扩展程序
作为可执行程序账户运行,保持相同的 Account
结构。
executable
- 设置为true
,表示可执行程序帐户。data
- 包含序列化数据(不同于钱包账户中的空数据)。程序账户的数据保存的是另一个账户(程序可执行数据账户)的地址,该账户包含程序的字节码。owner
- 可升级伯克利数据包过滤器 (BPF) 加载程序 (BPFLoaderUpgradeab1e11111111111111111111111
) 拥有此帐户,其功能相当于管理可执行帐户的本机程序。
可以检查 Solana Explorer 中的 代币扩展程序帐户 (opens new window) 及其相应的 程序可执行数据帐户 (opens new window) 。
# 获取账户 - Mint 账户
点击此链接 (opens new window)在 Solana Playground 中打开示例。您将看到以下代码:
import { PublicKey } from "@solana/web3.js";
const address = new PublicKey("C33qt1dZGZSsqTrHdtLKXPZNoxs6U1ZBfyDkzmj6mXeR");
const accountInfo = await pg.connection.getAccountInfo(address);
console.log(JSON.stringify(accountInfo, null, 2));
在此示例中,代码获取 devnet 上现有 Mint 帐户的地址。
run
输出:
Running client...
client.ts:
{
"data": {
"type": "Buffer",
"data": [
1,
0,
//... additional bytes
0,
0
]
},
"executable": false,
"lamports": 4176000,
"owner": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
"rentEpoch": 18446744073709552000,
"space": 430
}
owner
- 代币扩展程序 (TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
) 拥有 mint 账户。executable
- 设置为false
,因为此帐户存储状态而不是可执行代码。data
:包含有关代币的序列化数据(铸造权限、供应、小数等)。
反序列化 data
字段数据
import { PublicKey } from "@solana/web3.js";
import { getMint, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token";
const address = new PublicKey("C33qt1dZGZSsqTrHdtLKXPZNoxs6U1ZBfyDkzmj6mXeR");
const mintData = await getMint(
pg.connection,
address,
"confirmed",
TOKEN_2022_PROGRAM_ID,
);
console.log(
JSON.stringify(
mintData,
(key, value) => {
// Convert BigInt to String
if (typeof value === "bigint") {
return value.toString();
}
// Handle Buffer objects
if (Buffer.isBuffer(value)) {
return `<Buffer ${value.toString("hex")}>`;
}
return value;
},
2,
),
);
- 使用 @solana/spl-token
库的 getMint
方法将 Mint 帐户的 data 反序列回 Mint 类型。
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>,
}
address
- Mint 账户的地址mintAuthority
- 允许铸造新代币的权限supply
—— 代币总供应量decimals
- 代币的小数位数isInitialized
- 程序是否初始化了 Mint 数据freezeAuthority
- 允许冻结代币账户的权限tlvData
- 令牌扩展的额外数据(需要进一步反序列化)
# 转账 SOL
import {
LAMPORTS_PER_SOL,
SystemProgram,
Transaction,
sendAndConfirmTransaction,
Keypair,
} from "@solana/web3.js";
// 发送方钱包
const sender = pg.wallet.keypair;
// 新建一个钱包,作为接收方
const receiver = new Keypair();
// 构造一个转账指令来转移 0.01 SOL;即RequestBody
const transferInstruction = SystemProgram.transfer({
fromPubkey: sender.publicKey,
toPubkey: receiver.publicKey,
lamports: 0.01 * LAMPORTS_PER_SOL,
});
// 构建包含转账指令的交易
const transaction = new Transaction().add(transferInstruction);
// 发送并确认交易
const transactionSignature = await sendAndConfirmTransaction(
pg.connection,
transaction,
[sender],
);
// 在 Playground 终端中打印出 SolanaFM 浏览器的链接,以查看交易详情
console.log(
"Transaction Signature:",
`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`,
);
输出:
Transaction Signature: https://solana.fm/tx/he9dBwrEPhrfrx2BaX4cUmUbY22DEyqZ837zrGrFRnYEBmKhCb5SvoaUeRKSeLFXiGxC8hFY5eDbHqSJ7NYYo42?cluster=devnet-solana
# 创建 Token
import {
Connection,
Keypair,
SystemProgram,
Transaction,
clusterApiUrl,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
MINT_SIZE,
TOKEN_2022_PROGRAM_ID,
createInitializeMint2Instruction,
getMinimumBalanceForRentExemptMint,
} from "@solana/spl-token";
// 获取你的 Playground 钱包并创建与 Solana 开发网络的连接
const wallet = pg.wallet;
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
// 生成一个新的密钥对作为 Mint 账户的地址
const mint = new Keypair();
// 计算 Mint 账户所需的 Lamport
const rentLamports = await getMinimumBalanceForRentExemptMint(connection);
// 创建新帐户的指令与新的Mint帐户的空间
const createAccountInstruction = SystemProgram.createAccount({
fromPubkey: wallet.publicKey, // 从钱包中转移 Lamport 资金到新账户
newAccountPubkey: mint.publicKey,
space: MINT_SIZE, // 分配存储铸币数据所需的空间
lamports: rentLamports,
programId: TOKEN_2022_PROGRAM_ID, // 将帐户所有权分配给代币扩展程序 `TOKEN_2022_PROGRAM_ID`
});
// 构建代币扩展程序指令来初始化 Mint 账户数据
const initializeMintInstruction = createInitializeMint2Instruction(
mint.publicKey,
2, // decimals
wallet.publicKey, // mint authority
wallet.publicKey, // freeze authority
TOKEN_2022_PROGRAM_ID,
);
// 将两条指令添加到单个交易中
const transaction = new Transaction().add(
createAccountInstruction,
initializeMintInstruction,
);
// 发送并确认交易,需要传入2个钱包密钥对
const transactionSignature = await sendAndConfirmTransaction(
connection,
transaction,
[
wallet.keypair, // 用于支付账户创建和交易费用的钱包密钥对
mint, // 使用公钥作为账户地址来创建新账户的 mint 密钥对
],
);
console.log(
"\nTransaction Signature:",
`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`,
);
console.log(
"\nMint Account:",
`https://solana.fm/address/${mint.publicKey}?cluster=devnet-solana`,
);
输出
Transaction Signature: https://solana.fm/tx/3BEjFxqyGwHXWSrEBnc7vTSaXUGDJFY1Zr6L9iwLrjH8KBZdJSucoMrFUEJgWrWVRYzrFvbjX8TmxKUV88oKr86g?cluster=devnet-solana
Mint Account: https://solana.fm/address/CoZ3Nz488rmATDhy1hPk5fvwSZaipCngvf8rYBYVc4jN?cluster=devnet-solana