账户
Solana 网络上的所有数据都存储在账户中。您可以将 Solana 网络视为一个包含单一账户表的公共数据库。账户与其地址之间的关系类似于键值对,其中键是地址,值是账户。
三个账户及其地址的示意图,包括账户结构定义。
账户地址
账户地址是一个 32 字节的唯一 ID,用于在 Solana 区块链上定位账户。账户地址通常以 base58 编码字符串的形式显示。大多数账户使用 Ed25519 公钥 作为其地址,但这并不是强制性的,因为 Solana 还支持程序派生地址。
一个账户及其 base58 编码的公钥地址
公钥
以下示例展示了如何使用 Solana SDK 创建一个密钥对。
import { generateKeyPairSigner } from "@solana/kit";// Kit does not enable extractable private keysconst keypairSigner = await generateKeyPairSigner();console.log(keypairSigner);
程序派生地址
程序派生地址(PDA)是使用程序 ID 和一个或多个可选输入(种子)确定性派生的地址。以下示例展示了如何使用 Solana SDK 创建一个程序派生地址。
import { Address, getProgramDerivedAddress } from "@solana/kit";const programAddress = "11111111111111111111111111111111" as Address;const seeds = ["helloWorld"];const [pda, bump] = await getProgramDerivedAddress({programAddress,seeds});console.log(`PDA: ${pda}`);console.log(`Bump: ${bump}`);
账户结构
每个
Account
的最大大小为
10MiB,并包含以下信息:
lamports:账户中的lamports数量data:账户的数据owner:拥有账户的程序的 IDexecutable:指示账户是否包含可执行二进制文件rent_epoch:已弃用的租金周期字段
pub struct Account {/// lamports in the accountpub 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 rentpub rent_epoch: Epoch,}
Lamports
账户的余额以 lamports 为单位。
每个账户必须拥有最低 lamport 余额,称为 rent,以便其数据能够存储在链上。租金与账户的大小成正比。
尽管此余额被称为租金,但它更像是押金,因为当账户关闭时可以全额取回。("租金" 这个名称来源于现已弃用的 rent epoch 字段。)
数据
此字段通常被称为 "账户数据"。此字段中的 data
被视为任意数据,因为它可以包含任何字节序列。每个程序定义存储在此字段中的数据结构。
- 程序账户:此字段包含可执行的程序代码或存储可执行程序代码的 program data account 的地址。
- 数据账户:此字段通常存储状态数据,供读取使用。
从 Solana 账户读取数据涉及两个步骤:
- 使用其 地址 获取账户
- 将账户的
data字段从原始字节反序列化为适当的数据结构,由拥有该账户的程序定义。
所有者
此字段包含账户所有者的程序 ID。
每个 Solana 账户都有一个指定为其所有者的
程序。账户的所有者是唯一可以更改账户的 data
或根据程序指令扣除 lamports 的程序。
(对于程序账户,其所有者是其 loader program。)
可执行
此字段指示账户是 program account 还是 data account。
- 如果
true:该账户是一个 program account - 如果
false:该账户是一个 data account
租金 epoch
rent_epoch 字段已被弃用。
过去,此字段用于跟踪账户何时需要支付租金。然而,这种租金收取机制现已被弃用。
账户类型
账户分为两大类:
- Program accounts:包含可执行代码的账户
- Data accounts:不包含可执行代码的账户
这种区分意味着程序的可执行代码和其状态存储在不同的账户中。(类似于操作系统,通常将程序和其数据分开存储。)
Program accounts
每个程序都由一个 loader program 拥有,用于部署和管理账户。当部署一个新的 program 时,会创建一个账户来存储其 可执行 代码。这被称为 program account。(为了简化理解,可以将 program account 视为程序本身。)
在下图中,您可以看到一个 loader program 被用来部署一个 program account。Program
account 的 data 包含可执行的程序代码。
Program account 的示意图,包括其 4 个组成部分及其 loader program。
Program data accounts
使用 loader-v3 部署的程序不会在其 data 字段中包含程序代码。相反,其 data
指向一个单独的 program data account,该账户包含程序代码。(见下图。)
一个包含数据的 program account。数据指向一个单独的 program data account
下面的示例获取了 Token Program account。请注意, executable 字段被设置为
true,表明该账户是一个程序。
import { Address, createSolanaRpc } from "@solana/kit";const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com");const programId = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" as Address;const accountInfo = await rpc.getAccountInfo(programId, { encoding: "base64" }).send();console.log(accountInfo);
数据账户
数据账户不包含可执行代码。相反,它们存储信息。
程序状态账户
程序使用数据账户来维护其状态。为此,它们必须首先创建一个新的数据账户。创建程序状态账户的过程通常是抽象的,但了解其底层过程是有帮助的。
为了管理其状态,一个新程序必须:
- 调用 System Program 来创建一个账户。(然后 System Program 将所有权转移给新程序。)
- 根据其 指令 初始化账户数据。
一个由 program account 拥有的数据账户的示意图
下面的示例创建并获取了一个由 Token 2022 程序拥有的 Token Mint account。
import {airdropFactory,appendTransactionMessageInstructions,createSolanaRpc,createSolanaRpcSubscriptions,createTransactionMessage,generateKeyPairSigner,getSignatureFromTransaction,lamports,pipe,sendAndConfirmTransactionFactory,setTransactionMessageFeePayerSigner,setTransactionMessageLifetimeUsingBlockhash,signTransactionMessageWithSigners} from "@solana/kit";import { getCreateAccountInstruction } from "@solana-program/system";import {getInitializeMintInstruction,getMintSize,TOKEN_2022_PROGRAM_ADDRESS,fetchMint} from "@solana-program/token-2022";// Create Connection, local validator in this exampleconst rpc = createSolanaRpc("http://localhost:8899");const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");// Generate keypairs for fee payerconst feePayer = await generateKeyPairSigner();// Fund fee payerawait airdropFactory({ rpc, rpcSubscriptions })({recipientAddress: feePayer.address,lamports: lamports(1_000_000_000n),commitment: "confirmed"});// Generate keypair to use as address of mintconst mint = await generateKeyPairSigner();// Get default mint account size (in bytes), no extensions enabledconst space = BigInt(getMintSize());// Get minimum balance for rent exemptionconst rent = await rpc.getMinimumBalanceForRentExemption(space).send();// Instruction to create new account for mint (token 2022 program)// Invokes the system programconst createAccountInstruction = getCreateAccountInstruction({payer: feePayer,newAccount: mint,lamports: rent,space,programAddress: TOKEN_2022_PROGRAM_ADDRESS});// Instruction to initialize mint account data// Invokes the token 2022 programconst initializeMintInstruction = getInitializeMintInstruction({mint: mint.address,decimals: 9,mintAuthority: feePayer.address});const instructions = [createAccountInstruction, initializeMintInstruction];// Get latest blockhash to include in transactionconst { value: latestBlockhash } = await rpc.getLatestBlockhash().send();// Create transaction messageconst transactionMessage = pipe(createTransactionMessage({ version: 0 }), // Create transaction message(tx) => setTransactionMessageFeePayerSigner(feePayer, tx), // Set fee payer(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), // Set transaction blockhash(tx) => appendTransactionMessageInstructions(instructions, tx) // Append instructions);// Sign transaction message with required signers (fee payer and mint keypair)const signedTransaction =await signTransactionMessageWithSigners(transactionMessage);// Send and confirm transactionawait sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedTransaction,{ commitment: "confirmed" });// Get transaction signatureconst transactionSignature = getSignatureFromTransaction(signedTransaction);console.log("Mint Address:", mint.address);console.log("Transaction Signature:", transactionSignature);const accountInfo = await rpc.getAccountInfo(mint.address).send();console.log(accountInfo);const mintAccount = await fetchMint(rpc, mint.address);console.log(mintAccount);
系统账户
并非所有账户在由 System Program 创建后都会被分配一个新所有者。由 System Program 拥有的账户称为系统账户。所有钱包账户都是系统账户,这使它们能够支付交易费用。
一个由 System Program 拥有的钱包,包含 1,000,000 lamports
当 SOL 第一次被发送到一个新地址时,会在该地址创建一个由 System Program 拥有的账户。
在下面的示例中,生成了一个新的 keypair,并使用 SOL 进行了资助。运行代码后,您可以看到账户的地址
owner 是
11111111111111111111111111111111(System Program)。
import {airdropFactory,createSolanaRpc,createSolanaRpcSubscriptions,generateKeyPairSigner,lamports} from "@solana/kit";// Create a connection to Solana clusterconst rpc = createSolanaRpc("http://localhost:8899");const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");// Generate a new keypairconst keypair = await generateKeyPairSigner();console.log(`Public Key: ${keypair.address}`);// Funding an address with SOL automatically creates an accountconst signature = await airdropFactory({ rpc, rpcSubscriptions })({recipientAddress: keypair.address,lamports: lamports(1_000_000_000n),commitment: "confirmed"});const accountInfo = await rpc.getAccountInfo(keypair.address).send();console.log(accountInfo);
Sysvar 账户
Sysvar 账户存在于预定义的地址,并提供对集群状态数据的访问。它们会动态更新网络集群的相关数据。查看完整的 Sysvar 账户列表。
下面的示例从 Sysvar Clock 账户中获取并反序列化数据。
import { createSolanaRpc } from "@solana/kit";import { fetchSysvarClock, SYSVAR_CLOCK_ADDRESS } from "@solana/sysvars";const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com");const accountInfo = await rpc.getAccountInfo(SYSVAR_CLOCK_ADDRESS, { encoding: "base64" }).send();console.log(accountInfo);// Automatically fetch and deserialize the account dataconst clock = await fetchSysvarClock(rpc);console.log(clock);
Is this page helpful?