完成品
准备钱包(Solflare 或者 Phantom)
安装
按照步骤新建一个钱包,记下助记词短语,放到一个只有自己知道的地方,OK。设置
打开钱包插件,设置,通用,网络,设置为devnet(开发者网络)- 领取测试币
复制钱包地址
每8小时可领取两次,每次5sol,足够
回到钱包页面查看余额是否更新
开发环境
SOLANA部分
create-solana-dapp是一整套solana开发框架,整合了anchor+nextjs
pnpm dlx create-solana-dapp
- solana代码在anchor/programs/xxxx/lib.rs中
- 删除lib.rs中所有代码
先修改Cargo.toml配置,此文件类似前端里的package.json
[dependencies] anchor-lang = { version = "0.30.1", features = ["init-if-needed"] }
这是为了开启 init-if-needed 特性,后面账户初始化时会用到
开始逐行写一个区块链上的日记dapp
anchor/programs/xxxx/lib.rs// 引入anchor库 use anchor_lang::prelude::*; // 声明次程序的id,作为在solana上的凭证,一般在第一次build后自动生成 declare_id!("");
// 定义此程序的模块,这里面写着指令,也就是操作区块链的具体方法 pub mod solanajournaldapp { use super::*; }
// 先跳出mod,首先需要定义一下日记的数据结构 // 在solana上,一切都是账户 // 账户里存着代码,为可执行账户,比如正在写的这个 // 账户里存着数据,为数据账户,不可执行 // 得益于这种区分,可以轻松的升级程序而不破坏数据 // 还有系统账户,PDA... // 这是一个账户的数据结构,我们用这个账户存储用户日记 // anchor来标记这个struct是一个账户类型 #[account] // 用于自动计算初始化空间 #[derive(InitSpace)] pub struct JournalEntryState { // 表示这个账户的所有者(谁写的,日记就是谁的) pub owner: Pubkey, // 日记标题 限制长度 #[max_len(50)] pub title: String, // 日记内容 限制长度 #[max_len(1000)] pub message: String, // 日记记录时间 pub timestamp: i64, }
// 这是一个发起新增日记操作所包含的账户集合,表示当调用新增时需要哪些账户参与 // anchor声明这是一个账户集合,简化账户验证和操作 #[derive(Accounts)] // 声明调用该struct需要的参数 #[instruction(title:String,message:String)] pub struct CreateEntry<'info> { // 1. 数据账户,用来存储日记 // 因为是新增操作,所以调用的时候此账户还不存在,所以需要init_if_needed来初始化这个账户 // seeds表示初始化这个账户用到的种子,之后也能用到这个种子找到这篇日记(PDA) // bump值用于确保通过种子生成的账户地址是唯一且有效的 // payer表示谁来替这个账户买单,所有账户初始化时需要给solana支付一定的押金(sol)来持久存储数据,owner表示日记的所有者来支付这个押金 // space是这个账户所需要的空间大小,8是固定字节用于存储账户元数据,INIT_SPACE是通过#[derive(InitSpace)]自动计算出来的 #[account(init_if_needed,seeds=[title.as_bytes(),owner.key().as_ref()],bump, payer=owner,space=8+JournalEntryState::INIT_SPACE)] pub journal_entry: Account<'info, JournalEntryState>, // 2. 签名者账户,也就是写日记的人,这个账户是个mut类型,因为owner要支付一定的押金和网络费,自己的数据(余额)会发生变化,所以需要mut #[account(mut)] pub owner: Signer<'info>, // 3. 系统账户,执行基本的区块链操作,例如创建新账户(JournalEntryState)、分配存储空间和转移 SOL 代币 pub system_program: Program<'info, System>, }
// 这是一个发起更新日记操作所包含的账户集合 #[derive(Accounts)] #[instruction(title:String,message:String)] pub struct UpdateEntry<'info> { // 1. 数据账户 // 因为是修改操作,所以需要重新计算分配账户存储空间,标记为mut // 8字节用于账户的固定开销。 // 32字节用于存储公钥。 // 4字节用于存储标题长度。 // title.len()表示标题的实际长度。 // 4字节用于存储消息长度。 // message.len()表示消息的实际长度。 // 8字节用于timestamp长度。 // payer:账户重新分配开销的支付者 // zero:新分配的空间将被初始化为零。这有助于确保新分配的内存不会包含任何旧数据 #[account(mut,seeds=[title.as_bytes(),owner.key().as_ref()],bump, realloc = 8 + 32 + 4 +title.len() + 4 +message.len() + 8, realloc::payer=owner, realloc::zero = true )] pub journal_entry: Account<'info, JournalEntryState>, #[account(mut)] pub owner: Signer<'info>, pub system_program: Program<'info, System>, }
// 这是一个发起删除日记的操作所包含的账户合集 #[derive(Accounts)] #[instruction(title:String)] pub struct DeleteEntry<'info> { // 释放账户空间,标记为mut // 通过种子找到账户PDA // close=owner:当账户关闭时,将租金退给owner #[account(mut,seeds=[title.as_bytes(),owner.key().as_ref()],bump,close=owner)] pub journal_entry: Account<'info, JournalEntryState>, #[account(mut)] pub owner: Signer<'info>, pub system_program: Program<'info, System>, }
// 回看mod pub mod solanajournaldapp { // 用于获取unix时间戳 use anchor_lang::solana_program::clock; use super::*; // 指令(用户交互,ui调用) // 1. 新增指令 // ctx:调用指令的上下文(调用者信息) // title:日记标题 // message:日记内容 // return 调用结果 pub fn create_journal_entry( ctx: Context<CreateEntry>, title: String, message: String, ) -> Result<()> { // msg!宏输出日志记录,在区块链浏览器上可查询到,便于调试,测试 msg!("Creating a new journal entry"); msg!("Title: {}", title); msg!("Message: {}", message); // 从上下文中拿到账户,就是初始化后的JournalEntryState let journal_entry = &mut ctx.accounts.journal_entry; // 赋值 ctx.accounts.owner.key() 调用者的公钥(钱包地址) journal_entry.owner = ctx.accounts.owner.key(); journal_entry.title = title; journal_entry.message = message; journal_entry.timestamp = clock::Clock::get()?.unix_timestamp; // 返回ok Ok(()) } }
// 2. 更新日记指令 pub fn update_journal_entry( ctx: Context<UpdateEntry>, title: String, message: String, ) -> Result<()> { msg!("Updating a journal entry"); msg!("Title: {}", title); msg!("Message: {}", message); let journal_entry = &mut ctx.accounts.journal_entry; journal_entry.message = message; journal_entry.timestamp = clock::Clock::get()?.unix_timestamp; Ok(()) }
// 3. 删除日记指令 pub fn delete_journal_entry(_ctx: Context<DeleteEntry>, title: String) -> Result<()> { msg!("Deleting a journal entry"); msg!("Title: {}", title); Ok(()) }
solana代码到此完成,接下来构建和发布
# 使用solana cli 初始化一个本地钱包 solana-kegen new # 查看当前solana config solana config get # 将RPC切换到开发者网络 solana config set -ud # 空投一些测试sol到钱包里备用(发布solana program需要消耗sol) # 此操作等同上述在线空投领取测试sol solana airdrop 5
// 在anchor/Anchor.toml中修改网络配置 ... [programs.devnet] ... [provider] cluster = "Devnet" ...
# 构建与发布 pnpm anchor build pnpm anchor deploy -- --use-rpc
如果不出意外发布成功后,控制台可看到program id和signature,可以打开区块链浏览器切换到devnet,查询到program id
solana到这里已经完成,接下来是ui页面和solana程序交互
UI部分
因为create-solana-dapp已经集成了nextjs框架,所以直接在src目录下就可以写web页面,大部分功能已经集成,比如钱包连接,网络切换,页面布局,页面路由,只需要关心自己的页面逻辑即可。
找到src/components/项目名称文件夹里面有三个文件:
- data-access.tsx
定义与solana交互的接口 - feature.tsx
处理钱包链接状态,如果已连接钱包,则加载ui组件,反之加载连接钱包组件 - ui.tsx
页面ui逻辑
// data-access.tsx
export function useXXXXXProgram(){
...
// 创建日记
const createEntry = useMutation<string, Error, any>({
mutationKey: ['journalEntry', 'create', { cluster }],
mutationFn: async ({ title, message }) => {
// 与Solana区块链交互的方法,rpc()方法用于发送交易
return program.methods.createJournalEntry(title, message).rpc();
},
onSuccess: (signature) => {
transactionToast(signature);
accounts.refetch();
},
onError: (error) => {
toast.error(`Failed to create journal entry: ${error.message}`);
}
});
...
}
export function useXXXXXProgramAccount({ account }: { account: PublicKey }){
...
// 更新日记
const updateEntry = useMutation<string, Error, any>({
mutationKey: ['journalEntry', 'update', { cluster }],
mutationFn: async ({ title, message }) => {
return program.methods.updateJournalEntry(title, message).rpc();
},
onSuccess: (signature) => {
transactionToast(signature);
accounts.refetch();
},
onError: (error) => {
toast.error(`Failed to update journal entry: ${error.message}`);
}
});
// 删除日记
const deleteEntry = useMutation({
mutationKey: ['journalEntry', 'delete', { cluster, account }],
mutationFn: (title: string) => {
return program.methods.deleteJournalEntry(title).rpc();
},
onSuccess: (tx) => {
transactionToast(tx);
accounts.refetch();
}
});
...
}
最后是ui界面,随便写写,加一点点样式,略过
测试
# 启动nextjs
pnpm dev
每次对solana上的数据状态进行修改时都要支付一些sol,一部分是初始化账户的租金,这部分费用在删除日记时退还给用户。
另一个网络费(gas)中的一半被销毁(确保sol的价值稳定),一半给矿工(激励)
可以打开标题上的pubkey在区块链浏览器上查看信息
查看最新一条交易信息
在最后的log信息里可以看到mgs!宏正确的打印了日志信息
参考
How to create a CRUD dApp on Solana
最后特别感谢Copilot,唯一真神
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。