2
头图

完成品

1.gif

准备钱包(Solflare 或者 Phantom)

  1. 安装
    image.png
    按照步骤新建一个钱包,记下助记词短语,放到一个只有自己知道的地方,OK。


  2. 设置
    打开钱包插件,设置,通用,网络,设置为devnet(开发者网络)
    image.png


  3. 领取测试币
    复制钱包地址
    image.png
    每8小时可领取两次,每次5sol,足够
    image.png
    回到钱包页面查看余额是否更新

开发环境

  1. solana
    提供cli工具
  2. rust
    solana开发语言
  3. anchor
    solana开发框架,也是rust
  4. nextjs
    前端框架,与solana dapp交互

SOLANA部分

  1. create-solana-dapp是一整套solana开发框架,整合了anchor+nextjs

    pnpm dlx create-solana-dapp
  2. solana代码在anchor/programs/xxxx/lib.rs中
  3. 删除lib.rs中所有代码
  4. 先修改Cargo.toml配置,此文件类似前端里的package.json

    [dependencies]
    anchor-lang = { version = "0.30.1", features = ["init-if-needed"] }

    这是为了开启 init-if-needed 特性,后面账户初始化时会用到

  5. 开始逐行写一个区块链上的日记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
    image.png
    solana到这里已经完成,接下来是ui页面和solana程序交互

UI部分

因为create-solana-dapp已经集成了nextjs框架,所以直接在src目录下就可以写web页面,大部分功能已经集成,比如钱包连接,网络切换,页面布局,页面路由,只需要关心自己的页面逻辑即可。

找到src/components/项目名称文件夹里面有三个文件:

  1. data-access.tsx
    定义与solana交互的接口
  2. feature.tsx
    处理钱包链接状态,如果已连接钱包,则加载ui组件,反之加载连接钱包组件
  3. 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

1.gif
每次对solana上的数据状态进行修改时都要支付一些sol,一部分是初始化账户的租金,这部分费用在删除日记时退还给用户。
另一个网络费(gas)中的一半被销毁(确保sol的价值稳定),一半给矿工(激励)
可以打开标题上的pubkey在区块链浏览器上查看信息
image.png
查看最新一条交易信息
image.png
在最后的log信息里可以看到mgs!宏正确的打印了日志信息

参考

How to create a CRUD dApp on Solana

最后特别感谢Copilot,唯一真神


Rule
12 声望6 粉丝

感觉什么都不会