学习完以太坊基础知识和比特币白皮书后接着来学习 Solana,官方文档 https://solana.com/zh/docs?locale=docs
关于 Solana 采用的加密算法,在 Solana 中,采用了了 Ed25519 curve 非对称加密算法,用于生成数字签名和验证数字签名。与对称加密算法不同,非对称加密使用一对密钥:pubkey公钥和secretkey私钥,如果使用公钥加密,则只有对应的私钥能够进行解密;如果使用私钥加密,则可以使用对应的公钥验证签名,即判断该签名是否由私钥的持有者发起。
Solana 使用 Rust 语言进行开发,如果您之前没接触过 Rust 建议花点时间看看相关资料,以下是我整理的一些 Rust 教程:
-
• https://cheats.rs/ -
• https://course.rs/first-try/editor.html -
• https://cirno.me/note/597.html
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开发者使用,相对稳定
-
• DevNet: https://api.devnet.solana.com -
• TestNet: https://api.testnet.solana.com -
• MainNet: https://api.mainnet-beta.solana.com
Develop Environment
首先安装 Rust 环境
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
接着下载 Solana 的 CLI
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
如果下载完成,会提示让你配置环境变量,简单配置下就好了
❯ sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"downloading stable installer ✨ stable commit 035c4eb initializedAdding export PATH="/Users/icecliffs/.local/share/solana/install/active_release/bin:$PATH" to /Users/icecliffs/.profileAdding export PATH="/Users/icecliffs/.local/share/solana/install/active_release/bin:$PATH" to /Users/icecliffs/.zprofileAdding export PATH="/Users/icecliffs/.local/share/solana/install/active_release/bin:$PATH" to /Users/icecliffs/.bash_profileClose 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
查看版本
这里列举几条常用的命令
-
• 配置网络
solana config set --url https://api.mainnet-beta.solana.com # 主网# 或者solana config set --url mainnet-betasolana config set --url devnetsolana config set --url localhostsolana config set --url testnet
-
• 创建钱包
❯ solana-keygen new --outfile ~/icecliffs_test.json # 保管好Generating a new keypairFor added security, enter a BIP39 passphraseNOTE! This passphrase improves security of the recovery seed phrase NOT thekeypair file itself, which is stored as insecure plain textBIP39 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
-
• 查看账户信息,第一次创建可以到这里领水 https://faucet.solana.com/
❯ solana-keygen pubkey # 看看你的DxBvK8JrWVn26TWpmfUTr1Xdzyz9U64VP7UPFw8ALp6e
这里连接 Github 可以领到很多的 Solana
如果是真金白银就好了()
也可以通过下面这条命令来获取
solana airdrop
-
• 转账
solana transfer <destination-public-key> <amount> --from ~/my-wallet.json
-
• 创建代币账户(创建一个新的代币账户(用于接收和持有 SPL Token))
solana spl-token create-account <mint-address>
-
• 获取区块链的最新区块信息
solana block
-
• 部署智能合约
solana program deploy <path-to-your-program>
-
• 更新 Solana
solana update
需求一:假设我们现在有一个需求,拥有两个钱包,想要转来转去,可以用下面这条命令
❯ solana config set --keypair ~/icecliffs_a.jsonConfig File: /Users/icecliffs/.config/solana/cli/config.ymlRPC URL: https://api.devnet.solana.com WebSocket URL: wss://api.devnet.solana.com/ (computed)Keypair Path: /Users/icecliffs/icecliffs_a.json Commitment: confirmed ❯ solana address3sM7RwuEjzxcrCDTKfVoFiuKq8S84nXS5XpZiadMNUxM❯ solana config set --keypair ~/icecliffs_b.jsonConfig File: /Users/icecliffs/.config/solana/cli/config.ymlRPC URL: https://api.devnet.solana.com WebSocket URL: wss://api.devnet.solana.com/ (computed)Keypair Path: /Users/icecliffs/icecliffs_b.json Commitment: confirmed ❯ solana addressDxBvK8JrWVn26TWpmfUTr1Xdzyz9U64VP7UPFw8ALp6e
需求二:B 钱包给 A 钱包打币
❯ solana addressDxBvK8JrWVn26TWpmfUTr1Xdzyz9U64VP7UPFw8ALp6e❯ solana balance10 SOL❯ solana transfer 3sM7RwuEjzxcrCDTKfVoFiuKq8S84nXS5XpZiadMNUxM 5 --from icecliffs_b.jsonError: 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-recipientSignature: 4ks6o14TXUVkGpHY1P7NvPudeCiCAXrWg3mJS8JXVrseCm7pCnsWSnKAKNfqmSq8nZKJUJAQ1nY4kNZUbue4ASpd❯ solana config set --keypair ~/icecliffs_a.jsonConfig File: /Users/icecliffs/.config/solana/cli/config.ymlRPC URL: https://api.devnet.solana.com WebSocket URL: wss://api.devnet.solana.com/ (computed)Keypair Path: /Users/icecliffs/icecliffs_a.json Commitment: confirmed ❯ solana balance5 SOL
这里交易信息可以在开发网上看到
https://explorer.solana.com/tx/4ks6o14TXUVkGpHY1P7NvPudeCiCAXrWg3mJS8JXVrseCm7pCnsWSnKAKNfqmSq8nZKJUJAQ1nY4kNZUbue4ASpd?cluster=devnet
Solana Concept
账户模型
结构体
pubstructAccount {/// 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,}
这里有好多好多东西,自己看官网吧
https://solana.com/zh/docs/core/accounts
Solana RPC
这里关于 RPC 的详细介绍就不多说了,长话短说,在 Solana 中,RPC(Remote Procedure Call) 是与 Solana 区块链进行交互的核心方式,RPC 接口允许开发者通过调用预定义的 API 来获取区块链上的数据、提交交易或执行其他操作,Solana 的 RPC API 提供了高效的接口,能够支持快速读取链上数据及执行交易
这一部分在最开始的介绍有些到这里给出几条通过 CURL 调用的命令
-
• getAccountInfo,查询账户信息,包括账户余额、持有的代币等
❯ 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 余额
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 集群,然后启动一下
solana config set --url localhostsolana-test-validator # 启动本地集群
这里可以发包测试一下
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/
构建项目
npm run build:program-rust
启动客户端
npm run startsolana program deploy dist/program/helloworld.so # 不熟链上程序
代码解析,lib.rs
用于验证账户被调用的次数
// lib.rs// 引入本地作用域,用于序列化和反序列化use borsh::{BorshDeserialize, BorshSerialize};// 引入 Solanause 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)]pubstructGreetingAccount {/// number of greetingspub counter: u32,}// 程序入口点entrypoint!(process_instruction);// Program entrypoint's implementationpubfnprocess_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 indexingletaccounts_iter = &mut accounts.iter();// Get the account to say hello toletaccount = next_account_info(accounts_iter)?;// The account must be owned by the program in order to modify its dataif account.owner != program_id { msg!("Greeted account does not have the correct program id");returnErr(ProgramError::IncorrectProgramId); }// Increment and store the number of times the account has been greetedletmut 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]fntest_sanity() {letprogram_id = Pubkey::default();letkey = Pubkey::default();letmut lamports = 0;letmut data = vec![0; mem::size_of::<u32>()];letowner = Pubkey::default();letaccount = AccountInfo::new( &key,false,true, &mut lamports, &mut data, &owner,false, Epoch::default(), );letinstruction_data: Vec<u8> = Vec::new();letaccounts = 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
这个标准库,并且将下面这传代码纳入了作用域
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!
声明的宏,相对应的我们可以这样声明一个入口点
entrypoint!(process_instruction_one);fnprocess_instruction_one( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8],) -> ProgramResult {// do somethingOk(())}
正常来说我们写一个累加程序都需要定义一个变量用于存储我们的 cnt
,Solana 定义了一个 Account
数据账户来存储数据,这一点可以看看开发文档里的(链接在上面)
/// 定义数据账户的结构#[derive(BorshSerialize, BorshDeserialize, Debug)]pubstructCounterAccount {pub count: u32,}
之后通过 #[derive(BorshSerialize, BorshDeserialize, Debug)]
这两个派生宏实现
这里 CounterAccount
结构体对应的为数据账户,只能由 Owner
所有者更改,总的来说
先创建一个 AccountInfo
迭代器,用于遍历 accounts
数组。
letaccounts_iter = &mut accounts.iter();
然后从迭代器中获取下一个账户信息,即存储计数器的数据账户
letaccount = next_account_info(accounts_iter)?;
接着加载数据账户数据
letmut counter_account = CounterAccount::try_from_slice(&account.data.borrow())?;
从数据账户的 data
字段中反序列化出 CounterAccount
结构体。try_from_slice
方法会将字节数组转换为 CounterAccount
结构体,然后 +=1
接着写会账户
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
安装
cargo install --git https://github.com/coral-xyz/anchor avm --forceavm --versionavm install latest
创建项目 https://www.anchor-lang.com/docs/references/cli#init
anchor init anchor_solana_test# 创建程序anchor new xxx# 验证链上部署的程序是否与本地匹配anchor verify
构建智能合约,会生成一个二进制文件在 target/deploy
anchor build [anchor_solana_test]# 目录下就是anchor build
测试程序
anchor test
部署程序
anchor deploy --env devnet # 部署到测试网加个 --env 就行了
这里 Anchor 同样有一大堆宏,可以看看文档,补一个项目结构(基于 Playground)
还得是 DevOps 方便啊
Solana NFT
-
• 有单独的数量,超过了就铸造不了了
使用 Anchor 脚手架进行配置
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)?;
,补一张图
Release
切换网络
solana config set --url devnet
构建项目
anchor init icecliffs_nft
导入依赖
[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"
源代码
/// lib.rsuse 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]pubmod metaplex_nft {use super::*;pubfnmint_nft( ctx: Context<MintNFT>, name: String, symbol: String, uri: String, ) ->Result<()> {letcpi_program = ctx.accounts.token_program.to_account_info();letcpi_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(), };letcpi_context = CpiContext::new( cpi_program, cpi_accounts, );mint_to(cpi_context, 1)?;letcpi_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(), }, );letdata_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)?;letcpi_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)]pubstructMintNFT<'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>,}
部署程序
anchor build && anchor deploy
交互环节
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 mintconst associatedTokenAccount = awaitgetAssociatedTokenAddress( mint.publicKey, signer.publicKey );// derive the metadata accountletmetadataAccount = findMetadataPda(umi, { mint: publicKey(mint.publicKey), })[0];//derive the master edition pdaletmasterEditionAccount = 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
anchor test
查看 NFT
minted nft
差不多就这样吧,未完待续。
Reference
-
• https://solana.com/zh -
• https://solana.com/zh/docs?locale=docs -
• https://www.ironforge.cloud/,DevOps -
• https://discord.com/invite/qcMZEgydXP,Solana -
• https://rustmagazine.github.io/rust_magazine_2021/chapter_10/solana-learn-part1.html
原文始发于微信公众号(Gh0xE9):Solana 开发笔记
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论