Solana开发学习笔记(一)——从Hello World出发
笔者注:因近期笔者工作需要,开始接触 Solana 链上程序开发。本系列文章是笔者的学习笔记,既是为了备忘,也是希望得到 solana 开发者的指点与交流。本系列文章将默认读者已经掌握 rust 的基础语法,故不涉及对 rust 语法细节的解释。如果读者对 rust 基础语法还不熟练的话,本文下方推荐的 rust 入门书籍《rust 编程入门、实战与进阶》学习。
最安全的虚拟币交易平台推荐:
- OKX(欧易交易所)>>>进入官网<<< >>>官方下载<<<
- Binance(币安交易所)>>>进入官网<<< >>>官方下载<<<
Solana 是一个高性能、无许可的底层公链,专注于在不牺牲去中心化或安全性的前提下提供可扩展性。Solana 主网于 2020 年一季度上线,目前主网的全球节点超过 800 个,TPS 最高可达 6.5 万,出块时间约 400 毫秒。
Solana 的共识算法采用 PoH(历史证明),其核心是一个去中心化时钟,该时钟旨在解决缺乏单个可信赖时间源在分布式网络中的时间问题。PoH 免除了在节点网络中广播时间戳的需求,从而提高整个网络的效率。
1.1.1 链上程序Solana 的智能合约叫做链上程序(On-chain Program),Solana 最新提供了 Rust 和 C 的 SDK 来支持开发链上程序。链上程序的开发工作流如图 1-1 所示,开发者可以使用工具将程序编译成 Berkley Packet Filter (BPF) 字节码(文件以 .so 为扩展名),再部署到 Solana 链上,通过 Sealevel 并行智能合约运行时去执行智能合约的逻辑。此外,基于 Solana JSON RPC API,最新提供了诸多 SDK 用于客户端与 Solana 链上数据交互。
图 1-1 链上程序开发工作流
1.1.2 账户模型与以太坊类似,Solana 也是基于账户模型的区块链。通过将任意状态存储于链上账户并同步复制给集群中的所有节点,可以创建复杂而强大的去中心化应用程序。
Solana 提供了一套不同于以太坊的账户模型,账户定义的字段如表 1-1 所示。Solana 的账户可以分为可执行账户和不可执行账户。
可执行账户:存储不可变的数据,主要用于存储程序的 BPF 字节码。不可执行账户:存储可变的数据,主要用于存储程序的状态。表 1-1 账户定义字段
我们知道以太坊上每个智能合约的代码和状态都存储在同一个账户中,而 Solana 链上程序是只读或无状态的,即程序的账户(可执行账户)只存储 BPF 字节码,不存储任何状态,程序会把状态存储在其他独立的账户(不可执行账户)中。为了区分某个账户是用作哪个程序的状态存储,每个账户都指定了一个程序作为其所有者。程序可以读取其不作为所有者的账户中的状态,但只有作为所有者的程序才能修改账户中的状态,任何其他程序所做的修改都会被还原并导致交易失败。
更多关于账户模型的资料可以参见最新文档:https://solana.wiki/zh-cn/docs/account-model/
1.2 搭建编程环境在开始 Solana 链上程序开发之前,需要先安装和配置相关的编程环境。首先请正确安装 Node、NPM 和 Rust 的最新稳定版本,下面来安装 Solana CLI 并配置相关环境。
1.2.1 安装 Solana CLISolana CLI 是与 Solana 集群进行交互的命令行管理工具,包含节点程序 solana-validator、密钥对生成工具 solana-keygen,以及合约开发工具 cargo-build-bpf、cargo-test-bpf 等。
在终端运行以下命令,可完成 Solana CLI 最新稳定版的下载与安装。
sh -c "$(curl -sSfL https://release.solana.com/stable/install)"登录后复制
如果安装成功,会出现以下内容。
downloading stable installer ✨ stable commit e9bef425 initializedAdding export PATH="~/.local/share/solana/install/active_release/bin:$PATH" to ~/.profileAdding export PATH="~/.local/share/solana/install/active_release/bin:$PATH" to ~/.bash_profileClose and reopen your terminal to apply the PATH changes or run the following in your existing shell: export PATH="~/.local/share/solana/install/active_release/bin:$PATH"登录后复制
Solana CLI 的所有命令行工具都安装在 ~/.local/share/solana/install/active_release/bin 中,并会自动将该路径加入 ~/.profile 和 ~/.bash_profile 文件的 PATH 环境变量。
运行以下命令,检查 PATH 环境变量是否已正确设置。
solana --version// solana-cli 1.7.18 (src:e9bef425; feat:140464022)登录后复制
如果能显示 solana-cli 的版本号、版本哈希等信息,代表环境变量设置成功。如果未看到这些信息,请检查相关文件中 PATH 环境变量设置的路径是否正确。
如果已安装过 Solana CLI,想升级到最新版本,可在终端运行以下命令。
solana-install update登录后复制1.2.2 配置 Solana CLI1. 连接到集群
Solana 的集群有本地集群(localhost)和公开集群。根据不同的用途,公开集群又分为开发者网络(devnet)、测试网(testnet)和主网(mainnet-beta)。
devnet 是适用于开发者的集群,开发者可获得 SOL token 的空投,但这个 SOL token 不具有真实价值,仅限测试使用。devnet 的 RPC 链接是https://api.devnet.solana.com。testnet 是用于测试最新功能的集群,如网络性能、稳定性和验证程序行为等。同样可获得 SOL token 的空投,但也仅限测试使用。testnet 的 RPC 链接是https://api.testnet.solana.com。mainnet-beta 是主网集群,在 Mainnet Beta 上发行的 SOL token 具有真实价值。mainnet-beta 的 RPC 链接是https://api.mainnet-beta.solana.com。运行以下命令,根据实际需要来选择集群。
// 选择localhost集群solana config set --url localhost// 选择devnet集群solana config set --url devnet登录后复制2. 创建账户
如果是第一次使用 Solana CLI,需要先创建一个账户。运行以下命令,根据操作提示可以设置一个 BIP39 规范的密码,此密码用来增强助记词的安全性,当然也可以为空。生成新的账户后,密钥对会被自动写入 ~/.config/solana/id.json 文件中。需要注意的是,这种存储密钥对的方式是不安全的,仅限开发测试使用。
solana-keygen new登录后复制
要查看当前这个账户的公钥,运行以下命令。
solana-keygen pubkey登录后复制
当前如果是在 devnet 集群,该账户的余额为 0 SOL,可以运行以下命令查询余额。
solana balance登录后复制
在 devnet 上申请 SOL 空投,运行以下命令后再次查询当前账户的余额,会发现余额为 2 SOL。
solana airdrop 2登录后复制1.3 第一个 Solana 项目——Hello World
Hello World 是一个最新演示项目,展示了如何使用 Rust 和 C 开发链上程序,并使用 Solana CLI 来构建与部署,以及使用 Solana JavaScript SDK 与链上程序进行交互。
1.3.1 Hello World 源码解读example-helloworld 项目的目录结构如下所示,其中 program-rust 目录下是 Rust 开发的程序源代码,client 目录下是客户端的源代码。
example-helloworld|+-- src| || +-- client| | || | +-- hello_world.ts| | || | +-- main.ts| | || | +-- utils.ts| || +-- program-rust| | || | +-- src| | | || | | +-- lib.rs| | || | +-- tests| | | || | | +-- lib.rs| | || | +-- Cargo.toml| | || | +-- Xargo.toml|+-- .gitignore|+-- package.json|+-- tsconfig.json登录后复制1. 链上程序源码解读
program-rust/src/lib.rs 是链上程序的核心代码,如代码清单 1-1 所示,实现了将程序被调用次数存储在链上账户中。
第 1 行代码将 borsh::BorshDeserialize 和 borsh::BorshSerialize 引入本地作用域,用于序列化和反序列化数据。第 2~9 行代码将 Solana Rust SDK 的模块引入本地作用域,使用 Rust 编写程序都需要这个 SDK。
第 13~16 行代码定义了 GreetingAccount 结构体作为存储在账户中的状态类型,里面有一个 u32 类型的字段 counter,用于记录程序被有效调用的次数。
第 19 行代码 entrypoint 声明了 process_instruction 函数是程序入口,每个程序都有一个唯一的入口。第 22~26 行代码是 process_instruction 函数签名,它要接收 3 个参数:
program_id:链上程序的部署地址,在这里也就是 helloworld 程序账户的公钥。accounts:与程序交互的账户列表,当前程序会使用账户列表中的账户来存储状态或修改账户中的数据。如果当前程序不是某个账户的所有者,那就无法使用该账户存储状态或修改数据,当前交易会执行失败。instruction_data:指令数据,比如要转账的代币数量、转账地址等。process_instruction 函数的返回值类型是 ProgramResult,ProgramResult 类型的定义如下所示。
pub type ProgramResult = Result;登录后复制
当程序的逻辑执行成功时返回 Ok(()),否则将 ProgramError 错误返回。ProgramError 是自定义错误的枚举类型,其中包含程序可能失败的各种原因。
第 27 行代码使用 msg! 宏将字符串输出到日志中,方便观察业务的执行逻辑和调试信息。第 30 行代码通过 iter 方法将账户列表转换为迭代器,以安全的方式获取账户地址。第 33 行代码使用了 ? 操作符,如果迭代器中有账户地址,会将账户地址与变量 account 绑定。如果迭代器中没有账户地址,? 操作符会让程序执行失败。
第 36~39 行代码判断存储状态的账户所有者是否是当前程序。只有账户所有者才能修改数据,否则输出日志并返回。
第 42~44 行代码先对账户中的数据进行反序列化操作,再将 counter 加一,最后将其序列化后存储到账户中。
代码清单 1-1 helloworld 链上程序
use borsh::{BorshDeserialize, BorshSerialize};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,}// Declare and export the program's entrypointentrypoint!(process_instruction);// Program entrypoint's implementationpub 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 then 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(())}登录后复制2. 客户端程序源码解读要想测试链上程序,我们必须通过 Solana JSON RPC API 去和链上程序进行交互。example-helloworld 项目提供的客户端用 Typescript 编写,使用了 web3.js 库这个 Solana JavaScript SDK。
在 client 目录下,客户端执行的入口是 main.ts 文件,它按特定的顺序执行任务,每个任务的业务逻辑代码在 hello_world.ts 文件。
首先,客户端调用 establishConnection 函数与集群建立连接。
export async function establishConnection(): Promise登录后复制{ const rpcUrl = await getRpcUrl(); connection = new Connection(rpcUrl, 'confirmed'); const version = await connection.getVersion(); console.log('Connection to cluster established:', rpcUrl, version);}
接着,客户端调用 establishPayer 函数来确保有一个有支付能力的账户。
export async function establishPayer(): Promise{ let fees = 0; if (!payer) { const {feeCalculator} = await connection.getRecentBlockhash(); // Calculate the cost to fund the greeter account fees += await connection.getMinimumBalanceForRentExemption(GREETING_SIZE); // Calculate the cost of sending transactions fees += feeCalculator.lamportsPerSignature * 100; // wag try { // Get payer from cli config payer = await getPayer(); } catch (err) { // Fund a new payer via airdrop payer = await newAccountWithLamports(connection, fees); } } const lamports = await connection.getBalance(payer.publicKey); if (lamports 然后,客户端调用 checkProgram 函数从 src/program-rust/target/deploy/helloworld-keypair.json 中加载已部署程序的密钥对(此操作前需先构建链上程序,详见 1.3.2 节),并使用密钥对的公钥来获取程序账户。如果程序不存在,客户端会报错并停止执行。如果程序存在,将创建一个新账户来存储状态,并以该程序作为新账户所有者。这里新账户存储的状态,就是程序被调用的次数。
export async function checkProgram(): Promise登录后复制{ // Read program id from keypair file try { const programKeypair = await createKeypairFromFile(PROGRAM_KEYPAIR_PATH); programId = programKeypair.publicKey; } catch (err) { const errMsg = (err as Error).message; throw new Error( `Failed to read program keypair at '${PROGRAM_KEYPAIR_PATH}' due to error: ${errMsg}.`, ); } // Check if the program has been deployed const programInfo = await connection.getAccountInfo(programId); if (programInfo === null) { if (fs.existsSync(PROGRAM_SO_PATH)) { throw new Error( 'Program needs to be deployed with `solana program deploy dist/program/helloworld.so`', ); } else { throw new Error('Program needs to be built and deployed'); } } else if (!programInfo.executable) { throw new Error(`Program is not executable`); } console.log(`Using program ${programId.toBase58()}`); // Derive the address (public key) of a greeting account from the program so that it's easy to find later. const GREETING_SEED = 'hello'; greetedPubkey = await PublicKey.createWithSeed( payer.publicKey, GREETING_SEED, programId, ); // Check if the greeting account has already been created const greetedAccount = await connection.getAccountInfo(greetedPubkey); if (greetedAccount === null) { console.log( 'Creating account', greetedPubkey.toBase58(), 'to say hello to', ); const lamports = await connection.getMinimumBalanceForRentExemption( GREETING_SIZE, ); const transaction = new Transaction().add( SystemProgram.createAccountWithSeed({ fromPubkey: payer.publicKey, basePubkey: payer.publicKey, seed: GREETING_SEED, newAccountPubkey: greetedPubkey, lamports, space: GREETING_SIZE, programId, }), ); await sendAndConfirmTransaction(connection, transaction, [payer]); }} 客户端再调用 sayHello 函数向链上程序发送交易。一个交易可以包含一个或多个不同的指令,当前该交易包含了一个指令,指令中带有要调用链上程序的 Program Id 以及客户端要交互的账户地址。需要注意的是,如果交易中包含多个不同的指令,其中有一个指令执行失败,那么所有指令所做的操作都会被还原。
export async function sayHello(): Promise登录后复制{ console.log('Saying hello to', greetedPubkey.toBase58()); const instruction = new TransactionInstruction({ keys: [{pubkey: greetedPubkey, isSigner: false, isWritable: true}], programId, data: Buffer.alloc(0), // All instructions are hellos }); await sendAndConfirmTransaction( connection, new Transaction().add(instruction), [payer], );} 最后,客户端调用 reportGreetings 函数访问账户数据,查询链上程序被有效调用的次数。
export async function reportGreetings(): Promise登录后复制1.3.2 Hello World 构建与部署1. 创建项目{ const accountInfo = await connection.getAccountInfo(greetedPubkey); if (accountInfo === null) { throw 'Error: cannot find the greeted account'; } const greeting = borsh.deserialize( GreetingSchema, GreetingAccount, accountInfo.data, ); console.log( greetedPubkey.toBase58(), 'has been greeted', greeting.counter, 'time(s)', );} 使用 git clone 命令下载 example-helloworld 项目。
git clone https://github.com/solana-labs/example-helloworld.gitcd example-helloworld登录后复制2. 构建链上程序运行以下命令,在 program-rust 目录下构建链上程序。
cd src/program-rust/cargo build-bpf登录后复制构建完成后,src/program-rust/target/deploy 目录下的 helloworld.so 就是可在 Solana 集群部署的链上程序的 BPF 字节码文件。
3. 启动本地集群当前项目在本地集群部署运行,因此首先选择 localhost 集群,运行以下命令。
solana config set --url localhost登录后复制本地集群设置成功,会出现以下内容。
Config File: ~/.config/solana/cli/config.ymlRPC URL: http://localhost:8899WebSocket URL: ws://localhost:8900/ (computed)Keypair Path: ~/.config/solana/id.jsonCommitment: confirmed登录后复制再运行以下命令,启动 localhost 集群。
solana-test-validator登录后复制看到以下内容,代表本地集群已成功启动。
Ledger location: test-ledgerLog: test-ledger/validator.logIdentity: A4HuRgmABNCe94epY2mU7q6WqEHCo2B9iBFE5Yphiw5uGenesis Hash: 96TF9n1uuyFv4rAKECffA61jLrgYjMjNRZ3hJpP6HSr7Version: 1.7.18Shred Version: 13390Gossip Address: 127.0.0.1:1024TPU Address: 127.0.0.1:1027JSON RPC URL: http://127.0.0.1:8899⠉ 00:00:42 | Processed Slot: 45430 | Confirmed Slot: 45430 | Finalized Slot: 45398 | Snapshot Slot: 45300 | Transactions: 45452 | ◎499.772930000登录后复制4. 部署链上程序运行以下命令,在 localhost 集群部署链上程序。
solana program deploy target/deploy/helloworld.so// Program Id: 6AArMEBpFhhtU2mBnEMEPeEH7xkhfUwPseUeG4fhLYto登录后复制链上程序部署成功会返回 Program Id,它类似于以太坊智能合约的地址。
5. 调用链上程序helloworld 已成功部署,可以与它进行交互了!example-helloworld 项目提供了一个简单的客户端,在运行客户端之前先安装依赖软件包。
npm install登录后复制由于我们调整了链上程序的构建方式,没有使用该项目默认的 npm run build:program-rust 命令,因此需要修改 client 目录下的 hello_world.ts 文件,将第 48 行代码定义的变量 PROGRAM_PATH 的路径由“../../dist/program”改为“../program-rust/target/deploy”。 再运行以下命令,启动客户端去调用链上程序。
npm run start登录后复制客户端成功调用链上程序,输出内容如下所示。如果再次运行客户端,第 10 行所显示的次数会加一。至此,我们已经成功在 Solana 集群部署链上程序并与之交互了。
> helloworld@0.0.1 start> ts-node src/client/main.tsLet's say hello to a Solana account...Connection to cluster established: http://localhost:8899 { 'feature-set': 3179062686, 'solana-core': '1.6.23' }Using account 4xRm2FYmRB8WdxJk6nXicVMgsPnsxChEnpQwFDGwdcSS containing 499999999.93435186 SOL to pay for feesUsing program 6AArMEBpFhhtU2mBnEMEPeEH7xkhfUwPseUeG4fhLYtoCreating account Eq7bcsg5p6AaYiPnfiia99ESsuq4B4jYpVbWZhQ94Zvy to say hello toSaying hello to Eq7bcsg5p6AaYiPnfiia99ESsuq4B4jYpVbWZhQ94ZvyEq7bcsg5p6AaYiPnfiia99ESsuq4B4jYpVbWZhQ94Zvy has been greeted 1 time(s)Success登录后复制如果没有输出期望值,请首先确认是否已正确启动了本地集群,构建并部署好了链上程序。此外,可以运行以下命令查看程序日志,日志包括程序日志消息以及程序失败信息。
solana logs登录后复制包含程序失败信息的日志如下所示,检查日志找出程序失败的原因。
@@##@@登录后复制1.4 本章小节Transaction executed in slot 5621:Signature: 4pya5iyvNfAZj9sVWHzByrxdKB84uA5sCxLceBwr9UyuETX2QwnKg56MgBKWSM4breVRzHmpb1EZQXFPPmJnEtsJStatus: Error processing Instruction 0: Program failed to completeLog Messages: Program G5bbS1ipWzqQhekkiCLn6u7Y1jJdnGK85ceSYLx2kKbA invoke [1] Program log: Hello World Rust program entrypoint Program G5bbS1ipWzqQhekkiCLn6u7Y1jJdnGK85ceSYLx2kKbA consumed 200000 of 200000 compute units Program failed to complete: exceeded maximum number of instructions allowed (200000) at instruction #334 Program G5bbS1ipWzqQhekkiCLn6u7Y1jJdnGK85ceSYLx2kKbA failed: Program failed to complete本章对 Solana 区块链的基本概念进行了简要介绍,Solana 的智能合约叫做链上程序。在开始 Solana 链上程序开发之前,需要先安装和配置相关的编程环境,我们着重介绍了 Solana CLI 的安装和配置。
Hello World 是一个最新演示项目,通过对这个项目源码的解读,我们了解了如何使用 Rust 开发链上程序,并使用 Solana CLI 来构建与部署,以及使用 Solana JavaScript SDK 与链上程序进行交互。
游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。
同类文章
2026年比特币官方APP下载入口及官网安全访问指南
比特币官方入口在哪里?一个核心门户的权威指南 说起比特币,很多人第一反应是去找它的“官网”或“官方App”。但这里有个关键点需要先理清:比特币本质上是一种去中心化的全球数字货币,它不属于任何一家公司或机构,而是由一个庞大的、遍布全球的社区共同维护。因此,它并没有传统意义上由某个企业运营的“官方网站”
加密货币市值是什么?它能否真实衡量项目的价值与潜力?
加密货币市值:一把关键的标尺,但绝非万能钥匙 在纷繁复杂的加密货币世界里,如何快速判断一个项目的体量与市场地位?市值,无疑是投资者最先接触到的关键指标之一。它简单明了,却又暗藏玄机。今天,我们就来深入聊聊这个既基础又核心的概念,解析其计算逻辑、核心作用,并揭示其背后容易被忽略的局限性。 什么是加密货
精选五大免费加密货币行情平台:实时追踪币价与市场动态
想要在快速变化的加密货币市场中抓住机会,实时、准确的行情数据是必不可少的工具。本文为您精选了一系列功能强大且免费的加密货币行情平台,帮助您轻松追踪价格动态,做出更明智的投资决策。 顶级加密货币行情平台推荐 先说几个核心判断:在眼花缭乱的行情工具里,真正好用的往往就那么几个。下面这份清单,就帮你把那些
币圈行情实时查看渠道汇总与专业分析平台推荐
在瞬息万变的加密货币市场中,及时获取准确、全面的行情数据是每一位投资者和爱好者的核心需求 价格的剧烈波动、市值的实时更新,还有海量项目的动态,都要求我们必须依赖专业的数据平台来做决策。一个真正好用的行情查看渠道,绝不只是提供基础价格那么简单。它得能通过深度整合的数据、直观的图表工具和及时的行业资讯,
代币与加密货币的区别是什么?一文读懂Coin和Token
代币(Token)和币(Coin)有什么区别?它们是一回事吗? 在加密货币的日常讨论里,“代币”和“币”这两个词经常被混为一谈。但事实上,它们代表着截然不同的两种数字资产。最核心的区分标准在于:一个“币”拥有自己独立的、原生的区块链网络,而“代币”则没有自己的链,它更像是搭建在现有区块链平台之上的“
- 日榜
- 周榜
- 月榜
1
2
3
4
5
6
7
8
9
10
相关攻略
2015-03-10 11:25
2015-03-10 11:05
2021-08-04 13:30
2015-03-10 11:22
2015-03-10 12:39
2022-05-16 18:57
2025-05-23 13:43
2025-05-23 14:01
热门教程
- 游戏攻略
- 安卓教程
- 苹果教程
- 电脑教程
热门话题



