Solana 开发笔记

Solana 开发笔记

学习完以太坊基础知识和比特币白皮书后接着来学习 Solana,官方文档 https://solana.com/zh/docs?locale=docs

关于 Solana 采用的加密算法,在 Solana 中,采用了了 Ed25519 curve 非对称加密算法,用于生成数字签名和验证数字签名。与对称加密算法不同,非对称加密使用一对密钥:pubkey公钥和secretkey私钥,如果使用公钥加密,则只有对应的私钥能够进行解密;如果使用私钥加密,则可以使用对应的公钥验证签名,即判断该签名是否由私钥的持有者发起。

Solana 使用 Rust 语言进行开发,如果您之前没接触过 Rust 建议花点时间看看相关资料,以下是我整理的一些 Rust 教程:

Introduction

Solana 的高性能网络是它的一个亮点。为了实现高吞吐量,Solana 采用了许多独特的技术,其中最为关键的是它的时间戳技术——Proof of History(PoH)。PoH 是 Solana 的一个创新,它通过将时间序列作为区块链的一部分,允许节点在没有全网同步的情况下验证交易的顺序,从而大大提升了系统的处理效率,Solana 的区块链与其他区块链不同,它采用了”单链架构”,这一点有别于以太坊和比特币的多链架构。Solana 的设计允许它在每个区块内处理大量的交易,不需要进行链间的通信或者合并,这使得 Solana 可以达到每秒数万笔交易的吞吐量,Solana 还支持智能合约和去中心化应用(DApps),但与以太坊相比,Solana 的智能合约运行更加高效且低成本。Solana 的智能合约使用 Rust 或者 C 语言编写,而不是以太坊上的 Solidity 语言,这给开发者带来了一些新的挑战,但也提供了更大的灵活性和更强的性能。

Solana 白皮书:https://solana.com/solana-whitepaper.pdf

关于 Solana 的网络

Solana的网络环境分成开发网、测试网、主网三类,开发网为Solana节点开发使用,更新频繁,测试网主要 给到DApp开发者使用,相对稳定

Develop Environment

首先安装 Rust 环境

1
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y

接着下载 Solana 的 CLI

1
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"

如果下载完成,会提示让你配置环境变量,简单配置下就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
❯ sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
downloading stable installer
✨ stable commit 035c4eb initialized
Adding
export PATH="/Users/icecliffs/.local/share/solana/install/active_release/bin:$PATH" to /Users/icecliffs/.profile
Adding
export PATH="/Users/icecliffs/.local/share/solana/install/active_release/bin:$PATH" to /Users/icecliffs/.zprofile
Adding
export PATH="/Users/icecliffs/.local/share/solana/install/active_release/bin:$PATH" to /Users/icecliffs/.bash_profile

Close and reopen your terminal to apply the PATH changes or run the following in your existing shell:

export PATH="/Users/icecliffs/.local/share/solana/install/active_release/bin:$PATH"

接着输入 solana --version 查看版本

这里列举几条常用的命令

  • 配置网络
1
2
3
4
5
6
solana config set --url https://api.mainnet-beta.solana.com # 主网
# 或者
solana config set --url mainnet-beta
solana config set --url devnet
solana config set --url localhost
solana config set --url testnet
  • 创建钱包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
❯ solana-keygen new --outfile ~/icecliffs_test.json # 保管好
Generating a new keypair

For added security, enter a BIP39 passphrase

NOTE! This passphrase improves security of the recovery seed phrase NOT the
keypair file itself, which is stored as insecure plain text

BIP39 Passphrase (empty for none):

Wrote new keypair to /Users/icecliffs/icecliffs_test.json
==============================================================================
pubkey: XXXXXXXXXxcrXXXXXXXXXXXXXXXXdMNUxM
==============================================================================
Save this seed phrase and your BIP39 passphrase to recover your new keypair:
sun isolate mystery flock yellow stool entry ability since topple wealth input
==============================================================================

这里创建好的助记词对应 BIP39 里

https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki

1
2
❯ solana-keygen pubkey # 看看你的
DxBvK8JrWVn26TWpmfUTr1Xdzyz9U64VP7UPFw8ALp6e

这里连接 Github 可以领到很多的 Solana

image-20250211164309534

如果是真金白银就好了()

image-20250211164332301

也可以通过下面这条命令来获取

1
solana airdrop
  • 转账
1
solana transfer <destination-public-key> <amount> --from ~/my-wallet.json
  • 创建代币账户(创建一个新的代币账户(用于接收和持有 SPL Token))
1
solana spl-token create-account <mint-address>
  • 获取区块链的最新区块信息
1
solana block
  • 部署智能合约
1
solana program deploy <path-to-your-program>
  • 更新 Solana
1
solana update

需求一:假设我们现在有一个需求,拥有两个钱包,想要转来转去,可以用下面这条命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
❯ solana config set --keypair ~/icecliffs_a.json
Config File: /Users/icecliffs/.config/solana/cli/config.yml
RPC URL: https://api.devnet.solana.com
WebSocket URL: wss://api.devnet.solana.com/ (computed)
Keypair Path: /Users/icecliffs/icecliffs_a.json
Commitment: confirmed
❯ solana address
3sM7RwuEjzxcrCDTKfVoFiuKq8S84nXS5XpZiadMNUxM
❯ solana config set --keypair ~/icecliffs_b.json
Config File: /Users/icecliffs/.config/solana/cli/config.yml
RPC URL: https://api.devnet.solana.com
WebSocket URL: wss://api.devnet.solana.com/ (computed)
Keypair Path: /Users/icecliffs/icecliffs_b.json
Commitment: confirmed
❯ solana address
DxBvK8JrWVn26TWpmfUTr1Xdzyz9U64VP7UPFw8ALp6e

需求二:B 钱包给 A 钱包打币

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
❯ solana address
DxBvK8JrWVn26TWpmfUTr1Xdzyz9U64VP7UPFw8ALp6e
❯ solana balance
10 SOL
❯ solana transfer 3sM7RwuEjzxcrCDTKfVoFiuKq8S84nXS5XpZiadMNUxM 5 --from icecliffs_b.json
Error: The recipient address (3sM7RwuEjzxcrCDTKfVoFiuKq8S84nXS5XpZiadMNUxM) is not funded. Add `--allow-unfunded-recipient` to complete the transfer
❯ solana transfer 3sM7RwuEjzxcrCDTKfVoFiuKq8S84nXS5XpZiadMNUxM 5 --from icecliffs_b.json --allow-unfunded-recipient

Signature: 4ks6o14TXUVkGpHY1P7NvPudeCiCAXrWg3mJS8JXVrseCm7pCnsWSnKAKNfqmSq8nZKJUJAQ1nY4kNZUbue4ASpd

❯ solana config set --keypair ~/icecliffs_a.json
Config File: /Users/icecliffs/.config/solana/cli/config.yml
RPC URL: https://api.devnet.solana.com
WebSocket URL: wss://api.devnet.solana.com/ (computed)
Keypair Path: /Users/icecliffs/icecliffs_a.json
Commitment: confirmed
❯ solana balance
5 SOL

这里交易信息可以在开发网上看到

https://explorer.solana.com/tx/4ks6o14TXUVkGpHY1P7NvPudeCiCAXrWg3mJS8JXVrseCm7pCnsWSnKAKNfqmSq8nZKJUJAQ1nY4kNZUbue4ASpd?cluster=devnet

image-20250211165354086

Solana Concept

账户模型

结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
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,
}

这里有好多好多东西,自己看官网吧

https://solana.com/zh/docs/core/accounts

Solana RPC

这里关于 RPC 的详细介绍就不多说了,长话短说,在 Solana 中,RPC(Remote Procedure Call) 是与 Solana 区块链进行交互的核心方式,RPC 接口允许开发者通过调用预定义的 API 来获取区块链上的数据、提交交易或执行其他操作,Solana 的 RPC API 提供了高效的接口,能够支持快速读取链上数据及执行交易

这一部分在最开始的介绍有些到这里给出几条通过 CURL 调用的命令

  • getAccountInfo,查询账户信息,包括账户余额、持有的代币等
1
2
3
4
5
6
7
8
9
❯ curl -X POST https://api.devnet.solana.com \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "getAccountInfo",
"params": ["DxBvK8JrWVn26TWpmfUTr1Xdzyz9U64VP7UPFw8ALp6e"]
}'
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"2.1.11","slot":360308523},"value":{"data":"","executable":false,"lamports":4999995000,"owner":"11111111111111111111111111111111","rentEpoch":18446744073709551615,"space":0}},"id":1}
  • getBalance,获取指定账户的 SOL 余额
1
2
3
4
5
6
7
8
9
curl -X POST https://api.devnet.solana.com \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "getBalance",
"params": ["DxBvK8JrWVn26TWpmfUTr1Xdzyz9U64VP7UPFw8ALp6e"]
}'
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"2.1.11","slot":360308809},"value":4999995000},"id":1}

详见

https://solana.com/zh/docs/rpc

Solana Hello World

这里直接以官方 Hello World 项目为例子

git clone https://github.com/solana-labs/example-helloworld

比较基础的环境搭建这里就不说明了,把 URL 先设置成 localhost 集群,然后启动一下

1
2
solana config set --url localhost
solana-test-validator # 启动本地集群

image-20250213185235030

这里可以发包测试一下

1
2
3
4
5
6
7
8
curl -X POST http://127.0.0.1:8899 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "getAccountInfo",
"params": ["DxBvK8JrWVn26TWpmfUTr1Xdzyz9U64VP7UPFw8ALp6e"]
}'

这里引入 Web3.js 这个是啥具体就不多说了 npm install --save @solana/web3.js@1

https://github.com/solana-labs/solana-web3.js

https://solana-labs.github.io/solana-web3.js/

构建项目

1
npm run build:program-rust

启动客户端

1
2
npm run start
solana program deploy dist/program/helloworld.so # 不熟链上程序

代码解析,lib.rs 用于验证账户被调用的次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// lib.rs
// 引入本地作用域,用于序列化和反序列化
use borsh::{BorshDeserialize, BorshSerialize};
// 引入 Solana
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
};

/// Define the type of state stored in accounts
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct GreetingAccount {
/// number of greetings
pub counter: u32,
}

// 程序入口点
entrypoint!(process_instruction);

// Program entrypoint's implementation
pub fn process_instruction(
// 交互地址
program_id: &Pubkey, // Public key of the account the hello world program was loaded into
//程序交互的账户列表
accounts: &[AccountInfo], // The account to say hello to
_instruction_data: &[u8], // Ignored, all helloworld instructions are hellos
) -> ProgramResult {
msg!("Hello World Rust program entrypoint");

// Iterating accounts is safer than indexing
let accounts_iter = &mut accounts.iter();

// Get the account to say hello to
let account = next_account_info(accounts_iter)?;

// The account must be owned by the program in order to modify its data
if account.owner != program_id {
msg!("Greeted account does not have the correct program id");
return Err(ProgramError::IncorrectProgramId);
}

// Increment and store the number of times the account has been greeted
let mut greeting_account = GreetingAccount::try_from_slice(&account.data.borrow())?;
greeting_account.counter += 1;
greeting_account.serialize(&mut &mut account.data.borrow_mut()[..])?;

msg!("Greeted {} time(s)!", greeting_account.counter);
// 成功则返回
Ok(())
}

// Sanity tests
#[cfg(test)]
mod test {
use super::*;
use solana_program::clock::Epoch;
use std::mem;

#[test]
fn test_sanity() {
let program_id = Pubkey::default();
let key = Pubkey::default();
let mut lamports = 0;
let mut data = vec![0; mem::size_of::<u32>()];
let owner = Pubkey::default();
let account = AccountInfo::new(
&key,
false,
true,
&mut lamports,
&mut data,
&owner,
false,
Epoch::default(),
);
let instruction_data: Vec<u8> = Vec::new();

let accounts = vec![account];
// 搞了几个断言,可以自己跑起来理解一下
assert_eq!(
GreetingAccount::try_from_slice(&accounts[0].data.borrow())
.unwrap()
.counter,
0
);
process_instruction(&program_id, &accounts, &instruction_data).unwrap();
assert_eq!(
GreetingAccount::try_from_slice(&accounts[0].data.borrow())
.unwrap()
.counter,
1
);
process_instruction(&program_id, &accounts, &instruction_data).unwrap();
assert_eq!(
GreetingAccount::try_from_slice(&accounts[0].data.borrow())
.unwrap()
.counter,
2
);
}
}

如您对上述步骤感到繁琐,可以到 Solana 的 Playground 进行线上编译 https://beta.solpg.io/,从上面程序来看,可以总结一下 Solana 程序的一个大致逻辑,我们首先用到了 solana_program 这个标准库,并且将下面这传代码纳入了作用域

1
2
3
4
5
6
7
8
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
};
  • AccountInfo:account_info 模块中的一个结构体,允许我们访问帐户信息。

  • entrypoint:声明程序入口点的宏,类似于 Rust 中的 main 函数。

  • ProgramResult:entrypoint 模块中的返回值类型。

  • Pubkey:pubkey 模块中的一个结构体,允许我们将地址作为公钥访问。

  • msg:一个允许我们将消息打印到程序日志的宏,类似于 Rust 中的 println宏。

Solana 使用的入口点为 entrypoint! 声明的宏,相对应的我们可以这样声明一个入口点

1
2
3
4
5
6
7
8
9
entrypoint!(process_instruction_one);
fn process_instruction_one(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// do something
Ok(())
}

正常来说我们写一个累加程序都需要定义一个变量用于存储我们的 cnt,Solana 定义了一个 Account 数据账户来存储数据,这一点可以看看开发文档里的(链接在上面)

1
2
3
4
5
/// 定义数据账户的结构
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
pub count: u32,
}

之后通过 \#[derive(BorshSerialize, BorshDeserialize, Debug)] 这两个派生宏实现

image-20250213205842013

这里 CounterAccount 结构体对应的为数据账户,只能由 Owner 所有者更改,总的来说

先创建一个 AccountInfo 迭代器,用于遍历 accounts 数组。

1
let accounts_iter = &mut accounts.iter();

然后从迭代器中获取下一个账户信息,即存储计数器的数据账户

1
let account = next_account_info(accounts_iter)?;

接着加载数据账户数据

1
let mut counter_account = CounterAccount::try_from_slice(&account.data.borrow())?;

从数据账户的 data 字段中反序列化出 CounterAccount 结构体。try_from_slice 方法会将字节数组转换为 CounterAccount 结构体,然后 +=1 接着写会账户

1
2
counter_account.serialize(&mut &mut account.data.borrow_mut()[..])?;
Ok(()) /// 成功

剩下的例子可以看官方文档,我就不重复了

Anchor

Anchor 是 Solana Sealevel 运行时的一个框架,为编写智能合约提供了几个方便的开发工具。

  • Rust eDSL 用于编写 Solana 程序

  • IDL 规范

  • 用于从 IDL 生成客户端的 TypeScript 包

  • 用于开发完整应用程序的 CLI 和工作区管理

机翻自 Github https://github.com/coral-xyz/anchor,官方文档:https://www.anchor-lang.com/docs/installation

安装

1
2
3
cargo install --git https://github.com/coral-xyz/anchor avm --force
avm --version
avm install latest

创建项目 https://www.anchor-lang.com/docs/references/cli#init

1
2
3
4
5
anchor init anchor_solana_test
# 创建程序
anchor new xxx
# 验证链上部署的程序是否与本地匹配
anchor verify

构建智能合约,会生成一个二进制文件在 target/deploy

1
2
3
anchor build [anchor_solana_test]
# 目录下就是
anchor build

测试程序

1
anchor test

部署程序

1
anchor deploy --env devnet # 部署到测试网加个 --env 就行了

这里 Anchor 同样有一大堆宏,可以看看文档,补一个项目结构(基于 Playground)

image-20250213212621076

还得是 DevOps 方便啊

Solana NFT

  • 有单独的数量,超过了就铸造不了了

使用 Anchor 脚手架进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
use anchor_spl::{
associated_token::AssociatedToken,
metadata::{
/// 这两个函数用于创建 NFT 的元数据账户和主版本账户(Master Edition),这些是 Metaplex Token Metadata 程序中的核心功能。
create_master_edition_v3, create_metadata_accounts_v3, CreateMasterEditionV3,
CreateMetadataAccountsV3, Metadata
},
token::{mint_to, Mint, MintTo, Token, TokenAccount},
};

use mpl_token_metadata::types::DataV2;

use mpl_token_metadata::accounts::{MasterEdition, Metadata as MetadataAccount};

铸造 mint_to(cpi_context, 1)?;,补一张图

image-20250214122107376

Release

切换网络

1
solana config set --url devnet

构建项目

1
anchor init icecliffs_nft

导入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[package]
name = "icecliffs_nft"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"

[lib]
crate-type = ["cdylib", "lib"]
name = "icecliffs_nft"

[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]

[dependencies]
anchor-lang = "0.30.1"
anchor-spl = {version = "0.30.1",features = ["metadata"]}
mpl-token-metadata = "5.1.0"

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/// lib.rs
use anchor_lang::prelude::*;
use anchor_spl::{
associated_token::AssociatedToken,
metadata::{
create_master_edition_v3, create_metadata_accounts_v3, CreateMasterEditionV3,
CreateMetadataAccountsV3, Metadata
},
token::{mint_to, Mint, MintTo, Token, TokenAccount},
};

use mpl_token_metadata::types::DataV2;

use mpl_token_metadata::accounts::{ MasterEdition, Metadata as MetadataAccount};

declare_id!("6kPhe2z5A5Qw17qniDTHXuQV1vD9DMWD6KSBCEBj88Gv");
#[program]
pub mod metaplex_nft {

use super::*;

pub fn mint_nft(
ctx: Context<MintNFT>,
name: String,
symbol: String,
uri: String,
) -> Result<()> {
let cpi_program = ctx.accounts.token_program.to_account_info();

let cpi_accounts = MintTo {
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.associated_token_account.to_account_info(),
authority: ctx.accounts.signer.to_account_info(),
};

let cpi_context = CpiContext::new(
cpi_program,
cpi_accounts,
);

mint_to(cpi_context, 1)?;

let cpi_context = CpiContext::new(
ctx.accounts.token_metadata_program.to_account_info(),
CreateMetadataAccountsV3 {
metadata: ctx.accounts.metadata_account.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
mint_authority: ctx.accounts.signer.to_account_info(),
update_authority: ctx.accounts.signer.to_account_info(),
payer: ctx.accounts.signer.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
rent: ctx.accounts.rent.to_account_info(),
},
);

let data_v2 = DataV2 {
name: name,
symbol: symbol,
uri: uri,
seller_fee_basis_points: 0,
creators: None,
collection: None,
uses: None,
};

create_metadata_accounts_v3(cpi_context, data_v2, false, true, None)?;

let cpi_context = CpiContext::new(
ctx.accounts.token_metadata_program.to_account_info(),
CreateMasterEditionV3 {
edition: ctx.accounts.master_edition_account.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
update_authority: ctx.accounts.signer.to_account_info(),
mint_authority: ctx.accounts.signer.to_account_info(),
payer: ctx.accounts.signer.to_account_info(),
metadata: ctx.accounts.metadata_account.to_account_info(),
token_program: ctx.accounts.token_program.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
rent: ctx.accounts.rent.to_account_info(),
},
);

create_master_edition_v3(cpi_context, None)?;

Ok(())
}
}

#[derive(Accounts)]
pub struct MintNFT<'info> {
/// CHECK: The signer field is safe because it is verified elsewhere in the program.
#[account(mut, signer)]
pub signer: AccountInfo<'info>,
#[account(
init,
payer = signer,
mint::decimals = 0,
mint::authority = signer.key(),
mint::freeze_authority = signer.key(),
)]
pub mint: Account<'info, Mint>,
#[account(
init_if_needed,
payer = signer,
associated_token::mint = mint,
associated_token::authority = signer
)]
pub associated_token_account: Account<'info, TokenAccount>,
/// CHECK - address
#[account(
mut,
address = MetadataAccount::find_pda(&mint.key()).0,
)]
pub metadata_account: AccountInfo<'info>,
/// CHECK - address
#[account(
mut,
address = MasterEdition::find_pda(&mint.key()).0,
)]
pub master_edition_account: AccountInfo<'info>,

pub token_program: Program<'info, Token>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub token_metadata_program: Program<'info, Metadata>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,
}

部署程序

1
anchor build && anchor deploy

交互环节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { MetaplexNft } from ".https://bfs.iloli.moe/2025/02/14/target/types/metaplex_nft";
import { walletAdapterIdentity } from "@metaplex-foundation/umi-signer-wallet-adapters";
import { getAssociatedTokenAddress } from "@solana/spl-token";
import {
findMasterEditionPda,
findMetadataPda,
mplTokenMetadata,
MPL_TOKEN_METADATA_PROGRAM_ID,
} from "@metaplex-foundation/mpl-token-metadata";
import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
import { publicKey } from "@metaplex-foundation/umi";

import {
TOKEN_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID,
} from "@solana/spl-token";

describe("solana-nft-anchor", async () => {
// Configured the client to use the devnet cluster.
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace
.MetaplexNft as Program<MetaplexNft>;

const signer = provider.wallet;

const umi = createUmi("https://api.devnet.solana.com")
.use(walletAdapterIdentity(signer))
.use(mplTokenMetadata());

const mint = anchor.web3.Keypair.generate();

// Derive the associated token address account for the mint
const associatedTokenAccount = await getAssociatedTokenAddress(
mint.publicKey,
signer.publicKey
);

// derive the metadata account
let metadataAccount = findMetadataPda(umi, {
mint: publicKey(mint.publicKey),
})[0];

//derive the master edition pda
let masterEditionAccount = findMasterEditionPda(umi, {
mint: publicKey(mint.publicKey),
})[0];

const metadata = {
name: "HackQuest",
symbol: "HQ",
uri: "https://raw.githubusercontent.com/Louis-XWB/metaplex_nft/chore_branch/meta.json",
};

it("mints nft!", async () => {
const tx = await program.methods
.mintNft(metadata.name, metadata.symbol, metadata.uri)
.accounts({
signer: provider.publicKey,
mint: mint.publicKey,
associatedTokenAccount,
metadataAccount,
masterEditionAccount,
tokenProgram: TOKEN_PROGRAM_ID,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
tokenMetadataProgram: MPL_TOKEN_METADATA_PROGRAM_ID,
systemProgram: anchor.web3.SystemProgram.programId,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
})
.signers([mint])
.rpc();

console.log(
`mint nft tx: https://explorer.solana.com/tx/${tx}?cluster=devnet`
);
console.log(
`minted nft: https://explorer.solana.com/address/${mint.publicKey}?cluster=devnet`
);
});
});

铸造 NFT

1
anchor test

查看 NFT

1
minted nft

差不多就这样吧,未完待续。

Reference

Author

IceCliffs

Posted on

2025-02-11

Updated on

2025-03-07

Licensed under

Comments