solanaNotes 账户 账户有几种:
program:存储可执行代码,info无状态
data:由program创建,用英语存储和管理程序状态
native program: solana内置的程序
Sysvar: 存储network cluster state的特殊账户
每个账户有一个唯一的地址,以Ed25519格式表示为32字节
1.accountInfo 账户最大为10mb,存储的数据结构称为accountInfo
account
data
executable
lamports
owner
type
Bytes
Boolean
Number
Program Address
usage
存储账户状态
标志着该账户是否是程序
账户余额的数字形式
拥有该账户的程序的公钥
2.native program 常见的有the System Program和BPF loader
3.System program 所有新建账户默认归system program所有
系统程序执行以下关键任务:
New Account Creation
Space Allocation
Assign Program Ownership
钱包只是系统程序拥有的账户。只有系统程序拥有的账户才可以作为交易费用的支付者
4.BPFLoader BPF 加载程序 是指定为网络上所有其他程序(不包括本机程序)的“所有者”的程序。它负责部署、升级和执行自定义程序。
5.Sysvar Accounts Sysvar 帐户是位于预定义地址的特殊帐户,可提供对集群状态数据的访问。这些帐户会使用有关网络集群的数据动态更新。
6.Custom Programs 智能合约被称为程序Program,指包含可执行代码的账户,且拥有executable=true
创建program account时会创建三个独立账户:
program account:代表链上程序的主账户。该帐户存储可执行数据帐户(2)的地址(存储已编译的程序代码)和程序的更新权限(被授权对程序进行更改的地址)
program executable data account:包含程序的可执行二进制代码的账户
buffer account:在主动部署或升级程序时存储字节代码的临时帐户。该过程完成后,数据将传输到程序可执行数据帐户(2),并且缓冲区帐户将关闭。
可近似将program account当作程序自身
7.data account 因为程序账户无法靠自身存储和修改数据,所以它会创建新账户存储修改数据,这些账户称为data account
创建data account有两个步骤:
调用系统程序创建账户,并转移所有权给程序账户
调用已拥有新账户的程序账户,按照程序代码中的定义初始化账户数据
以下是一个程序部署的示例:
和正常交易类似的info
play ground默认的账户输入
一点部署时的流程detail
挺小一示例程序花了我挺多token,但多数都是后面能退回给我的rent。花的最多的是Program Executable Data Account,存储这个地址的program account花的反而很少。这两个都有一个相同的Upgrade Authority,指向fee payer和signer也就是我的账户。
在尝试close了program account后,两个account 剩下的token都退回到我原有账户。此时的token差值才是实际上的损耗
来点anchor 以下是一个anchor框架的案例程序的实践,目的在于使用程序派生地址PDA作为账户地址存储用户信息 。这里也有playground的同款链接https://beta.solpg.io/66734b7bcffcf4b13384d1ad
0.PDA? PDA 是使用用户定义的种子、bump种子和程序 ID 的组合确定性导出的地址。导出地址对应的账户存在与否,由之前是否显式地创建决定。在没有创建时访问PDA所在的账户是无效的。PDA可以被solana程序通过ID签名,从而不需要私钥。当调用了api产生的第一个有效的bump种子称为”canonical bump“(规范凹凸),而官方文档建议在使用PDA时使用规范bump,否则可能访问到空账户或者不属于运行程序的无效账户
PDA的bump值范围为0~255,每次调用api时的bump值创建账户时,给定相同的可选种子和programId
,具有不同值的bump种子仍然可以派生出有效的PDA(可能无效而且不推荐使用)。在anchor框架中,bump值可以由anchor自己推导出有效值,不用自己指定,从而避免了会遇到的问题。
1.Starter Code 项目结构如下
src/lib.rs
tests/anchor.test.ts
use anchor_lang::prelude::*;declare_id!("8KPzbM2Cwn4Yjak7QYAEH9wyoQh86NcBicaLuzPaejdw" ); #[program] pub mod pda { use super::*; pub fn create (_ctx: Context<Create>) -> Result <()> { Ok (()) } pub fn update (_ctx: Context<Update>) -> Result <()> { Ok (()) } pub fn delete (_ctx: Context<Delete>) -> Result <()> { Ok (()) } } #[derive(Accounts)] #[instruction(message: String)] pub struct Create <'info > { #[account(mut)] pub user: Signer<'info >, #[account( init, seeds = [b"message" .user.key().as_ref()] , bump, payer = user, space = 8 + 32 + 4 + message.len () + 1 )] pub message_account: Account<'info ,MessageAccount>, pub system_program: Program<'info ,System>, }; #[derive(Accounts)] pub struct Update {}#[derive(Accounts)] pub struct Delete {}#[account] pub struct MessageAccount { pub user: PubKey, pub message: String , pub bump: u8 , }
这段代码定义了一个message account和创建此message account需要的数据结构
grammer explanation:
#[derive(Accounts)]
用于注释表示指令所需账户列表的结构,这个结构的每一个字段都是一个账户。
结构中的每个帐户(字段)都用帐户类型进行注释(例如Signer<'info>
),并且可以使用约束进一步注释(例如#[account(mut)]
)。账户类型和账户限制用于对传递给指令的账户执行安全检查。
每个字段命名对账户验证没有影响,但是推荐描述性账户名称
对Create struct功能的描述:
定义了create指令所需的账户,比如user: Signer<'info>
代表创建message account,并通过#[account(mut)]
标记为可变的,因为它为新账户付费,而且只能是签名者批准交易,因为lamport将从账户扣除
message_account: Account<'info,MessageAccount>
这个创建的新账户用于存储用户的消息,init约束表示将在指令中创建账户,seeds和bump表明账户的地址是PDA(program derived Address),payer=user指定”给创建新用户”付费的账户,space指定分配给新账户数据字段的字节数
system_program: Program<'info, System>
这个是创建新账户时需要调用的系统账户,init调用系统程序来创建分配有指定space的新账户,并将所有权重新分配给当前程序
#[instruction(message: String)]
使Create能够从create指令访问message参数.
seeds和bump一起使用来指定账户的地址是PDA.seeds的两个参数:b"message"
是第一个种子的硬编码字符串,user.key().as_ref()
引用user的公钥作为第二个种子,bump
告诉anchor自动查找并使用正确的bump种子.最终anchor将使用seeds和bump导出PDA
space
的各参数:
Anchor Account discriminator (identifier): 8 bytes anchor账户鉴别符(标识符):8字节
User Address (Pubkey): 32 bytes 用户地址(公钥):32字节
User Message (String): 4 bytes for length + variable message length 用户消息(字符串):4字节长度+可变消息长度
PDA Bump seed (u8): 1 byte PDA bump种子 (u8):1 字节
再对对应的create函数修改:
pub fn create (ctx: Context<Create>, message: String ) -> Result <()> { msg!("Create Message: {}" , message); let account_data = &mut ctx.accounts.message_account; account_data.user = ctx.accounts.user.key (); account_data.message = message; account_data.bump = ctx.bumps.message_account; Ok (()) }
功能描述:create通过ctx: Context<Create>
获得对Create
结构中指定账户的访问,通过message
获得需要存储的用户信息
函数逻辑:
用msg!()
宏打印消息到logs
初始化账户数据message
,user为用户账户的地址,message为消息,bump用于后续派生PDA
类似的可仿照实现Update
结构:
#[derive(Accounts)] #[instruction(message: String)] pub struct Update <'info > { #[account(mut)] pub user: Signer<'info >, #[account( mut, seeds = [b"message" , user.key().as_ref()] , bump = message_account.bump, realloc = 8 + 32 + 4 + message.len () + 1 , realloc::payer = user, realloc::zero = true , )] pub message_account: Account<'info , MessageAccount>, pub system_program: Program<'info , System>, }
和Create
有所区别的在于,使用了realloc
调整账户的数据大小,比如realloc::payer
可能与存储在message_account上的不同,而bump与存储在message_account上的相同.相同的seeds和bump保证了PDA的不变
接下来实现update函数:
pub fn update (ctx: Context<Update>, message: String ) -> Result <()> { msg!("Update Message: {}" , message); let account_data = &mut ctx.accounts.message_account; account_data.message = message; Ok (()) }
再来实现Delete
结构
#[derive(Accounts)] pub struct Delete <'info > { #[account(mut)] pub user: Signer<'info >, #[account( mut, seeds = [b"message" , user.key().as_ref()] , bump = message_account.bump, close= user, )] pub message_account: Account<'info , MessageAccount>, }
其中close= user
指定账户将被关闭,其lamports将转移给user,对应的user标记为mut.奇怪的是此时没有系统程序的参与
最后实现delete函数:
pub fn delete (_ctx: Context<Delete>) -> Result <()> { msg!("Delete Message" ); Ok (()) }
功能被结构Delete
之中的close实现,这个函数无事可干
此时如果你也是用Playground的在线ide,就可以直接build && deploy
了
2.test import { PublicKey } from "@solana/web3.js" ; describe ("pda" , () => { > to be added firstly it ("Create Message Account" , async () => {}); it ("Update Message Account" , async () => {}); it ("Delete Message Account" , async () => {}); });
这是test最开始的样子,向第一个it前添加以下code
const program = pg.program ;const wallet = pg.wallet ; const [messagePda, messageBump] = PublicKey .findProgramAddressSync ( [Buffer .from ("message" ), wallet.publicKey .toBuffer ()], program.programId , );
program行允许访问客户端库,wallet行是我的plaground 钱包.其余的演示如何用程序中指定的种子导出pda
接下来写create的test部分:
it ("Create Message Account" , async () => { const message = "Hello, World!" ; const transactionSignature = await program.methods .create (message) .accounts ({ messageAccount : messagePda, }) .rpc ({ commitment : "confirmed" }); const messageAccount = await program.account .messageAccount .fetch ( messagePda, "confirmed" , ); console .log (JSON .stringify (messageAccount, null , 2 )); console .log ( "Transaction Signature:" , `https://solana.fm/tx/${transactionSignature} ?cluster=devnet-solana` , ); });
签名,获取messageAccount.输出日志
继续update部分:
it ("Update Message Account" , async () => { const message = "Hello, Solana!" ; const transactionSignature = await program.methods .update (message) .accounts ({ messageAccount : messagePda, }) .rpc ({ commitment : "confirmed" }); const messageAccount = await program.account .messageAccount .fetch ( messagePda, "confirmed" , ); console .log (JSON .stringify (messageAccount, null , 2 )); console .log ( "Transaction Signature:" , `https://solana.fm/tx/${transactionSignature} ?cluster=devnet-solana` , ); });
还有delete:
it ("Delete Message Account" , async () => { const transactionSignature = await program.methods .delete () .accounts ({ messageAccount : messagePda, }) .rpc ({ commitment : "confirmed" }); const messageAccount = await program.account .messageAccount .fetchNullable ( messagePda, "confirmed" , ); console .log ("Expect Null:" , JSON .stringify (messageAccount, null , 2 )); console .log ( "Transaction Signature:" , `https://solana.fm/tx/${transactionSignature} ?cluster=devnet-solana` , ); });
写完了就test看一眼吧
3.Cross Program Invocation(CPI) 前两节的程序将在这里开始得到修改,用CPI实现从这个程序与其他程序的交互。
CPI:一个程序调用另一个程序的指令,另一个程序的指令类似于向互联网公开的API。
发起CPI时,在A上的签名者权限会扩展到被调用的B上。这样的迭代调用深度最高为4,程序可以代表从其 ID 派生的PDA进行“签名”。
在solana_program crate中的invoke
和invoke_signed
各自对应没有PDA签名和有PDA签名的调用。
用CPI实现transfer的案例:
use anchor_lang::prelude::*;use anchor_lang::solana_program::{program::invoke, system_instruction};declare_id!("55xRZZnhSk1aN6seNTj75mThJEjZkBRYPQJ8qvKVh1eC" ); #[program] pub mod cpi_invoke { use super::*; pub fn sol_transfer (ctx: Context<SolTransfer>, amount: u64 ) -> Result <()> { let from_pubkey = ctx.accounts.sender.to_account_info (); let to_pubkey = ctx.accounts.recipient.to_account_info (); let program_id = ctx.accounts.system_program.to_account_info (); let instruction = &system_instruction::transfer (&from_pubkey.key (), &to_pubkey.key (), amount); invoke (instruction, &[from_pubkey, to_pubkey, program_id])?; Ok (()) } } #[derive(Accounts)] pub struct SolTransfer <'info > { #[account(mut)] sender: Signer<'info >, #[account(mut)] recipient: SystemAccount<'info >, system_program: Program<'info , System>, }
可以看到,invoke时需要传入的参数有instruction和一个切片
实际上调用的是system的transfer,transfer需要from和to两个公钥,所以传的切片里有这三个地址
再看一个invoke_signed
的例子:
use anchor_lang::prelude::*;use anchor_lang::solana_program::{program::invoke_signed, system_instruction};declare_id!("EyxvVL2akUZZHx4DXzYzCroKLmigPrS2WgpSetKzM9wh" ); #[program] pub mod cpi_invoke_signed { use super::*; pub fn sol_transfer (ctx: Context<SolTransfer>, amount: u64 ) -> Result <()> { let from_pubkey = ctx.accounts.pda_account.to_account_info (); let to_pubkey = ctx.accounts.recipient.to_account_info (); let program_id = ctx.accounts.system_program.to_account_info (); let seed = to_pubkey.key (); let bump_seed = ctx.bumps.pda_account; let signer_seeds : &[&[&[u8 ]]] = &[&[b"pda" , seed.as_ref (), &[bump_seed]]]; let instruction = &system_instruction::transfer (&from_pubkey.key (), &to_pubkey.key (), amount); invoke_signed ( instruction, &[from_pubkey, to_pubkey, program_id], signer_seeds, )?; Ok (()) } } #[derive(Accounts)] pub struct SolTransfer <'info > { #[account( mut, seeds = [b"pda" , recipient.key().as_ref()] , bump, )] pda_account: SystemAccount<'info >, #[account(mut)] recipient: SystemAccount<'info >, system_program: Program<'info , System>, }
和前一个对比:
sender不同,这个是从pda_account开始send sol
recipient对invoke_signed
提供seed,pda提供bump seed并一起签名
4.CPI in anchor 继续更改之前的程序,从Update开始:(这里的修改和Delete的struct相同)
use anchor_lang::system_program::{transfer, Transfer};#[derive(Accounts)] #[instruction(message: String)] pub struct Update <'info > { #[account(mut)] pub user: Signer<'info >, #[account( mut, seeds = [b"vault" , user.key().as_ref()] , bump, )] pub vault_account: SystemAccount<'info >, #[account( mut, seeds = [b"message" , user.key().as_ref()] , bump = message_account.bump, realloc = 8 + 32 + 4 + message.len () + 1 , realloc::payer = user, realloc::zero = true , )] pub message_account: Account<'info , MessageAccount>, pub system_program: Program<'info , System>, }
新增的vault将在用户修改message时接受sol并存储这些token
在update的函数中补全这一功能的实现:
pub fn update (ctx: Context<Update>, message: String ) -> Result <()> { msg!("Update Message: {}" , message); let account_data = &mut ctx.accounts.message_account; account_data.message = message; + let transfer_accounts = Transfer { + from: ctx.accounts.user.to_account_info (), + to: ctx.accounts.vault_account.to_account_info (), + }; + let cpi_context = CpiContext::new ( + ctx.accounts.system_program.to_account_info (), + transfer_accounts, + ); + transfer (cpi_context, 1_000_000 )?; Ok (()) }
可以看到anchor封装了cpi,简洁地实现了transfer
在delete函数里也加上sol的处理
+ pub fn delete (ctx: Context<Delete>) -> Result <()> { msg!("Delete Message" ); + let user_key = ctx.accounts.user.key (); + let signer_seeds : &[&[&[u8 ]]] = + &[&[b"vault" , user_key.as_ref (), &[ctx.bumps.vault_account]]]; + + let transfer_accounts = Transfer { + from: ctx.accounts.vault_account.to_account_info (), + to: ctx.accounts.user.to_account_info (), + }; + let cpi_context = CpiContext::new ( + ctx.accounts.system_program.to_account_info (), + transfer_accounts, + ).with_signer (signer_seeds); + transfer (cpi_context, ctx.accounts.vault_account.lamports ())?; Ok (()) }
这里的cpi加上了with_signer
,也是实现了功能
再rebuild程序并deploy。和etherum不一样的是,新程序可以直接在旧程序的地址上部署,共享相同的id
新的test如下:(不熟ts,直接给)
import { PublicKey } from "@solana/web3.js" ; describe ("pda" , () => { const program = pg.program; const wallet = pg.wallet; const [messagePda, messageBump] = PublicKey.findProgramAddressSync ( [Buffer.from ("message" ), wallet.publicKey.toBuffer ()], program.programId, ); const [vaultPda, vaultBump] = PublicKey.findProgramAddressSync ( [Buffer.from ("vault" ),wallet.publicKey.toBuffer ()], program.programId, ); it ("Create Message Account" , async () => { const message = "Hello, World!" ; const transactionSignature = await program.methods .create (message) .accounts ({ messageAccount: messagePda, }) .rpc ({ commitment: "confirmed" }); const messageAccount = await program.account.messageAccount.fetch ( messagePda, "confirmed" , ); console.log (JSON.stringify (messageAccount, null, 2 )); console.log ( "Transaction Signature:" , `https: ); }); it ("Update Message Account" , async () => { const message = "Hello, Solana!" ; const transactionSignature = await program.methods .update (message) .accounts ({ messageAccount: messagePda, vaultAccount: vaultPda }) .rpc ({ commitment: "confirmed" }); const messageAccount = await program.account.messageAccount.fetch ( messagePda, "confirmed" , ); console.log (JSON.stringify (messageAccount, null, 2 )); console.log ( "Transaction Signature:" , `https: ); }); it ("Delete Message Account" , async () => { const transactionSignature = await program.methods .delete () .accounts ({ messageAccount: messagePda, vaultAccount: vaultPda }) .rpc ({ commitment: "confirmed" }); const messageAccount = await program.account.messageAccount.fetchNullable ( messagePda, "confirmed" , ); console.log ("Expect Null:" , JSON.stringify (messageAccount, null, 2 )); console.log ( "Transaction Signature:" , `https: ); }); });
就是不知道为啥我只有10sol,在前一个程序部署后剩下6.any,test并再次部署+test后我的sol增加到了11.6···有空一定得溯源看看怎么多出来的