diff --git a/404.html b/404.html index 9bec7181a..0e13659cf 100644 --- a/404.html +++ b/404.html @@ -9,13 +9,13 @@ - - + +
Skip to main content

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

- - + + \ No newline at end of file diff --git a/Solana-Co-Learn/index.html b/Solana-Co-Learn/index.html index c5d976995..40d190af0 100644 --- a/Solana-Co-Learn/index.html +++ b/Solana-Co-Learn/index.html @@ -9,13 +9,13 @@ - - + +
-
Skip to main content
- - +
Skip to main content
+ + \ No newline at end of file diff --git a/Solana-Co-Learn/module1/block-chain-basic/index.html b/Solana-Co-Learn/module1/block-chain-basic/index.html index acce69eb2..01da6b1ab 100644 --- a/Solana-Co-Learn/module1/block-chain-basic/index.html +++ b/Solana-Co-Learn/module1/block-chain-basic/index.html @@ -9,16 +9,16 @@ - - + +
Skip to main content

区块链基本概念介绍

1. 什么是区块链?

1.1 区块链的定义

来自维基百科的定义:

  • 区块链是借由密码学和共识机制等技术建立与存储庞大交易资料串连的点对点网路系统。

网络上其它一些定义:

  • 区块链是一个去中心化的、可共享的、防篡改的分布式账本。
  • 区块链技术是一种高级数据库机制,允许在企业网络中透明的共享信息。
  • 区块链是一个共享的、不可篡改的账本,旨在促进业务网络中的交易记录和资产跟踪流程。

总结如下:

  • 从功能上说,区块链系统是一个去中心化的、可共享的、不可篡改的分布式账本。
  • 从使用技术上说,区块链系统是由密码学、共识机制、p2p等技术建立的软件系统。

1.2 比特币和区块链的关系

比特币的出现标志着区块链技术的产生,换句话说,比特币系统的主要技术就是区块链技术。 比特币是第一个区块链系统,中本聪可以说是区块链技术的始祖。

1.3 区块链技术的演进

  • 2008年,中本聪在网上发布了一篇名为《Bitcoin: A Peer-to-Peer Electronic Cash System》。
  • 2008年11月16日,中本聪公布了比特币系统的源代码。
  • 2009年1月3日(比特币的创世时刻),中本聪在互联网上上线了比特币网络。比特币的产生标志着区块链进入1.0时代。
  • 2013年,V神提出:图灵完备、可编程的区块链。从此之后区块链有了智能合约。以太坊的产生标志着区块链进入2.0时代。
  • 从以太坊之后,不断有新的区块链技术出现,像SolanaPolkadotComosNear等等。

2 从使用者的角度看,区块链是怎样运行的?

下面主要以比特币为例,看看区块链系统是怎么运行的。在开始之前,我们需要明确一下比特币的功能。比特币系统是一个软件系统,这个软件系统的主要功能实际就是记账,记录的内容就是谁有多少个比特币,例如“小明有3个比特币”。当然比特币真正的记账不会这么简单,因为它实际上是以一种叫做UTXO的结构在记账,但是此处希望大家明白的是它主要的功能就是记账。

2.1 比特币的运行

下面我们就从小明和小红之间的转账过程来看看区块链系统是怎么样运行的。 s

  • 假定开始时:小明已经有了3个比特币,小红没有比特币,此时在比特币系统中就会类似记录“小明, 3个比特币”。
  • 当小明通过比特币系统转给小红1个比特币后,比特币系统中的记录就会变成“小明,2个比特币;小红,1个比特币”。 -特别说明:比特币是使用UTXO的方式记账,此处为了方便演示区块链系统运行的过程,所以对记账以账户类型方式做替代,如需了解比特币正在的记账方式请查阅UTXO相关资料。

当“小明转给小红1个比特币”时,其发生的过程主要如下:

  1. 小明通过终端(手机或者电脑)将交易发给比特币系统的某个节点(例如上图中的计算机1),这个交易记录的主要信息就是“小明转给小红1个比特币”。

  2. 计算机1上的比特币程序就会做如下几件事情:

  • 检查交易的合法性。
  • 将交易广播给其它相连的节点(计算机上的比特币程序实际上就是比特币网络中的一个节点,后面我们都称为节点)。
  1. 其它节点通过网络收到这个交易后,也都会做如下的事情:
  • 检查交易的合法性。
  • 广播给其它相连的节点。
  1. 除了上述的上述的动作外,每个节点还会持续进行挖矿的操作(这里假定系统中所有的节点都是全节点,便于说明原理),具体行为为:
  • 将收到的交易进行打包,打包之后的结构叫做区块。
  • 区块的区块头里面要填充一些信息,这些信息包括前一个区块头的哈希,区块链中所有交易哈希的默克尔树的树根,还有解一个难题的难度值等。
  • 将打包好的区块广播给网络中的其它节点。
  1. 每个节点收到区块后(每个节点自己打包区块,同时也会从网络中收到其它的区块),会进行如下处理:
  • 对收到的区块进行验证。
  • 然后将新收到的区块进行存储,这样每个节点上就会有一个区块组成的链条形式的账本(因为每个区块头里面有前一个区块头的信息)。
  1. “小明转给小红1个比特币”这个交易信息最终就被记录在某个区块中,我们就可以认为这个信息已经被记录在比特币系统中了,其对应的效果就类似于记录“小明,2个比特币;小红,1个比特币”。

2.2 几个重要的问题

  1. 如何验证交易的合法性?

每个节点收到交易后如何验证交易的合法性,换句话说,就是怎么知道“小明可以给小红转1个比特币”。这是因为,每个节点都保存有区块链的账本(一个区块连着一个区块),在收到交易后,它会查自己账本中对应的情况,看小明是不是有3个比特币。

  1. 如何保证挖矿的节点真实的打包区块?

上面的交易相对来说好验证,但是如果某个节点打包时,自己私自写一个交易或者篡改一个交易到区块中,其它区块怎么验证,如果无法验证的话,那么这个账本的可信度岂不是无法保证。

这里就涉及到每个节点挖矿时都需要解决一个难题,而这个难题的计算需要一定的代价(需要不断的计算,花费电费),当它打包的区块被大部分节点都加入到区块链条中后,会得到相应的奖励。当其它节点收到别的节点打包的区块时,会进行下面的验证:

  • 验证这个难题的答案是不是正确。
  • 验证是不是区块链里面的每笔交易是不是正确(会根据自己本地的账本验证交易是不是正确)。

试想一下,如果有一个作恶的节点,当它花费一定的电量打包了一个区块,而此区块中有一笔或者多笔篡改的交易;当其它节点收到该区块后,根据上面的第二点验证时,验证不过,那么就会抛弃此区块。此时作恶节点就得不到挖矿的奖励,但是它挖矿时是花费了成本的,这对它来说并不划算,所以正常来说,挖矿的节点不会做这种吃力不讨好的事情。即使有个别的节点做这样的事情,也不会对最终的结果产生影响。

  1. 多个节点都在打包区块,那么当其它节点收到区块后,必然会出现一个高度有多个区块的情况(即每个区块的前一个区块头信息都一样),那么存储时以那个区块中的信息为准?

这个问题描述是下面这种情形:

在上图中,当节点5收到多个高度为n+1的区块,它的区块链链条该选择那个区块呢?在比特币中,选择链条最长的作为最终的账本,其它分叉上的区块就会无效。每个矿工节点在挖矿时,都会基于自己看到的最长的链条上的最新区块进行打包(例如接下来节点5要打包第n+3个区块,它就会选择在中间那个链条上block n+2作为父区块挖矿。),当所有节点都如此选择时,在所有节点上的链条最终会一致(就是那条最长的链条)。

小结:

上面的第2和第3个问题实际上可以合成一个问题,就是如何保证所有节点上的账本一致?而在区块链系统中解决这部分问题的机制就叫做共识机制。

3 从实现的角度看,看区块链的架构

3.1 比特币系统架构

根据上一节比特币系统的运行,我们大致可以知道比特币系统(区块链系统类似)的架构。

  • 用户可以发送交易到比特币系统,所以比特币系统中有Rpc server或者是http server
  • 节点之间可以通信,且节点之间的地位对等,那么需要P2P网络部分。
  • 每个节点收到交易后,首先是将交易放在交易池中,挖矿时是从交易池取出交易进行打包,所以需要交易池。
  • 需要保证所有节点上的账本一致,所以需要共识模块。
  • 区块形成的链条在每个节点上都是持久化存储的,所以需要数据库用于节点上数据的持久化。
  • 共识里面涉及到的区块链条、区块、交易等结构的一些定义和组织形式,所以需要账本模块。
  • 其它还涉及到一些安全、加密相关的模块。

根据这些推断,我们可以得出比特币的大致架构如下:

3.2 以太坊系统架构

下图是从网络中找到的以太坊的架构图:

我们可以看到和比特币的架构图大体上差不多,主要是多了智能合约相关的内容。

3. 通常的区块链系统架构

其实从前面的比特币和以太坊架构,我们也可以推断出来,大部分区块链系统的架构都差不太多,尤其是现在常见的区块链系统,其架构都类似于以太坊的架构。有差别的主要在于共识模块、分布式账本的具体实现。

4 solana的原理

前面主要介绍了区块链的基础知识,再讲讲Solana的相关原理。具体内容可以参考这篇文章

5 推荐资料

  • 《精通比特币》

参考资料💾

- - +特别说明:比特币是使用UTXO的方式记账,此处为了方便演示区块链系统运行的过程,所以对记账以账户类型方式做替代,如需了解比特币正在的记账方式请查阅UTXO相关资料。

当“小明转给小红1个比特币”时,其发生的过程主要如下:

  1. 小明通过终端(手机或者电脑)将交易发给比特币系统的某个节点(例如上图中的计算机1),这个交易记录的主要信息就是“小明转给小红1个比特币”。

  2. 计算机1上的比特币程序就会做如下几件事情:

  1. 其它节点通过网络收到这个交易后,也都会做如下的事情:
  1. 除了上述的上述的动作外,每个节点还会持续进行挖矿的操作(这里假定系统中所有的节点都是全节点,便于说明原理),具体行为为:
  1. 每个节点收到区块后(每个节点自己打包区块,同时也会从网络中收到其它的区块),会进行如下处理:
  1. “小明转给小红1个比特币”这个交易信息最终就被记录在某个区块中,我们就可以认为这个信息已经被记录在比特币系统中了,其对应的效果就类似于记录“小明,2个比特币;小红,1个比特币”。

2.2 几个重要的问题

  1. 如何验证交易的合法性?

每个节点收到交易后如何验证交易的合法性,换句话说,就是怎么知道“小明可以给小红转1个比特币”。这是因为,每个节点都保存有区块链的账本(一个区块连着一个区块),在收到交易后,它会查自己账本中对应的情况,看小明是不是有3个比特币。

  1. 如何保证挖矿的节点真实的打包区块?

上面的交易相对来说好验证,但是如果某个节点打包时,自己私自写一个交易或者篡改一个交易到区块中,其它区块怎么验证,如果无法验证的话,那么这个账本的可信度岂不是无法保证。

这里就涉及到每个节点挖矿时都需要解决一个难题,而这个难题的计算需要一定的代价(需要不断的计算,花费电费),当它打包的区块被大部分节点都加入到区块链条中后,会得到相应的奖励。当其它节点收到别的节点打包的区块时,会进行下面的验证:

试想一下,如果有一个作恶的节点,当它花费一定的电量打包了一个区块,而此区块中有一笔或者多笔篡改的交易;当其它节点收到该区块后,根据上面的第二点验证时,验证不过,那么就会抛弃此区块。此时作恶节点就得不到挖矿的奖励,但是它挖矿时是花费了成本的,这对它来说并不划算,所以正常来说,挖矿的节点不会做这种吃力不讨好的事情。即使有个别的节点做这样的事情,也不会对最终的结果产生影响。

  1. 多个节点都在打包区块,那么当其它节点收到区块后,必然会出现一个高度有多个区块的情况(即每个区块的前一个区块头信息都一样),那么存储时以那个区块中的信息为准?

这个问题描述是下面这种情形:

在上图中,当节点5收到多个高度为n+1的区块,它的区块链链条该选择那个区块呢?在比特币中,选择链条最长的作为最终的账本,其它分叉上的区块就会无效。每个矿工节点在挖矿时,都会基于自己看到的最长的链条上的最新区块进行打包(例如接下来节点5要打包第n+3个区块,它就会选择在中间那个链条上block n+2作为父区块挖矿。),当所有节点都如此选择时,在所有节点上的链条最终会一致(就是那条最长的链条)。

小结:

上面的第2和第3个问题实际上可以合成一个问题,就是如何保证所有节点上的账本一致?而在区块链系统中解决这部分问题的机制就叫做共识机制。

3 从实现的角度看,看区块链的架构

3.1 比特币系统架构

根据上一节比特币系统的运行,我们大致可以知道比特币系统(区块链系统类似)的架构。

根据这些推断,我们可以得出比特币的大致架构如下:

3.2 以太坊系统架构

下图是从网络中找到的以太坊的架构图:

我们可以看到和比特币的架构图大体上差不多,主要是多了智能合约相关的内容。

3. 通常的区块链系统架构

其实从前面的比特币和以太坊架构,我们也可以推断出来,大部分区块链系统的架构都差不太多,尤其是现在常见的区块链系统,其架构都类似于以太坊的架构。有差别的主要在于共识模块、分布式账本的具体实现。

4 solana的原理

前面主要介绍了区块链的基础知识,再讲讲Solana的相关原理。具体内容可以参考这篇文章

5 推荐资料

参考资料💾

+ + \ No newline at end of file diff --git a/Solana-Co-Learn/module1/client-side-development/build-an-interaction-script/index.html b/Solana-Co-Learn/module1/client-side-development/build-an-interaction-script/index.html index 8b32dfae5..d2e179d09 100644 --- a/Solana-Co-Learn/module1/client-side-development/build-an-interaction-script/index.html +++ b/Solana-Co-Learn/module1/client-side-development/build-an-interaction-script/index.html @@ -9,13 +9,13 @@ - - + +
-
Skip to main content

📝 构建一个互动脚本

你准备好与Solana网络交互了吗?

我们将编写一个脚本,生成一个密钥对,使用devnet SOL进行充值,并与Solana网络上的现有程序进行交互。

这个程序是一个简单的“ping”计数器:每次我们触发它,它就会记录我们的ping行为,并增加一个计数器。稍后我们会深入了解Rust和自己的程序开发,但现在我们先使用JS/TS来实现。

🚧 在本地设置 Solana 客户端

现在让我们换一种方式来操作 - 我们将不再使用React/Next.js,而是采用纯TypeScript来构建一个本地客户端。这样的方法比搭建前端并构建复杂用户界面要快得多。你可以在单独的 TS 文件中进行开发,并通过异步方式让它与网络进行交互。

首先,在你的Solana工作区中创建一个新文件夹,然后使用以下便捷命令来设置本地客户端:

npx create-solana-client solana-intro-client

如果系统询问你是否要安装create-solana-client软件包,请选择“是”。

接下来,只需导航到新创建的目录,并使用文本编辑器打开它:

cd solana-intro-client
code .

现在你可以开始你的Solana客户端开发之旅了!

⚙ 客户端脚本的设置

使用 create-solana-client 的好处在于,我们可以立即开始编写客户端代码!打开 index.ts,导入必要的依赖项,并添加这个 initializeKeypair 函数:

import * as Web3 from '@solana/web3.js';
import * as fs from 'fs';
import dotenv from 'dotenv';
dotenv.config();

async function main() {

}

main()
.then(() => {
console.log('执行成功完成');
process.exit(0);
})
.catch((error) => {
console.log(error);
process.exit(1);
});

在终端中运行 npm start 后,你将看到脚本已经开始运行!只需一条命令,Solana 客户端就已设置完毕。

现在让我们添加一个 initializeKeypair 函数。如果我们没有密钥对,它将自动为我们创建一个。在导入之后添加以下代码:

async function initializeKeypair(connection: Web3.Connection): Promise<Web3.Keypair> {
// 如果没有私钥,生成新密钥对
if (!process.env.PRIVATE_KEY) {
console.log('正在生成新密钥对... 🗝️');
const signer = Web3.Keypair.generate();

console.log('正在创建 .env 文件');
fs.writeFileSync('.env', `PRIVATE_KEY=[${signer.secretKey.toString()}]`);

return signer;
}

const secret = JSON.parse(process.env.PRIVATE_KEY ?? '') as number[];
const secretKey = Uint8Array.from(secret);
const keypairFromSecret = Web3.Keypair.fromSecretKey(secretKey);
return keypairFromSecret;
}

这个函数非常聪明 - 如果你的 .env 文件中没有私钥,它就会创建一个新的!

你已经非常熟悉这里的操作了 - 我们调用 Web3.Keypair.generate() 函数并将结果写入本地的 dotenv 文件。创建后,我们返回密钥对,以便我们可以在脚本的其他部分中使用它。

你可以更新 main 函数并使用 npm start 运行脚本进行测试:

async function main() {
const connection = new Web3.Connection(Web3.clusterApiUrl('devnet'));
const signer = await initializeKeypair(connection);

console.log("公钥:", signer.publicKey.toBase58());
}

你应该会在终端中看到类似的输出:

> solana-course-client@1.0.0 start
> ts-node src/index.ts

正在生成新密钥对... 🗝️
正在创建 .env 文件
公钥: jTAsqBrjsYp4uEJNmED5R66gHPnFW4wvQrbmFG3c4QS
执行成功完成

很好!如果你检查 .env 文件,你会发现一串字节格式的私钥!请注意保密此文件。如果你将此文件推送到公共的 GitHub 存储库,任何人都可以访问其中的资金,因此请确保不要用它处理真实的货币。

再次运行 npm start 会使用现有的私钥而不会创建新的。

保持测试账户的独立非常重要,这也是这个脚本特别酷的原因 - 它消除了创建和管理测试钱包的麻烦。

现在,如果我们还能自动获取 devnet SOL 就更好了。哦等等,我们确实可以!

看看这个超酷的空投功能。

async function airdropSolIfNeeded(
signer: Web3.Keypair,
connection: Web3.Connection
) {
// 检查余额
const balance = await connection.getBalance(signer.publicKey);
console.log('当前余额为', balance / Web3.LAMPORTS_PER_SOL, 'SOL');

// 如果余额少于 1 SOL,执行空投
if (balance / Web3.LAMPORTS_PER_SOL < 1) {
console.log('正在空投 1 SOL');
const airdropSignature = await connection.requestAirdrop(
signer.publicKey,
Web3.LAMPORTS_PER_SOL
);

const latestBlockhash = await connection.getLatestBlockhash();

await connection.confirmTransaction({
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
signature: airdropSignature,
});

const newBalance = await connection.getBalance(signer.publicKey);
console.log('新余额为', newBalance / Web3.LAMPORTS_PER_SOL, 'SOL');
}
}

这可能看得让人有些头大,但其实你对于这里正在发生的事情应该相当了解!我们正在借助我们熟悉的getBalance来查看我们的余额是否不足,如果不足,我们就会用requestAirdrop函数来获取一些资金。

区块哈希和区块高度是识别区块的标识符,用以确保我们是最新的,也不会发送陈旧的交易。

不过,别试图反复运行它,因为水龙头有冷却时间,如果你不停地向它请求,请求将会失败。

在创建或获取密钥对之后,请确保更新initializeKeypair函数,以便调用空投。

// 当生成密钥对时
await airdropSolIfNeeded(signer, connection);

// 当从密钥解析时
await airdropSolIfNeeded(keypairFromSecret, connection);

现在,当你运行npm run start时,你将看到空投的情况:

当前余额为 0 SOL
正在空投 1 SOL
新的余额为 1 SOL
公共密钥: 7Fw3bXskk5eonycvET6BSufxAsuNudvuxF7MMnS8KMqX

我们已经准备好大展身手了,让我们一展拳脚吧 🥊!

🖱 调用链上程序

现在是时候让我们的客户端显示实力了。我们将在Solana网络上的现有程序中写入数据。有人可能会以为Solana的开发只和用Rust编写程序有关,其实不然!大部分区块链开发实际上与现有程序进行交互。

你可以构建数百个只与现有程序交互的应用,这就是真正有趣的地方!我们会让事情保持简单——我们的客户端会发送一个计数器程序,并递增计数器。这样你就能在网络上公告你是一名开发者了。

首先,我们需要告诉客户端它将与哪些程序交互。在导入语句下方的开头部分,添加这些地址:

const PROGRAM_ID = new Web3.PublicKey("ChT1B39WKLS8qUrkLvFDXMhEJ4F1XZzwUNHUt4AU9aVa")
const PROGRAM_DATA_PUBLIC_KEY = new Web3.PublicKey("Ah9K7dQ8EHaZqcAsgBW8w37yN2eAy3koFmUn4x3CJtod")

PROGRAM_ID 是“ping”程序本身的地址。PROGRAM_DATA_PUBLIC_KEY 是存储程序数据的账户地址。记得,可执行代码和状态数据在Solana上是分开存储的!

然后,添加下列函数以在任何地方调用“ping”程序:

async function pingProgram(connection: Web3.Connection, payer: Web3.Keypair) {
const transaction = new Web3.Transaction()
const instruction = new Web3.TransactionInstruction({
// Instructions need 3 things

// 1. The public keys of all the accounts the instruction will read/write
keys: [
{
pubkey: PROGRAM_DATA_PUBLIC_KEY,
isSigner: false,
isWritable: true
}
],

// 2. The ID of the program this instruction will be sent to
programId: PROGRAM_ID

// 3. Data - in this case, there's none!
})

transaction.add(instruction)
const transactionSignature = await Web3.sendAndConfirmTransaction(connection, transaction, [payer])

console.log(
`Transaction https://explorer.solana.com/tx/${transactionSignature}?cluster=devnet`
)
}

这个过程并不像看起来那么复杂!你已经熟悉这部分了:

  • 我们创建一个交易
  • 我们制定一项指令
  • 我们将指令添加到交易中
  • 我们将交易发送到网络!

回顾一下上面的代码注释,了解指令的三个主要部分。

其中关键的部分是keys值——它是一个数组,包含指令将读取或写入的每个账户的元数据。在我们的例子中,我告诉你该指令将处理哪些账户。

你必须知道这个是什么——可以通过阅读程序本身或其文档来了解。如果你不了解这一点,就无法与程序互动,因为指令会无效。

可以将这个过程想象成试图开车去一个没有 GPS 地址的地方。你知道你想去哪里,但不知道如何到达那里。

由于此操作不需要数据账户的签名,我们将isSigner设置为falseisWritable设置为true,因为该账户将被写入。

通过告知网络我们需要与哪些账户交互,以及我们是否正在向它们写入数据,Solana运行时就会知道哪些交易可以并行运行。这部分就是Solana速度如此之快的原因之一!

main()中加入此函数的调用await pingProgram(connection, signer),并用npm start运行脚本。访问所记录的资源管理器链接,你将在页面底部看到你写入的数据(其他所有内容可以忽略)。

你刚刚将数据写入了区块链。感觉简单吗?

虽然看起来很简单,但你确实已经成功了。当推特上的人们都在热衷于猴子图片时,你正在构建真正有价值的东西。你在本节学到的内容——从Solana网络读取和写入数据——足以制作价值 1 万美元的产品。想象一下,当你完成这个项目时,你还能做些什么🤘!

🚢 挑战 - SOL 转账脚本

既然我们一同学习了如何将交易发送到网络,现在是时候让你独立尝试了。

参照前一步骤的流程,从头开始创建一个脚本,让你能够在Devnet上从一个账户转移 SOL 到另一个账户。请确保打印交易签名,以便你可以在Solana Explorer上查看它。

回顾一下到目前为止你学到的东西:

  • 将数据写入网络是通过事务实现的
  • 交易需要指令
  • 指令向网络指示涉及哪些程序及其功能
  • SOL的转移是通过系统程序完成的(嗯,我在想这个过程叫什么名字。🤔 转移吗?)

你在这里需要做的就是找出准确的函数名称,以及指令应该是怎样的。你可以从Google开始查找:P

附注:如果你确定自己已经了解了这些内容,但转账仍然失败,那么问题可能是转账金额太少——尝试至少转账0.1 SOL

就像以往一样,在查看解决方案代码之前,尽量自己完成这个任务。当你真正需要参考解决方案时,请点击这里查看。👀

- - +
Skip to main content

📝 构建一个互动脚本

你准备好与Solana网络交互了吗?

我们将编写一个脚本,生成一个密钥对,使用devnet SOL进行充值,并与Solana网络上的现有程序进行交互。

这个程序是一个简单的“ping”计数器:每次我们触发它,它就会记录我们的ping行为,并增加一个计数器。稍后我们会深入了解Rust和自己的程序开发,但现在我们先使用JS/TS来实现。

🚧 在本地设置 Solana 客户端

现在让我们换一种方式来操作 - 我们将不再使用React/Next.js,而是采用纯TypeScript来构建一个本地客户端。这样的方法比搭建前端并构建复杂用户界面要快得多。你可以在单独的 TS 文件中进行开发,并通过异步方式让它与网络进行交互。

首先,在你的Solana工作区中创建一个新文件夹,然后使用以下便捷命令来设置本地客户端:

npx create-solana-client solana-intro-client

如果系统询问你是否要安装create-solana-client软件包,请选择“是”。

接下来,只需导航到新创建的目录,并使用文本编辑器打开它:

cd solana-intro-client
code .

现在你可以开始你的Solana客户端开发之旅了!

⚙ 客户端脚本的设置

使用 create-solana-client 的好处在于,我们可以立即开始编写客户端代码!打开 index.ts,导入必要的依赖项,并添加这个 initializeKeypair 函数:

import * as Web3 from '@solana/web3.js';
import * as fs from 'fs';
import dotenv from 'dotenv';
dotenv.config();

async function main() {

}

main()
.then(() => {
console.log('执行成功完成');
process.exit(0);
})
.catch((error) => {
console.log(error);
process.exit(1);
});

在终端中运行 npm start 后,你将看到脚本已经开始运行!只需一条命令,Solana 客户端就已设置完毕。

现在让我们添加一个 initializeKeypair 函数。如果我们没有密钥对,它将自动为我们创建一个。在导入之后添加以下代码:

async function initializeKeypair(connection: Web3.Connection): Promise<Web3.Keypair> {
// 如果没有私钥,生成新密钥对
if (!process.env.PRIVATE_KEY) {
console.log('正在生成新密钥对... 🗝️');
const signer = Web3.Keypair.generate();

console.log('正在创建 .env 文件');
fs.writeFileSync('.env', `PRIVATE_KEY=[${signer.secretKey.toString()}]`);

return signer;
}

const secret = JSON.parse(process.env.PRIVATE_KEY ?? '') as number[];
const secretKey = Uint8Array.from(secret);
const keypairFromSecret = Web3.Keypair.fromSecretKey(secretKey);
return keypairFromSecret;
}

这个函数非常聪明 - 如果你的 .env 文件中没有私钥,它就会创建一个新的!

你已经非常熟悉这里的操作了 - 我们调用 Web3.Keypair.generate() 函数并将结果写入本地的 dotenv 文件。创建后,我们返回密钥对,以便我们可以在脚本的其他部分中使用它。

你可以更新 main 函数并使用 npm start 运行脚本进行测试:

async function main() {
const connection = new Web3.Connection(Web3.clusterApiUrl('devnet'));
const signer = await initializeKeypair(connection);

console.log("公钥:", signer.publicKey.toBase58());
}

你应该会在终端中看到类似的输出:

> solana-course-client@1.0.0 start
> ts-node src/index.ts

正在生成新密钥对... 🗝️
正在创建 .env 文件
公钥: jTAsqBrjsYp4uEJNmED5R66gHPnFW4wvQrbmFG3c4QS
执行成功完成

很好!如果你检查 .env 文件,你会发现一串字节格式的私钥!请注意保密此文件。如果你将此文件推送到公共的 GitHub 存储库,任何人都可以访问其中的资金,因此请确保不要用它处理真实的货币。

再次运行 npm start 会使用现有的私钥而不会创建新的。

保持测试账户的独立非常重要,这也是这个脚本特别酷的原因 - 它消除了创建和管理测试钱包的麻烦。

现在,如果我们还能自动获取 devnet SOL 就更好了。哦等等,我们确实可以!

看看这个超酷的空投功能。

async function airdropSolIfNeeded(
signer: Web3.Keypair,
connection: Web3.Connection
) {
// 检查余额
const balance = await connection.getBalance(signer.publicKey);
console.log('当前余额为', balance / Web3.LAMPORTS_PER_SOL, 'SOL');

// 如果余额少于 1 SOL,执行空投
if (balance / Web3.LAMPORTS_PER_SOL < 1) {
console.log('正在空投 1 SOL');
const airdropSignature = await connection.requestAirdrop(
signer.publicKey,
Web3.LAMPORTS_PER_SOL
);

const latestBlockhash = await connection.getLatestBlockhash();

await connection.confirmTransaction({
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
signature: airdropSignature,
});

const newBalance = await connection.getBalance(signer.publicKey);
console.log('新余额为', newBalance / Web3.LAMPORTS_PER_SOL, 'SOL');
}
}

这可能看得让人有些头大,但其实你对于这里正在发生的事情应该相当了解!我们正在借助我们熟悉的getBalance来查看我们的余额是否不足,如果不足,我们就会用requestAirdrop函数来获取一些资金。

区块哈希和区块高度是识别区块的标识符,用以确保我们是最新的,也不会发送陈旧的交易。

不过,别试图反复运行它,因为水龙头有冷却时间,如果你不停地向它请求,请求将会失败。

在创建或获取密钥对之后,请确保更新initializeKeypair函数,以便调用空投。

// 当生成密钥对时
await airdropSolIfNeeded(signer, connection);

// 当从密钥解析时
await airdropSolIfNeeded(keypairFromSecret, connection);

现在,当你运行npm run start时,你将看到空投的情况:

当前余额为 0 SOL
正在空投 1 SOL
新的余额为 1 SOL
公共密钥: 7Fw3bXskk5eonycvET6BSufxAsuNudvuxF7MMnS8KMqX

我们已经准备好大展身手了,让我们一展拳脚吧 🥊!

🖱 调用链上程序

现在是时候让我们的客户端显示实力了。我们将在Solana网络上的现有程序中写入数据。有人可能会以为Solana的开发只和用Rust编写程序有关,其实不然!大部分区块链开发实际上与现有程序进行交互。

你可以构建数百个只与现有程序交互的应用,这就是真正有趣的地方!我们会让事情保持简单——我们的客户端会发送一个计数器程序,并递增计数器。这样你就能在网络上公告你是一名开发者了。

首先,我们需要告诉客户端它将与哪些程序交互。在导入语句下方的开头部分,添加这些地址:

const PROGRAM_ID = new Web3.PublicKey("ChT1B39WKLS8qUrkLvFDXMhEJ4F1XZzwUNHUt4AU9aVa")
const PROGRAM_DATA_PUBLIC_KEY = new Web3.PublicKey("Ah9K7dQ8EHaZqcAsgBW8w37yN2eAy3koFmUn4x3CJtod")

PROGRAM_ID 是“ping”程序本身的地址。PROGRAM_DATA_PUBLIC_KEY 是存储程序数据的账户地址。记得,可执行代码和状态数据在Solana上是分开存储的!

然后,添加下列函数以在任何地方调用“ping”程序:

async function pingProgram(connection: Web3.Connection, payer: Web3.Keypair) {
const transaction = new Web3.Transaction()
const instruction = new Web3.TransactionInstruction({
// Instructions need 3 things

// 1. The public keys of all the accounts the instruction will read/write
keys: [
{
pubkey: PROGRAM_DATA_PUBLIC_KEY,
isSigner: false,
isWritable: true
}
],

// 2. The ID of the program this instruction will be sent to
programId: PROGRAM_ID

// 3. Data - in this case, there's none!
})

transaction.add(instruction)
const transactionSignature = await Web3.sendAndConfirmTransaction(connection, transaction, [payer])

console.log(
`Transaction https://explorer.solana.com/tx/${transactionSignature}?cluster=devnet`
)
}

这个过程并不像看起来那么复杂!你已经熟悉这部分了:

  • 我们创建一个交易
  • 我们制定一项指令
  • 我们将指令添加到交易中
  • 我们将交易发送到网络!

回顾一下上面的代码注释,了解指令的三个主要部分。

其中关键的部分是keys值——它是一个数组,包含指令将读取或写入的每个账户的元数据。在我们的例子中,我告诉你该指令将处理哪些账户。

你必须知道这个是什么——可以通过阅读程序本身或其文档来了解。如果你不了解这一点,就无法与程序互动,因为指令会无效。

可以将这个过程想象成试图开车去一个没有 GPS 地址的地方。你知道你想去哪里,但不知道如何到达那里。

由于此操作不需要数据账户的签名,我们将isSigner设置为falseisWritable设置为true,因为该账户将被写入。

通过告知网络我们需要与哪些账户交互,以及我们是否正在向它们写入数据,Solana运行时就会知道哪些交易可以并行运行。这部分就是Solana速度如此之快的原因之一!

main()中加入此函数的调用await pingProgram(connection, signer),并用npm start运行脚本。访问所记录的资源管理器链接,你将在页面底部看到你写入的数据(其他所有内容可以忽略)。

你刚刚将数据写入了区块链。感觉简单吗?

虽然看起来很简单,但你确实已经成功了。当推特上的人们都在热衷于猴子图片时,你正在构建真正有价值的东西。你在本节学到的内容——从Solana网络读取和写入数据——足以制作价值 1 万美元的产品。想象一下,当你完成这个项目时,你还能做些什么🤘!

🚢 挑战 - SOL 转账脚本

既然我们一同学习了如何将交易发送到网络,现在是时候让你独立尝试了。

参照前一步骤的流程,从头开始创建一个脚本,让你能够在Devnet上从一个账户转移 SOL 到另一个账户。请确保打印交易签名,以便你可以在Solana Explorer上查看它。

回顾一下到目前为止你学到的东西:

  • 将数据写入网络是通过事务实现的
  • 交易需要指令
  • 指令向网络指示涉及哪些程序及其功能
  • SOL的转移是通过系统程序完成的(嗯,我在想这个过程叫什么名字。🤔 转移吗?)

你在这里需要做的就是找出准确的函数名称,以及指令应该是怎样的。你可以从Google开始查找:P

附注:如果你确定自己已经了解了这些内容,但转账仍然失败,那么问题可能是转账金额太少——尝试至少转账0.1 SOL

就像以往一样,在查看解决方案代码之前,尽量自己完成这个任务。当你真正需要参考解决方案时,请点击这里查看。👀

+ + \ No newline at end of file diff --git a/Solana-Co-Learn/module1/client-side-development/index.html b/Solana-Co-Learn/module1/client-side-development/index.html index c61b60095..0a5111840 100644 --- a/Solana-Co-Learn/module1/client-side-development/index.html +++ b/Solana-Co-Learn/module1/client-side-development/index.html @@ -9,13 +9,13 @@ - - + +
-
Skip to main content
- - +
Skip to main content
+ + \ No newline at end of file diff --git a/Solana-Co-Learn/module1/client-side-development/read-data-from-the-solana-network/index.html b/Solana-Co-Learn/module1/client-side-development/read-data-from-the-solana-network/index.html index 723d48290..c8c283760 100644 --- a/Solana-Co-Learn/module1/client-side-development/read-data-from-the-solana-network/index.html +++ b/Solana-Co-Learn/module1/client-side-development/read-data-from-the-solana-network/index.html @@ -9,13 +9,13 @@ - - + +
-
Skip to main content

从 Solana 🤓 区块链读取数据

作为Solana开发者,这正是你学习之旅的起点。我们将从基础开始——从区块链中读取数据。

👜 Solana上的账户

Solana的字母表开始,我们有A代表账户。我们从账户开始,因为Solana上的智能合约(也称为“程序”)是无状态的——除了代码之外,它们不存储任何信息。一切都存储在账户中,所以账户对Solana来说至关重要。它们负责存储、合约和本地区块链程序的管理。

Solana 上有三种类型的账户:

  • 数据帐户 - 就是用来存储数据的!
  • 程序帐户 - 存储可执行程序(又称智能合约)的地方
  • 原生账户 - 用于核心区块链功能,例如权益和投票的账户

至于原生账户,是区块链运行所必需的,我们稍后会详细介绍。目前,我们只关注数据和程序账户。

在数据账户方面,还有两种进一步的类型:

  • 系统拥有的帐户
  • PDA(程序派生地址)帐户

我们很快会介绍这些具体是什么,现在请跟着一起学习。

每个账户都有一些你应该了解的字段:

字段描述
lamports账户拥有的lamports数量
owner账户的所有者程序
executable账户是否可以处理指令
data账户存储的数据的字节码
rent_epoch下一个需要付租金的epoch(周期)

我们现在只关注我们需要了解的部分,所以即使有些内容不那么清晰,也请勇往直前 - 我们将在学习过程中填补这些空白。

LamportSolana的最小单位,类似于以太坊的Gwei。一个Lamport等于0.000000001 SOL,所以这个字段告诉我们账户拥有多少SOL

每个账户都有一个公钥,就像账户的地址一样。你知道你的钱包有一个地址来接收那些炫酷的NFT吗?这就是同样的原理!Solana的地址只是用base58编码的字符串。

executable 是一个布尔字段,表示该帐户是否包含可执行数据。至于数据,就是存储在帐户中的内容,至于租金,我们稍后会谈到!

现在,让我们从简单的事情开始学习吧 :)

📫 从网络中读取

好的,我们现在明白了什么是账户,那么如何读取它们呢?我们将借助一个名为 JSON RPC 终端点的工具!你可以通过下图了解,你将在 Solana 网络中充当客户端角色,尝试获取信息。

你可以通过调用 JSON RPCAPI来获取所需的内容,它会与网络沟通并返回你所需的数据。

假设我们要查询账户的余额,API调用将如下所示:

async function getBalanceUsingJSONRPC(address: string): Promise<number> {
const url = clusterApiUrl('devnet')
console.log(url);
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
"jsonrpc": "2.0",
"id": 1,
"method": "getBalance",
"params": [
address
]
})
}).then(response => response.json())
.then(json => {
if (json.error) {
throw json.error
}

return json['result']['value'] as number;
})
.catch(error => {
throw error
})
}

这里涉及了很多步骤。我们正在进行一个POST请求,请求体中包含特定的参数来指导RPC执行什么操作。我们要指定RPC的版本、id、方法(本例中是getBalance),以及该方法所需的参数(本例中只有address)。

由于我们对一个非常简单的方法有大量的样板代码,我们可以使用SolanaWeb3.js SDK。以下是所需的代码:

async function getBalanceUsingWeb3(address: PublicKey): Promise<number> {
const connection = new Connection(clusterApiUrl('devnet'));
return connection.getBalance(address);
}

这里有Solana RPC API 关于介绍如何使用getBalance文档

是不是很美观?我们仅用三行代码就能获取某人的Solana余额。试想一下,如果获取任何人的银行余额也能如此简单。

现在你已经了解了如何从Solana的账户中读取数据!虽然这看起来可能很简单,但只要使用这个函数,你就能查询Solana上任何账户的余额。试想一下,如果你能获取地球上任何银行账户的余额,这将是多么强大的能力。

🤑 构建一个余额获取器

现在是时候构建一个通用的余额查询器了(假设整个宇宙都在Solana上运行)。这将是一个简洁而强大的应用程序,能查询Solana上任何账户的余额。

首先,在你的工作空间中创建一个文件夹,比如放在桌面上。克隆起始库并按照以下步骤设置:

git clone https://github.com/all-in-one-solana/solana-intro-frontend
cd solana-intro-frontend
git checkout starter
npm i

这是一个简单的Next.js应用程序,安装了所有依赖项后,你可以使用 npm run dev 命令启动它。你应该能在localhost上看到如下内容:

我们为你准备了一个基本的Next.js应用程序,并添加了一些样式。如果你在地址栏中输入并点击“检查SOL余额”按钮,你将看到1,000 SOL的余额显示。现在是时候让它真正起作用了。

首先,你需要安装Solana/web3.js库:

npm install @solana/web3.js

这将为我们提供一个非常简洁的函数来查询余额。转到 index.tsx 并将其导入到顶部:

import * as web3 from '@solana/web3.js'

接下来,我们将开始处理 addressSubmittedHandler 函数。首先,我们要将地址从字符串转换为公钥。记住,地址实际上并不是字符串,而是在JS中用字符串表示的。

const addressSubmittedHandler = (address: string) => {
const key = new web3.PublicKey(address);
setAddress(address)
setBalance(1000)
}

这将验证你输入的内容是否是一个Solana地址。现在,如果你在地址栏中输入的内容不是一个密钥,应用程序会崩溃。很好!

现在,我们要使用这个密钥建立一个新的连接到JSON RPC。通过这个连接,我们将使用 getBalance 函数并使用 setBalance 设置结果!下面是完整的函数:

const addressSubmittedHandler = (address: string) => {
const key = new web3.PublicKey(address);
setAddress(key.toBase58())

const connection = new web3.Connection(web3.clusterApiUrl('devnet'))

connection.getBalance(key).then(balance => {
setBalance(balance / web3.LAMPORTS_PER_SOL)
})
}

这里有一些新元素:

  • 我们正在使用 key.toBase58 来设置地址,这是Solana地址的字符串编码方式。
  • 我们正在连接到 devnet 网络,有三个网络可供选择:主网、测试网和开发网。我们将在开发网上进行所有操作。
  • 我们正在将余额从Lamports转换为SOL,因为余额是以Lamports返回的,而不是SOL

我们做完了!如果你在这里粘贴一个地址,你会看到余额显示。确保你的账户有开发网络上的SOL!如果没有,你可以使用我的账户来测试应用 - B1aLAAe4vW8nSQCetXnYqJfRxzTjnbooczwkUJAr7yMS

这个方案不错,但如果地址输入错误,就会出现一个很难处理的错误。让我们添加一些错误处理机制来解决这个问题。

const addressSubmittedHandler = (address: string) => {
try {
setAddress(address)
const key = new web3.PublicKey(address)
const connection = new web3.Connection(web3.clusterApiUrl('devnet'))
connection.getBalance(key).then(balance => {
setBalance(balance / web3.LAMPORTS_PER_SOL)
})
} catch (error) {
setAddress('')
setBalance(0)
alert(error)
}
}

现在,如果出现错误,你将会收到一个提示信息了 :D

哇塞,你刚刚成功发布了自己的第一个Solana应用。真棒!继续加油!

🚢 挑战

现在,让我们通过一个小挑战来考验一下你所学的知识。从你刚刚完成的应用出发,你需要在用户界面中添加一项功能,显示输入的地址是否为可执行账户。

要判断一个账户是否可执行,你需要执行以下操作:

  1. 使用方法 getAccountInfo 来获取包含账户信息的JSON对象。
  2. 检查该对象的属性以确定其是否可执行。
  3. useState 中添加另一个调用,允许你从账户信息中设置 executable 属性值,并在用户界面中展示。

下面是一个可供测试的账户地址:ComputeBudget111111111111111111111111111111

请尽量自己尝试解决,不要提前查看答案!相信你会发现挑战其实并不复杂。

完成后,你可以在这里查看挑战解决方案的参考代码。

📚 更多关于账户相关的资源

- - +
Skip to main content

从 Solana 🤓 区块链读取数据

作为Solana开发者,这正是你学习之旅的起点。我们将从基础开始——从区块链中读取数据。

👜 Solana上的账户

Solana的字母表开始,我们有A代表账户。我们从账户开始,因为Solana上的智能合约(也称为“程序”)是无状态的——除了代码之外,它们不存储任何信息。一切都存储在账户中,所以账户对Solana来说至关重要。它们负责存储、合约和本地区块链程序的管理。

Solana 上有三种类型的账户:

  • 数据帐户 - 就是用来存储数据的!
  • 程序帐户 - 存储可执行程序(又称智能合约)的地方
  • 原生账户 - 用于核心区块链功能,例如权益和投票的账户

至于原生账户,是区块链运行所必需的,我们稍后会详细介绍。目前,我们只关注数据和程序账户。

在数据账户方面,还有两种进一步的类型:

  • 系统拥有的帐户
  • PDA(程序派生地址)帐户

我们很快会介绍这些具体是什么,现在请跟着一起学习。

每个账户都有一些你应该了解的字段:

字段描述
lamports账户拥有的lamports数量
owner账户的所有者程序
executable账户是否可以处理指令
data账户存储的数据的字节码
rent_epoch下一个需要付租金的epoch(周期)

我们现在只关注我们需要了解的部分,所以即使有些内容不那么清晰,也请勇往直前 - 我们将在学习过程中填补这些空白。

LamportSolana的最小单位,类似于以太坊的Gwei。一个Lamport等于0.000000001 SOL,所以这个字段告诉我们账户拥有多少SOL

每个账户都有一个公钥,就像账户的地址一样。你知道你的钱包有一个地址来接收那些炫酷的NFT吗?这就是同样的原理!Solana的地址只是用base58编码的字符串。

executable 是一个布尔字段,表示该帐户是否包含可执行数据。至于数据,就是存储在帐户中的内容,至于租金,我们稍后会谈到!

现在,让我们从简单的事情开始学习吧 :)

📫 从网络中读取

好的,我们现在明白了什么是账户,那么如何读取它们呢?我们将借助一个名为 JSON RPC 终端点的工具!你可以通过下图了解,你将在 Solana 网络中充当客户端角色,尝试获取信息。

你可以通过调用 JSON RPCAPI来获取所需的内容,它会与网络沟通并返回你所需的数据。

假设我们要查询账户的余额,API调用将如下所示:

async function getBalanceUsingJSONRPC(address: string): Promise<number> {
const url = clusterApiUrl('devnet')
console.log(url);
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
"jsonrpc": "2.0",
"id": 1,
"method": "getBalance",
"params": [
address
]
})
}).then(response => response.json())
.then(json => {
if (json.error) {
throw json.error
}

return json['result']['value'] as number;
})
.catch(error => {
throw error
})
}

这里涉及了很多步骤。我们正在进行一个POST请求,请求体中包含特定的参数来指导RPC执行什么操作。我们要指定RPC的版本、id、方法(本例中是getBalance),以及该方法所需的参数(本例中只有address)。

由于我们对一个非常简单的方法有大量的样板代码,我们可以使用SolanaWeb3.js SDK。以下是所需的代码:

async function getBalanceUsingWeb3(address: PublicKey): Promise<number> {
const connection = new Connection(clusterApiUrl('devnet'));
return connection.getBalance(address);
}

这里有Solana RPC API 关于介绍如何使用getBalance文档

是不是很美观?我们仅用三行代码就能获取某人的Solana余额。试想一下,如果获取任何人的银行余额也能如此简单。

现在你已经了解了如何从Solana的账户中读取数据!虽然这看起来可能很简单,但只要使用这个函数,你就能查询Solana上任何账户的余额。试想一下,如果你能获取地球上任何银行账户的余额,这将是多么强大的能力。

🤑 构建一个余额获取器

现在是时候构建一个通用的余额查询器了(假设整个宇宙都在Solana上运行)。这将是一个简洁而强大的应用程序,能查询Solana上任何账户的余额。

首先,在你的工作空间中创建一个文件夹,比如放在桌面上。克隆起始库并按照以下步骤设置:

git clone https://github.com/all-in-one-solana/solana-intro-frontend
cd solana-intro-frontend
git checkout starter
npm i

这是一个简单的Next.js应用程序,安装了所有依赖项后,你可以使用 npm run dev 命令启动它。你应该能在localhost上看到如下内容:

我们为你准备了一个基本的Next.js应用程序,并添加了一些样式。如果你在地址栏中输入并点击“检查SOL余额”按钮,你将看到1,000 SOL的余额显示。现在是时候让它真正起作用了。

首先,你需要安装Solana/web3.js库:

npm install @solana/web3.js

这将为我们提供一个非常简洁的函数来查询余额。转到 index.tsx 并将其导入到顶部:

import * as web3 from '@solana/web3.js'

接下来,我们将开始处理 addressSubmittedHandler 函数。首先,我们要将地址从字符串转换为公钥。记住,地址实际上并不是字符串,而是在JS中用字符串表示的。

const addressSubmittedHandler = (address: string) => {
const key = new web3.PublicKey(address);
setAddress(address)
setBalance(1000)
}

这将验证你输入的内容是否是一个Solana地址。现在,如果你在地址栏中输入的内容不是一个密钥,应用程序会崩溃。很好!

现在,我们要使用这个密钥建立一个新的连接到JSON RPC。通过这个连接,我们将使用 getBalance 函数并使用 setBalance 设置结果!下面是完整的函数:

const addressSubmittedHandler = (address: string) => {
const key = new web3.PublicKey(address);
setAddress(key.toBase58())

const connection = new web3.Connection(web3.clusterApiUrl('devnet'))

connection.getBalance(key).then(balance => {
setBalance(balance / web3.LAMPORTS_PER_SOL)
})
}

这里有一些新元素:

  • 我们正在使用 key.toBase58 来设置地址,这是Solana地址的字符串编码方式。
  • 我们正在连接到 devnet 网络,有三个网络可供选择:主网、测试网和开发网。我们将在开发网上进行所有操作。
  • 我们正在将余额从Lamports转换为SOL,因为余额是以Lamports返回的,而不是SOL

我们做完了!如果你在这里粘贴一个地址,你会看到余额显示。确保你的账户有开发网络上的SOL!如果没有,你可以使用我的账户来测试应用 - B1aLAAe4vW8nSQCetXnYqJfRxzTjnbooczwkUJAr7yMS

这个方案不错,但如果地址输入错误,就会出现一个很难处理的错误。让我们添加一些错误处理机制来解决这个问题。

const addressSubmittedHandler = (address: string) => {
try {
setAddress(address)
const key = new web3.PublicKey(address)
const connection = new web3.Connection(web3.clusterApiUrl('devnet'))
connection.getBalance(key).then(balance => {
setBalance(balance / web3.LAMPORTS_PER_SOL)
})
} catch (error) {
setAddress('')
setBalance(0)
alert(error)
}
}

现在,如果出现错误,你将会收到一个提示信息了 :D

哇塞,你刚刚成功发布了自己的第一个Solana应用。真棒!继续加油!

🚢 挑战

现在,让我们通过一个小挑战来考验一下你所学的知识。从你刚刚完成的应用出发,你需要在用户界面中添加一项功能,显示输入的地址是否为可执行账户。

要判断一个账户是否可执行,你需要执行以下操作:

  1. 使用方法 getAccountInfo 来获取包含账户信息的JSON对象。
  2. 检查该对象的属性以确定其是否可执行。
  3. useState 中添加另一个调用,允许你从账户信息中设置 executable 属性值,并在用户界面中展示。

下面是一个可供测试的账户地址:ComputeBudget111111111111111111111111111111

请尽量自己尝试解决,不要提前查看答案!相信你会发现挑战其实并不复杂。

完成后,你可以在这里查看挑战解决方案的参考代码。

📚 更多关于账户相关的资源

+ + \ No newline at end of file diff --git a/Solana-Co-Learn/module1/client-side-development/write-data-to-the-blockchain/index.html b/Solana-Co-Learn/module1/client-side-development/write-data-to-the-blockchain/index.html index 9005c0091..684f399f4 100644 --- a/Solana-Co-Learn/module1/client-side-development/write-data-to-the-blockchain/index.html +++ b/Solana-Co-Learn/module1/client-side-development/write-data-to-the-blockchain/index.html @@ -9,13 +9,13 @@ - - + +
-
Skip to main content

✍将数据写入区块链

我们已经熟练掌握了区块链的阅读操作,现在开始学习如何将数据写入Solana区块链。

🔐 密钥对

要将数据写入区块链,我们需要提交一笔交易,这就像是一条写入命令。如果不满足某些条件,该命令就会被拒绝。

要深入理解交易及其工作原理,你需要先了解密钥对。

密钥对包括一对密钥:

  • 一个公钥,公钥代表网络上的账户地址。
  • 一个私钥,每个公钥都与一个相应的私钥/秘密密钥配对。

Web3.js 库提供了几个用于处理密钥对的辅助函数。你可以使用它们生成密钥对,并获取公钥或私钥。

// 创建一个新的密钥对
const ownerKeypair = Keypair.generate()

// 获取公钥(地址)
const publicKey = ownerKeypair.publicKey

// 获取私钥
const secretKey = ownerKeypair.secretKey

密钥可以有以下几种格式:

  1. 助记词——这是最常用的格式:
pill tomorrow foster begin walnut borrow virtual kick shift mutual shoe scatter
  1. bs58 字符串 - 有时钱包会导出此格式的字符串:
5MaiiCavjCmn9Hs1o3eznqDEhRwxo7pXiAYez7keQUviUkauRiTMD8DrESdrNjN8zd9mTmVhRvBJeg5vhyvgrAhG
  1. Bytes - 在编程时,我们通常将原始字节作为数字数组处理:
// 字节数组示例
[ 174, 47, 154, 16, 202, 193, 206, 113, 199, 190, 53, 133, 169, 175, 31, 56, 222, 53, 138, 189, 224, 216, 117,173, 10, 149, 53, 45, 73, 251, 237, 246, 15, 185, 186, 82, 177, 240, 148, 69, 241, 227, 167, 80, 141, 89, 240, 121, 121, 35, 172, 247, 68, 251, 226, 218, 48, 63, 176, 109, 168, 89, 238, 135, ]

如果你已经有了要使用的密钥对,你可以使用 Keypair.fromSecretKey() 函数从密钥创建 Keypair 对象。

当涉及到主网时,你需要面对真实的金钱和后果。因此,投入时间研究秘密管理的各种方法是值得的。你可能不想使用 .env 变量来注入密钥。这里有一篇关于这方面的好文章。

// 以字节数组的形式私钥
const secret = JSON.parse(process.env.PRIVATE_KEY ?? "") as number[]
const secretKey = Uint8Array.from(secret)
const keypairFromSecretKey = Keypair.fromSecretKey(secretKey)

我们在这里所做的是将私钥的字节格式解析为数字数组,然后转换为Uint数组。我们使用这个Uint数组来创建密钥对。你不需要深入了解它是如何工作的,但你可以在这里这里阅读更多相关信息。

好了,现在你对Solana密钥对的了解已经超过了98%的Solana开发人员 🕶️

让我们回到交易的话题。

Solana网络上,所有对数据的修改都是通过交易来完成的。所有交易都与网络上的程序交互,这些程序可以是系统程序或用户构建的程序。交易向程序表达你想要执行的一系列指令,如果它们有效,程序就会执行这些操作!

这些指令到底是什么样子的呢?它们包括:

  1. 你打算调用的程序的标识符。
  2. 将要读取和/或写入的账户数组。
  3. 以字节数组形式结构化的数据,根据被调用的程序进行指定。

如果这听起来很复杂,不要担心,随着我们的深入学习,一切都会变得明朗的!

🚆 创建并发送一笔交易

我们来进行一笔交易吧!我们要调用系统程序来转移一些SOL代币。幸好,web3.js库中提供了一些辅助函数,使得这个过程变得非常便捷!

const transaction = new Transaction()

const sendSolInstruction = SystemProgram.transfer({
fromPubkey: sender,
toPubkey: recipient,
lamports: LAMPORTS_PER_SOL * amount
})

transaction.add(sendSolInstruction)

以上代码便是创建转账交易所需的全部内容。你还可以向交易中添加多个指令,系统会按顺序执行它们。稍后我们会试试这个功能😈。

web3.js库还能帮助我们发送交易。下面是我们发送交易的方法:

const signature = sendAndConfirmTransaction(
connection,
transaction,
[senderKeypair]
)

这里的内容涵盖了所有你需要了解的事项。

  • connection是我们通过JSON RPC与网络通信的方式;
  • transaction是我们刚刚使用转账指令创建的任务;
  • 最后一个参数是签名者的数组。这些密钥对就是“签署”事务的凭证,这样Solana的运行时环境和你的程序就知道谁授权了该事务。某些交易可能需要多个地址签名。

签名是授权更改的必要步骤。因为这笔交易会将SOL从一个账户转移到另一个账户,我们需要证明我们确实掌控着要发送的账户。

现在,你已经了解了所有关于交易的知识,还知道了我提到的“条件”是什么含义了 :)

✍ 指令

我们在之前的交易中有所简化。当我们与非本地程序或不在web3库中构建的程序协同工作时,我们需要明确指定我们所创建的指令。以下是创建指令所需传递给构造函数的类型。我们来看一下:

export type TransactionInstructionCtorFields = {
keys: Array<AccountMeta>;
programId: PublicKey;
data?: Buffer;
};

本质上,一个指令包括:

  • 一个AccountMeta类型的键数组
  • 要调用的程序的公钥/地址
  • 可选项 - 一个包含要传递给程序的数据的Buffer

keys开始,这个数组中的每个对象都代表着在事务执行期间将被读取或写入的一个账户。这样,节点就能了解哪些账户将参与交易,进而提高处理速度!这意味着你需要清楚了解你调用的程序的操作,并确保在数组中提供所有必要的账户。

Keys数组中的每个对象必须包括以下内容:

  • pubkey - 账户的公钥
  • isSigner - 一个布尔值,表示该账户是否是交易的签名者
  • isWritable - 一个布尔值,表示该账户在交易执行期间是否可写

programId字段则相对直观:它是与你想要交互的程序关联的公钥。它就是告诉系统你想要与谁沟通!

关于数据字段,我们暂时不去深究,将来会重新审查它。

下面是实际操作中的示例:

async function callProgram(
connection: web3.Connection,
payer: web3.Keypair,
programId: web3.PublicKey,
programDataAccount: web3.PublicKey
) {
const instruction = new web3.TransactionInstruction({
// 这里我们只有一个键
keys: [
{
pubkey: programDataAccount,
isSigner: false,
isWritable: true
},
],

// 我们要互动的程序
programId

// 这里我们没有任何数据!
})

const sig = await web3.sendAndConfirmTransaction(
connection,
new web3.Transaction().add(instruction),
[payer]
)
}

看,没那么难吧!我们搞定了:P

⛽ 交易费用

有一件事我们还没有讨论,那就是费用。Solana的交易费用非常低,以至于你几乎可以忽略它们!但可惜的是,作为开发者,我们还是必须关心这些费用的。Solana的费用机制与以太坊等EVM链相似。每当你提交一笔交易时,网络上总有人为其提供存储空间和处理能力。费用的存在就是为了激励人们提供这些资源。

主要需要注意的一点是,在交易的签名者数组中,第一个签名者总是负责支付交易费用。如果你没有足够的SOL怎么办呢?交易将会被取消!

当你在devnetLocalHost上进行开发时,你可以通过Solana的命令行界面(CLI)使用airdrop功能来获取devnet SOL。此外,你还可以通过SPL代币水龙头来获取SPL代币(稍后我们会了解这些是什么东西:P)。

- - +
Skip to main content

✍将数据写入区块链

我们已经熟练掌握了区块链的阅读操作,现在开始学习如何将数据写入Solana区块链。

🔐 密钥对

要将数据写入区块链,我们需要提交一笔交易,这就像是一条写入命令。如果不满足某些条件,该命令就会被拒绝。

要深入理解交易及其工作原理,你需要先了解密钥对。

密钥对包括一对密钥:

  • 一个公钥,公钥代表网络上的账户地址。
  • 一个私钥,每个公钥都与一个相应的私钥/秘密密钥配对。

Web3.js 库提供了几个用于处理密钥对的辅助函数。你可以使用它们生成密钥对,并获取公钥或私钥。

// 创建一个新的密钥对
const ownerKeypair = Keypair.generate()

// 获取公钥(地址)
const publicKey = ownerKeypair.publicKey

// 获取私钥
const secretKey = ownerKeypair.secretKey

密钥可以有以下几种格式:

  1. 助记词——这是最常用的格式:
pill tomorrow foster begin walnut borrow virtual kick shift mutual shoe scatter
  1. bs58 字符串 - 有时钱包会导出此格式的字符串:
5MaiiCavjCmn9Hs1o3eznqDEhRwxo7pXiAYez7keQUviUkauRiTMD8DrESdrNjN8zd9mTmVhRvBJeg5vhyvgrAhG
  1. Bytes - 在编程时,我们通常将原始字节作为数字数组处理:
// 字节数组示例
[ 174, 47, 154, 16, 202, 193, 206, 113, 199, 190, 53, 133, 169, 175, 31, 56, 222, 53, 138, 189, 224, 216, 117,173, 10, 149, 53, 45, 73, 251, 237, 246, 15, 185, 186, 82, 177, 240, 148, 69, 241, 227, 167, 80, 141, 89, 240, 121, 121, 35, 172, 247, 68, 251, 226, 218, 48, 63, 176, 109, 168, 89, 238, 135, ]

如果你已经有了要使用的密钥对,你可以使用 Keypair.fromSecretKey() 函数从密钥创建 Keypair 对象。

当涉及到主网时,你需要面对真实的金钱和后果。因此,投入时间研究秘密管理的各种方法是值得的。你可能不想使用 .env 变量来注入密钥。这里有一篇关于这方面的好文章。

// 以字节数组的形式私钥
const secret = JSON.parse(process.env.PRIVATE_KEY ?? "") as number[]
const secretKey = Uint8Array.from(secret)
const keypairFromSecretKey = Keypair.fromSecretKey(secretKey)

我们在这里所做的是将私钥的字节格式解析为数字数组,然后转换为Uint数组。我们使用这个Uint数组来创建密钥对。你不需要深入了解它是如何工作的,但你可以在这里这里阅读更多相关信息。

好了,现在你对Solana密钥对的了解已经超过了98%的Solana开发人员 🕶️

让我们回到交易的话题。

Solana网络上,所有对数据的修改都是通过交易来完成的。所有交易都与网络上的程序交互,这些程序可以是系统程序或用户构建的程序。交易向程序表达你想要执行的一系列指令,如果它们有效,程序就会执行这些操作!

这些指令到底是什么样子的呢?它们包括:

  1. 你打算调用的程序的标识符。
  2. 将要读取和/或写入的账户数组。
  3. 以字节数组形式结构化的数据,根据被调用的程序进行指定。

如果这听起来很复杂,不要担心,随着我们的深入学习,一切都会变得明朗的!

🚆 创建并发送一笔交易

我们来进行一笔交易吧!我们要调用系统程序来转移一些SOL代币。幸好,web3.js库中提供了一些辅助函数,使得这个过程变得非常便捷!

const transaction = new Transaction()

const sendSolInstruction = SystemProgram.transfer({
fromPubkey: sender,
toPubkey: recipient,
lamports: LAMPORTS_PER_SOL * amount
})

transaction.add(sendSolInstruction)

以上代码便是创建转账交易所需的全部内容。你还可以向交易中添加多个指令,系统会按顺序执行它们。稍后我们会试试这个功能😈。

web3.js库还能帮助我们发送交易。下面是我们发送交易的方法:

const signature = sendAndConfirmTransaction(
connection,
transaction,
[senderKeypair]
)

这里的内容涵盖了所有你需要了解的事项。

  • connection是我们通过JSON RPC与网络通信的方式;
  • transaction是我们刚刚使用转账指令创建的任务;
  • 最后一个参数是签名者的数组。这些密钥对就是“签署”事务的凭证,这样Solana的运行时环境和你的程序就知道谁授权了该事务。某些交易可能需要多个地址签名。

签名是授权更改的必要步骤。因为这笔交易会将SOL从一个账户转移到另一个账户,我们需要证明我们确实掌控着要发送的账户。

现在,你已经了解了所有关于交易的知识,还知道了我提到的“条件”是什么含义了 :)

✍ 指令

我们在之前的交易中有所简化。当我们与非本地程序或不在web3库中构建的程序协同工作时,我们需要明确指定我们所创建的指令。以下是创建指令所需传递给构造函数的类型。我们来看一下:

export type TransactionInstructionCtorFields = {
keys: Array<AccountMeta>;
programId: PublicKey;
data?: Buffer;
};

本质上,一个指令包括:

  • 一个AccountMeta类型的键数组
  • 要调用的程序的公钥/地址
  • 可选项 - 一个包含要传递给程序的数据的Buffer

keys开始,这个数组中的每个对象都代表着在事务执行期间将被读取或写入的一个账户。这样,节点就能了解哪些账户将参与交易,进而提高处理速度!这意味着你需要清楚了解你调用的程序的操作,并确保在数组中提供所有必要的账户。

Keys数组中的每个对象必须包括以下内容:

  • pubkey - 账户的公钥
  • isSigner - 一个布尔值,表示该账户是否是交易的签名者
  • isWritable - 一个布尔值,表示该账户在交易执行期间是否可写

programId字段则相对直观:它是与你想要交互的程序关联的公钥。它就是告诉系统你想要与谁沟通!

关于数据字段,我们暂时不去深究,将来会重新审查它。

下面是实际操作中的示例:

async function callProgram(
connection: web3.Connection,
payer: web3.Keypair,
programId: web3.PublicKey,
programDataAccount: web3.PublicKey
) {
const instruction = new web3.TransactionInstruction({
// 这里我们只有一个键
keys: [
{
pubkey: programDataAccount,
isSigner: false,
isWritable: true
},
],

// 我们要互动的程序
programId

// 这里我们没有任何数据!
})

const sig = await web3.sendAndConfirmTransaction(
connection,
new web3.Transaction().add(instruction),
[payer]
)
}

看,没那么难吧!我们搞定了:P

⛽ 交易费用

有一件事我们还没有讨论,那就是费用。Solana的交易费用非常低,以至于你几乎可以忽略它们!但可惜的是,作为开发者,我们还是必须关心这些费用的。Solana的费用机制与以太坊等EVM链相似。每当你提交一笔交易时,网络上总有人为其提供存储空间和处理能力。费用的存在就是为了激励人们提供这些资源。

主要需要注意的一点是,在交易的签名者数组中,第一个签名者总是负责支付交易费用。如果你没有足够的SOL怎么办呢?交易将会被取消!

当你在devnetLocalHost上进行开发时,你可以通过Solana的命令行界面(CLI)使用airdrop功能来获取devnet SOL。此外,你还可以通过SPL代币水龙头来获取SPL代币(稍后我们会了解这些是什么东西:P)。

+ + \ No newline at end of file diff --git a/Solana-Co-Learn/module1/custom-instruction/build-a-movie-review-app/index.html b/Solana-Co-Learn/module1/custom-instruction/build-a-movie-review-app/index.html index 77cbce360..6cb4a04fe 100644 --- a/Solana-Co-Learn/module1/custom-instruction/build-a-movie-review-app/index.html +++ b/Solana-Co-Learn/module1/custom-instruction/build-a-movie-review-app/index.html @@ -9,14 +9,14 @@ - - + +
Skip to main content

🎥 构建一个电影评论应用

现在我们已经完成了钱包连接的设置,是时候让我们的ping按钮发挥作用了!我们将整合所有元素,构建一个基于区块链的电影评论应用——它将允许任何人提交对他们最喜欢的电影的评论,有点像烂番茄网站那样。

Solana工作空间中设置初始代码

首先,你可以从GitHub克隆起始代码,并安装所需的依赖项:

git clone https://github.com/all-in-one-solana/solana-movie-frontend.git
cd solana-movie-frontend
git checkout starter
npm i

运行 npm run dev 后,你应该能在 localhost:3000 上看到如下内容:

这是一个基于 Next.js 的常规应用程序,已预安装了一些模板组件和Solana依赖项,以帮助你节省时间。你会看到一些模拟评论,请浏览各个组件以了解应用程序的工作方式。

结构调整

你会注意到,我们已将钱包上下文提供程序从 _app.tsx 移至了它自己的组件中。这样做的效果是一样的,但将其与更大的应用程序隔离开,可以提高性能。

功能构建

目前,应用程序所做的只是将你的评论记录到控制台中。接下来的工作将集中在 Form.tsx 文件中,我们将在其中设置 handleTransactionSubmit 函数。

通过这个项目,你将学习如何在Solana上构建真实世界的应用程序,把钱包集成、交易处理、和区块链数据管理结合在一起。

准备好了吗?让我们开始吧!

🗺 定义架构

序列化的第一步是为我们要序列化的数据创建一个模式或映射。我们需要告诉Borsh数据将被称为什么,以及每个项目的大小。

安装 borsh

首先,你需要安装 borsh 库。在终端中运行以下命令:

npm install @project-serum/borsh --force

Movie.ts 中定义架构

接下来,前往 Movie.ts 文件,导入 borsh,然后在 Movie 类中添加架构。以下是你需要做的代码段:

// 引入borsh库
import * as borsh from '@project-serum/borsh'

export class Movie {
title: string;
rating: number;
description: string;

// 构造函数和模拟将保持不变
constructor(title: string, rating: number, description: string) {}
static mocks: Movie[] = []

// 这里是我们的架构定义!
borshInstructionSchema = borsh.struct([
borsh.u8('variant'),
borsh.str('title'),
borsh.u8('rating'),
borsh.str('description'),
])
}

在电影评论程序中,我们期望指令数据包括:

  • variant:一个无符号的8位整数,表示要执行的指令(换句话说,应在程序上调用哪个函数)。
  • title:一个字符串,代表你正在评价的电影的标题。
  • rating:一个无符号的8位整数,表示你对正在评论的电影的评分(满分为5)。
  • description:一个字符串,表示你为电影留下的书面评论。

这个架构必须与程序所期望的完全匹配,包括结构中项目的顺序。当程序读取你的数据时,它将按照定义的顺序进行反序列化。如果你的顺序不同,它生成的数据将无效。由于我们使用的是已部署的程序,所以我已经为你提供了架构。通常,你会需要阅读文档或自己检查程序代码来了解这些细节。

🌭 创建序列化方法

我们已经知道数据的结构,现在需要编写一个方法将其序列化。在 Movie 类中的架构下方添加以下代码:

serialize(): Buffer {
const buffer = Buffer.alloc(1000) // 创建一个1000字节的缓冲区
this.borshInstructionSchema.encode({ ...this, variant: 0 }, buffer) // 使用模式对数据进行编码
return buffer.slice(0, this.borshInstructionSchema.getSpan(buffer)) // 返回缓冲区中的有效数据部分
}

首先,我们创建了一个超大的缓冲区——1000字节。为什么是1000字节呢?因为我知道它足以容纳我们需要的所有内容,并且在最后还留有额外空间。

接下来,我们使用创建的模式对数据进行编码。encode 接受两个参数——我们要编码的数据和我们要存储它的位置。this 指的是我们当前所在的对象,因此我们通过解构电影对象,并将其与 ...this 一起传递,就像传递 { title, rating, description, variant } 一样。

最后,我们移除缓冲区中的多余空间。getSpan 就像 array.length 一样——它根据模式为我们提供缓冲区中最后使用的项目的索引,因此我们的缓冲区只包含我们需要的数据,而不包括任何多余的内容。

以下是最终的 Movie.ts 文件:

import * as borsh from '@project-serum/borsh'

export class Movie {
title: string;
rating: number;
description: string;

constructor(title: string, rating: number, description: string) {
this.title = title;
this.rating = rating;
this.description = description;
}

static mocks: Movie[] = [
new Movie('The Shawshank Redemption', 5, `For a movie shot entirely in prison where there is no hope at all, Shawshank redemption's main message and purpose is to remind us of hope, that even in the darkest places hope exists, and only needs someone to find it. Combine this message with a brilliant screenplay, lovely characters, and Martin freeman, and you get a movie that can teach you a lesson every time you watch it. An all-time Classic!!!`),
new Movie('The Godfather', 5, `One of Hollywood's greatest critical and commercial successes, The Godfather gets everything right; not only did the movie transcend expectations, it established new benchmarks for American cinema.`),
new Movie('The Godfather: Part II', 4, `The Godfather: Part II is a continuation of the saga of the late Italian-American crime boss, Francis Ford Coppola, and his son, Vito Corleone. The story follows the continuing saga of the Corleone family as they attempt to successfully start a new life for themselves after years of crime and corruption.`),
new Movie('The Dark Knight', 5, `The Dark Knight is a 2008 superhero film directed, produced, and co-written by Christopher Nolan. Batman, in his darkest hour, faces his greatest challenge yet: he must become the symbol of the opposite of the Batmanian order, the League of Shadows.`),
]

borshInstructionSchema = borsh.struct([
borsh.u8('variant'),
borsh.str('title'),
borsh.u8('rating'),
borsh.str('description'),
])

serialize(): Buffer {
const buffer = Buffer.alloc(1000)
this.borshInstructionSchema.encode({ ...this, variant: 0 }, buffer)
return buffer.slice(0, this.borshInstructionSchema.getSpan(buffer))
}
}

就是这样!我们已经完成了序列化部分。现在你可以尽情欣赏几部电影了🍿。

🤝 用数据创建交易

最后一块拼图是获取用户的数据,使用刚刚创建的方法对其进行序列化,并用它创建一个交易。

首先,我们要更新 Form.tsx 中的导入:

import { FC } from 'react'
import { Movie } from '../models/Movie'
import { useState } from 'react'
import { Box, Button, FormControl, FormLabel, Input, NumberDecrementStepper, NumberIncrementStepper, NumberInput, NumberInputField, NumberInputStepper, Textarea } from '@chakra-ui/react'
import * as web3 from '@solana/web3.js'
import { useConnection, useWallet } from '@solana/wallet-adapter-react'

接下来,在 handleSubmit 函数前,我们需要建立 RPC 连接并获取钱包的详细信息:

const { connection } = useConnection();
const { publicKey, sendTransaction } = useWallet();

现在来看看重点,handleTransactionSubmit 函数。除了序列化部分,这对于之前进行的交易来说非常熟悉:处理交易、定义指令、提交交易。

前半部分代码如下:

const handleTransactionSubmit = async (movie: Movie) => {
if (!publicKey) {
alert('请连接你的钱包!')
return
}

const buffer = movie.serialize()
const transaction = new web3.Transaction()

const [pda] = await web3.PublicKey.findProgramAddress(
[publicKey.toBuffer(), new TextEncoder().encode(movie.title)],
new web3.PublicKey(MOVIE_REVIEW_PROGRAM_ID)
)
}

除了 pda 外,你应该对所有内容都很熟悉。回想一下指令的要求。它需要与之交互的程序ID、可选的数据和它将从中读取或写入的账户列表。由于我们要将数据提交到网络上进行存储,我们将创建一个新的账户来存储它。

在提到PDA(程序派生地址)时出现了“Patrick”!这是用来存储我们电影评论的账户。你可能开始注意到了,这里出现了经典的“先有鸡还是先有蛋”的情况...

我们需要知道账户地址才能进行有效交易,但我们又需要处理交易才能创建账户。解决方案呢?理论上先有的蛋。如果交易创建者和程序都使用相同的过程来选择地址,我们就可以在交易处理之前确定地址。

这就是 web3.PublicKey.findProgramAddress 方法的功能。它接受种子和生成种子的程序两个变量。在我们的例子中,种子是发件人的地址和电影的标题。通常你需要通过阅读文档、查看程序代码或逆向工程来了解种子的要求。

完成 handleTransactionSubmit 功能的剩余部分就是创建指令并发送它。以下是完整代码:

const handleTransactionSubmit = async (movie: Movie) => {
if (!publicKey) {
alert('请连接你的钱包!')
return
}

const buffer = movie.serialize()
const transaction = new web3.Transaction()

const [pda] = await web3.PublicKey.findProgramAddress(
[publicKey.toBuffer(), new TextEncoder().encode(movie.title)],
new web3.PublicKey(MOVIE_REVIEW_PROGRAM_ID)
)

const instruction = new web3.TransactionInstruction({
keys: [
{
// 你的帐户将支付费用,因此会写入网络
pubkey: publicKey,
isSigner: true,
isWritable: false,
},
{
// PDA将存储电影评论
pubkey: pda,
isSigner: false,
isWritable: true
},
{
// 系统程序将用于创建PDA
pubkey: web3.SystemProgram.programId,
isSigner: false,
isWritable: false
}
],
// 这是最重要的部分!
data: buffer,
programId: new web3.PublicKey(MOVIE_REVIEW_PROGRAM_ID)
})

transaction.add(instruction)

try {
let txid = await sendTransaction(transaction, connection)
console.log(`提交的交易:https://explorer.solana.com/tx/${txid}?cluster=devnet`)
} catch (e) {
alert(JSON.stringify(e))
}
}

通过细致地阅读代码中的注释,你将理解为何我们在指令键数组中需要每一个地址。

就这样了!请确保你的钱包连接到开发网络,并且拥有一些开发网络的SOL,然后访问 localhost:3000。提交评论后,访问控制台中记录的浏览器链接。向下滚动到底部,你会看到你的电影名称以及其他一些信息:

哇,你刚刚将自定义数据写入 Solana 网络。

给自己一个掌声,这可不是件容易的事情!到了这个阶段,可能有人已经放弃了这个项目。给他们一些鼓励,展示你所建立的成果!如果你已经坚持到了这一步,我相信你会一直坚持到最后 :)

本地部署 Movie Review 程序

这里Moview Review Program的程序: https://github.com/all-in-one-solana/movie-review-program

你需要在本地部署这个程序,然后才能在本地运行这个项目。

然后你还需要修改下前端代码的 MOVIE_REVIEW_PROGRAM_ID 常量,改成你本地部署的程序的地址。

这个commit : https://github.com/all-in-one-solana/solana-movie-frontend/commit/6451fcfb60ea5feba485a7d1d1cb882833329654#diff-70f76b2487583dcb8b512614274040921abaa29ab8b993b19a45140fdbe7b8c8R10 包含了你需要修改的两个地方,一个就是 program id ,还有一个是你需要将链接的 devnet 换成localhost网络。

🚢 挑战:Solana构建者的自我介绍

现在,是时候挑战你的思维能力了,让我们的大脑多折几道皱纹 🧠。 -我们的目标是继续创建一个应用程序,允许Solana Core中的构建者进行自我介绍。我们将会使用地址HdE95RSVsdb315jfJtaykXhXY478h53X6okDupVfY9yf上的Solana程序来实现这个目的。

caution

HdE95RSVsdb315jfJtaykXhXY478h53X6okDupVfY9yf 合约是devnent 上的一个测试合约。所以,运行你的 dapp 之前,保证你的 钱包和应用的网络设置均为 devnent。

最终,你的应用程序应该看起来与电影评论应用程序相似:

起始代码和设置

你可以通过以下命令设置项目:

git clone https://github.com/all-in-one-solana/solana-student-intros-frontend.git
cd solana-student-intros-frontend
git checkout starter
npm i

提示与指导

程序预计将接收以下顺序的指令数据:

  1. variant 以无符号8位整数表示,用于指示要调用的指令(在本例中应为0)。
  2. name 以字符串形式表示名字。
  3. message 以字符串形式表示消息。

值得注意的是,程序将使用连接到钱包的公钥来生成每个学生的介绍账户。这意味着每个公钥只能初始化一个学生介绍账户,如果使用相同的公钥提交两次,则交易将失败。

自我挑战

与往常一样,首先请尝试独立完成此操作。如果你遇到困难,或者只是想将你的解决方案与我们的解决方案进行比较,请查看此存储库中的solution-serialize-instruction-data分支。

祝你好运,期待看到你的成果!

- - +我们的目标是继续创建一个应用程序,允许Solana Core中的构建者进行自我介绍。我们将会使用地址HdE95RSVsdb315jfJtaykXhXY478h53X6okDupVfY9yf上的Solana程序来实现这个目的。

caution

HdE95RSVsdb315jfJtaykXhXY478h53X6okDupVfY9yf 合约是devnent 上的一个测试合约。所以,运行你的 dapp 之前,保证你的 钱包和应用的网络设置均为 devnent。

最终,你的应用程序应该看起来与电影评论应用程序相似:

起始代码和设置

你可以通过以下命令设置项目:

git clone https://github.com/all-in-one-solana/solana-student-intros-frontend.git
cd solana-student-intros-frontend
git checkout starter
npm i

提示与指导

程序预计将接收以下顺序的指令数据:

  1. variant 以无符号8位整数表示,用于指示要调用的指令(在本例中应为0)。
  2. name 以字符串形式表示名字。
  3. message 以字符串形式表示消息。

值得注意的是,程序将使用连接到钱包的公钥来生成每个学生的介绍账户。这意味着每个公钥只能初始化一个学生介绍账户,如果使用相同的公钥提交两次,则交易将失败。

自我挑战

与往常一样,首先请尝试独立完成此操作。如果你遇到困难,或者只是想将你的解决方案与我们的解决方案进行比较,请查看此存储库中的solution-serialize-instruction-data分支。

祝你好运,期待看到你的成果!

+ + \ No newline at end of file diff --git a/Solana-Co-Learn/module1/custom-instruction/custom-instructions/index.html b/Solana-Co-Learn/module1/custom-instruction/custom-instructions/index.html index 0ba2c888d..b6153d6da 100644 --- a/Solana-Co-Learn/module1/custom-instruction/custom-instructions/index.html +++ b/Solana-Co-Learn/module1/custom-instruction/custom-instructions/index.html @@ -9,13 +9,13 @@ - - + +
-
Skip to main content

🤔 自定义指令

既然我们已经完成了钱包连接的设置,那么让我们使我们的ping按钮真正有所作为吧!你现在知道如何读取数据并通过简单的交易将其写入网络。几乎立刻,你可能会发现自己想要通过交易发送数据。那么让我们了解一下如何向Solana区块链讲述你的故事。

Solana中关于数据的棘手之处在于程序是无状态的。与以太坊等其他区块链中的智能合约不同,程序不存储任何数据,只存储逻辑。

图为:Solana 创始人 Anatoly Yakovenko 正在制作 Solana

Solana 程序中绝对不存储任何内容。它不知道所有者是谁,甚至不知道是谁部署了它。所有的信息都存储在帐户内。

📧 指令数据

我们来稍微了解一下引擎盖下面的工作机制。在这部分,许多工作实际上会由像Anchor这样的库来处理,但是了解原子指令级别上发生的事情还是很重要的。

让我们退后一步,了解一下指令数据是如何放置的。

交易可以包含一个或多个指令,每个指令可以附带一些数据。

关于指令数据的关键在于其格式 - 它是8位数据。"位"是指它是机器码:108仅仅是指其大小,就像32位或64位一样。如果你的指令数据不符合这个格式,Solana运行时将无法识别它。

这就是Solana如此快速的原因之一!它不是让网络转换你的数据,而是由你提供已经转换好的数据,然后网络只需处理它。可以想象一下,如果你在开始烹饪之前已经准备好了所有的食材,你将能够更快地完成烹饪,因为你不需要再切割食材。

你不需要了解机器码是如何工作的。你只需要记住指令数据是某种特定类型的,当你想要在指令中包含数据时,你需要将数据转换为该类型。

info

这段文字解释了Solana网络如何处理事务和指令数据。在Solana中,一个事务可以包含一条或多条指令,每条指令都可以携带一些数据。

重点是,这些指令数据需要以特定的8位数据格式提供。这里的“8位”并不是指数据的大小,而是指数据的格式,这种格式是机器代码格式,用10表示。如果你提供的指令数据不是这种格式,Solana运行时就无法识别和处理它。

这种处理方式使得Solana能够高速运行。你不需要让网络转换你的数据,而是自己转换数据并提供给网络,网络只负责处理它。这就像在开始烹饪前就准备好所有食材,你就能更快地烹饪,因为你不需要在烹饪过程中去切东西。

作者强调,你并不需要了解机器代码是如何工作的。你需要记住的是,当你想要在指令中包含一些数据时,这些数据需要是特定类型的,你需要把数据转换为这种类型。这意味着你在编写并提交给Solana网络的代码中,需要负责将数据转换为适当的格式。

这是低级别编程的一个常见特点。虽然许多高级编程语言(如PythonJavaScript)会自动处理这些类型转换,但在低级语言(如Rust,也是Solana主要使用的语言)中,你需要自己处理这些转换。不过,有些库,如Anchor,可以帮助你处理这些转换,使编程更为简单。

🔨 序列化和borsh

序列化就是将常规的代码或数据转换为字节数组(机器代码:10)的过程。

在我们的项目中,我们将使用 Borsh 序列化格式,因为它提供了一个方便的库供我们使用。

以装备一个链上游戏物品为例,我们需要以下三个数据:

  • variant - 我们想要调用的命令的名称(如“装备”或“删除”)
  • playerId - 将装备物品的玩家的ID
  • itemId - 我们想要装备的物品ID

将这些数据序列化包括以下四个步骤:

  1. 创建数据模式/映射,明确数据的预期结构。
  2. 分配一个比实际所需空间大得多的缓冲区。
  3. 将数据编码并添加到缓冲区中。
  4. 去掉缓冲区末端的额外空白。

作为网络开发人员,通常不需要处理这样的底层内容,下图可以让这个概念更具形象化:

下面的代码片段展示了如何使用Borsh库实现这一过程:

import * as Borsh from "@project-serum/borsh"

const equipPlayerSchema = Borsh.struct([
Borsh.u8("variant"),
Borsh.u8("playerId"),
Borsh.u8("itemId"),
])

// 创建一个1000字节的缓冲区
const buffer = Buffer.alloc(1000)
equipPlayerSchema.encode({ variant: 2, playerId: 1435, itemId: 737498}, buffer)

// 截取缓冲区以达到所需的长度
const instructBuffer = buffer.slice(0, equipPlayerSchema.getSpan(buffer))

在这里,我们定义了一个包括三个无符号整数的Borsh结构,并将它们编码为一个字节缓冲区。图示解释了如何将这些数据分解为适当的长度,就像切香肠一样。

接下来的代码片段展示了如何构建和发送交易:

const endpoint = clusterApiUrl("devnet")
const connection = new Connection(endpoint)

const transaction = new Transaction().add({
keys: [
{
pubkey: player.Publickey,
isSigner: true,
isWritable: false,
},
{
pubkey: playerInfoAccount,
isSigner: false,
isWritable: true,
},
{
pubkey: SystemProgram.programId,
isSigner: false,
isWritable: false,
},
],
data: instructBuffer,
programId: PROGRAM_ID,
})

sendAndConfirmTransaction(connection, transaction, [player])

一旦我们拥有正确格式的数据,剩下的部分就相对简单了。这个交易的结构应该看起来很熟悉,唯一的新元素是我们之前没有的可选项data

如果你对机器码和内存分配不太了解,也不必担心。你不必深入学习,只需观看一两个相关视频,了解大致的概念即可。

现代开发人员很少直接处理字节缓冲区,因为这被认为是较低级别的工作。所以,如果你感觉这些内容陌生或新奇,也不必担心。通过实际应用,你将能更好地掌握这些概念,从而更接近真正的软件工程师的角色。😎

- - +
Skip to main content

🤔 自定义指令

既然我们已经完成了钱包连接的设置,那么让我们使我们的ping按钮真正有所作为吧!你现在知道如何读取数据并通过简单的交易将其写入网络。几乎立刻,你可能会发现自己想要通过交易发送数据。那么让我们了解一下如何向Solana区块链讲述你的故事。

Solana中关于数据的棘手之处在于程序是无状态的。与以太坊等其他区块链中的智能合约不同,程序不存储任何数据,只存储逻辑。

图为:Solana 创始人 Anatoly Yakovenko 正在制作 Solana

Solana 程序中绝对不存储任何内容。它不知道所有者是谁,甚至不知道是谁部署了它。所有的信息都存储在帐户内。

📧 指令数据

我们来稍微了解一下引擎盖下面的工作机制。在这部分,许多工作实际上会由像Anchor这样的库来处理,但是了解原子指令级别上发生的事情还是很重要的。

让我们退后一步,了解一下指令数据是如何放置的。

交易可以包含一个或多个指令,每个指令可以附带一些数据。

关于指令数据的关键在于其格式 - 它是8位数据。"位"是指它是机器码:108仅仅是指其大小,就像32位或64位一样。如果你的指令数据不符合这个格式,Solana运行时将无法识别它。

这就是Solana如此快速的原因之一!它不是让网络转换你的数据,而是由你提供已经转换好的数据,然后网络只需处理它。可以想象一下,如果你在开始烹饪之前已经准备好了所有的食材,你将能够更快地完成烹饪,因为你不需要再切割食材。

你不需要了解机器码是如何工作的。你只需要记住指令数据是某种特定类型的,当你想要在指令中包含数据时,你需要将数据转换为该类型。

info

这段文字解释了Solana网络如何处理事务和指令数据。在Solana中,一个事务可以包含一条或多条指令,每条指令都可以携带一些数据。

重点是,这些指令数据需要以特定的8位数据格式提供。这里的“8位”并不是指数据的大小,而是指数据的格式,这种格式是机器代码格式,用10表示。如果你提供的指令数据不是这种格式,Solana运行时就无法识别和处理它。

这种处理方式使得Solana能够高速运行。你不需要让网络转换你的数据,而是自己转换数据并提供给网络,网络只负责处理它。这就像在开始烹饪前就准备好所有食材,你就能更快地烹饪,因为你不需要在烹饪过程中去切东西。

作者强调,你并不需要了解机器代码是如何工作的。你需要记住的是,当你想要在指令中包含一些数据时,这些数据需要是特定类型的,你需要把数据转换为这种类型。这意味着你在编写并提交给Solana网络的代码中,需要负责将数据转换为适当的格式。

这是低级别编程的一个常见特点。虽然许多高级编程语言(如PythonJavaScript)会自动处理这些类型转换,但在低级语言(如Rust,也是Solana主要使用的语言)中,你需要自己处理这些转换。不过,有些库,如Anchor,可以帮助你处理这些转换,使编程更为简单。

🔨 序列化和borsh

序列化就是将常规的代码或数据转换为字节数组(机器代码:10)的过程。

在我们的项目中,我们将使用 Borsh 序列化格式,因为它提供了一个方便的库供我们使用。

以装备一个链上游戏物品为例,我们需要以下三个数据:

  • variant - 我们想要调用的命令的名称(如“装备”或“删除”)
  • playerId - 将装备物品的玩家的ID
  • itemId - 我们想要装备的物品ID

将这些数据序列化包括以下四个步骤:

  1. 创建数据模式/映射,明确数据的预期结构。
  2. 分配一个比实际所需空间大得多的缓冲区。
  3. 将数据编码并添加到缓冲区中。
  4. 去掉缓冲区末端的额外空白。

作为网络开发人员,通常不需要处理这样的底层内容,下图可以让这个概念更具形象化:

下面的代码片段展示了如何使用Borsh库实现这一过程:

import * as Borsh from "@project-serum/borsh"

const equipPlayerSchema = Borsh.struct([
Borsh.u8("variant"),
Borsh.u8("playerId"),
Borsh.u8("itemId"),
])

// 创建一个1000字节的缓冲区
const buffer = Buffer.alloc(1000)
equipPlayerSchema.encode({ variant: 2, playerId: 1435, itemId: 737498}, buffer)

// 截取缓冲区以达到所需的长度
const instructBuffer = buffer.slice(0, equipPlayerSchema.getSpan(buffer))

在这里,我们定义了一个包括三个无符号整数的Borsh结构,并将它们编码为一个字节缓冲区。图示解释了如何将这些数据分解为适当的长度,就像切香肠一样。

接下来的代码片段展示了如何构建和发送交易:

const endpoint = clusterApiUrl("devnet")
const connection = new Connection(endpoint)

const transaction = new Transaction().add({
keys: [
{
pubkey: player.Publickey,
isSigner: true,
isWritable: false,
},
{
pubkey: playerInfoAccount,
isSigner: false,
isWritable: true,
},
{
pubkey: SystemProgram.programId,
isSigner: false,
isWritable: false,
},
],
data: instructBuffer,
programId: PROGRAM_ID,
})

sendAndConfirmTransaction(connection, transaction, [player])

一旦我们拥有正确格式的数据,剩下的部分就相对简单了。这个交易的结构应该看起来很熟悉,唯一的新元素是我们之前没有的可选项data

如果你对机器码和内存分配不太了解,也不必担心。你不必深入学习,只需观看一两个相关视频,了解大致的概念即可。

现代开发人员很少直接处理字节缓冲区,因为这被认为是较低级别的工作。所以,如果你感觉这些内容陌生或新奇,也不必担心。通过实际应用,你将能更好地掌握这些概念,从而更接近真正的软件工程师的角色。😎

+ + \ No newline at end of file diff --git a/Solana-Co-Learn/module1/custom-instruction/index.html b/Solana-Co-Learn/module1/custom-instruction/index.html index 2002dd521..4c30d0df9 100644 --- a/Solana-Co-Learn/module1/custom-instruction/index.html +++ b/Solana-Co-Learn/module1/custom-instruction/index.html @@ -9,13 +9,13 @@ - - + +
-
Skip to main content
- - +
Skip to main content
+ + \ No newline at end of file diff --git a/Solana-Co-Learn/module1/custom-instruction/run-it-back-deserialization/index.html b/Solana-Co-Learn/module1/custom-instruction/run-it-back-deserialization/index.html index 45e8fbda0..6f9ff9a23 100644 --- a/Solana-Co-Learn/module1/custom-instruction/run-it-back-deserialization/index.html +++ b/Solana-Co-Learn/module1/custom-instruction/run-it-back-deserialization/index.html @@ -9,13 +9,13 @@ - - + +
-
Skip to main content

📡 Run is back - 反序列化

现在我们已经完成了钱包连接的设置,是时候让我们的 ping 按钮真正起作用了!向网络账户写入数据只是任务的一半,另一半则是读取这些数据。在第一部分,我们借助Web3.js库中的内置函数来读取内容,这只适用于基础数据,如余额和交易详情。但正如我们在上一部分所见,所有精彩的东西都藏在 PDAs 里。

🧾 程序派生地址(Program Derived Addresses

账户是Solana的重要话题。如果你听过“账户”这个词,你可能听说过有人提到 PDAPDA 是Solana上用于存储数据的特殊类型账户。实际上,它们不被称为账户,而是称为地址,因为它们没有私钥,而只能由创建它们的程序控制。

caution

这里说了PDA是一种特殊类型的账户,但是它们不是真正的账户。它们只是一种特殊的地址,由程序控制,而不是私钥。这意味着它们不是真正的账户,因为它们没有私钥,因此无法对其进行签名。

普通的Solana账户是使用Ed25519签名系统创建的,这个系统为我们提供了公钥和私钥。而 PDA 由程序控制,因此它们不需要私钥。因此,我们使用不在 Ed25519 曲线上的地址来制作 PDA

有时,findProgramAddress会给我们一个位于曲线上的密钥(意味着它也有一个私钥),所以我们添加了一个可选的“bump”参数以将其移出曲线。

事实就是如此,你不需要深入了解Ed25519或数字签名算法是什么。你只需了解 PDA 看起来就像普通的Solana地址,并且由程序控制。

你需要了解 PDA 的工作原理的原因是,它们提供了链上和链下程序确定性地定位数据的方法。可以将其视为一个键值存储,其中 seedsprogramIdbump 结合形成密钥,以及网络在该地址上存储的值。知道密钥是什么使我们能够可靠且一致地查找存储在网络上的数据。

多亏了程序派生地址(PDAs),我们在Solana上拥有了一个可以被所有程序访问的通用数据库。想想我们与第一个程序互动的情景——我们向其发送了一个 ping 请求,然后它递增了一个数字。以下是你可能与所有账户互动的程序共享数据的示例代码:

// 用于全局状态的示例
const [pda, bump] = await PublicKey.findProgramAddress(
[Buffer.from("GLOBAL_STATE")],
programId
);

// 为每个用户存储单独计数器的示例
const [pda, bump] = await PublicKey.findProgramAddress(
[
publickey.toBuffer()
],
programId
);

// 创建链上笔记系统,每个用户都可以存储自己的笔记的示例
const [pda, bump] = await PublicKey.findProgramAddress(
[
publickey.toBuffer(),
Buffer.from("First Note")
],
programId
);

请注意,无论是你还是调用方都必须支付存储费用,并且每个账户有10 Mb 的限制,所以要谨慎选择要放在链上的内容。

caution

每个数据账户的大小最大是10MB的大小。

🎢 反序列化

在找到要读取的账户之后,你需要将数据反序列化,以便你的应用程序可以使用它。回忆一下我们在这个程序中学到的第一件事——账户及其包含的内容。来回顾一下:

字段描述
lamports账户所拥有的 lamports 数量
owner该账户的程序所有者
executable该账户是否可以处理指令(可执行)
data该账户存储的原始数据字节数组
rent_epoch这个账户将要支付的下一个时期的租金

数据字段包含了一个庞大的字节数组。就像我们将可读数据转换为字节进行指令处理一样,我们在此处要做的是相反的操作:将字节数组转换为我们的应用程序可以处理的数据。这是真正的魔法开始之处,你会感受到自己就像在玻璃上冲浪一样😎。

我们在这里见到了我们最好的新老朋友 Borsh 先生:

import * as borsh from '@project-serum/borsh';

const borshAccountSchema = borsh.struct({
borsh.bool('initialized'),
borsh.u16('playerId'),
borsh.str('name')
});

const { playerId, name } = borshAccountSchema.decode(buffer);

这些步骤与我们对序列化所做的工作相似:

  1. 创建一个描述字节数组中存储内容的模式/映射。
  2. 使用该模式来解码数据。
  3. 提取我们想要的信息。

这一流程应该感觉很熟悉,但如果你不熟悉,不用担心,当我们付诸实践时,一切都会变得清晰!

构建一个反序列化器

曾经考虑过要如何构建一个反序列化器吗?我们将继续前面的电影评论应用开发。你可以在上一节的项目基础上继续(推荐),或者使用已完成的版本开始设置:

git clone https://github.com/all-in-one-solana/solana-movie-frontend.git
cd solana-movie-frontend
git checkout solution-serialize-instruction-data
npm i

当你运行 npm run dev 时,你将看到一些模拟数据。与假冒的鞋子不同,假数据总是让人失望的。让我们在 Movie.ts 文件中保持真实(仅复制/粘贴新内容):

import * as borsh from '@project-serum/borsh'

export class Movie {
title: string;
rating: number;
description: string;
...

static borshAccountSchema = borsh.struct([
borsh.bool('initialized'),
borsh.u8('rating'),
borsh.str('title'),
borsh.str('description'),
])

static deserialize(buffer?: Buffer): Movie|null {
if (!buffer) {
return null
}

try {
const { title, rating, description } = this.borshAccountSchema.decode(buffer)
return new Movie(title, rating, description)
} catch(error) {
console.log('Deserialization error:', error)
return null
}
}
}

就像序列化一样,我们定义了一个模式和一个方法。该架构包括:

  • initialized:一个布尔值,表示账户是否已初始化。
  • rating:无符号8位整数,代表评论者对电影的评分(满分5分)。
  • title:表示所评论电影的标题的字符串。
  • description:表示评论的文字部分的字符串。

这应该看起来很熟悉!真正的精髓在 deserialize 方法中。此处的返回类型可以是 Movienull,因为账户可能根本没有任何数据。

最后,我们需要在页面加载时使用此方法从 PDA 获取数据。我们在 MovieList.tsx 文件中执行此操作。

import { Card } from './Card'
import { FC, useEffect, useState } from 'react'
import { Movie } from '../models/Movie'
import * as web3 from '@solana/web3.js'

const MOVIE_REVIEW_PROGRAM_ID = 'CenYq6bDRB7p73EjsPEpiYN7uveyPUTdXkDkgUduboaN'

export const MovieList: FC = () => {
const connection = new web3.Connection(web3.clusterApiUrl('devnet'))
const [movies, setMovies] = useState<Movie[]>([])

useEffect(() => {
connection.getProgramAccounts(new web3.PublicKey(MOVIE_REVIEW_PROGRAM_ID))
.then(async (accounts) => {
const movies: Movie[] = accounts.reduce((accum: Movie[], { pubkey, account }) => {
const movie = Movie.deserialize(account.data)
if (!movie) {
return accum
}

return [...accum, movie]
}, [])
setMovies(movies)
})
}, [])

return (
<div>
{
movies.map((movie, i) => <Card key={i} movie={movie} /> )
}
</div>
)
}

就像以前一样,我们配置了导入和连接。主要的变化发生在 useEffect 中。

connection.getProgramAccounts(new web3.PublicKey(MOVIE_REVIEW_PROGRAM_ID))

在获取电影评论之前,我们需要获取包含它们的账户。我们可以通过获取电影评论程序的所有程序账户来实现,这可以使用 getProgramAccounts 端点完成。

caution

注意,这是一个非常消耗资源的端点。如果在大型程序上尝试,结果可能会非常庞大,甚至会引起问题。在现实世界中,你很少需要同时获取多个账户,所以现在不用担心。但你应该意识到,不应该对数据进行建模,使得 getProgramAccounts 成为必要条件。

.then(async (accounts) => {
const movies: Movie[] = accounts.reduce((accum: Movie[], { pubkey, account }) => {
// Try to extract movie item from account data
const movie = Movie.deserialize(account.data)

// If the account does not have a review, movie will be null
if (!movie) {
return accum
}

return [...accum, movie]
}, [])
setMovies(movies)
})

为了存储电影评论,我们将创建一个 Movies 类型的数组。为了填充它,我们使用 reduce 反序列化每个账户,并尝试解构 movie 项。如果该账户有电影数据,那么这就会起作用!如果没有,电影将为空,我们可以返回累积的电影列表。

如果这看起来有些复杂,请逐行阅读代码,并确保你理解 reduce 方法是如何工作的。

确保你正在运行 npm run dev 并访问 localhost:3000,你应该会看到其他开发者添加的许多随机评论。😃

🚢 挑战

现在,我们已经能够序列化和反序列化数据了,不错吧?现在让我们回到序列化部分开始时的 "Student Intros" 应用程序。

目标:更新应用程序,以便能够获取并反序列化程序的帐户数据。支撑此功能的 Solana 程序地址是:HdE95RSVsdb315jfJtaykXhXY478h53X6okDupVfY9yf

你可以从上次挑战的地方继续,或者从此代码库开始。确保从 solution-serialize-instruction-data 分支开始。

提示:

  1. StudentIntro.ts 文件中创建帐户缓冲区布局。

    帐户数据应该包括:

    • initialized:一个布尔值,表示账户是否已初始化。
    • name:一个表示学生姓名的字符串。
    • message:一个表示学生分享的关于 Solana 旅程的消息的字符串。
  2. StudentIntro.ts 文件中创建一个静态方法,使用缓冲区布局将帐户数据缓冲区反序列化为 StudentIntro 对象。

  3. StudentIntroList 组件的 useEffect 中,获取程序的帐户,并将其数据反序列化到 StudentIntro 对象列表中。

解决方案代码:

像往常一样,首先尝试自己完成此挑战。但如果你陷入困境,或者只是想把你的解决方案与我们的解决方案进行对比,你可以在此代码库中查看 solution-deserialize-account-data 分支

祝你好运,开发者朋友!🚀

- - +
Skip to main content

📡 Run is back - 反序列化

现在我们已经完成了钱包连接的设置,是时候让我们的 ping 按钮真正起作用了!向网络账户写入数据只是任务的一半,另一半则是读取这些数据。在第一部分,我们借助Web3.js库中的内置函数来读取内容,这只适用于基础数据,如余额和交易详情。但正如我们在上一部分所见,所有精彩的东西都藏在 PDAs 里。

🧾 程序派生地址(Program Derived Addresses

账户是Solana的重要话题。如果你听过“账户”这个词,你可能听说过有人提到 PDAPDA 是Solana上用于存储数据的特殊类型账户。实际上,它们不被称为账户,而是称为地址,因为它们没有私钥,而只能由创建它们的程序控制。

caution

这里说了PDA是一种特殊类型的账户,但是它们不是真正的账户。它们只是一种特殊的地址,由程序控制,而不是私钥。这意味着它们不是真正的账户,因为它们没有私钥,因此无法对其进行签名。

普通的Solana账户是使用Ed25519签名系统创建的,这个系统为我们提供了公钥和私钥。而 PDA 由程序控制,因此它们不需要私钥。因此,我们使用不在 Ed25519 曲线上的地址来制作 PDA

有时,findProgramAddress会给我们一个位于曲线上的密钥(意味着它也有一个私钥),所以我们添加了一个可选的“bump”参数以将其移出曲线。

事实就是如此,你不需要深入了解Ed25519或数字签名算法是什么。你只需了解 PDA 看起来就像普通的Solana地址,并且由程序控制。

你需要了解 PDA 的工作原理的原因是,它们提供了链上和链下程序确定性地定位数据的方法。可以将其视为一个键值存储,其中 seedsprogramIdbump 结合形成密钥,以及网络在该地址上存储的值。知道密钥是什么使我们能够可靠且一致地查找存储在网络上的数据。

多亏了程序派生地址(PDAs),我们在Solana上拥有了一个可以被所有程序访问的通用数据库。想想我们与第一个程序互动的情景——我们向其发送了一个 ping 请求,然后它递增了一个数字。以下是你可能与所有账户互动的程序共享数据的示例代码:

// 用于全局状态的示例
const [pda, bump] = await PublicKey.findProgramAddress(
[Buffer.from("GLOBAL_STATE")],
programId
);

// 为每个用户存储单独计数器的示例
const [pda, bump] = await PublicKey.findProgramAddress(
[
publickey.toBuffer()
],
programId
);

// 创建链上笔记系统,每个用户都可以存储自己的笔记的示例
const [pda, bump] = await PublicKey.findProgramAddress(
[
publickey.toBuffer(),
Buffer.from("First Note")
],
programId
);

请注意,无论是你还是调用方都必须支付存储费用,并且每个账户有10 Mb 的限制,所以要谨慎选择要放在链上的内容。

caution

每个数据账户的大小最大是10MB的大小。

🎢 反序列化

在找到要读取的账户之后,你需要将数据反序列化,以便你的应用程序可以使用它。回忆一下我们在这个程序中学到的第一件事——账户及其包含的内容。来回顾一下:

字段描述
lamports账户所拥有的 lamports 数量
owner该账户的程序所有者
executable该账户是否可以处理指令(可执行)
data该账户存储的原始数据字节数组
rent_epoch这个账户将要支付的下一个时期的租金

数据字段包含了一个庞大的字节数组。就像我们将可读数据转换为字节进行指令处理一样,我们在此处要做的是相反的操作:将字节数组转换为我们的应用程序可以处理的数据。这是真正的魔法开始之处,你会感受到自己就像在玻璃上冲浪一样😎。

我们在这里见到了我们最好的新老朋友 Borsh 先生:

import * as borsh from '@project-serum/borsh';

const borshAccountSchema = borsh.struct({
borsh.bool('initialized'),
borsh.u16('playerId'),
borsh.str('name')
});

const { playerId, name } = borshAccountSchema.decode(buffer);

这些步骤与我们对序列化所做的工作相似:

  1. 创建一个描述字节数组中存储内容的模式/映射。
  2. 使用该模式来解码数据。
  3. 提取我们想要的信息。

这一流程应该感觉很熟悉,但如果你不熟悉,不用担心,当我们付诸实践时,一切都会变得清晰!

构建一个反序列化器

曾经考虑过要如何构建一个反序列化器吗?我们将继续前面的电影评论应用开发。你可以在上一节的项目基础上继续(推荐),或者使用已完成的版本开始设置:

git clone https://github.com/all-in-one-solana/solana-movie-frontend.git
cd solana-movie-frontend
git checkout solution-serialize-instruction-data
npm i

当你运行 npm run dev 时,你将看到一些模拟数据。与假冒的鞋子不同,假数据总是让人失望的。让我们在 Movie.ts 文件中保持真实(仅复制/粘贴新内容):

import * as borsh from '@project-serum/borsh'

export class Movie {
title: string;
rating: number;
description: string;
...

static borshAccountSchema = borsh.struct([
borsh.bool('initialized'),
borsh.u8('rating'),
borsh.str('title'),
borsh.str('description'),
])

static deserialize(buffer?: Buffer): Movie|null {
if (!buffer) {
return null
}

try {
const { title, rating, description } = this.borshAccountSchema.decode(buffer)
return new Movie(title, rating, description)
} catch(error) {
console.log('Deserialization error:', error)
return null
}
}
}

就像序列化一样,我们定义了一个模式和一个方法。该架构包括:

  • initialized:一个布尔值,表示账户是否已初始化。
  • rating:无符号8位整数,代表评论者对电影的评分(满分5分)。
  • title:表示所评论电影的标题的字符串。
  • description:表示评论的文字部分的字符串。

这应该看起来很熟悉!真正的精髓在 deserialize 方法中。此处的返回类型可以是 Movienull,因为账户可能根本没有任何数据。

最后,我们需要在页面加载时使用此方法从 PDA 获取数据。我们在 MovieList.tsx 文件中执行此操作。

import { Card } from './Card'
import { FC, useEffect, useState } from 'react'
import { Movie } from '../models/Movie'
import * as web3 from '@solana/web3.js'

const MOVIE_REVIEW_PROGRAM_ID = 'CenYq6bDRB7p73EjsPEpiYN7uveyPUTdXkDkgUduboaN'

export const MovieList: FC = () => {
const connection = new web3.Connection(web3.clusterApiUrl('devnet'))
const [movies, setMovies] = useState<Movie[]>([])

useEffect(() => {
connection.getProgramAccounts(new web3.PublicKey(MOVIE_REVIEW_PROGRAM_ID))
.then(async (accounts) => {
const movies: Movie[] = accounts.reduce((accum: Movie[], { pubkey, account }) => {
const movie = Movie.deserialize(account.data)
if (!movie) {
return accum
}

return [...accum, movie]
}, [])
setMovies(movies)
})
}, [])

return (
<div>
{
movies.map((movie, i) => <Card key={i} movie={movie} /> )
}
</div>
)
}

就像以前一样,我们配置了导入和连接。主要的变化发生在 useEffect 中。

connection.getProgramAccounts(new web3.PublicKey(MOVIE_REVIEW_PROGRAM_ID))

在获取电影评论之前,我们需要获取包含它们的账户。我们可以通过获取电影评论程序的所有程序账户来实现,这可以使用 getProgramAccounts 端点完成。

caution

注意,这是一个非常消耗资源的端点。如果在大型程序上尝试,结果可能会非常庞大,甚至会引起问题。在现实世界中,你很少需要同时获取多个账户,所以现在不用担心。但你应该意识到,不应该对数据进行建模,使得 getProgramAccounts 成为必要条件。

.then(async (accounts) => {
const movies: Movie[] = accounts.reduce((accum: Movie[], { pubkey, account }) => {
// Try to extract movie item from account data
const movie = Movie.deserialize(account.data)

// If the account does not have a review, movie will be null
if (!movie) {
return accum
}

return [...accum, movie]
}, [])
setMovies(movies)
})

为了存储电影评论,我们将创建一个 Movies 类型的数组。为了填充它,我们使用 reduce 反序列化每个账户,并尝试解构 movie 项。如果该账户有电影数据,那么这就会起作用!如果没有,电影将为空,我们可以返回累积的电影列表。

如果这看起来有些复杂,请逐行阅读代码,并确保你理解 reduce 方法是如何工作的。

确保你正在运行 npm run dev 并访问 localhost:3000,你应该会看到其他开发者添加的许多随机评论。😃

🚢 挑战

现在,我们已经能够序列化和反序列化数据了,不错吧?现在让我们回到序列化部分开始时的 "Student Intros" 应用程序。

目标:更新应用程序,以便能够获取并反序列化程序的帐户数据。支撑此功能的 Solana 程序地址是:HdE95RSVsdb315jfJtaykXhXY478h53X6okDupVfY9yf

你可以从上次挑战的地方继续,或者从此代码库开始。确保从 solution-serialize-instruction-data 分支开始。

提示:

  1. StudentIntro.ts 文件中创建帐户缓冲区布局。

    帐户数据应该包括:

    • initialized:一个布尔值,表示账户是否已初始化。
    • name:一个表示学生姓名的字符串。
    • message:一个表示学生分享的关于 Solana 旅程的消息的字符串。
  2. StudentIntro.ts 文件中创建一个静态方法,使用缓冲区布局将帐户数据缓冲区反序列化为 StudentIntro 对象。

  3. StudentIntroList 组件的 useEffect 中,获取程序的帐户,并将其数据反序列化到 StudentIntro 对象列表中。

解决方案代码:

像往常一样,首先尝试自己完成此挑战。但如果你陷入困境,或者只是想把你的解决方案与我们的解决方案进行对比,你可以在此代码库中查看 solution-deserialize-account-data 分支

祝你好运,开发者朋友!🚀

+ + \ No newline at end of file diff --git a/Solana-Co-Learn/module1/index.html b/Solana-Co-Learn/module1/index.html index 8421b323f..69ef4ede1 100644 --- a/Solana-Co-Learn/module1/index.html +++ b/Solana-Co-Learn/module1/index.html @@ -9,13 +9,13 @@ - - + +
-
Skip to main content
- - +
Skip to main content
+ + \ No newline at end of file diff --git a/Solana-Co-Learn/module1/local_program_development/anchor_program_hello/index.html b/Solana-Co-Learn/module1/local_program_development/anchor_program_hello/index.html index 3ee202a18..156a85933 100644 --- a/Solana-Co-Learn/module1/local_program_development/anchor_program_hello/index.html +++ b/Solana-Co-Learn/module1/local_program_development/anchor_program_hello/index.html @@ -9,13 +9,13 @@ - - + +
-
Skip to main content

Anchor 合约框架实现 - hello, World 🌍 With PDA

让我们通过构建和部署 Hello World! 程序来进行练习。

我们将在本地完成所有操作,包括部署到本地测试验证器。在开始之前,请确保你已经安装了RustSolana CLI。如果你还没有安装,请参考概述中的说明进行设置。

0. Anchor 安装

这里是Anchor安装官方指南.

需要你按照步骤安装好 Anchor

安装完成后我们可以通过执行下面的命令,检测 Anchor 是否安装完成✅。

anchor --version
anchor --version
anchor-cli 0.28.0

1. 创建一个新的Rust项目

让我们从创建一个新的Rust项目开始。运行下面的anchor init <you-project-name>命令。随意用你自己的目录名替换它。

anchor init hello_world

2. 编写你的程序

接下来,使用下面的Hello World!程序更新hello_world/program/src/lib.rs。当程序被调用时,该程序会将传入的数据保存到数据存储账户中去也就是下面的HelloWorld账户。

use anchor_lang::prelude::*;

declare_id!("22sSSi7GtQgwXFcjJmGNNKSCLEsiJxyYLFfP3CMWeMLj");

#[program]
pub mod hello_world {
use super::*;

pub fn initialize(ctx: Context<Initialize>, data: String) -> Result<()> {

msg!("{}", data);

*ctx.accounts.hello_world = HelloWorld {
authority: *ctx.accounts.authority.key,
data,
};

Ok(())
}

pub fn update(ctx: Context<UpdateHelloWorld>, data: String) -> Result<()> {
ctx.accounts.hello_world.data = data;
msg!("{}", ctx.accounts.hello_world.data);
Ok(())
}
}

#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = authority,
space = 8 + HelloWorld::INIT_SPACE,
seeds = [b"hello-world"],
bump
)]
pub hello_world: Account<'info, HelloWorld>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct UpdateHelloWorld<'info> {
#[account(
mut,
seeds = [b"hello-world"],
bump
)]
pub hello_world: Account<'info, HelloWorld>,
#[account(mut)]
pub authority: Signer<'info>,
}

#[account]
#[derive(InitSpace)]
pub struct HelloWorld {
pub authority: Pubkey,
#[max_len(100)]
pub data: String,
}

#[error_code]
pub enum ErrorCode {
#[msg("You are not authorized to perform this action.")]
Unauthorized,
#[msg("Cannot get the bump.")]
CannotGetBump,
}

下面这是一个本地的测试脚本文件,用来调用上面的合约程序。

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { HelloWorld } from "../target/types/hello_world";

describe("hello-world", () => {
let provider = anchor.AnchorProvider.env();
// Configure the client to use the local cluster.
anchor.setProvider(provider);

const program = anchor.workspace.HelloWorld as Program<HelloWorld>;

const authority = provider.wallet.publicKey;

let [helloWorld] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("hello-world")],
program.programId
);

it("Is initialized!", async () => {
// Add your test here.
const tx = await program.methods.initialize("Hello World!").accounts({
helloWorld,
authority,
systemProgram: anchor.web3.SystemProgram.programId,
}).rpc();

console.log("tx signature: ", tx);

// Fetch the state struct from the network.
const accountState = await program.account.helloWorld.fetch(helloWorld);
console.log("account state: ", accountState);

});

it("get hello world!", async () => {

// Add your test here.
const tx = await program.methods.update("Davirain").accounts({
helloWorld,
}).rpc();

console.log("tx signature: ", tx);


// Fetch the state struct from the network.
const accountState = await program.account.helloWorld.fetch(helloWorld);
console.log("account state: ", accountState);
});


it("read account name", async () => {

// Fetch the state struct from the network.
const accountState = await program.account.helloWorld.fetch(helloWorld);
console.log("account state: ", accountState);
});
});

3. 运行本地测试验证器

在编写好你的程序之后,让我们确保我们的Solana CLI配置指向本地主机,使用solana config set --url命令。

solana config set --url localhost

接下来,使用solana config get命令检查Solana CLI配置是否已更新。

solana config get

最后,运行本地测试验证器。在一个单独的终端窗口中运行solana-test-validator命令。只有当我们的RPC URL设置为localhost时才需要这样做。

solana-test-validator
caution

这里一定要注意⚠️,solana-test-validator 这个命令启动的是solana的本地测试验证器。

4. 构建和部署

我们现在准备好构建和部署我们的程序了。通过运行 anchor build 命令来构建程序。

anchor build

现在让我们部署我们的程序。

anchor deploy

Solana程序部署将输出你的程序的程序ID。你现在可以在Solana Explorer上查找已部署的程序(对于localhost,请选择“自定义RPC URL”作为集群)。

5. 查看程序日志

在我们调用程序之前,打开一个单独的终端并运行solana logs命令。这将允许我们在终端中查看程序日志。

solana logs <PROGRAM_ID>

或者也可以通过Solana Exporer,查看产生的日志📔。

在测试验证器仍在运行时,尝试使用此处的客户端脚本调用你的程序。

这将返回一个Solana Explorer的URL(Transaction https://explorer.solana.com/tx/${transactionSignature}?cluster=custom)。将URL复制到浏览器中,在Solana Explorer上查找该交易,并检查程序日志中是否打印了Hello, world!。或者,你可以在运行solana logs命令的终端中查看程序日志。

就是这样!你刚刚在本地开发环境中创建并部署了你的第一个程序。

- - +
Skip to main content

Anchor 合约框架实现 - hello, World 🌍 With PDA

让我们通过构建和部署 Hello World! 程序来进行练习。

我们将在本地完成所有操作,包括部署到本地测试验证器。在开始之前,请确保你已经安装了RustSolana CLI。如果你还没有安装,请参考概述中的说明进行设置。

0. Anchor 安装

这里是Anchor安装官方指南.

需要你按照步骤安装好 Anchor

安装完成后我们可以通过执行下面的命令,检测 Anchor 是否安装完成✅。

anchor --version
anchor --version
anchor-cli 0.28.0

1. 创建一个新的Rust项目

让我们从创建一个新的Rust项目开始。运行下面的anchor init <you-project-name>命令。随意用你自己的目录名替换它。

anchor init hello_world

2. 编写你的程序

接下来,使用下面的Hello World!程序更新hello_world/program/src/lib.rs。当程序被调用时,该程序会将传入的数据保存到数据存储账户中去也就是下面的HelloWorld账户。

use anchor_lang::prelude::*;

declare_id!("22sSSi7GtQgwXFcjJmGNNKSCLEsiJxyYLFfP3CMWeMLj");

#[program]
pub mod hello_world {
use super::*;

pub fn initialize(ctx: Context<Initialize>, data: String) -> Result<()> {

msg!("{}", data);

*ctx.accounts.hello_world = HelloWorld {
authority: *ctx.accounts.authority.key,
data,
};

Ok(())
}

pub fn update(ctx: Context<UpdateHelloWorld>, data: String) -> Result<()> {
ctx.accounts.hello_world.data = data;
msg!("{}", ctx.accounts.hello_world.data);
Ok(())
}
}

#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = authority,
space = 8 + HelloWorld::INIT_SPACE,
seeds = [b"hello-world"],
bump
)]
pub hello_world: Account<'info, HelloWorld>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct UpdateHelloWorld<'info> {
#[account(
mut,
seeds = [b"hello-world"],
bump
)]
pub hello_world: Account<'info, HelloWorld>,
#[account(mut)]
pub authority: Signer<'info>,
}

#[account]
#[derive(InitSpace)]
pub struct HelloWorld {
pub authority: Pubkey,
#[max_len(100)]
pub data: String,
}

#[error_code]
pub enum ErrorCode {
#[msg("You are not authorized to perform this action.")]
Unauthorized,
#[msg("Cannot get the bump.")]
CannotGetBump,
}

下面这是一个本地的测试脚本文件,用来调用上面的合约程序。

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { HelloWorld } from "../target/types/hello_world";

describe("hello-world", () => {
let provider = anchor.AnchorProvider.env();
// Configure the client to use the local cluster.
anchor.setProvider(provider);

const program = anchor.workspace.HelloWorld as Program<HelloWorld>;

const authority = provider.wallet.publicKey;

let [helloWorld] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("hello-world")],
program.programId
);

it("Is initialized!", async () => {
// Add your test here.
const tx = await program.methods.initialize("Hello World!").accounts({
helloWorld,
authority,
systemProgram: anchor.web3.SystemProgram.programId,
}).rpc();

console.log("tx signature: ", tx);

// Fetch the state struct from the network.
const accountState = await program.account.helloWorld.fetch(helloWorld);
console.log("account state: ", accountState);

});

it("get hello world!", async () => {

// Add your test here.
const tx = await program.methods.update("Davirain").accounts({
helloWorld,
}).rpc();

console.log("tx signature: ", tx);


// Fetch the state struct from the network.
const accountState = await program.account.helloWorld.fetch(helloWorld);
console.log("account state: ", accountState);
});


it("read account name", async () => {

// Fetch the state struct from the network.
const accountState = await program.account.helloWorld.fetch(helloWorld);
console.log("account state: ", accountState);
});
});

3. 运行本地测试验证器

在编写好你的程序之后,让我们确保我们的Solana CLI配置指向本地主机,使用solana config set --url命令。

solana config set --url localhost

接下来,使用solana config get命令检查Solana CLI配置是否已更新。

solana config get

最后,运行本地测试验证器。在一个单独的终端窗口中运行solana-test-validator命令。只有当我们的RPC URL设置为localhost时才需要这样做。

solana-test-validator
caution

这里一定要注意⚠️,solana-test-validator 这个命令启动的是solana的本地测试验证器。

4. 构建和部署

我们现在准备好构建和部署我们的程序了。通过运行 anchor build 命令来构建程序。

anchor build

现在让我们部署我们的程序。

anchor deploy

Solana程序部署将输出你的程序的程序ID。你现在可以在Solana Explorer上查找已部署的程序(对于localhost,请选择“自定义RPC URL”作为集群)。

5. 查看程序日志

在我们调用程序之前,打开一个单独的终端并运行solana logs命令。这将允许我们在终端中查看程序日志。

solana logs <PROGRAM_ID>

或者也可以通过Solana Exporer,查看产生的日志📔。

在测试验证器仍在运行时,尝试使用此处的客户端脚本调用你的程序。

这将返回一个Solana Explorer的URL(Transaction https://explorer.solana.com/tx/${transactionSignature}?cluster=custom)。将URL复制到浏览器中,在Solana Explorer上查找该交易,并检查程序日志中是否打印了Hello, world!。或者,你可以在运行solana logs命令的终端中查看程序日志。

就是这样!你刚刚在本地开发环境中创建并部署了你的第一个程序。

+ + \ No newline at end of file diff --git a/Solana-Co-Learn/module1/local_program_development/index.html b/Solana-Co-Learn/module1/local_program_development/index.html index 0a7d965ba..5ad7ec65c 100644 --- a/Solana-Co-Learn/module1/local_program_development/index.html +++ b/Solana-Co-Learn/module1/local_program_development/index.html @@ -9,15 +9,15 @@ - - + +
Skip to main content

本地开发环境配置

概述

本地开发的基本流程如下

  1. 安装 RustSolana CLI
  2. 使用Solana CLI,你可以使用solana-test-validator命令运行本地测试验证器,初始化账户等基本操作
  3. 使用 cargo build-sbfsolana program deploy 命令在本地构建和部署程序
  4. 使用 solana logs 命令查看程序日志

本地环境配置

Solana Program 使用Rust 编写,调试运行。建议使用Unix 系列系统: Mac , Linux 等。 如果很不幸你使用的是Windows,建议使用 WSL 下载Ubuntu ,并在其中完成运行。

在Windows上设置(带有Linux)

下载Windows子系统Linux(WSL)

如果你使用的是Windows电脑,建议使用Windows子系统Linux(WSL)来构建你的Solana程序。

打开管理员权限的PowerShellWindows命令提示符,检查Windows版本

winver

如果你使用的是Windows 10版本2004及更高版本(Build 19041及更高版本)或Windows 11,请运行以下命令。

wsl --install

如果你正在使用较旧版本的Windows,请按照这里的说明进行操作。

你可以在这里阅读更多关于安装WSL的信息。

下载Ubuntu

接下来,在这里下载UbuntuUbuntu提供了一个终端,可以让你在Windows电脑上运行Linux。这就是你将运行Solana CLI命令的地方。

下载 Rust(适用于 WSL)

接下来,打开Ubuntu终端并使用以下命令下载适用于WSLRust。你可以在此处阅读有关下载Rust的更多信息。

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

下载 Solana CLI

现在我们准备下载适用于LinuxSolana CLI。请在Ubuntu终端中运行以下命令。你可以在此处阅读有关下载Solana CLI的更多信息。

sh -c "$(curl -sSfL https://release.solana.com/v1.16.10/install)"

在 macOS 上进行设置

下载 Rust

首先,按照这里的说明下载Rust

下载Solana CLI

接下来,在终端中运行以下命令下载Solana CLI

sh -c "$(curl -sSfL https://release.solana.com/v1.16.10/install)"

你可以在这里了解更多关于下载Solana CLI的信息。

Solana CLI基础

Solana CLI是一个命令行界面工具,提供了一系列命令,用于与Solana集群进行交互。

在本课程中,我们将介绍一些最常见的命令,但你始终可以通过运行solana --help来查看所有可能的Solana CLI命令列表。

Solana CLI 配置

Solana CLI存储了一些配置设置,这些设置会影响某些命令的行为。你可以使用以下命令查看当前的配置:

solana config get

solana config get命令将返回以下内容:

  • 配置文件 - Solana CLI所在的文件位于你的计算机上
  • RPC URL - 你正在使用的端点,将你连接到本地主机、开发网络或主网络
  • WebSocket URL - 监听来自目标集群的事件的WebSocket(在设置RPC URL时计算)
  • 密钥对路径 - 在运行Solana CLI子命令时使用的密钥对路径
  • Commitment - 提供了网络确认的度量,并描述了一个区块在特定时间点上的最终性程度

你可以随时使用solana config set命令更改你的Solana CLI配置,然后跟上你想要更新的设置。

最常见的更改将是你要定位的集群。使用solana config set --url命令更改RPC URL

# localhost
solana config set --url localhost

# devnet
solana config set --url devnet

# mainnet-beta
solana config set --url mainnet-beta

caution

由于某些你知道的原因,devnet 或者 mainnet 可能访问不佳。建议开发过程中使用 localhost 网络。最后需要部署应用的使用,建议使用 quicknode 的rpc 节点。

同样地,你可以使用solana config set --keypair命令来更改密钥对路径。当运行命令时,Solana CLI将使用指定路径下的密钥对。

solana config set --keypair ~/<FILE_PATH>

测试验证器

你会发现在测试和调试时运行本地验证器比部署到开发网络更有帮助。

你可以使用solana-test-validator命令运行本地测试验证器。该命令会创建一个持续运行的进程,需要单独的命令行窗口。

Stream program logs

通常在打开一个新的控制台并在测试验证器旁边运行solana logs命令会很有帮助。这将创建另一个持续进行的进程,用于流式传输与你配置的集群相关的日志。

如果你的CLI配置指向本地主机,则日志将始终与你创建的测试验证器相关联,但你也可以从其他集群(如DevnetMainnet Beta)流式传输日志。当从其他集群流式传输日志时,你需要在命令中包含一个程序ID,以限制你所看到的日志仅针对你的特定程序。

密钥相关

你可以使用solana-keygen new --outfile命令生成一个新的密钥对,并指定文件路径以存储该密钥对。

solana-keygen new --outfile ~/<FILE_PATH>

有时候你可能需要检查你的配置指向哪个密钥对。要查看当前在solana config中设置的密钥对的公钥,请使用solana address命令。

solana address

要查看在Solana配置中设置的当前密钥对的SOL余额,请使用solana balance命令。

solana balance

要在Devnetlocalhost上进行SOL的空投,请使用solana airdrop命令。请注意,在Devnet上,每次空投限制为2个SOL。

solana airdrop 2

在你开发和测试本地环境中的程序时,很可能会遇到由以下原因引起的错误:

  • 使用错误的密钥对
  • 没有足够的SOL来部署你的程序或执行交易
  • 指向错误的集群

到目前为止,我们已经介绍了一些CLI命令,这些命令应该能帮助你快速解决那些问题。

hello world 程序

挑战

现在轮到你独立构建一些东西了。尝试创建一个新的程序,将你自己的消息打印到程序日志中。这次将你的程序部署到Devnet而不是本地主机。

记得使用solana config set --url命令将你的RPC URL更新为Devnet

只要你将连接和Solana ExplorerURL更新为指向Devnet而不是localhost,你就可以使用与演示中相同的客户端脚本来调用该程序。

let connection = new web3.Connection(web3.clusterApiUrl("devnet"));
console.log(
`Transaction: https://explorer.solana.com/tx/${transactionSignature}?cluster=devnet`
);

你还可以打开一个单独的命令行窗口,并使用solana logs | grep " invoke" -A 。在Devnet上使用solana logs时,你必须指定程序ID。否则,solana logs命令将返回来自Devnet的持续日志流。例如,你可以按照以下步骤监视对Token程序的调用,并显示每个调用的前5行日志:

solana logs | grep "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke" -A 5

官方参考文档

- - +
  • Solang solidity合约实现 - hello, World
  • 挑战

    现在轮到你独立构建一些东西了。尝试创建一个新的程序,将你自己的消息打印到程序日志中。这次将你的程序部署到Devnet而不是本地主机。

    记得使用solana config set --url命令将你的RPC URL更新为Devnet

    只要你将连接和Solana ExplorerURL更新为指向Devnet而不是localhost,你就可以使用与演示中相同的客户端脚本来调用该程序。

    let connection = new web3.Connection(web3.clusterApiUrl("devnet"));
    console.log(
    `Transaction: https://explorer.solana.com/tx/${transactionSignature}?cluster=devnet`
    );

    你还可以打开一个单独的命令行窗口,并使用solana logs | grep " invoke" -A 。在Devnet上使用solana logs时,你必须指定程序ID。否则,solana logs命令将返回来自Devnet的持续日志流。例如,你可以按照以下步骤监视对Token程序的调用,并显示每个调用的前5行日志:

    solana logs | grep "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke" -A 5

    官方参考文档

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module1/local_program_development/native_program_hello/index.html b/Solana-Co-Learn/module1/local_program_development/native_program_hello/index.html index f9ca03f19..77b1f2519 100644 --- a/Solana-Co-Learn/module1/local_program_development/native_program_hello/index.html +++ b/Solana-Co-Learn/module1/local_program_development/native_program_hello/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    Native Solana合约实现 - hello, World

    让我们通过构建和部署 Hello World! 程序来进行练习。

    我们将在本地完成所有操作,包括部署到本地测试验证器。在开始之前,请确保你已经安装了RustSolana CLI。如果你还没有安装,请参考概述中的说明进行设置。

    1. 创建一个新的Rust项目

    让我们从创建一个新的Rust项目开始。运行下面的cargo new --lib命令。随意用你自己的目录名替换它。

    cargo new --lib solana-hello-world-local

    记得更新 Cargo.toml 文件,将 solana-program 添加为依赖项,并检查 crate-type 是否已经存在。

    这里的solana-program 可以通过在命令行执行cargo add solana-program添加到依赖管理的配置文件中。

    [package]
    name = "solana-hello-world-local"
    version = "0.1.0"
    edition = "2021"

    [dependencies]
    solana-program = "1.16.10"

    [lib]
    crate-type = ["cdylib", "lib"]
    caution

    需要注意这里的solana-program的版本,不要直接copy这个Cargo.toml的配置,因为solana-program的版本也是在更新的,可能以后直接使用这里的会出问题。建议使用cargo add solana-program添加。

    2. 编写你的程序

    接下来,使用下面的Hello World! 程序更新lib.rs。当程序被调用时,该程序会简单地将Hello, world! 打印到程序日志中。

    use solana_program::{
    account_info::AccountInfo,
    entrypoint,
    entrypoint::ProgramResult,
    pubkey::Pubkey,
    msg
    };

    entrypoint!(process_instruction);

    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
    ) -> ProgramResult{
    msg!("Hello, world!");

    Ok(())
    }

    3. 运行本地测试验证器

    在编写好你的程序之后,让我们确保我们的Solana CLI配置指向本地主机,使用solana config set --url命令。

    solana config set --url localhost

    接下来,使用solana config get命令检查Solana CLI配置是否已更新。

    solana config get

    最后,运行本地测试验证器。在一个单独的终端窗口中运行solana-test-validator命令。只有当我们的RPC URL设置为localhost时才需要这样做。

    solana-test-validator
    caution

    这里一定要注意⚠️,solana-test-validator 这个命令启动的是solana的本地测试验证器。

    4. 构建和部署

    我们现在准备好构建和部署我们的程序了。通过运行 cargo build-sbf 命令来构建程序。

    cargo build-sbf

    现在让我们部署我们的程序。部署从cargo build-sbf命令的输出target/deploy/*.so文件。

    ls --tree target/ --depth 2
     target
    ├──  .rustc_info.json
    ├──  CACHEDIR.TAG
    ├──  debug
    │ ├──  .cargo-lock
    │ ├──  .fingerprint
    │ ├──  build
    │ ├──  deps
    │ ├──  examples
    │ └──  incremental
    ├──  deploy
    │ ├──  solana_hello_world_local-keypair.json
    │ └──  solana_hello_world_local.so
    ├──  release
    │ ├──  .cargo-lock
    │ ├──  .fingerprint
    │ ├──  build
    │ ├──  deps
    │ ├──  examples
    │ └──  incremental
    └──  sbf-solana-solana
    ├──  CACHEDIR.TAG
    └──  release

    这里的Path 是上面的target/deploy/*.so文件的路径。运行solana program deploy命令来部署你的程序。

    solana program deploy <PATH>

    Solana程序部署将输出你的程序的程序ID。你现在可以在Solana Explorer上查找已部署的程序(对于Localhost,请选择“自定义RPC URL”作为集群)。

    5. 查看程序日志

    在我们调用程序之前,打开一个单独的终端并运行solana logs命令。这将允许我们在终端中查看程序日志。

    solana logs <PROGRAM_ID>

    在测试验证器仍在运行时,尝试使用此处的客户端脚本调用你的程序。

    index.ts中用刚刚部署的PROGRAM ID替换掉原来的PROGRAM ID,然后运行npm install,接着运行npm start。这将返回一个Solana ExplorerURL。将URL复制到浏览器中,在Solana Explorer上查找该交易,并检查程序日志中是否打印了Hello, world!。或者,你可以在运行solana logs命令的终端中查看程序日志。

    就是这样!你刚刚在本地开发环境中创建并部署了你的第一个程序。

    - - +
    Skip to main content

    Native Solana合约实现 - hello, World

    让我们通过构建和部署 Hello World! 程序来进行练习。

    我们将在本地完成所有操作,包括部署到本地测试验证器。在开始之前,请确保你已经安装了RustSolana CLI。如果你还没有安装,请参考概述中的说明进行设置。

    1. 创建一个新的Rust项目

    让我们从创建一个新的Rust项目开始。运行下面的cargo new --lib命令。随意用你自己的目录名替换它。

    cargo new --lib solana-hello-world-local

    记得更新 Cargo.toml 文件,将 solana-program 添加为依赖项,并检查 crate-type 是否已经存在。

    这里的solana-program 可以通过在命令行执行cargo add solana-program添加到依赖管理的配置文件中。

    [package]
    name = "solana-hello-world-local"
    version = "0.1.0"
    edition = "2021"

    [dependencies]
    solana-program = "1.16.10"

    [lib]
    crate-type = ["cdylib", "lib"]
    caution

    需要注意这里的solana-program的版本,不要直接copy这个Cargo.toml的配置,因为solana-program的版本也是在更新的,可能以后直接使用这里的会出问题。建议使用cargo add solana-program添加。

    2. 编写你的程序

    接下来,使用下面的Hello World! 程序更新lib.rs。当程序被调用时,该程序会简单地将Hello, world! 打印到程序日志中。

    use solana_program::{
    account_info::AccountInfo,
    entrypoint,
    entrypoint::ProgramResult,
    pubkey::Pubkey,
    msg
    };

    entrypoint!(process_instruction);

    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
    ) -> ProgramResult{
    msg!("Hello, world!");

    Ok(())
    }

    3. 运行本地测试验证器

    在编写好你的程序之后,让我们确保我们的Solana CLI配置指向本地主机,使用solana config set --url命令。

    solana config set --url localhost

    接下来,使用solana config get命令检查Solana CLI配置是否已更新。

    solana config get

    最后,运行本地测试验证器。在一个单独的终端窗口中运行solana-test-validator命令。只有当我们的RPC URL设置为localhost时才需要这样做。

    solana-test-validator
    caution

    这里一定要注意⚠️,solana-test-validator 这个命令启动的是solana的本地测试验证器。

    4. 构建和部署

    我们现在准备好构建和部署我们的程序了。通过运行 cargo build-sbf 命令来构建程序。

    cargo build-sbf

    现在让我们部署我们的程序。部署从cargo build-sbf命令的输出target/deploy/*.so文件。

    ls --tree target/ --depth 2
     target
    ├──  .rustc_info.json
    ├──  CACHEDIR.TAG
    ├──  debug
    │ ├──  .cargo-lock
    │ ├──  .fingerprint
    │ ├──  build
    │ ├──  deps
    │ ├──  examples
    │ └──  incremental
    ├──  deploy
    │ ├──  solana_hello_world_local-keypair.json
    │ └──  solana_hello_world_local.so
    ├──  release
    │ ├──  .cargo-lock
    │ ├──  .fingerprint
    │ ├──  build
    │ ├──  deps
    │ ├──  examples
    │ └──  incremental
    └──  sbf-solana-solana
    ├──  CACHEDIR.TAG
    └──  release

    这里的Path 是上面的target/deploy/*.so文件的路径。运行solana program deploy命令来部署你的程序。

    solana program deploy <PATH>

    Solana程序部署将输出你的程序的程序ID。你现在可以在Solana Explorer上查找已部署的程序(对于Localhost,请选择“自定义RPC URL”作为集群)。

    5. 查看程序日志

    在我们调用程序之前,打开一个单独的终端并运行solana logs命令。这将允许我们在终端中查看程序日志。

    solana logs <PROGRAM_ID>

    在测试验证器仍在运行时,尝试使用此处的客户端脚本调用你的程序。

    index.ts中用刚刚部署的PROGRAM ID替换掉原来的PROGRAM ID,然后运行npm install,接着运行npm start。这将返回一个Solana ExplorerURL。将URL复制到浏览器中,在Solana Explorer上查找该交易,并检查程序日志中是否打印了Hello, world!。或者,你可以在运行solana logs命令的终端中查看程序日志。

    就是这样!你刚刚在本地开发环境中创建并部署了你的第一个程序。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module1/local_program_development/solang_program_hello/index.html b/Solana-Co-Learn/module1/local_program_development/solang_program_hello/index.html index 4c958ac66..26669ce4e 100644 --- a/Solana-Co-Learn/module1/local_program_development/solang_program_hello/index.html +++ b/Solana-Co-Learn/module1/local_program_development/solang_program_hello/index.html @@ -9,14 +9,14 @@ - - + +
    Skip to main content

    Solang solidity合约实现 - hello, World

    欢迎来到Solana入门指南!Solang是一个Solidity编译器,它允许你使用Solidity编程语言编写Solana程序,其他区块链中称为“智能合约”。

    如果你是一位对Solana网络的高速和低费用感兴趣的EVM开发者,那么Solang是你的完美工具。通过Solang,你可以利用你对Solidity的现有知识开始在Solana上进行构建!

    安装

    在本节中,我们将帮助你设置Solang的开发环境。只需按照下面列出的步骤进行操作即可:

    1. 检查先决条件:在开始之前,请确保你的系统上已安装了RustNode.js。Windows用户还需要设置好Windows子系统以便运行Linux

    2. Solana工具套件安装:首先安装Solana工具套件,其中包括Solana命令行界面(CLI)和最新版本的Solang

    3. Anchor框架安装:接下来,安装Anchor框架AnchorSolana生态系统中广泛使用的框架,可以简化构建Solana程序的过程。从0.28版本开始,你可以直接通过Anchor开始使用Solang进行构建。

    截至撰写本文时,请使用以下命令安装Anchor,以确保与Solang版本0.3.1兼容:

    cargo install --git https://github.com/coral-xyz/anchor anchor-cli --locked --force
    1. Solang扩展适用于VSCode:如果你是Visual Studio Code(VSCode)的用户,建议安装Solang扩展以辅助语法高亮显示。请记得禁用任何活动的Solidity扩展,以确保Solang扩展正常工作。

    创建一个新项目

    一旦你安装了Solana CLIAnchor,你可以使用以下命令创建一个新项目:

    anchor init project_name --solidity

    该命令生成一个新项目,其中包含一个基本的Solang on-chain程序(相当于EVM上的智能合约)和一个测试文件,演示了如何从客户端与该程序进行交互。

    链上程序概述

    接下来,让我们来看一下从链上程序本身开始的初始代码。在你的项目的 ./solidity 目录中,你将找到下面的合约,其中包括:

    • 一个 constructor 用于初始化状态变量的函数
    • 一个用于将消息打印到程序日志的函数
    • 一个用于更新状态变量的函数
    • 一个函数,用于返回状态变量的当前值
    @program_id("F1ipperKF9EfD821ZbbYjS319LXYiBmjhzkkf5a26rC")
    contract starter {
    bool private value = true;

    @payer(payer)
    constructor(address payer) {
    print("Hello, World!");
    }

    /// A message that can be called on instantiated contracts.
    /// This one flips the value of the stored `bool` from `true`
    /// to `false` and vice versa.
    function flip() public {
    value = !value;
    }

    /// Simply returns the current value of our `bool`.
    function get() public view returns (bool) {
    return value;
    }
    }

    重要的差异

    EVM智能合约相比,你可能会注意到两个重要的区别:

    1. @program_id 注解: -在Solana上,智能合约被称为“程序”。使用 @program_id 注释来指定程序的链上地址。
    @program_id("F1ipperKF9EfD821ZbbYjS319LXYiBmjhzkkf5a26rC") // on-chain program address
    1. @payer 注解:

    在链上存储数据时,需要分配一定数量的SOL来支付存储成本。注释 @payer 指定了将支付所需SOL以创建用于存储状态变量的账户的用户。

    @payer(payer) // payer for the "data account"
    constructor(address payer) {
    print("Hello, World!");
    }

    状态数据的存储

    EVM智能合约和Solana程序之间的一个重要区别在于它们如何存储“状态”变量/数据:

    • EVM智能合约可以直接存储状态变量。
    • Solana的链上程序则会创建单独的账户来存储状态数据。这些账户通常被称为“数据账户”,并且是由程序“拥有”。

    在这个例子中,当合约部署时,它被部署到 @program_id 中指定的地址。当程序部署后调用 constructor 时,会创建一个独立的帐户,用于存储状态变量,而不是存储在合约本身内部。

    这可能听起来有点不同于你所习惯的,但别担心!让我们来看一下测试文件,以更好地理解这个概念。

    测试文件概述

    起始测试文件可以在 ./tests 目录中找到。该文件提供了一个与客户端交互的示例。

    Anchor设置了 providerprogram ,以帮助我们从客户端连接到合约。这是通过使用IDL文件来完成的,该文件描述了程序的公共接口,类似于EVM智能合约中使用的ABI文件。如果你运行 anchor build ,则会生成IDL文件,并且可以在 ./target/idl 找到。

    import * as anchor from "@coral-xyz/anchor"
    import { Program } from "@coral-xyz/anchor"
    import { Starter } from "../target/types/starter"

    describe("starter", () => {
    // Configure the client to use the local cluster.
    const provider = anchor.AnchorProvider.env()
    anchor.setProvider(provider)

    const dataAccount = anchor.web3.Keypair.generate()
    const wallet = provider.wallet

    const program = anchor.workspace.Starter as Program<Starter>

    it("Is initialized!", async () => {
    // Add your test here.
    const tx = await program.methods
    .new(wallet.publicKey)
    .accounts({ dataAccount: dataAccount.publicKey })
    .signers([dataAccount])
    .rpc()
    console.log("Your transaction signature", tx)

    const val1 = await program.methods
    .get()
    .accounts({ dataAccount: dataAccount.publicKey })
    .view()

    console.log("state", val1)

    await program.methods
    .flip()
    .accounts({ dataAccount: dataAccount.publicKey })
    .rpc()

    const val2 = await program.methods
    .get()
    .accounts({ dataAccount: dataAccount.publicKey })
    .view()

    console.log("state", val2)
    })
    })

    在测试文件中,我们首先生成一个新的密钥对,用于创建存储合约状态的“数据账户”。

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

    接下来,我们使用 new 指令来创建一个新的数据账户。这个指令对应于合约的 constructor 。新创建的数据账户将被初始化,用于存储合约中定义的状态变量。

    在这里, payer 被指定为 wallet.publicKey ,并提供了我们计划创建的 dataAccount 的地址。生成的 dataAccount Keypair作为交易的附加签名者包含在其中,因为它被用于创建一个新的账户。基本上,这个操作验证了我们持有与我们正在创建的新账户地址相对应的私钥。

    // Client
    const tx = await program.methods
    .new(wallet.publicKey)
    .accounts({ dataAccount: dataAccount.publicKey })
    .signers([dataAccount])
    .rpc()

    // on-chain program
    @payer(payer)
    constructor(address payer) {
    print("Hello, World!");
    }

    合约的 get 函数被调用以获取存储在指定 dataAccount 中的值。

    // Client
    const val1 = await program.methods
    .get()
    .accounts({ dataAccount: dataAccount.publicKey })
    .view()

    // on-chain program
    function get() public view returns (bool) {
    return value;
    }

    接下来,合约的 flip 函数被用来修改指定 dataAccount 的状态。

    // Client
    await program.methods
    .flip()
    .accounts({ dataAccount: dataAccount.publicKey })
    .rpc()

    // on-chain program
    function flip() public {
    value = !value;
    }

    要运行测试,请在终端中使用 anchor test 命令。

    anchor test 命令执行以下任务:

    • 启动本地Solana验证节点
    • 构建并部署你的链上程序到本地验证节点
    • 运行测试文件

    接下来应该在控制台中显示以下输出:

    Your transaction signature 2x7jh3yka9LU6ZeJLUZNNDJSzq6vdUAXk3mUKuP1MYwr6ArYMHDGw6i15jJnMtnC7BP7zKactStHhTekjq2vh6hP
    state true
    state false
    ✔ Is initialized! (782ms)

    你可以在 ./.anchor/program-logs 中查看程序日志,那里会找到“Hello, World!”的消息

    Program F1ipperKF9EfD821ZbbYjS319LXYiBmjhzkkf5a26rC invoke [1]
    Program 11111111111111111111111111111111 invoke [2]
    Program 11111111111111111111111111111111 success
    Program log: Hello, World!

    恭喜!你成功地使用 Solang 构建了你的第一个 Solana 程序!虽然与标准 Solidity 智能合约相比可能存在一些差异,但 Solang 提供了一个极好的桥梁,帮助你利用现有的 Solidity 技能和经验来构建 Solana 上的应用。

    下一步

    有兴趣深入了解吗?请查看 solana-developers/program-examples 存储库。你将在 basicstokens 部分找到适用于常见Solana用例的Solang实现。

    如果你有问题,请随时在Solana Stack exchange上发布。如果你有关于Solang维护者的问题,可以直接在Hyperledger Foundationdiscord上联系他们。

    玩得开心,尽情建造吧!

    - - +在Solana上,智能合约被称为“程序”。使用 @program_id 注释来指定程序的链上地址。
    @program_id("F1ipperKF9EfD821ZbbYjS319LXYiBmjhzkkf5a26rC") // on-chain program address
    1. @payer 注解:

    在链上存储数据时,需要分配一定数量的SOL来支付存储成本。注释 @payer 指定了将支付所需SOL以创建用于存储状态变量的账户的用户。

    @payer(payer) // payer for the "data account"
    constructor(address payer) {
    print("Hello, World!");
    }

    状态数据的存储

    EVM智能合约和Solana程序之间的一个重要区别在于它们如何存储“状态”变量/数据:

    在这个例子中,当合约部署时,它被部署到 @program_id 中指定的地址。当程序部署后调用 constructor 时,会创建一个独立的帐户,用于存储状态变量,而不是存储在合约本身内部。

    这可能听起来有点不同于你所习惯的,但别担心!让我们来看一下测试文件,以更好地理解这个概念。

    测试文件概述

    起始测试文件可以在 ./tests 目录中找到。该文件提供了一个与客户端交互的示例。

    Anchor设置了 providerprogram ,以帮助我们从客户端连接到合约。这是通过使用IDL文件来完成的,该文件描述了程序的公共接口,类似于EVM智能合约中使用的ABI文件。如果你运行 anchor build ,则会生成IDL文件,并且可以在 ./target/idl 找到。

    import * as anchor from "@coral-xyz/anchor"
    import { Program } from "@coral-xyz/anchor"
    import { Starter } from "../target/types/starter"

    describe("starter", () => {
    // Configure the client to use the local cluster.
    const provider = anchor.AnchorProvider.env()
    anchor.setProvider(provider)

    const dataAccount = anchor.web3.Keypair.generate()
    const wallet = provider.wallet

    const program = anchor.workspace.Starter as Program<Starter>

    it("Is initialized!", async () => {
    // Add your test here.
    const tx = await program.methods
    .new(wallet.publicKey)
    .accounts({ dataAccount: dataAccount.publicKey })
    .signers([dataAccount])
    .rpc()
    console.log("Your transaction signature", tx)

    const val1 = await program.methods
    .get()
    .accounts({ dataAccount: dataAccount.publicKey })
    .view()

    console.log("state", val1)

    await program.methods
    .flip()
    .accounts({ dataAccount: dataAccount.publicKey })
    .rpc()

    const val2 = await program.methods
    .get()
    .accounts({ dataAccount: dataAccount.publicKey })
    .view()

    console.log("state", val2)
    })
    })

    在测试文件中,我们首先生成一个新的密钥对,用于创建存储合约状态的“数据账户”。

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

    接下来,我们使用 new 指令来创建一个新的数据账户。这个指令对应于合约的 constructor 。新创建的数据账户将被初始化,用于存储合约中定义的状态变量。

    在这里, payer 被指定为 wallet.publicKey ,并提供了我们计划创建的 dataAccount 的地址。生成的 dataAccount Keypair作为交易的附加签名者包含在其中,因为它被用于创建一个新的账户。基本上,这个操作验证了我们持有与我们正在创建的新账户地址相对应的私钥。

    // Client
    const tx = await program.methods
    .new(wallet.publicKey)
    .accounts({ dataAccount: dataAccount.publicKey })
    .signers([dataAccount])
    .rpc()

    // on-chain program
    @payer(payer)
    constructor(address payer) {
    print("Hello, World!");
    }

    合约的 get 函数被调用以获取存储在指定 dataAccount 中的值。

    // Client
    const val1 = await program.methods
    .get()
    .accounts({ dataAccount: dataAccount.publicKey })
    .view()

    // on-chain program
    function get() public view returns (bool) {
    return value;
    }

    接下来,合约的 flip 函数被用来修改指定 dataAccount 的状态。

    // Client
    await program.methods
    .flip()
    .accounts({ dataAccount: dataAccount.publicKey })
    .rpc()

    // on-chain program
    function flip() public {
    value = !value;
    }

    要运行测试,请在终端中使用 anchor test 命令。

    anchor test 命令执行以下任务:

    接下来应该在控制台中显示以下输出:

    Your transaction signature 2x7jh3yka9LU6ZeJLUZNNDJSzq6vdUAXk3mUKuP1MYwr6ArYMHDGw6i15jJnMtnC7BP7zKactStHhTekjq2vh6hP
    state true
    state false
    ✔ Is initialized! (782ms)

    你可以在 ./.anchor/program-logs 中查看程序日志,那里会找到“Hello, World!”的消息

    Program F1ipperKF9EfD821ZbbYjS319LXYiBmjhzkkf5a26rC invoke [1]
    Program 11111111111111111111111111111111 invoke [2]
    Program 11111111111111111111111111111111 success
    Program log: Hello, World!

    恭喜!你成功地使用 Solang 构建了你的第一个 Solana 程序!虽然与标准 Solidity 智能合约相比可能存在一些差异,但 Solang 提供了一个极好的桥梁,帮助你利用现有的 Solidity 技能和经验来构建 Solana 上的应用。

    下一步

    有兴趣深入了解吗?请查看 solana-developers/program-examples 存储库。你将在 basicstokens 部分找到适用于常见Solana用例的Solang实现。

    如果你有问题,请随时在Solana Stack exchange上发布。如果你有关于Solang维护者的问题,可以直接在Hyperledger Foundationdiscord上联系他们。

    玩得开心,尽情建造吧!

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module1/start-your-own-custom-project/build-an-nft-minter-front-end/index.html b/Solana-Co-Learn/module1/start-your-own-custom-project/build-an-nft-minter-front-end/index.html index 590a8a2ad..fae93f2c0 100644 --- a/Solana-Co-Learn/module1/start-your-own-custom-project/build-an-nft-minter-front-end/index.html +++ b/Solana-Co-Learn/module1/start-your-own-custom-project/build-an-nft-minter-front-end/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    💻 构建 NFT 铸造者前端

    欢迎来到第一周的挑战环节。每周,你都会有一个特定的部分,用来将你所学的内容应用到自定义的NFT质押应用程序上,并且还有战利品箱子等你拿!

    这些部分的核心目的是鼓励你走出本地开发环境,构建真实的、可以供他人使用的项目。许多成功的构建者都是通过在公众面前展示和开发他们的作品而获得成功的。这是你一直在准备的时刻——让我们开始吧🤘。

    今天,我们要开始从前端制作那些炫酷的登录和铸造页面。

    在第一个屏幕上,唯一的功能是连接到用户的钱包。你可以通过屏幕顶部的按钮或中间的按钮来实现。

    第二个屏幕的功能将在下一个核心项目中实现,所以不必为“mint buildoor”按钮实现任何功能。

    🕸 项目设置

    我们将从零开始,没有模板!设置一个新的 Next.js 应用程序,并向其中添加 Chakra UI

    npx create-next-app <you-project-name> --typescript

    cd <you-project-name>

    npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^10 @chakra-ui/icons

    npm i @solana/wallet-adapter-base @solana/wallet-adapter-react @solana/wallet-adapter-react-ui @solana/wallet-adapter-wallets @solana/web3.js

    注意:在整个项目中,我们将使用Typescript!当然,如果你更喜欢,完全可以使用普通的Javascript :)。

    如果系统要求安装 create-next-app,请确认安装。你可以为你的应用程序取任何你想要的名字,比如我就给我的应用程序命名为“buildoor”。

    下一步,你可能想添加一些视觉素材。你可以在这里找到资源包,也可以自己创建。包里有五个“头像”文件和一个背景的svg文件。请将它们放入项目的public文件夹中。

    现在,一切准备就绪,让我们开始构建吧!🚀

    ✨ 配置 Chakra UI

    首个任务是配置 Chakra UI,这样我们就能避免手动编写大量的 CSS。我们将在 pages/_app.tsx 文件中执行此操作:

    import type { AppProps } from "next/app"
    import { ChakraProvider } from "@chakra-ui/react"
    import { extendTheme } from "@chakra-ui/react"

    const colors = {
    background: "#1F1F1F",
    accent: "#833BBE",
    bodyText: "rgba(255, 255, 255, 0.75)",
    }

    const theme = extendTheme({ colors })

    function MyApp({ Component, pageProps }: AppProps) {
    return (
    <ChakraProvider theme={theme}>
    <Component {...pageProps} />
    </ChakraProvider>
    )
    }

    export default MyApp

    这里我添加了一些自定义的颜色,你也可以根据自己的喜好进行调整!

    🌶 添加一些样式

    接下来,打开 styles/Home.module.css 文件并将其修改如下:

    .container {
    background: #1F1F1F;
    }
    .wallet-adapter-button-trigger {
    background-color: #833BBE;
    }

    如果样式文件夹中有 globals.css 文件,请将其删除。我们不会用到它!

    然后,我们将处理 index.tsx 文件。我们将更新导入语句,以使用 Chakra UI,并修改渲染代码(只需保留一个 <div className={styles.container})。然后将导入更新为:

    import { Box, Center, Spacer, Stack } from "@chakra-ui/react"
    import type { NextPage } from "next"
    import Head from "next/head"
    import styles from "../styles/Home.module.css"

    const Home: NextPage = () => {

    return (
    <div className={styles.container}>
    <Head>
    <title>Buildoors</title>
    <meta name="The NFT Collection for Buildoors" />
    <link rel="icon" href="/favicon.ico" />
    </Head>

    <Box
    w="full"
    h="calc(100vh)"
    bgImage={"url(/home-background.svg)"}
    backgroundPosition="center"
    >
    <Stack w="full" h="calc(100vh)" justify="center">
    { /* 导航栏 */ }

    <Spacer />
    <Center>
    { /* 如果已连接,则显示第二个视图,否则显示第一个视图 */ }
    </Center>
    <Spacer />

    <Center>
    <Box marginBottom={4} color="white">
    <a
    href="https://twitter.com/_buildspace"
    target="_blank"
    rel="noopener noreferrer"
    >
    @_buildspace 一同打造
    </a>
    </Box>
    </Center>
    </Stack>
    </Box>
    </div>
    )
    }

    export default Home

    这段代码设置了应用程序的主页面,并使用了Chakra UI的一些组件来简化布局和样式。现在,你的前端页面应该已经具备了基本的结构和风格,接下来你可以继续添加更多的功能和内容!🎨

    🎫 添加导航栏

    现在让我们构建导航栏(NavBar)。请创建一个 components 文件夹,并在其中添加一个新文件 NavBar.tsx。我们将其构建为一个水平堆栈,其中包括一个空间间隔器和一个用于连接钱包的按钮:

    import { HStack, Spacer } from "@chakra-ui/react"
    import { FC } from "react"
    import styles from "../styles/Home.module.css"
    import dynamic from "next/dynamic";

    const WalletMultiButtonDynamic = dynamic(
    async () =>
    (await import("@solana/wallet-adapter-react-ui")).WalletMultiButton,
    { ssr: false }
    );

    const NavBar: FC = () => {
    return (
    <HStack width="full" padding={4}>
    <Spacer />
    <WalletMultiButtonDynamic className={styles["wallet-adapter-button-trigger"]}/>
    </HStack>
    )
    }

    export default NavBar

    这里我们使用 import dynamic from "next/dynamic"@solana/wallet-adapter-react-ui 动态导入 WalletMultiButton,并将其分配给 WalletMultiButtonDynamic

    这是必需的,因为 NextJS 是服务器端渲染的,在客户端加载之前无法访问依赖于浏览器 API(例如 window)的外部依赖项或组件。

    因此,通过 { ssr: false },我们禁用了导入的服务器渲染。关于动态导入的更多信息,你可以在这里阅读。

    现在返回到 index.tsx 文件,导入 NavBar 并将其放在堆栈的顶部(我已留下评论说明它应该放在哪里):

    // 现有的导入
    import NavBar from "../components/NavBar"

    const Home: NextPage = () => {

    return (
    <div className={styles.container}>
    <Head>
    // ... 其他代码 ...

    <Box
    w="full"
    h="calc(100vh)"
    bgImage={"url(/home-background.svg)"}
    backgroundPosition="center"
    >
    <Stack w="full" h="calc(100vh)" justify="center">
    { /* NavBar */ }
    <NavBar />

    // 其余的文件保持不变

    至此,除了“连接钱包(Connect Wallet)”按钮外,在 localhost:3000 上还没有任何内容。但我们已经迈出了实现更多功能的重要一步。让我们继续前进!🚀

    🏠 创建登录页面

    components 文件夹中创建一个名为 Disconnected.tsx 的文件,并添加以下内容:

    import { FC, MouseEventHandler, useCallback } from "react"
    import {
    Button,
    Container,
    Heading,
    HStack,
    Text,
    VStack,
    } from "@chakra-ui/react"
    import { ArrowForwardIcon } from "@chakra-ui/icons"

    const Disconnected: FC = () => {

    const handleClick: MouseEventHandler<HTMLButtonElement> = useCallback(
    (event) => {
    if (event.defaultPrevented) {
    return
    }
    },
    []
    )

    return (
    <Container>
    <VStack spacing={20}>
    <Heading
    color="white"
    as="h1"
    size="3xl"
    noOfLines={2}
    textAlign="center"
    >
    打造你的 buildoor。赚取 $BLD。升级。
    </Heading>
    <Button
    bgColor="accent"
    color="white"
    maxW="380px"
    onClick={handleClick}
    >
    <HStack>
    <Text>成为 buildoor</Text>
    <ArrowForwardIcon />
    </HStack>
    </Button>
    </VStack>
    </Container>
    )
    }

    export default Disconnected

    这将是我们的登录页面 - 用户首次访问网站时会看到的视图。你需要将其导入到 index.tsx 中,并将其放置在渲染组件的中间位置(你可以再次查找相应的注释来找到正确的位置)。

    // 现有的导入
    import Disconnected from '../components/Disconnected'

    const Home: NextPage = () => {

    return (
    <div className={styles.container}>
    <Head>
    // ... 其他代码 ...

    <Box
    w="full"
    h="calc(100vh)"
    bgImage={"url(/home-background.svg)"}
    backgroundPosition="center"
    >
    <Stack w="full" h="calc(100vh)" justify="center">
    { /* NavBar */ }
    <NavBar />

    <Spacer />
    <Center>
    <Disconnected />
    </Center>
    <Spacer />

    // 其余的文件保持不变

    现在,如果你访问 localhost:3000,你应该会看到一个带有“成为 buildoor”按钮的登录页面。如果你点击它,目前什么也不会发生。这显然不是我们想要的,所以接下来我们要处理这个问题!让我们继续!

    🔌 连接到用户的钱包

    这一部分中,我们将连接到用户的钱包,确保你的应用可以与用户的钱包互动。

    首先,我们需要安装一些必要的依赖包:

    npm install @solana/wallet-adapter-base \
    @solana/wallet-adapter-react \
    @solana/wallet-adapter-react-ui \
    @solana/wallet-adapter-wallets \
    @solana/web3.js

    这些库将帮助我们与用户的Solana钱包连接。

    如果你要为特定钱包构建,你可以在这里自定义设置。这里我们只是使用默认配置。

    components 文件夹中,创建一个名为 WalletContextProvider.tsx 的文件,我们将在其中放置所有这些配置:

    import { FC, ReactNode, useMemo } from "react"
    import {
    ConnectionProvider,
    WalletProvider,
    } from "@solana/wallet-adapter-react"
    import { WalletModalProvider } from "@solana/wallet-adapter-react-ui"
    import { clusterApiUrl } from "@solana/web3.js"
    import { BackpackWalletAdapter } from "@solana/wallet-adapter-wallets"
    require("@solana/wallet-adapter-react-ui/styles.css")

    const WalletContextProvider: FC<{ children: ReactNode }> = ({ children }) => {
    const url = useMemo(() => clusterApiUrl("devnet"), [])
    const backpack = new BackpackWalletAdapter()

    return (
    <ConnectionProvider endpoint={url}>
    <WalletProvider wallets={[backpack]}>
    <WalletModalProvider>{children}</WalletModalProvider>
    </WalletProvider>
    </ConnectionProvider>
    )
    }

    export default WalletContextProvider

    然后,我们需要将这个组件导入到 _app.tsx 文件中:

    import WalletContextProvider from '../components/WalletContextProvider'

    <ChakraProvider theme={theme}>
    <WalletContextProvider>
    <Component {...pageProps} />
    </WalletContextProvider>
    </ChakraProvider>

    我们现在想让“成为建造者”按钮也能连接到钱包。在 Disconnected.tsx 文件中,添加以下导入:

    import { useWalletModal } from "@solana/wallet-adapter-react-ui"
    import { useWallet } from "@solana/wallet-adapter-react"

    然后在渲染之前,更新 Disconnected 组件的主体如下:

    const modalState = useWalletModal()
    const { wallet, connect } = useWallet()

    const handleClick: MouseEventHandler<HTMLButtonElement> = useCallback(
    (event) => {
    if (event.defaultPrevented) {
    return
    }

    if (!wallet) {
    modalState.setVisible(true)
    } else {
    connect().catch(() => {})
    }
    },
    [wallet, connect, modalState]
    )

    现在一切准备就绪,你应该可以连接到用户的钱包了!这一步骤使你的应用程序能够与Solana区块链进行交互,从而为用户提供更丰富的体验。

    🎇 创建连接视图

    现在我们已经可以连接钱包了,下一步就是更新视图来展示连接状态下的用户界面。首先,在components文件夹中创建一个名为Connected.tsx的文件,它将定义连接成功后的页面。

    import { FC } from "react"
    import {
    Button,
    Container,
    Heading,
    HStack,
    Text,
    VStack,
    Image,
    } from "@chakra-ui/react"
    import { ArrowForwardIcon } from "@chakra-ui/icons"

    const Connected: FC = () => {
    return (
    <VStack spacing={20}>
    <Container>
    <VStack spacing={8}>
    <Heading
    color="white"
    as="h1"
    size="2xl"
    noOfLines={1}
    textAlign="center"
    >
    欢迎,Buildoor。
    </Heading>

    <Text color="bodyText" fontSize="xl" textAlign="center">
    每个buildoor都是随机生成的,可以抵押接收
    <Text as="b"> $BLD</Text>。使用你的 <Text as="b"> $BLD</Text>
    升级你的buildoor,并在社区内获得特权!
    </Text>
    </VStack>
    </Container>

    <HStack spacing={10}>
    <Image src="avatar1.png" alt="" />
    <Image src="avatar2.png" alt="" />
    <Image src="avatar3.png" alt="" />
    <Image src="avatar4.png" alt="" />
    <Image src="avatar5.png" alt="" />
    </HStack>

    <Button bgColor="accent" color="white" maxW="380px">
    <HStack>
    <Text>铸造buildoor</Text>
    <ArrowForwardIcon />
    </HStack>
    </Button>
    </VStack>
    )
    }

    export default Connected

    接下来,我们需要将该视图嵌入到主页面。回到index.tsx文件,添加以下导入:

    import { useWallet } from "@solana/wallet-adapter-react"
    import Connected from "../components/Connected"

    然后,我们可以使用useWallet hooks来访问一个告诉我们是否已连接的变量。我们可以用它来有条件地渲染ConnectedDisconnected视图。

    const Home: NextPage = () => {
    const { connected } = useWallet()

    return (
    <div className={styles.container}>
    <Head>
    <title>Buildoors</title>
    <meta name="Buildoors的NFT收藏" />
    <link rel="icon" href="/favicon.ico" />
    </Head>

    <Box
    w="full"
    h="calc(100vh)"
    bgImage={connected ? "" : "url(/home-background.svg)"}
    backgroundPosition="center"
    >
    <Stack w="full" h="calc(100vh)" justify="center">
    <NavBar />

    <Spacer />
    <Center>{connected ? <Connected /> : <Disconnected />}</Center>
    <Spacer />

    完成了!现在我们已经配置好了前端,并且在用户铸造buildoors时流程顺畅。这个界面不仅直观,还提供了丰富的用户体验。

    - - +
    Skip to main content

    💻 构建 NFT 铸造者前端

    欢迎来到第一周的挑战环节。每周,你都会有一个特定的部分,用来将你所学的内容应用到自定义的NFT质押应用程序上,并且还有战利品箱子等你拿!

    这些部分的核心目的是鼓励你走出本地开发环境,构建真实的、可以供他人使用的项目。许多成功的构建者都是通过在公众面前展示和开发他们的作品而获得成功的。这是你一直在准备的时刻——让我们开始吧🤘。

    今天,我们要开始从前端制作那些炫酷的登录和铸造页面。

    在第一个屏幕上,唯一的功能是连接到用户的钱包。你可以通过屏幕顶部的按钮或中间的按钮来实现。

    第二个屏幕的功能将在下一个核心项目中实现,所以不必为“mint buildoor”按钮实现任何功能。

    🕸 项目设置

    我们将从零开始,没有模板!设置一个新的 Next.js 应用程序,并向其中添加 Chakra UI

    npx create-next-app <you-project-name> --typescript

    cd <you-project-name>

    npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^10 @chakra-ui/icons

    npm i @solana/wallet-adapter-base @solana/wallet-adapter-react @solana/wallet-adapter-react-ui @solana/wallet-adapter-wallets @solana/web3.js

    注意:在整个项目中,我们将使用Typescript!当然,如果你更喜欢,完全可以使用普通的Javascript :)。

    如果系统要求安装 create-next-app,请确认安装。你可以为你的应用程序取任何你想要的名字,比如我就给我的应用程序命名为“buildoor”。

    下一步,你可能想添加一些视觉素材。你可以在这里找到资源包,也可以自己创建。包里有五个“头像”文件和一个背景的svg文件。请将它们放入项目的public文件夹中。

    现在,一切准备就绪,让我们开始构建吧!🚀

    ✨ 配置 Chakra UI

    首个任务是配置 Chakra UI,这样我们就能避免手动编写大量的 CSS。我们将在 pages/_app.tsx 文件中执行此操作:

    import type { AppProps } from "next/app"
    import { ChakraProvider } from "@chakra-ui/react"
    import { extendTheme } from "@chakra-ui/react"

    const colors = {
    background: "#1F1F1F",
    accent: "#833BBE",
    bodyText: "rgba(255, 255, 255, 0.75)",
    }

    const theme = extendTheme({ colors })

    function MyApp({ Component, pageProps }: AppProps) {
    return (
    <ChakraProvider theme={theme}>
    <Component {...pageProps} />
    </ChakraProvider>
    )
    }

    export default MyApp

    这里我添加了一些自定义的颜色,你也可以根据自己的喜好进行调整!

    🌶 添加一些样式

    接下来,打开 styles/Home.module.css 文件并将其修改如下:

    .container {
    background: #1F1F1F;
    }
    .wallet-adapter-button-trigger {
    background-color: #833BBE;
    }

    如果样式文件夹中有 globals.css 文件,请将其删除。我们不会用到它!

    然后,我们将处理 index.tsx 文件。我们将更新导入语句,以使用 Chakra UI,并修改渲染代码(只需保留一个 <div className={styles.container})。然后将导入更新为:

    import { Box, Center, Spacer, Stack } from "@chakra-ui/react"
    import type { NextPage } from "next"
    import Head from "next/head"
    import styles from "../styles/Home.module.css"

    const Home: NextPage = () => {

    return (
    <div className={styles.container}>
    <Head>
    <title>Buildoors</title>
    <meta name="The NFT Collection for Buildoors" />
    <link rel="icon" href="/favicon.ico" />
    </Head>

    <Box
    w="full"
    h="calc(100vh)"
    bgImage={"url(/home-background.svg)"}
    backgroundPosition="center"
    >
    <Stack w="full" h="calc(100vh)" justify="center">
    { /* 导航栏 */ }

    <Spacer />
    <Center>
    { /* 如果已连接,则显示第二个视图,否则显示第一个视图 */ }
    </Center>
    <Spacer />

    <Center>
    <Box marginBottom={4} color="white">
    <a
    href="https://twitter.com/_buildspace"
    target="_blank"
    rel="noopener noreferrer"
    >
    @_buildspace 一同打造
    </a>
    </Box>
    </Center>
    </Stack>
    </Box>
    </div>
    )
    }

    export default Home

    这段代码设置了应用程序的主页面,并使用了Chakra UI的一些组件来简化布局和样式。现在,你的前端页面应该已经具备了基本的结构和风格,接下来你可以继续添加更多的功能和内容!🎨

    🎫 添加导航栏

    现在让我们构建导航栏(NavBar)。请创建一个 components 文件夹,并在其中添加一个新文件 NavBar.tsx。我们将其构建为一个水平堆栈,其中包括一个空间间隔器和一个用于连接钱包的按钮:

    import { HStack, Spacer } from "@chakra-ui/react"
    import { FC } from "react"
    import styles from "../styles/Home.module.css"
    import dynamic from "next/dynamic";

    const WalletMultiButtonDynamic = dynamic(
    async () =>
    (await import("@solana/wallet-adapter-react-ui")).WalletMultiButton,
    { ssr: false }
    );

    const NavBar: FC = () => {
    return (
    <HStack width="full" padding={4}>
    <Spacer />
    <WalletMultiButtonDynamic className={styles["wallet-adapter-button-trigger"]}/>
    </HStack>
    )
    }

    export default NavBar

    这里我们使用 import dynamic from "next/dynamic"@solana/wallet-adapter-react-ui 动态导入 WalletMultiButton,并将其分配给 WalletMultiButtonDynamic

    这是必需的,因为 NextJS 是服务器端渲染的,在客户端加载之前无法访问依赖于浏览器 API(例如 window)的外部依赖项或组件。

    因此,通过 { ssr: false },我们禁用了导入的服务器渲染。关于动态导入的更多信息,你可以在这里阅读。

    现在返回到 index.tsx 文件,导入 NavBar 并将其放在堆栈的顶部(我已留下评论说明它应该放在哪里):

    // 现有的导入
    import NavBar from "../components/NavBar"

    const Home: NextPage = () => {

    return (
    <div className={styles.container}>
    <Head>
    // ... 其他代码 ...

    <Box
    w="full"
    h="calc(100vh)"
    bgImage={"url(/home-background.svg)"}
    backgroundPosition="center"
    >
    <Stack w="full" h="calc(100vh)" justify="center">
    { /* NavBar */ }
    <NavBar />

    // 其余的文件保持不变

    至此,除了“连接钱包(Connect Wallet)”按钮外,在 localhost:3000 上还没有任何内容。但我们已经迈出了实现更多功能的重要一步。让我们继续前进!🚀

    🏠 创建登录页面

    components 文件夹中创建一个名为 Disconnected.tsx 的文件,并添加以下内容:

    import { FC, MouseEventHandler, useCallback } from "react"
    import {
    Button,
    Container,
    Heading,
    HStack,
    Text,
    VStack,
    } from "@chakra-ui/react"
    import { ArrowForwardIcon } from "@chakra-ui/icons"

    const Disconnected: FC = () => {

    const handleClick: MouseEventHandler<HTMLButtonElement> = useCallback(
    (event) => {
    if (event.defaultPrevented) {
    return
    }
    },
    []
    )

    return (
    <Container>
    <VStack spacing={20}>
    <Heading
    color="white"
    as="h1"
    size="3xl"
    noOfLines={2}
    textAlign="center"
    >
    打造你的 buildoor。赚取 $BLD。升级。
    </Heading>
    <Button
    bgColor="accent"
    color="white"
    maxW="380px"
    onClick={handleClick}
    >
    <HStack>
    <Text>成为 buildoor</Text>
    <ArrowForwardIcon />
    </HStack>
    </Button>
    </VStack>
    </Container>
    )
    }

    export default Disconnected

    这将是我们的登录页面 - 用户首次访问网站时会看到的视图。你需要将其导入到 index.tsx 中,并将其放置在渲染组件的中间位置(你可以再次查找相应的注释来找到正确的位置)。

    // 现有的导入
    import Disconnected from '../components/Disconnected'

    const Home: NextPage = () => {

    return (
    <div className={styles.container}>
    <Head>
    // ... 其他代码 ...

    <Box
    w="full"
    h="calc(100vh)"
    bgImage={"url(/home-background.svg)"}
    backgroundPosition="center"
    >
    <Stack w="full" h="calc(100vh)" justify="center">
    { /* NavBar */ }
    <NavBar />

    <Spacer />
    <Center>
    <Disconnected />
    </Center>
    <Spacer />

    // 其余的文件保持不变

    现在,如果你访问 localhost:3000,你应该会看到一个带有“成为 buildoor”按钮的登录页面。如果你点击它,目前什么也不会发生。这显然不是我们想要的,所以接下来我们要处理这个问题!让我们继续!

    🔌 连接到用户的钱包

    这一部分中,我们将连接到用户的钱包,确保你的应用可以与用户的钱包互动。

    首先,我们需要安装一些必要的依赖包:

    npm install @solana/wallet-adapter-base \
    @solana/wallet-adapter-react \
    @solana/wallet-adapter-react-ui \
    @solana/wallet-adapter-wallets \
    @solana/web3.js

    这些库将帮助我们与用户的Solana钱包连接。

    如果你要为特定钱包构建,你可以在这里自定义设置。这里我们只是使用默认配置。

    components 文件夹中,创建一个名为 WalletContextProvider.tsx 的文件,我们将在其中放置所有这些配置:

    import { FC, ReactNode, useMemo } from "react"
    import {
    ConnectionProvider,
    WalletProvider,
    } from "@solana/wallet-adapter-react"
    import { WalletModalProvider } from "@solana/wallet-adapter-react-ui"
    import { clusterApiUrl } from "@solana/web3.js"
    import { BackpackWalletAdapter } from "@solana/wallet-adapter-wallets"
    require("@solana/wallet-adapter-react-ui/styles.css")

    const WalletContextProvider: FC<{ children: ReactNode }> = ({ children }) => {
    const url = useMemo(() => clusterApiUrl("devnet"), [])
    const backpack = new BackpackWalletAdapter()

    return (
    <ConnectionProvider endpoint={url}>
    <WalletProvider wallets={[backpack]}>
    <WalletModalProvider>{children}</WalletModalProvider>
    </WalletProvider>
    </ConnectionProvider>
    )
    }

    export default WalletContextProvider

    然后,我们需要将这个组件导入到 _app.tsx 文件中:

    import WalletContextProvider from '../components/WalletContextProvider'

    <ChakraProvider theme={theme}>
    <WalletContextProvider>
    <Component {...pageProps} />
    </WalletContextProvider>
    </ChakraProvider>

    我们现在想让“成为建造者”按钮也能连接到钱包。在 Disconnected.tsx 文件中,添加以下导入:

    import { useWalletModal } from "@solana/wallet-adapter-react-ui"
    import { useWallet } from "@solana/wallet-adapter-react"

    然后在渲染之前,更新 Disconnected 组件的主体如下:

    const modalState = useWalletModal()
    const { wallet, connect } = useWallet()

    const handleClick: MouseEventHandler<HTMLButtonElement> = useCallback(
    (event) => {
    if (event.defaultPrevented) {
    return
    }

    if (!wallet) {
    modalState.setVisible(true)
    } else {
    connect().catch(() => {})
    }
    },
    [wallet, connect, modalState]
    )

    现在一切准备就绪,你应该可以连接到用户的钱包了!这一步骤使你的应用程序能够与Solana区块链进行交互,从而为用户提供更丰富的体验。

    🎇 创建连接视图

    现在我们已经可以连接钱包了,下一步就是更新视图来展示连接状态下的用户界面。首先,在components文件夹中创建一个名为Connected.tsx的文件,它将定义连接成功后的页面。

    import { FC } from "react"
    import {
    Button,
    Container,
    Heading,
    HStack,
    Text,
    VStack,
    Image,
    } from "@chakra-ui/react"
    import { ArrowForwardIcon } from "@chakra-ui/icons"

    const Connected: FC = () => {
    return (
    <VStack spacing={20}>
    <Container>
    <VStack spacing={8}>
    <Heading
    color="white"
    as="h1"
    size="2xl"
    noOfLines={1}
    textAlign="center"
    >
    欢迎,Buildoor。
    </Heading>

    <Text color="bodyText" fontSize="xl" textAlign="center">
    每个buildoor都是随机生成的,可以抵押接收
    <Text as="b"> $BLD</Text>。使用你的 <Text as="b"> $BLD</Text>
    升级你的buildoor,并在社区内获得特权!
    </Text>
    </VStack>
    </Container>

    <HStack spacing={10}>
    <Image src="avatar1.png" alt="" />
    <Image src="avatar2.png" alt="" />
    <Image src="avatar3.png" alt="" />
    <Image src="avatar4.png" alt="" />
    <Image src="avatar5.png" alt="" />
    </HStack>

    <Button bgColor="accent" color="white" maxW="380px">
    <HStack>
    <Text>铸造buildoor</Text>
    <ArrowForwardIcon />
    </HStack>
    </Button>
    </VStack>
    )
    }

    export default Connected

    接下来,我们需要将该视图嵌入到主页面。回到index.tsx文件,添加以下导入:

    import { useWallet } from "@solana/wallet-adapter-react"
    import Connected from "../components/Connected"

    然后,我们可以使用useWallet hooks来访问一个告诉我们是否已连接的变量。我们可以用它来有条件地渲染ConnectedDisconnected视图。

    const Home: NextPage = () => {
    const { connected } = useWallet()

    return (
    <div className={styles.container}>
    <Head>
    <title>Buildoors</title>
    <meta name="Buildoors的NFT收藏" />
    <link rel="icon" href="/favicon.ico" />
    </Head>

    <Box
    w="full"
    h="calc(100vh)"
    bgImage={connected ? "" : "url(/home-background.svg)"}
    backgroundPosition="center"
    >
    <Stack w="full" h="calc(100vh)" justify="center">
    <NavBar />

    <Spacer />
    <Center>{connected ? <Connected /> : <Disconnected />}</Center>
    <Spacer />

    完成了!现在我们已经配置好了前端,并且在用户铸造buildoors时流程顺畅。这个界面不仅直观,还提供了丰富的用户体验。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module1/start-your-own-custom-project/deploy-to-vercel/index.html b/Solana-Co-Learn/module1/start-your-own-custom-project/deploy-to-vercel/index.html index 39422a254..292848d70 100644 --- a/Solana-Co-Learn/module1/start-your-own-custom-project/deploy-to-vercel/index.html +++ b/Solana-Co-Learn/module1/start-your-own-custom-project/deploy-to-vercel/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🌐 部署到 Vercel

    这一步是你本周工作中至关重要的一环,即将你的项目从本地环境部署到线上环境。

    我们将会把前端部署到Vercel上。Vercel是一个托管平台,可以让你轻松地部署Web应用程序,而且最棒的是,它是免费的!

    首先,你需要将项目推送到Github。如果你不确定如何操作,只需要在Google中搜索相关教程,整个过程应该只需要大约5分钟。

    完成后,请前往Vercel并将其与你的Github帐户连接。它应该会自动检测到这是一个Next.js项目,并会引导你进行非常简单的部署过程。构建完成后,它会提供一个链接。

    恭喜你,现在你的项目已经成功部署到线上了!🎉 在Vercel托管的优势在于,它不仅提供了强大的基础设施,而且还能确保你的网站响应迅速、安全可靠。现在你可以分享你的链接,让全世界的人都可以访问你的应用了!

    - - +
    Skip to main content

    🌐 部署到 Vercel

    这一步是你本周工作中至关重要的一环,即将你的项目从本地环境部署到线上环境。

    我们将会把前端部署到Vercel上。Vercel是一个托管平台,可以让你轻松地部署Web应用程序,而且最棒的是,它是免费的!

    首先,你需要将项目推送到Github。如果你不确定如何操作,只需要在Google中搜索相关教程,整个过程应该只需要大约5分钟。

    完成后,请前往Vercel并将其与你的Github帐户连接。它应该会自动检测到这是一个Next.js项目,并会引导你进行非常简单的部署过程。构建完成后,它会提供一个链接。

    恭喜你,现在你的项目已经成功部署到线上了!🎉 在Vercel托管的优势在于,它不仅提供了强大的基础设施,而且还能确保你的网站响应迅速、安全可靠。现在你可以分享你的链接,让全世界的人都可以访问你的应用了!

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module1/start-your-own-custom-project/index.html b/Solana-Co-Learn/module1/start-your-own-custom-project/index.html index 156b7f9ed..49b209e0e 100644 --- a/Solana-Co-Learn/module1/start-your-own-custom-project/index.html +++ b/Solana-Co-Learn/module1/start-your-own-custom-project/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module1/wallet-usage/index.html b/Solana-Co-Learn/module1/wallet-usage/index.html index b54da43bf..ec604bcc1 100644 --- a/Solana-Co-Learn/module1/wallet-usage/index.html +++ b/Solana-Co-Learn/module1/wallet-usage/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    Solana钱包使用 - Backpack 🎒

    Solana的钱包种类繁多,如众所周知的Phantom钱包。然而,在此我并不推荐使用Phantom,因为对于开发者来说,它并不够友好。

    在本地开发时,Phantom不支持本地RPC地址,只能使用Solana官方的测试网络。这样一来,开发者在本地开发时就会受限制。正因如此,我推荐使用Backpack钱包。

    下载Backpack钱包

    下载地址:https://www.backpack.app/。点击页面上的“Download”按钮,然后选择适合自己浏览器的插件。

    目前Backpack钱包只支持ChromeBraveArc浏览器。移动端版本还在开发中。

    创建账户

    下载安装完毕后,你将看到登录界面。点击Create Account创建账户。

    接下来是Claim账户名字的步骤,输入你想要的账户名字,然后点击Claim Name

    此后,系统将引导你选择创建新的钱包或导入私钥。在此我们选择创建新的钱包。

    接下来将展示助记词界面,你可以将助记词保存到本地,然后继续下一步。

    由于Backpack是一个多链钱包,在此我们选择Solana链,然后继续下一步。

    设置自定义的RPC Endpoint

    账户创建完毕后,我们来设置自定义的RPC Endpoint。点击右上角的设置按钮,然后选择Preference

    然后点击Preference选项。

    你将看到有两个网络选项,分别是Solana和以太坊。我们在这里选择Solana

    接下来就是设置自定义RPC的环节,我们在此选择了localnet

    Custom选项下,你可以自定义RPC地址。除了官方提供的testnetmainbeta地址,你还可以通过QuicknodeHelius申请自己的RPC地址。

    参考资料

    总而言之,Backpack钱包由于其对开发者友好的特性,以及灵活的自定义RPC设置功能,成为了一款值得推荐的钱包选项。它使得开发者能够更便捷、灵活地进行Solana开发和测试工作。

    - - +
    Skip to main content

    Solana钱包使用 - Backpack 🎒

    Solana的钱包种类繁多,如众所周知的Phantom钱包。然而,在此我并不推荐使用Phantom,因为对于开发者来说,它并不够友好。

    在本地开发时,Phantom不支持本地RPC地址,只能使用Solana官方的测试网络。这样一来,开发者在本地开发时就会受限制。正因如此,我推荐使用Backpack钱包。

    下载Backpack钱包

    下载地址:https://www.backpack.app/。点击页面上的“Download”按钮,然后选择适合自己浏览器的插件。

    目前Backpack钱包只支持ChromeBraveArc浏览器。移动端版本还在开发中。

    创建账户

    下载安装完毕后,你将看到登录界面。点击Create Account创建账户。

    接下来是Claim账户名字的步骤,输入你想要的账户名字,然后点击Claim Name

    此后,系统将引导你选择创建新的钱包或导入私钥。在此我们选择创建新的钱包。

    接下来将展示助记词界面,你可以将助记词保存到本地,然后继续下一步。

    由于Backpack是一个多链钱包,在此我们选择Solana链,然后继续下一步。

    设置自定义的RPC Endpoint

    账户创建完毕后,我们来设置自定义的RPC Endpoint。点击右上角的设置按钮,然后选择Preference

    然后点击Preference选项。

    你将看到有两个网络选项,分别是Solana和以太坊。我们在这里选择Solana

    接下来就是设置自定义RPC的环节,我们在此选择了localnet

    Custom选项下,你可以自定义RPC地址。除了官方提供的testnetmainbeta地址,你还可以通过QuicknodeHelius申请自己的RPC地址。

    参考资料

    总而言之,Backpack钱包由于其对开发者友好的特性,以及灵活的自定义RPC设置功能,成为了一款值得推荐的钱包选项。它使得开发者能够更便捷、灵活地进行Solana开发和测试工作。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module1/wallets-and-frontends/connecting-to-wallet/index.html b/Solana-Co-Learn/module1/wallets-and-frontends/connecting-to-wallet/index.html index f844393e3..f66ccbd70 100644 --- a/Solana-Co-Learn/module1/wallets-and-frontends/connecting-to-wallet/index.html +++ b/Solana-Co-Learn/module1/wallets-and-frontends/connecting-to-wallet/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🔌 连接到钱包

    现在我们已经知道如何使用代码与网络交互,通过直接使用私钥来初始化账户。显然,在正常的去中心化应用(dapp)中,这样做是不可行的(永远不要将你的私钥暴露给任何人或任何dapp)。

    接下来,我们将介绍如何通过SDK和钱包进行交互。

    “钱包”这个词可能听起来有些奇怪,因为它们不仅仅用来存储东西。钱包的关键功能是使用其中的密钥进行安全的交易签名。钱包有很多形式,最常见的是浏览器扩展,它们为你(作为开发者)提供API,以向用户建议交易。钱包让你能够安全地进行操作。

    推荐使用 BackPack

    🛠 Solana 钱包适配器

    在开发过程中,我们使用 Solana Wallet-Adapter 来适配各种钱包,并实现通用的Solana API。支持的钱包列表可以在这里找到。

    你需要使用的适配器库包括 wallet-adapter-basewallet-adapter-react,这两者都是必选的。然后,你可以根据需求选择支持的钱包或使用 wallet-adapter-wallets

    下面是一个使用BackPack🎒登录的示例代码:

    npm install @solana/wallet-adapter-base \
    @solana/wallet-adapter-react \
    @solana/wallet-adapter-backpack \
    @solana/wallet-adapter-react-ui
    • wallet-adapter-react-ui 为我们处理了整个UI,包括连接、选择钱包、断开连接等,一切都已经安排妥当!
    • 可选择使用 @solana/wallet-adapter-backpack 钱包。

    👜 创建一个钱包连接按钮

    下面的教程将指导你创建一个钱包连接按钮,并将其集成到你的Solana项目中。

    1. 初始化项目模板

    首先,你需要从指定的仓库克隆项目模板并进行必要的初始化操作:

    git clone https://github.com/all-in-one-solana/solana-ping-frontend
    cd solana-ping-frontend
    git checkout starter
    npm i
    npm run dev

    该模板继承了我们上次构建的内容——我们为ping客户端提供了一个前端界面,以便将数据写入区块链。

    这是一个初步的系统UI。接下来,让我们将其连接到wallet-adapter-react库。

    2. 修改 _app.tsx,使其具备以下外观

    在此步骤中,你需要修改_app.tsx文件,确保其具有正确的结构和内容。你可以根据项目需求,添加或修改代码,使其与你的钱包适配器完美集成。

    import React, { useMemo } from "react";
    import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
    import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
    import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react";
    import {
    GlowWalletAdapter,
    PhantomWalletAdapter,
    BackpackWalletAdapter
    } from "@solana/wallet-adapter-wallets";
    import { clusterApiUrl } from "@solana/web3.js";

    require("@solana/wallet-adapter-react-ui/styles.css");
    require("../styles/globals.css");
    require ("../styles/Home.module.css");

    const App = ({ Component, pageProps }) => {
    // Can be set to 'devnet', 'testnet', or 'mainnet-beta'
    const network = WalletAdapterNetwork.Devnet;

    // You can provide a custom RPC endpoint here
    const endpoint = useMemo(() => clusterApiUrl(network), [network]);

    // @solana/wallet-adapter-wallets includes all the adapters but supports tree shaking and lazy loading --
    // Only the wallets you configure here will be compiled into your application, and only the dependencies
    // of wallets that your users connect to will be loaded
    const wallets = useMemo(
    () => [
    new PhantomWalletAdapter(),
    new GlowWalletAdapter(),
    new BackpackWalletAdapter()
    ],
    [network]
    );

    return (
    <ConnectionProvider endpoint={endpoint}>
    <WalletProvider wallets={wallets} autoConnect>
    <WalletModalProvider>
    <Component {...pageProps} />
    </WalletModalProvider>
    </WalletProvider>
    </ConnectionProvider>
    );
    };

    export default App;

    通过上述步骤,你将成功创建一个钱包连接按钮,并能与Solana网络进行交互。现在,用户可以方便地使用这个按钮连接到他们的钱包,并享受无缝的区块链体验。

    框架介绍

    以下是关于如何连接和使用钱包的详细步骤和解释。

    1. 这是一个基于React的应用框架。通过useMemo,它会根据网络连接状态确定与Solana网络交互的rpc endpoint
    2. 使用@solana/wallet-adapter-baseWalletAdapterNetwork来展示可用的网络。
    3. WalletModalProvider会向用户提示选择钱包。
    4. ConnectionProvider接受一个RPC端点,并允许我们直接与Solana区块链上的节点通信。我们将在整个应用程序中使用它来发送交易。
    5. WalletProvider为我们提供了连接各种钱包的统一接口。
    6. wallet-adapter-wallets提供了钱包适配器。我们将使用从中导入的内容来创建我们将提供给WalletProvider的钱包列表。在本例中,选择了PhantomGlow, BackPack🎒。
    7. 最后,我们有clusterApiURL,这只是一个根据我们提供的网络为我们生成RPC端点的函数。
    8. 总结一下:这个文件是我们网页应用程序的核心。其实它是一个由Vercel构建的React应用程序,使用_app.tx来构建应用的基本骨架。

    🧞 使用连接钱包

    我们将通过React hook的方式使用钱包,比如在components/AppBar.tsx中设置一个React hook

    import { FC } from 'react'
    import styles from '../styles/Home.module.css'
    import Image from 'next/image'
    import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'

    export const AppBar: FC = () => {
    return (
    <div className={styles.AppHeader}>
    <Image src="/solanaLogo.png" height={30} width={200} />
    <span>Wallet-Adapter Example</span>
    <WalletMultiButton/>
    </div>
    )
    }

    WalletMultiButton为我们处理了许多工作,处理了所有连接的细节。如果你现在强制刷新应用程序,你应该能在右上角看到一个紫色的按钮!

    - - +
    Skip to main content

    🔌 连接到钱包

    现在我们已经知道如何使用代码与网络交互,通过直接使用私钥来初始化账户。显然,在正常的去中心化应用(dapp)中,这样做是不可行的(永远不要将你的私钥暴露给任何人或任何dapp)。

    接下来,我们将介绍如何通过SDK和钱包进行交互。

    “钱包”这个词可能听起来有些奇怪,因为它们不仅仅用来存储东西。钱包的关键功能是使用其中的密钥进行安全的交易签名。钱包有很多形式,最常见的是浏览器扩展,它们为你(作为开发者)提供API,以向用户建议交易。钱包让你能够安全地进行操作。

    推荐使用 BackPack

    🛠 Solana 钱包适配器

    在开发过程中,我们使用 Solana Wallet-Adapter 来适配各种钱包,并实现通用的Solana API。支持的钱包列表可以在这里找到。

    你需要使用的适配器库包括 wallet-adapter-basewallet-adapter-react,这两者都是必选的。然后,你可以根据需求选择支持的钱包或使用 wallet-adapter-wallets

    下面是一个使用BackPack🎒登录的示例代码:

    npm install @solana/wallet-adapter-base \
    @solana/wallet-adapter-react \
    @solana/wallet-adapter-backpack \
    @solana/wallet-adapter-react-ui
    • wallet-adapter-react-ui 为我们处理了整个UI,包括连接、选择钱包、断开连接等,一切都已经安排妥当!
    • 可选择使用 @solana/wallet-adapter-backpack 钱包。

    👜 创建一个钱包连接按钮

    下面的教程将指导你创建一个钱包连接按钮,并将其集成到你的Solana项目中。

    1. 初始化项目模板

    首先,你需要从指定的仓库克隆项目模板并进行必要的初始化操作:

    git clone https://github.com/all-in-one-solana/solana-ping-frontend
    cd solana-ping-frontend
    git checkout starter
    npm i
    npm run dev

    该模板继承了我们上次构建的内容——我们为ping客户端提供了一个前端界面,以便将数据写入区块链。

    这是一个初步的系统UI。接下来,让我们将其连接到wallet-adapter-react库。

    2. 修改 _app.tsx,使其具备以下外观

    在此步骤中,你需要修改_app.tsx文件,确保其具有正确的结构和内容。你可以根据项目需求,添加或修改代码,使其与你的钱包适配器完美集成。

    import React, { useMemo } from "react";
    import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
    import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
    import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react";
    import {
    GlowWalletAdapter,
    PhantomWalletAdapter,
    BackpackWalletAdapter
    } from "@solana/wallet-adapter-wallets";
    import { clusterApiUrl } from "@solana/web3.js";

    require("@solana/wallet-adapter-react-ui/styles.css");
    require("../styles/globals.css");
    require ("../styles/Home.module.css");

    const App = ({ Component, pageProps }) => {
    // Can be set to 'devnet', 'testnet', or 'mainnet-beta'
    const network = WalletAdapterNetwork.Devnet;

    // You can provide a custom RPC endpoint here
    const endpoint = useMemo(() => clusterApiUrl(network), [network]);

    // @solana/wallet-adapter-wallets includes all the adapters but supports tree shaking and lazy loading --
    // Only the wallets you configure here will be compiled into your application, and only the dependencies
    // of wallets that your users connect to will be loaded
    const wallets = useMemo(
    () => [
    new PhantomWalletAdapter(),
    new GlowWalletAdapter(),
    new BackpackWalletAdapter()
    ],
    [network]
    );

    return (
    <ConnectionProvider endpoint={endpoint}>
    <WalletProvider wallets={wallets} autoConnect>
    <WalletModalProvider>
    <Component {...pageProps} />
    </WalletModalProvider>
    </WalletProvider>
    </ConnectionProvider>
    );
    };

    export default App;

    通过上述步骤,你将成功创建一个钱包连接按钮,并能与Solana网络进行交互。现在,用户可以方便地使用这个按钮连接到他们的钱包,并享受无缝的区块链体验。

    框架介绍

    以下是关于如何连接和使用钱包的详细步骤和解释。

    1. 这是一个基于React的应用框架。通过useMemo,它会根据网络连接状态确定与Solana网络交互的rpc endpoint
    2. 使用@solana/wallet-adapter-baseWalletAdapterNetwork来展示可用的网络。
    3. WalletModalProvider会向用户提示选择钱包。
    4. ConnectionProvider接受一个RPC端点,并允许我们直接与Solana区块链上的节点通信。我们将在整个应用程序中使用它来发送交易。
    5. WalletProvider为我们提供了连接各种钱包的统一接口。
    6. wallet-adapter-wallets提供了钱包适配器。我们将使用从中导入的内容来创建我们将提供给WalletProvider的钱包列表。在本例中,选择了PhantomGlow, BackPack🎒。
    7. 最后,我们有clusterApiURL,这只是一个根据我们提供的网络为我们生成RPC端点的函数。
    8. 总结一下:这个文件是我们网页应用程序的核心。其实它是一个由Vercel构建的React应用程序,使用_app.tx来构建应用的基本骨架。

    🧞 使用连接钱包

    我们将通过React hook的方式使用钱包,比如在components/AppBar.tsx中设置一个React hook

    import { FC } from 'react'
    import styles from '../styles/Home.module.css'
    import Image from 'next/image'
    import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'

    export const AppBar: FC = () => {
    return (
    <div className={styles.AppHeader}>
    <Image src="/solanaLogo.png" height={30} width={200} />
    <span>Wallet-Adapter Example</span>
    <WalletMultiButton/>
    </div>
    )
    }

    WalletMultiButton为我们处理了许多工作,处理了所有连接的细节。如果你现在强制刷新应用程序,你应该能在右上角看到一个紫色的按钮!

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module1/wallets-and-frontends/index.html b/Solana-Co-Learn/module1/wallets-and-frontends/index.html index b9d9e177b..4f98852ae 100644 --- a/Solana-Co-Learn/module1/wallets-and-frontends/index.html +++ b/Solana-Co-Learn/module1/wallets-and-frontends/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module1/wallets-and-frontends/interact-with-a-program/index.html b/Solana-Co-Learn/module1/wallets-and-frontends/interact-with-a-program/index.html index bda09d0eb..7814be1c2 100644 --- a/Solana-Co-Learn/module1/wallets-and-frontends/interact-with-a-program/index.html +++ b/Solana-Co-Learn/module1/wallets-and-frontends/interact-with-a-program/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🦺 与程序进行交互

    在成功设置了钱包连接后,我们可以让ping按钮真正执行操作了。以下是如何实现的详细说明。

    这是PingButton.tsx的代码示例,你可以根据下面的解释理解每个部分的功能:

    import { useConnection, useWallet } from '@solana/wallet-adapter-react';
    import * as Web3 from '@solana/web3.js'
    import { FC } from 'react'
    import styles from '../styles/PingButton.module.css'

    const PROGRAM_ID = new Web3.PublicKey("ChT1B39WKLS8qUrkLvFDXMhEJ4F1XZzwUNHUt4AU9aVa")
    const PROGRAM_DATA_PUBLIC_KEY = new Web3.PublicKey("Ah9K7dQ8EHaZqcAsgBW8w37yN2eAy3koFmUn4x3CJtod")

    export const PingButton: FC = () => {
    const { connection } = useConnection();
    const { publicKey, sendTransaction } = useWallet();

    const onClick = () => {
    if (!connection || !publicKey) {
    alert("请先连接你的钱包!")
    return
    }

    const transaction = new Web3.Transaction()

    const instruction = new Web3.TransactionInstruction({
    keys: [
    {
    pubkey: PROGRAM_DATA_PUBLIC_KEY,
    isSigner: false,
    isWritable: true
    },
    ],
    programId: PROGRAM_ID,
    });

    transaction.add(instruction)
    sendTransaction(transaction, connection).then(sig => {
    console.log(`浏览器URL: https://explorer.solana.com/tx/${sig}?cluster=devnet`)
    })
    }

    return (
    <div className={styles.buttonContainer} onClick={onClick}>
    <button className={styles.button}>Ping!</button>
    </div>
    )
    }

    你应该对这一块代码比较熟悉,它与我们在本地客户端上做的事情完全一样,但是使用了React hooks

    接下来就是测试时间了。

    可以查看这里的Solana钱包使用 - Backpack 🎒钱包使用教程切换到开发测试网。

    连接你的钱包并点击那个ping按钮,你将会看到交互反馈。

    点击确认后,控制台将打印出交易链接。向下滚动,你会发现数字已经增加了🚀。

    现在你可以让用户与应用程序互动了!你之前所创建价值1万美元的产品?现在它已经升级成了一个价值百万美元的产品。想象一下所有的程序,比如MetaplexSolana程序库中的任何程序,你现在可以将它们与用户界面连接起来,让人们使用。

    🚢 挑战 - SOL 发送者

    是时候挑战自己了。

    在此挑战中,你将使用此起始代码创建一个应用程序,允许用户连接其Backpack🎒钱包并将SOL发送到另一个账户。确保克隆后使用git checkout starter切换到起始分支。

    你需要通过以下两个关键步骤来完成这个挑战:

    • 使用适当的上下文提供程序包装启动应用程序。
    • 在表单组件中,设置交易并将其发送到用户的钱包以供批准。

    完成后,应用程序应该看起来像这样:

    别忘了验证地址!

    完成后,你可以将你的解决方案与解决方案代码进行比较。

    - - +
    Skip to main content

    🦺 与程序进行交互

    在成功设置了钱包连接后,我们可以让ping按钮真正执行操作了。以下是如何实现的详细说明。

    这是PingButton.tsx的代码示例,你可以根据下面的解释理解每个部分的功能:

    import { useConnection, useWallet } from '@solana/wallet-adapter-react';
    import * as Web3 from '@solana/web3.js'
    import { FC } from 'react'
    import styles from '../styles/PingButton.module.css'

    const PROGRAM_ID = new Web3.PublicKey("ChT1B39WKLS8qUrkLvFDXMhEJ4F1XZzwUNHUt4AU9aVa")
    const PROGRAM_DATA_PUBLIC_KEY = new Web3.PublicKey("Ah9K7dQ8EHaZqcAsgBW8w37yN2eAy3koFmUn4x3CJtod")

    export const PingButton: FC = () => {
    const { connection } = useConnection();
    const { publicKey, sendTransaction } = useWallet();

    const onClick = () => {
    if (!connection || !publicKey) {
    alert("请先连接你的钱包!")
    return
    }

    const transaction = new Web3.Transaction()

    const instruction = new Web3.TransactionInstruction({
    keys: [
    {
    pubkey: PROGRAM_DATA_PUBLIC_KEY,
    isSigner: false,
    isWritable: true
    },
    ],
    programId: PROGRAM_ID,
    });

    transaction.add(instruction)
    sendTransaction(transaction, connection).then(sig => {
    console.log(`浏览器URL: https://explorer.solana.com/tx/${sig}?cluster=devnet`)
    })
    }

    return (
    <div className={styles.buttonContainer} onClick={onClick}>
    <button className={styles.button}>Ping!</button>
    </div>
    )
    }

    你应该对这一块代码比较熟悉,它与我们在本地客户端上做的事情完全一样,但是使用了React hooks

    接下来就是测试时间了。

    可以查看这里的Solana钱包使用 - Backpack 🎒钱包使用教程切换到开发测试网。

    连接你的钱包并点击那个ping按钮,你将会看到交互反馈。

    点击确认后,控制台将打印出交易链接。向下滚动,你会发现数字已经增加了🚀。

    现在你可以让用户与应用程序互动了!你之前所创建价值1万美元的产品?现在它已经升级成了一个价值百万美元的产品。想象一下所有的程序,比如MetaplexSolana程序库中的任何程序,你现在可以将它们与用户界面连接起来,让人们使用。

    🚢 挑战 - SOL 发送者

    是时候挑战自己了。

    在此挑战中,你将使用此起始代码创建一个应用程序,允许用户连接其Backpack🎒钱包并将SOL发送到另一个账户。确保克隆后使用git checkout starter切换到起始分支。

    你需要通过以下两个关键步骤来完成这个挑战:

    • 使用适当的上下文提供程序包装启动应用程序。
    • 在表单组件中,设置交易并将其发送到用户的钱包以供批准。

    完成后,应用程序应该看起来像这样:

    别忘了验证地址!

    完成后,你可以将你的解决方案与解决方案代码进行比较。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/displaying-nfts-from-a-candy-machine/index.html b/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/displaying-nfts-from-a-candy-machine/index.html index d59c93e2b..0fa90d4cc 100644 --- a/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/displaying-nfts-from-a-candy-machine/index.html +++ b/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/displaying-nfts-from-a-candy-machine/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🖼 从糖果机展示NFTs

    现在我们已经铸造了一个NFT,接下来我们将学习如何铸造一系列的NFT。为此,我们将利用Candy Machine来实现——这是Solana的一个程序,使创作者能够将他们的资产上链。虽然这并非创建系列的唯一方式,但在Solana上它却成了标准,因为它具备了许多有用的功能,例如机器人保护和安全随机化。你是否感受到过看到闪亮的新iPhone时的那股兴奋感?稀有的NFT有点儿类似于此。对于优秀的艺术家而言,仅仅是观看这些NFT也极富乐趣。毕竟,艺术的本质就是用来欣赏的!接下来,我们将探讨如果我们只有Candy Machine的地址,应该如何展示NFTs

    你能猜到这里有何不同之处吗?没错,我们仅在SDK上采用了一种不同的方法!

    由于这里并没有钱包,我们不需要使用 walletAdapterIdentity,只需使用metaplex对象即可。

    我们在此有几个选择 - findByAddress 就是我们所需的。

    与我们为单个NFT所获取的相似,我们将会得到整个糖果机实例的元数据。items 字段是糖果机内所有NFT的数组。每个项目都不会直接包含我们想要的内容,而是会引向一个我们可以从中提取资产的URI。

    鉴于收藏品可能会相当庞大,我们不会一次性获取所有的NFT。相反,我们将基于分页系统,只获取我们想要展示的NFT

    那么,让我们一起来绘制一些像素吧!

    🥁 请准备一个糖果机

    你可以从上一节的进度继续,或者使用我们上次使用的相同模板(从起始分支开始即可)。

    赶紧跳入 FetchCandyMachine.tsx 文件吧。你会发现一些设置已经为你准备好了。我们将使用 getPage 函数从糖果机上获取某“页面”上的物品。在此之前,我们需要获取糖果机的元数据账户。

    在空的 fetchCandyMachine 函数上方设置 metaplex 对象的连接。

    export const FetchCandyMachine: FC = () => {
    // 占位符 CMv2 地址
    const [candyMachineAddress, setCandyMachineAddress] = useState("")
    const [candyMachineData, setCandyMachineData] = useState(null)
    const [pageItems, setPageItems] = useState(null)
    const [page, setPage] = useState(1)

    const { connection } = useConnection()
    const metaplex = Metaplex.make(connection)

    在创建有状态变量时,请确保添加你的Candy Machine地址。

    export const FetchCandyMachine: FC = () => {
    const [candyMachineAddress, setCandyMachineAddress] = useState("CM_ADDRESS_HERE")
    ...

    接下来,我们将完善 fetchCandyMachine 函数。我们将使用之前看到的 findByAddress 方法。

    export const FetchCandyMachine: FC = () => {
    ...

    // 通过地址获取糖果机
    const fetchCandyMachine = async () => {

    // 设置页面为1 - 我们想要在获取新糖果机时始终位于第一页
    setPage(1)

    // 获取糖果机数据
    try {
    const candyMachine = await metaplex
    .candyMachinesV2()
    .findByAddress({ address: new PublicKey(candyMachineAddress) })

    setCandyMachineData(candyMachine)
    } catch (e) {
    alert("请输入有效的CMv2地址。")
    }
    }
    ...
    }

    注意:Metaplex CLI的最新版本在函数调用的末尾不需要 run()

    现在来到重要的部分 - 浏览我们将获取的CM数据。以下是 getPage 函数的样子:

    export const FetchCandyMachine: FC = () => {
    ...

    // 分页
    const getPage = async (page, perPage) => {
    const pageItems = candyMachineData.items.slice(
    (page - 1) * perPage,
    page * perPage
    )

    // 获取页面中NFT的元数据
    let nftData = []
    for (let i = 0; i < pageItems.length; i++) {
    let fetchResult = await fetch(pageItems[i].uri)
    let json = await fetchResult.json()
    nftData.push(json)
    }

    // 设置状态
    setPageItems(nftData)
    }
    ...
    }

    我们在这里做的是将 items 数组切割成大小为10的部分。然后我们获取页面上每个NFT的元数据,并将其存储在 nftData 中。最后,我们将 pageItems 状态变量设置为刚刚获取的 nftData

    这意味着我们的应用程序在任何时候只会渲染当前页面的NFT。相当棒!

    让我们填写 prevnext 函数:

    // 上一页
    const prev = async () => {
    if (page - 1 < 1) {
    setPage(1)
    } else {
    setPage(page - 1)
    }
    }

    // 下一页
    const next = async () => {
    setPage(page + 1)
    }

    当用户点击“上一页”和“下一页”按钮时,这些功能将运行。这些按钮只会在 pageItems 不为空时显示(即当我们获取了CM的NFT时)。

    现在我们需要一些 useEffects 来开始。整个过程一开始可能有点复杂,所以让我们一步一步解释。

      1. 在页面加载时运行 fetchCandyMachine 函数(如果 candyMachineAddress 不为空)。
      1. 每当使用 fetchCandyMachine 获取糖果机时,将 page 设置为1,这样你就可以从第一页开始。
      1. 每当 candyMachineDatapage 发生变化时(即输入新的CM地址或点击下一个/上一个按钮),重新加载页面。

    以下是代码示例:

    export const FetchCandyMachine: FC = () => {
    ...

    // 页面加载时获取占位符糖果机
    useEffect(() => {
    fetchCandyMachine()
    }, [])

    // 当页面或糖果机发生变化时获取NFT的元数据
    useEffect(() => {
    if (!candyMachineData) {
    return
    }
    getPage(page, 9)
    }, [candyMachineData, page])

    }

    快到 localhost:3000 上试试吧!你应该能够看到你的糖果机上NFT的第一页。

    - - +
    Skip to main content

    🖼 从糖果机展示NFTs

    现在我们已经铸造了一个NFT,接下来我们将学习如何铸造一系列的NFT。为此,我们将利用Candy Machine来实现——这是Solana的一个程序,使创作者能够将他们的资产上链。虽然这并非创建系列的唯一方式,但在Solana上它却成了标准,因为它具备了许多有用的功能,例如机器人保护和安全随机化。你是否感受到过看到闪亮的新iPhone时的那股兴奋感?稀有的NFT有点儿类似于此。对于优秀的艺术家而言,仅仅是观看这些NFT也极富乐趣。毕竟,艺术的本质就是用来欣赏的!接下来,我们将探讨如果我们只有Candy Machine的地址,应该如何展示NFTs

    你能猜到这里有何不同之处吗?没错,我们仅在SDK上采用了一种不同的方法!

    由于这里并没有钱包,我们不需要使用 walletAdapterIdentity,只需使用metaplex对象即可。

    我们在此有几个选择 - findByAddress 就是我们所需的。

    与我们为单个NFT所获取的相似,我们将会得到整个糖果机实例的元数据。items 字段是糖果机内所有NFT的数组。每个项目都不会直接包含我们想要的内容,而是会引向一个我们可以从中提取资产的URI。

    鉴于收藏品可能会相当庞大,我们不会一次性获取所有的NFT。相反,我们将基于分页系统,只获取我们想要展示的NFT

    那么,让我们一起来绘制一些像素吧!

    🥁 请准备一个糖果机

    你可以从上一节的进度继续,或者使用我们上次使用的相同模板(从起始分支开始即可)。

    赶紧跳入 FetchCandyMachine.tsx 文件吧。你会发现一些设置已经为你准备好了。我们将使用 getPage 函数从糖果机上获取某“页面”上的物品。在此之前,我们需要获取糖果机的元数据账户。

    在空的 fetchCandyMachine 函数上方设置 metaplex 对象的连接。

    export const FetchCandyMachine: FC = () => {
    // 占位符 CMv2 地址
    const [candyMachineAddress, setCandyMachineAddress] = useState("")
    const [candyMachineData, setCandyMachineData] = useState(null)
    const [pageItems, setPageItems] = useState(null)
    const [page, setPage] = useState(1)

    const { connection } = useConnection()
    const metaplex = Metaplex.make(connection)

    在创建有状态变量时,请确保添加你的Candy Machine地址。

    export const FetchCandyMachine: FC = () => {
    const [candyMachineAddress, setCandyMachineAddress] = useState("CM_ADDRESS_HERE")
    ...

    接下来,我们将完善 fetchCandyMachine 函数。我们将使用之前看到的 findByAddress 方法。

    export const FetchCandyMachine: FC = () => {
    ...

    // 通过地址获取糖果机
    const fetchCandyMachine = async () => {

    // 设置页面为1 - 我们想要在获取新糖果机时始终位于第一页
    setPage(1)

    // 获取糖果机数据
    try {
    const candyMachine = await metaplex
    .candyMachinesV2()
    .findByAddress({ address: new PublicKey(candyMachineAddress) })

    setCandyMachineData(candyMachine)
    } catch (e) {
    alert("请输入有效的CMv2地址。")
    }
    }
    ...
    }

    注意:Metaplex CLI的最新版本在函数调用的末尾不需要 run()

    现在来到重要的部分 - 浏览我们将获取的CM数据。以下是 getPage 函数的样子:

    export const FetchCandyMachine: FC = () => {
    ...

    // 分页
    const getPage = async (page, perPage) => {
    const pageItems = candyMachineData.items.slice(
    (page - 1) * perPage,
    page * perPage
    )

    // 获取页面中NFT的元数据
    let nftData = []
    for (let i = 0; i < pageItems.length; i++) {
    let fetchResult = await fetch(pageItems[i].uri)
    let json = await fetchResult.json()
    nftData.push(json)
    }

    // 设置状态
    setPageItems(nftData)
    }
    ...
    }

    我们在这里做的是将 items 数组切割成大小为10的部分。然后我们获取页面上每个NFT的元数据,并将其存储在 nftData 中。最后,我们将 pageItems 状态变量设置为刚刚获取的 nftData

    这意味着我们的应用程序在任何时候只会渲染当前页面的NFT。相当棒!

    让我们填写 prevnext 函数:

    // 上一页
    const prev = async () => {
    if (page - 1 < 1) {
    setPage(1)
    } else {
    setPage(page - 1)
    }
    }

    // 下一页
    const next = async () => {
    setPage(page + 1)
    }

    当用户点击“上一页”和“下一页”按钮时,这些功能将运行。这些按钮只会在 pageItems 不为空时显示(即当我们获取了CM的NFT时)。

    现在我们需要一些 useEffects 来开始。整个过程一开始可能有点复杂,所以让我们一步一步解释。

      1. 在页面加载时运行 fetchCandyMachine 函数(如果 candyMachineAddress 不为空)。
      1. 每当使用 fetchCandyMachine 获取糖果机时,将 page 设置为1,这样你就可以从第一页开始。
      1. 每当 candyMachineDatapage 发生变化时(即输入新的CM地址或点击下一个/上一个按钮),重新加载页面。

    以下是代码示例:

    export const FetchCandyMachine: FC = () => {
    ...

    // 页面加载时获取占位符糖果机
    useEffect(() => {
    fetchCandyMachine()
    }, [])

    // 当页面或糖果机发生变化时获取NFT的元数据
    useEffect(() => {
    if (!candyMachineData) {
    return
    }
    getPage(page, 9)
    }, [candyMachineData, page])

    }

    快到 localhost:3000 上试试吧!你应该能够看到你的糖果机上NFT的第一页。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/displaying-nfts-from-a-wallet/index.html b/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/displaying-nfts-from-a-wallet/index.html index 6b959026f..280a84e02 100644 --- a/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/displaying-nfts-from-a-wallet/index.html +++ b/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/displaying-nfts-from-a-wallet/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    📱 在钱包中展示NFTs

    现在我们已经铸造了一个NFT,接下来我们要探索如何铸造一系列的NFT。我们将使用Candy Machine来完成这项任务,这是一款Solana程序,能让创作者方便地将他们的资产上链。虽然这不是创建系列的唯一方式,但在Solana上它已经成为标准,因为它具有诸如防机器人保护和安全随机化等有用的功能。你懂的,模板时间到了。然而,随着我们构建的项目越来越复杂,我们的模板也会变得更先进。这次我们将基于Solana dApp脚手架构建一个模板。与之前的模板一样,它是一个由create-next-app创建的Next.js应用程序。不过这次,它具有更多功能。不用担心!我们依然会使用相同的工具。

    git clone https://github.com/all-in-one-solana/solana-display-nfts-frontend
    cd solana-display-nfts-frontend
    git checkout starter
    npm install @metaplex-foundation/js@latest
    npm i
    npm run dev

    localhost:3000 上,你应该能看到如下内容:

    “展示NFT”页面目前还没有展示任何内容——这就是你的任务所在。

    打开src/components/FetchNFT.tsx,让我们开始吧。我们会从组件顶部的Metaplex设置开始:

    export const FetchNft: FC = () => {
    const [nftData, setNftData] = useState(null)

    const { connection } = useConnection()
    const wallet = useWallet()
    const metaplex = Metaplex.make(connection).use(walletAdapterIdentity(wallet))

    const fetchNfts = async () => {}

    return <div></div>
    }

    看上去似曾相识吧。

    现在我们来填写fetchNfts函数。我们将使用之前看到的findAllByOwner方法,并借助useWallet钩子来获取钱包地址。

    const fetchNfts = async () => {
    if (!wallet.connected) {
    return
    }

    // 为连接的钱包获取NFTs
    const nfts = await metaplex
    .nfts()
    .findAllByOwner({ owner: wallet.publicKey })

    // 为每个NFT获取链下元数据
    let nftData = []
    for (let i = 0; i < nfts.length; i++) {
    let fetchResult = await fetch(nfts[i].uri)
    let json = await fetchResult.json()
    nftData.push(json)
    }

    // 设置状态
    setNftData(nftData)
    }

    由于我们希望在钱包更改时更新展示的NFTs,因此我们将在useEffect函数下方添加一个钩子来调用fetchNfts函数。

    export const FetchNft: FC = () => {
    ...

    const fetchNfts = async () => {
    ...
    }

    // 当连接的钱包改变时获取nfts
    useEffect(() => {
    fetchNfts()
    }, [wallet])

    return <div></div>
    }

    最后,我们需要更新return语句以展示NFTs。我们将使用之前创建的nftData状态变量。

    return (
    <div>
    {nftData && (
    <div className={styles.gridNFT}>
    {nftData.map((nft) => (
    <div>
    <ul>{nft.name}</ul>
    <img src={nft.image} />
    </div>
    ))}
    </div>
    )}
    </div>
    )

    现在我们可以看到我们的NFT了!🎉 这就是我的钱包的样子 😆

    回顾过去的日子,那时我不得不手动完成所有这些工作,并且我一直受到RPC的速率限制,所以请花一些时间感谢Metaplex的开发人员为我们带来了这个精彩的SDK

    nftData上玩一下。将其记录到控制台,并尝试显示其他值,如符号或描述!也许你还可以添加一个过滤器,让用户只能显示特定收藏的NFT

    - - +
    Skip to main content

    📱 在钱包中展示NFTs

    现在我们已经铸造了一个NFT,接下来我们要探索如何铸造一系列的NFT。我们将使用Candy Machine来完成这项任务,这是一款Solana程序,能让创作者方便地将他们的资产上链。虽然这不是创建系列的唯一方式,但在Solana上它已经成为标准,因为它具有诸如防机器人保护和安全随机化等有用的功能。你懂的,模板时间到了。然而,随着我们构建的项目越来越复杂,我们的模板也会变得更先进。这次我们将基于Solana dApp脚手架构建一个模板。与之前的模板一样,它是一个由create-next-app创建的Next.js应用程序。不过这次,它具有更多功能。不用担心!我们依然会使用相同的工具。

    git clone https://github.com/all-in-one-solana/solana-display-nfts-frontend
    cd solana-display-nfts-frontend
    git checkout starter
    npm install @metaplex-foundation/js@latest
    npm i
    npm run dev

    localhost:3000 上,你应该能看到如下内容:

    “展示NFT”页面目前还没有展示任何内容——这就是你的任务所在。

    打开src/components/FetchNFT.tsx,让我们开始吧。我们会从组件顶部的Metaplex设置开始:

    export const FetchNft: FC = () => {
    const [nftData, setNftData] = useState(null)

    const { connection } = useConnection()
    const wallet = useWallet()
    const metaplex = Metaplex.make(connection).use(walletAdapterIdentity(wallet))

    const fetchNfts = async () => {}

    return <div></div>
    }

    看上去似曾相识吧。

    现在我们来填写fetchNfts函数。我们将使用之前看到的findAllByOwner方法,并借助useWallet钩子来获取钱包地址。

    const fetchNfts = async () => {
    if (!wallet.connected) {
    return
    }

    // 为连接的钱包获取NFTs
    const nfts = await metaplex
    .nfts()
    .findAllByOwner({ owner: wallet.publicKey })

    // 为每个NFT获取链下元数据
    let nftData = []
    for (let i = 0; i < nfts.length; i++) {
    let fetchResult = await fetch(nfts[i].uri)
    let json = await fetchResult.json()
    nftData.push(json)
    }

    // 设置状态
    setNftData(nftData)
    }

    由于我们希望在钱包更改时更新展示的NFTs,因此我们将在useEffect函数下方添加一个钩子来调用fetchNfts函数。

    export const FetchNft: FC = () => {
    ...

    const fetchNfts = async () => {
    ...
    }

    // 当连接的钱包改变时获取nfts
    useEffect(() => {
    fetchNfts()
    }, [wallet])

    return <div></div>
    }

    最后,我们需要更新return语句以展示NFTs。我们将使用之前创建的nftData状态变量。

    return (
    <div>
    {nftData && (
    <div className={styles.gridNFT}>
    {nftData.map((nft) => (
    <div>
    <ul>{nft.name}</ul>
    <img src={nft.image} />
    </div>
    ))}
    </div>
    )}
    </div>
    )

    现在我们可以看到我们的NFT了!🎉 这就是我的钱包的样子 😆

    回顾过去的日子,那时我不得不手动完成所有这些工作,并且我一直受到RPC的速率限制,所以请花一些时间感谢Metaplex的开发人员为我们带来了这个精彩的SDK

    nftData上玩一下。将其记录到控制台,并尝试显示其他值,如符号或描述!也许你还可以添加一个过滤器,让用户只能显示特定收藏的NFT

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/displaying-nfts/index.html b/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/displaying-nfts/index.html index 3010b29d8..c09d4372d 100644 --- a/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/displaying-nfts/index.html +++ b/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/displaying-nfts/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    💃 展示NFTs

    既然我们已经铸造了一个NFT,现在我们将进一步探讨如何铸造一整套NFT。我们将借助Candy Machine来实现这个任务——这是一个Solana程序,可以让创作者轻松地将他们的资产上链。当然,这并不是在Solana上创建系列的唯一方法,但它确实成为了标准,因为它具备了许多实用功能,例如防机器人保护和安全随机化。毕竟,如果你不能向人们展示你的NFT,那它还有什么价值呢!在这一节,我们将引导你展示你的作品——首先在钱包中展示,然后在Candy Machine中展示。

    你可能会好奇为什么要这样做。想象一下,你的朋友在你的网站上从你的收藏中铸造了一个很酷的Pepe NFT。他们已经铸造了许多与Pepe有关的项目,因此他们的钱包中有几十个NFT。他们怎么知道哪一个是从你的收藏中铸造的呢?你得向他们展示!

    你可能还记得,从第一周开始,我们想要的一切都存储在账户中。这意味着你可以仅通过使用钱包地址来获取他们的NFT,尽管这需要付出更多努力。

    相反,我们将利用Metaplex SDK,它让一切都变得就像调用API一样简单。以下是它的样子:

    你需要进行通常的Metaplex设置,但是我们将使用 walletAdapterIdentity 而不是 keypairIdentity 来进行连接,因为我们并不需要他们的密钥对。一旦完成,我们只需使用Metaplex对象调用 findAllByOwner 方法。

    下图显示了单个NFTNFT数据在控制台上的打印结果,我们主要关注的是 uri 字段:

    顺便提一下,还有许多其他方法可以获取NFT

    现在,让我们开始编写代码吧!

    - - +
    Skip to main content

    💃 展示NFTs

    既然我们已经铸造了一个NFT,现在我们将进一步探讨如何铸造一整套NFT。我们将借助Candy Machine来实现这个任务——这是一个Solana程序,可以让创作者轻松地将他们的资产上链。当然,这并不是在Solana上创建系列的唯一方法,但它确实成为了标准,因为它具备了许多实用功能,例如防机器人保护和安全随机化。毕竟,如果你不能向人们展示你的NFT,那它还有什么价值呢!在这一节,我们将引导你展示你的作品——首先在钱包中展示,然后在Candy Machine中展示。

    你可能会好奇为什么要这样做。想象一下,你的朋友在你的网站上从你的收藏中铸造了一个很酷的Pepe NFT。他们已经铸造了许多与Pepe有关的项目,因此他们的钱包中有几十个NFT。他们怎么知道哪一个是从你的收藏中铸造的呢?你得向他们展示!

    你可能还记得,从第一周开始,我们想要的一切都存储在账户中。这意味着你可以仅通过使用钱包地址来获取他们的NFT,尽管这需要付出更多努力。

    相反,我们将利用Metaplex SDK,它让一切都变得就像调用API一样简单。以下是它的样子:

    你需要进行通常的Metaplex设置,但是我们将使用 walletAdapterIdentity 而不是 keypairIdentity 来进行连接,因为我们并不需要他们的密钥对。一旦完成,我们只需使用Metaplex对象调用 findAllByOwner 方法。

    下图显示了单个NFTNFT数据在控制台上的打印结果,我们主要关注的是 uri 字段:

    顺便提一下,还有许多其他方法可以获取NFT

    现在,让我们开始编写代码吧!

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/index.html b/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/index.html index 46063741e..281ddac45 100644 --- a/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/index.html +++ b/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module2/index.html b/Solana-Co-Learn/module2/index.html index 4c75b89f5..69180e693 100644 --- a/Solana-Co-Learn/module2/index.html +++ b/Solana-Co-Learn/module2/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-candy-machine/index.html b/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-candy-machine/index.html index d70522b55..65f924d7f 100644 --- a/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-candy-machine/index.html +++ b/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-candy-machine/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🍬 创建糖果机

    现在我们已经铸造了一个NFT,接下来我们要学习如何铸造一系列NFT。我们将使用Candy Machine来完成这个任务。Candy Machine是一个Solana程序,它可以让创作者将他们的艺术品和资产上链。虽然还有其他方式可以创建NFT系列,但Candy MachineSolana上已成为一项标准,因为它具备许多实用功能,如防机器人保护和安全随机化。准备好添加一些内容到我们上一课创建但未使用的文件夹中了吗?

    首先,让我们在你的 candy-machine 文件夹中创建一个新的资产文件夹,并将所有NFT图像和元数据放入其中。你可以在这里找到有关如何准备NFT资产的详细信息。

    使用Sugar CLI

    现在你已经成功创建了所有NFT资产,我们可以开始使用Sugar CLI来部署它们。如果你还没有安装它,可以按照这个链接上的指南进行安装。

    首先,让我们通过运行 cd tokens/candy-machine/ 命令导航到candy-machine文件夹。接下来,运行 sugar launch 来启动Sugar CLI。它会询问你一系列问题,你可以根据自己的需求来进行配置。最关键的是,确保将NFT的价格设为 0,并将存储方式设为 bundlr。你可以选择将 yes 设置为所有选项。

    ⬆️ 上传你的NFT

    现在你已经创建了配置文件,可以通过在终端中运行 sugar upload 来开始上传你的NFT。这将会把所有的NFT和它们的元数据上传到你选择的存储方式中。成功上传NFT后,终端中的输出应该如下图所示。

    你还会在你的文件夹中看到一个名为 cache.json 的生成文件。它包括了你的NFT和它们的元数据的所有必要信息。复制 collectionMint 地址并粘贴到https://explorer.solana.com/?cluster=devnet,你应该能看到与下图相似的NFT

    就这样,你已经成功创建了自己的NFT系列,并通过Candy Machine上链了。你可以继续探索其他Candy Machine的功能,并尝试在Solana上更广泛地展示和销售你的NFT

    - - +
    Skip to main content

    🍬 创建糖果机

    现在我们已经铸造了一个NFT,接下来我们要学习如何铸造一系列NFT。我们将使用Candy Machine来完成这个任务。Candy Machine是一个Solana程序,它可以让创作者将他们的艺术品和资产上链。虽然还有其他方式可以创建NFT系列,但Candy MachineSolana上已成为一项标准,因为它具备许多实用功能,如防机器人保护和安全随机化。准备好添加一些内容到我们上一课创建但未使用的文件夹中了吗?

    首先,让我们在你的 candy-machine 文件夹中创建一个新的资产文件夹,并将所有NFT图像和元数据放入其中。你可以在这里找到有关如何准备NFT资产的详细信息。

    使用Sugar CLI

    现在你已经成功创建了所有NFT资产,我们可以开始使用Sugar CLI来部署它们。如果你还没有安装它,可以按照这个链接上的指南进行安装。

    首先,让我们通过运行 cd tokens/candy-machine/ 命令导航到candy-machine文件夹。接下来,运行 sugar launch 来启动Sugar CLI。它会询问你一系列问题,你可以根据自己的需求来进行配置。最关键的是,确保将NFT的价格设为 0,并将存储方式设为 bundlr。你可以选择将 yes 设置为所有选项。

    ⬆️ 上传你的NFT

    现在你已经创建了配置文件,可以通过在终端中运行 sugar upload 来开始上传你的NFT。这将会把所有的NFT和它们的元数据上传到你选择的存储方式中。成功上传NFT后,终端中的输出应该如下图所示。

    你还会在你的文件夹中看到一个名为 cache.json 的生成文件。它包括了你的NFT和它们的元数据的所有必要信息。复制 collectionMint 地址并粘贴到https://explorer.solana.com/?cluster=devnet,你应该能看到与下图相似的NFT

    就这样,你已经成功创建了自己的NFT系列,并通过Candy Machine上链了。你可以继续探索其他Candy Machine的功能,并尝试在Solana上更广泛地展示和销售你的NFT

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-reward-tokens/index.html b/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-reward-tokens/index.html index 6075642b5..858046900 100644 --- a/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-reward-tokens/index.html +++ b/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-reward-tokens/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🎨 创建奖励代币

    既然我们已经铸造了一个NFT,那么接下来我们要探讨如何铸造一系列的NFT。为了达成这个目标,我们将采用Candy Machine——一款在Solana上广泛使用的程序,允许创作者将其资产上链。Candy MachineSolana上受到欢迎的原因在于,它具备了如机器人防护和安全随机化等实用功能。现在我们可以回归到我们自定义的NFT质押应用上来,借助我们在代币程序和Candy Machine上的经验来构建这个应用。

    请按照以下步骤操作:

    1. 在根目录中创建名为tokens的新文件夹。

    2. tokens文件夹内,我们要创建2个子文件夹,分别命名为bldcandy-machine,它们的结构应如下图所示:

    我们这样做的目的是整理建立Builder时所需的奖励代币和与我们的NFT有关的内容。

    1. 接下来,我们要创建资源文件夹,用于存放代币的图像。请进入你的bld文件夹,并创建名为assets的新文件夹。同时,在你的bld文件夹内,创建名为index.ts的新文件。目录结构应如下所示:
    ├── styles
    ├── tokens
    │ ├── bld
    | ├── assets
    | ├── index.ts

    注意:确保你的index.ts文件位于bld文件夹中,而不是在assets文件夹中。

    1. 你可能会发现index.ts文件被标记为红色,这是因为我们目前还没有任何代码。让我们通过向你的index.ts文件中添加以下代码来解决这个问题。我们还需将initializeKeypair文件移动到bld文件夹中,并在bld/assets文件夹中添加一张图片作为你的代币图片。
    import * as web3 from "@solana/web3.js";
    import * as token from "@solana/spl-token";
    import { initializeKeypair } from "./initializeKeypair";

    async function main() {
    const connection = new web3.Connection(web3.clusterApiUrl("devnet"));
    const payer = await initializeKeypair(connection);
    }

    main()
    .then(() => {
    console.log("Finished successfully");
    process.exit(0);
    })
    .catch((error) => {
    console.log(error);
    process.exit(1);
    });

    太棒了!现在我们已经有了初始代码,可以继续将下一段代码粘贴到你的index.ts文件中,你可以将其放在main函数的上方。

    import * as fs from "fs";
    import {
    bundlrStorage,
    keypairIdentity,
    Metaplex,
    toMetaplexFile,
    } from "@metaplex-foundation/js";

    import {
    DataV2,
    createCreateMetadataAccountV2Instruction,
    } from "@metaplex-foundation/mpl-token-metadata";

    const TOKEN_NAME = "BUILD";
    const TOKEN_SYMBOL = "BLD";
    const TOKEN_DESCRIPTION = "A token for buildoors";
    const TOKEN_IMAGE_NAME = "unicorn.png"; // Replace unicorn.png with your image name
    const TOKEN_IMAGE_PATH = `tokens/bld/assets/${TOKEN_IMAGE_NAME}`;

    async function createBldToken(
    connection: web3.Connection,
    payer: web3.Keypair
    ) {
    // This will create a token with all the necessary inputs
    const tokenMint = await token.createMint(
    connection, // Connection
    payer, // Payer
    payer.publicKey, // Your wallet public key
    payer.publicKey, // Freeze authority
    2 // Decimals
    );

    // Create a metaplex object so that we can create a metaplex metadata
    const metaplex = Metaplex.make(connection)
    .use(keypairIdentity(payer))
    .use(
    bundlrStorage({
    address: "https://devnet.bundlr.network",
    providerUrl: "https://api.devnet.solana.com",
    timeout: 60000,
    })
    );

    // Read image file
    const imageBuffer = fs.readFileSync(TOKEN_IMAGE_PATH);
    const file = toMetaplexFile(imageBuffer, TOKEN_IMAGE_NAME);
    const imageUri = await metaplex.storage().upload(file);

    // Upload the rest of offchain metadata
    const { uri } = await metaplex
    .nfts()
    .uploadMetadata({
    name: TOKEN_NAME,
    description: TOKEN_DESCRIPTION,
    image: imageUri,
    });

    // Finding out the address where the metadata is stored
    const metadataPda = metaplex.nfts().pdas().metadata({mint: tokenMint});
    const tokenMetadata = {
    name: TOKEN_NAME,
    symbol: TOKEN_SYMBOL,
    uri: uri,
    sellerFeeBasisPoints: 0,
    creators: null,
    collection: null,
    uses: null,
    } as DataV2

    const instruction = createCreateMetadataAccountV2Instruction({
    metadata: metadataPda,
    mint: tokenMint,
    mintAuthority: payer.publicKey,
    payer: payer.publicKey,
    updateAuthority: payer.publicKey
    },
    {
    createMetadataAccountArgsV2: {
    data: tokenMetadata,
    isMutable: true
    }
    })

    const transaction = new web3.Transaction()
    transaction.add(instruction)
    const transactionSignature = await web3.sendAndConfirmTransaction(
    connection,
    transaction,
    [payer]
    )

    这段代码将创建一个代币,并与其关联所有必需的输入。它还会读取图像文件,上传文件,并执行其他必要操作,从而为你的代币创建完整的元数据。

    通过上述步骤,你将能够在Solana上成功创建并管理你的代币。

    🥳 代码解析

    好的,我们逐一分析一下上述代码的各个部分。

    首先,我们通过调用 createMint 函数来创建并初始化一个新的代币铸造。你可以通过这个链接了解更多有关该函数的信息。

    // 这段代码将根据所有必要的输入创建一个代币
    const tokenMint = await token.createMint(
    connection, // 连接信息
    payer, // 付款方
    payer.publicKey, // 你的钱包公钥
    payer.publicKey, // 冻结权限
    2 // 小数位数
    );

    接下来,我们创建一个Metaplex对象,以便我们可以创建Metaplex元数据,并将其上传到BundlrStorage中。

    // 创建一个Metaplex对象,这样我们就可以创建Metaplex元数据了
    const metaplex = Metaplex.make(connection)
    .use(keypairIdentity(payer))
    .use(
    bundlrStorage({
    address: "https://devnet.bundlr.network",
    providerUrl: "https://api.devnet.solana.com",
    timeout: 60000,
    })
    );

    这部分代码比较直观。我们正在尝试读取存储在bld/assets文件夹中的图像文件,并将元数据上传到存储空间中。

    // 读取图像文件
    const imageBuffer = fs.readFileSync(TOKEN_IMAGE_PATH);
    const file = toMetaplexFile(imageBuffer, TOKEN_IMAGE_NAME);
    const imageUri = await metaplex.storage().upload(file);
    // 上传其余的离线元数据
    const { uri } = await metaplex
    .nfts()
    .uploadMetadata({
    name: TOKEN_NAME,
    description: TOKEN_DESCRIPTION,
    image: imageUri,
    });

    一旦我们成功将图像上传到Metaplex,我们就可以通过调用以下部分来查找元数据存储的地址。

    // 查找元数据存储的地址
    const metadataPda = metaplex.nfts().pdas().metadata({mint: tokenMint});
    const tokenMetadata = {
    name: TOKEN_NAME,
    symbol: TOKEN_SYMBOL,
    uri: uri,
    sellerFeeBasisPoints: 0,
    creators: null,
    collection: null,
    uses: null,
    } as DataV2

    const instruction = createCreateMetadataAccountV2Instruction({
    metadata: metadataPda,
    mint: tokenMint,
    mintAuthority: payer.publicKey,
    payer: payer.publicKey,
    updateAuthority: payer.publicKey
    },
    {
    createMetadataAccountArgsV2: {
    data: tokenMetadata,
    isMutable: true
    }
    })

    const transaction = new web3.Transaction()
    transaction.add(instruction)
    const transactionSignature = await web3.sendAndConfirmTransaction(
    connection,
    transaction,
    [payer]
    )

    这部分代码将创建一个代币,并将其所需的所有输入与其关联起来。它还会读取图像文件,上传文件,并完成为你的代币创建完整元数据所需的其他操作。

    通过这样的操作,你将能够在Solana上成功创建并管理你的代币。

    🫙 元数据的存储

    现在,我们已经创建了带有特定元数据的代币铸造(薄荷)。下一步我们要将这些元数据信息存储到我们的文件夹中。让我们看看如何做到这一点:

    就在你定义了 transactionSignature 的代码下方,添加以下代码。

    fs.writeFileSync(
    "tokens/bld/cache.json",
    JSON.stringify({
    mint: tokenMint.toBase58(),
    imageUri: imageUri,
    metadataUri: uri,
    tokenMetadata: metadataPda.toBase58(),
    metadataTransaction: transactionSignature,
    })
    );

    太棒了!这样就完成了 createBldToken 函数的编写。现在,我们可以在下面的主函数中调用它。你的 main 函数现在应该是这个样子的。

    async function main() {
    const connection = new web3.Connection(web3.clusterApiUrl("devnet"));
    const payer = await initializeKeypair(connection);

    await createBldToken(connection, payer);
    }

    这样就可以了。你现在已经准备好了。让我们开始运行代码吧!

    🚀 运行我们的代码

    首先,我们需要在VS Code中打开终端,并安装一个名为 ts-node 的模块,因为我们要运行一些TypeScript命令。在终端中输入 npm install --save-dev ts-node。然后,转到你的 package.json 文件,并将以下行添加到 scripts 部分。

    "create-bld-token": "ts-node ./src/tokens/bld/index.ts"

    现在你的配置应该是这个样子的。

    记得保存更改!现在你可以通过在终端中运行 npm run create-bld-token 来使用新添加的命令。这将在开发网络中为你创建和铸造代币。完成后,你应该能在文件夹中看到一个名为 cache.json 的文件。打开它,你将看到类似以下的内容。

    注意:如果你收到如 SyntaxError: Cannot use import statement outside a module 的错误,请检查你的 tsconfig.json 文件,并更改或添加 "module": "CommonJS"

    现在,请复制 mint 下列出的地址,并将其粘贴到 https://explorer.solana.com/?cluster=devnet。你现在应该能够看到显示你选择图像的代币,就像下图所示。

    以上就是如何在Solana网络上创建和铸造你自己的代币的全部步骤。现在你已经成功地执行了这一过程,可以在Solana网络上与你的代币互动了。

    - - +
    Skip to main content

    🎨 创建奖励代币

    既然我们已经铸造了一个NFT,那么接下来我们要探讨如何铸造一系列的NFT。为了达成这个目标,我们将采用Candy Machine——一款在Solana上广泛使用的程序,允许创作者将其资产上链。Candy MachineSolana上受到欢迎的原因在于,它具备了如机器人防护和安全随机化等实用功能。现在我们可以回归到我们自定义的NFT质押应用上来,借助我们在代币程序和Candy Machine上的经验来构建这个应用。

    请按照以下步骤操作:

    1. 在根目录中创建名为tokens的新文件夹。

    2. tokens文件夹内,我们要创建2个子文件夹,分别命名为bldcandy-machine,它们的结构应如下图所示:

    我们这样做的目的是整理建立Builder时所需的奖励代币和与我们的NFT有关的内容。

    1. 接下来,我们要创建资源文件夹,用于存放代币的图像。请进入你的bld文件夹,并创建名为assets的新文件夹。同时,在你的bld文件夹内,创建名为index.ts的新文件。目录结构应如下所示:
    ├── styles
    ├── tokens
    │ ├── bld
    | ├── assets
    | ├── index.ts

    注意:确保你的index.ts文件位于bld文件夹中,而不是在assets文件夹中。

    1. 你可能会发现index.ts文件被标记为红色,这是因为我们目前还没有任何代码。让我们通过向你的index.ts文件中添加以下代码来解决这个问题。我们还需将initializeKeypair文件移动到bld文件夹中,并在bld/assets文件夹中添加一张图片作为你的代币图片。
    import * as web3 from "@solana/web3.js";
    import * as token from "@solana/spl-token";
    import { initializeKeypair } from "./initializeKeypair";

    async function main() {
    const connection = new web3.Connection(web3.clusterApiUrl("devnet"));
    const payer = await initializeKeypair(connection);
    }

    main()
    .then(() => {
    console.log("Finished successfully");
    process.exit(0);
    })
    .catch((error) => {
    console.log(error);
    process.exit(1);
    });

    太棒了!现在我们已经有了初始代码,可以继续将下一段代码粘贴到你的index.ts文件中,你可以将其放在main函数的上方。

    import * as fs from "fs";
    import {
    bundlrStorage,
    keypairIdentity,
    Metaplex,
    toMetaplexFile,
    } from "@metaplex-foundation/js";

    import {
    DataV2,
    createCreateMetadataAccountV2Instruction,
    } from "@metaplex-foundation/mpl-token-metadata";

    const TOKEN_NAME = "BUILD";
    const TOKEN_SYMBOL = "BLD";
    const TOKEN_DESCRIPTION = "A token for buildoors";
    const TOKEN_IMAGE_NAME = "unicorn.png"; // Replace unicorn.png with your image name
    const TOKEN_IMAGE_PATH = `tokens/bld/assets/${TOKEN_IMAGE_NAME}`;

    async function createBldToken(
    connection: web3.Connection,
    payer: web3.Keypair
    ) {
    // This will create a token with all the necessary inputs
    const tokenMint = await token.createMint(
    connection, // Connection
    payer, // Payer
    payer.publicKey, // Your wallet public key
    payer.publicKey, // Freeze authority
    2 // Decimals
    );

    // Create a metaplex object so that we can create a metaplex metadata
    const metaplex = Metaplex.make(connection)
    .use(keypairIdentity(payer))
    .use(
    bundlrStorage({
    address: "https://devnet.bundlr.network",
    providerUrl: "https://api.devnet.solana.com",
    timeout: 60000,
    })
    );

    // Read image file
    const imageBuffer = fs.readFileSync(TOKEN_IMAGE_PATH);
    const file = toMetaplexFile(imageBuffer, TOKEN_IMAGE_NAME);
    const imageUri = await metaplex.storage().upload(file);

    // Upload the rest of offchain metadata
    const { uri } = await metaplex
    .nfts()
    .uploadMetadata({
    name: TOKEN_NAME,
    description: TOKEN_DESCRIPTION,
    image: imageUri,
    });

    // Finding out the address where the metadata is stored
    const metadataPda = metaplex.nfts().pdas().metadata({mint: tokenMint});
    const tokenMetadata = {
    name: TOKEN_NAME,
    symbol: TOKEN_SYMBOL,
    uri: uri,
    sellerFeeBasisPoints: 0,
    creators: null,
    collection: null,
    uses: null,
    } as DataV2

    const instruction = createCreateMetadataAccountV2Instruction({
    metadata: metadataPda,
    mint: tokenMint,
    mintAuthority: payer.publicKey,
    payer: payer.publicKey,
    updateAuthority: payer.publicKey
    },
    {
    createMetadataAccountArgsV2: {
    data: tokenMetadata,
    isMutable: true
    }
    })

    const transaction = new web3.Transaction()
    transaction.add(instruction)
    const transactionSignature = await web3.sendAndConfirmTransaction(
    connection,
    transaction,
    [payer]
    )

    这段代码将创建一个代币,并与其关联所有必需的输入。它还会读取图像文件,上传文件,并执行其他必要操作,从而为你的代币创建完整的元数据。

    通过上述步骤,你将能够在Solana上成功创建并管理你的代币。

    🥳 代码解析

    好的,我们逐一分析一下上述代码的各个部分。

    首先,我们通过调用 createMint 函数来创建并初始化一个新的代币铸造。你可以通过这个链接了解更多有关该函数的信息。

    // 这段代码将根据所有必要的输入创建一个代币
    const tokenMint = await token.createMint(
    connection, // 连接信息
    payer, // 付款方
    payer.publicKey, // 你的钱包公钥
    payer.publicKey, // 冻结权限
    2 // 小数位数
    );

    接下来,我们创建一个Metaplex对象,以便我们可以创建Metaplex元数据,并将其上传到BundlrStorage中。

    // 创建一个Metaplex对象,这样我们就可以创建Metaplex元数据了
    const metaplex = Metaplex.make(connection)
    .use(keypairIdentity(payer))
    .use(
    bundlrStorage({
    address: "https://devnet.bundlr.network",
    providerUrl: "https://api.devnet.solana.com",
    timeout: 60000,
    })
    );

    这部分代码比较直观。我们正在尝试读取存储在bld/assets文件夹中的图像文件,并将元数据上传到存储空间中。

    // 读取图像文件
    const imageBuffer = fs.readFileSync(TOKEN_IMAGE_PATH);
    const file = toMetaplexFile(imageBuffer, TOKEN_IMAGE_NAME);
    const imageUri = await metaplex.storage().upload(file);
    // 上传其余的离线元数据
    const { uri } = await metaplex
    .nfts()
    .uploadMetadata({
    name: TOKEN_NAME,
    description: TOKEN_DESCRIPTION,
    image: imageUri,
    });

    一旦我们成功将图像上传到Metaplex,我们就可以通过调用以下部分来查找元数据存储的地址。

    // 查找元数据存储的地址
    const metadataPda = metaplex.nfts().pdas().metadata({mint: tokenMint});
    const tokenMetadata = {
    name: TOKEN_NAME,
    symbol: TOKEN_SYMBOL,
    uri: uri,
    sellerFeeBasisPoints: 0,
    creators: null,
    collection: null,
    uses: null,
    } as DataV2

    const instruction = createCreateMetadataAccountV2Instruction({
    metadata: metadataPda,
    mint: tokenMint,
    mintAuthority: payer.publicKey,
    payer: payer.publicKey,
    updateAuthority: payer.publicKey
    },
    {
    createMetadataAccountArgsV2: {
    data: tokenMetadata,
    isMutable: true
    }
    })

    const transaction = new web3.Transaction()
    transaction.add(instruction)
    const transactionSignature = await web3.sendAndConfirmTransaction(
    connection,
    transaction,
    [payer]
    )

    这部分代码将创建一个代币,并将其所需的所有输入与其关联起来。它还会读取图像文件,上传文件,并完成为你的代币创建完整元数据所需的其他操作。

    通过这样的操作,你将能够在Solana上成功创建并管理你的代币。

    🫙 元数据的存储

    现在,我们已经创建了带有特定元数据的代币铸造(薄荷)。下一步我们要将这些元数据信息存储到我们的文件夹中。让我们看看如何做到这一点:

    就在你定义了 transactionSignature 的代码下方,添加以下代码。

    fs.writeFileSync(
    "tokens/bld/cache.json",
    JSON.stringify({
    mint: tokenMint.toBase58(),
    imageUri: imageUri,
    metadataUri: uri,
    tokenMetadata: metadataPda.toBase58(),
    metadataTransaction: transactionSignature,
    })
    );

    太棒了!这样就完成了 createBldToken 函数的编写。现在,我们可以在下面的主函数中调用它。你的 main 函数现在应该是这个样子的。

    async function main() {
    const connection = new web3.Connection(web3.clusterApiUrl("devnet"));
    const payer = await initializeKeypair(connection);

    await createBldToken(connection, payer);
    }

    这样就可以了。你现在已经准备好了。让我们开始运行代码吧!

    🚀 运行我们的代码

    首先,我们需要在VS Code中打开终端,并安装一个名为 ts-node 的模块,因为我们要运行一些TypeScript命令。在终端中输入 npm install --save-dev ts-node。然后,转到你的 package.json 文件,并将以下行添加到 scripts 部分。

    "create-bld-token": "ts-node ./src/tokens/bld/index.ts"

    现在你的配置应该是这个样子的。

    记得保存更改!现在你可以通过在终端中运行 npm run create-bld-token 来使用新添加的命令。这将在开发网络中为你创建和铸造代币。完成后,你应该能在文件夹中看到一个名为 cache.json 的文件。打开它,你将看到类似以下的内容。

    注意:如果你收到如 SyntaxError: Cannot use import statement outside a module 的错误,请检查你的 tsconfig.json 文件,并更改或添加 "module": "CommonJS"

    现在,请复制 mint 下列出的地址,并将其粘贴到 https://explorer.solana.com/?cluster=devnet。你现在应该能够看到显示你选择图像的代币,就像下图所示。

    以上就是如何在Solana网络上创建和铸造你自己的代币的全部步骤。现在你已经成功地执行了这一过程,可以在Solana网络上与你的代币互动了。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-the-minting-ui/index.html b/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-the-minting-ui/index.html index 9faa0a392..5a1c7a5ed 100644 --- a/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-the-minting-ui/index.html +++ b/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-the-minting-ui/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🎨 创建铸币用户界面

    现在我们成功创建了代币和非同质化代币(NFT),让我们继续着手构建我们的铸币用户界面。这样一来,我们就能直观地与智能合约互动,并允许他人在我们的浏览器上铸造我们的NFT。是不是非常酷?你可能已经注意到,你的网站上现有一个名为 minting 的按钮,但它目前尚未实现任何功能。让我们从定义一个函数开始,然后添加逻辑来允许我们铸造NFT。如果你没有起始代码,可以在这里克隆。

    首先,我们将以下代码添加到你的 newMint.tsx 文件中。注意:不要盲目地复制粘贴代码。我只提供了必要的部分,你需要明白这些代码应放在何处。提示:应该放在 Container 元素下方。

    // 你的其余代码
    import { Button, Text, HStack } from "@chakra-ui/react";
    import { MouseEventHandler, useCallback } from "react";
    import { ArrowForwardIcon } from "@chakra-ui/icons";

    const Home: NextPage = () => {
    const handleClick: MouseEventHandler<HTMLButtonElement> = useCallback(
    async (event) => {},
    []
    );

    return (
    <MainLayout>
    {/* 你的其余代码 */}
    <Image src="" alt="" />
    <Button
    bgColor="accent"
    color="white"
    maxWidth="380px"
    onClick={handleClick}
    >
    <HStack>
    <Text>stake my buildoor</Text>
    <ArrowForwardIcon />
    </HStack>
    </Button>
    </MainLayout>
    );
    };

    完成后,我们可以进入 Connected.tsx 并添加一些代码。在 handleClick 函数上方,我们可以添加 const router = useRouter()。记得在文件顶部导入 useRouter 函数。然后,在你的 handleClick 函数中添加 router.push("/newMint")。现在它应该是这个样子。

    const handleClick: MouseEventHandler<HTMLButtonElement> = useCallback(
    async (event) => {
    if (event.defaultPrevented) return;
    if (!walletAdapter.connected || !candyMachine) return;

    try {
    setIsMinting(true);
    const nft = await metaplex
    .candyMachinesV2()
    .mint({ candyMachine });

    console.log(nft);
    router.push(`/newMint?mint=${nft.nft.address.toBase58()}`);
    } catch (error) {
    alert(error);
    } finally {
    setIsMinting(false);
    }
    },
    [metaplex, walletAdapter, candyMachine]
    );

    现在,当你点击 stake my buildoor 按钮时,将提示你从幽灵钱包批准交易。但是,你可能会注意到一旦成功批准交易,页面会刷新并导致你的钱包被登出。别担心,下一部分我们将解决这个问题。

    接下来,请前往 newMint.tsx。我们将创建一个接口来解决这个问题。将此代码添加到你的 Home 函数之上。

    import { PublicKey } from "@solana/web3.js";

    interface NewMintProps {
    mint: PublicKey;
    }

    一旦完成,你应该看到以下代码结构。

    // 你的其余代码
    import { PublicKey } from "@solana/web3.js";
    import { Metaplex, walletAdapterIdentity } from "@metaplex-foundation/js";

    interface NewMintProps {
    mint: PublicKey;
    }

    const Home: NextPage<NewMintProps> = ({ mint }) => {
    const [metadata, setMetadata] = useState<any>()
    const { connection } = useConnection()
    const walletAdapter = useWallet()
    const metaplex = useMemo(() => {
    return Metaplex.make(connection).use(walletAdapterIdentity(walletAdapter))
    }, [connection, walletAdapter])

    useEffect(() => {
    // What this does is to allow us to find the NFT object
    // based on the given mint address
    metaplex.nfts().findByMint({ mintAddress: new PublicKey(mint) })
    .then((nft) => {
    // We then fetch the NFT uri to fetch the NFT metadata
    fetch(nft.uri)
    .then((res) => res.json())
    .then((metadata) => {
    setMetadata(metadata)
    })
    })
    }, [mint, metaplex, walletAdapter])
    };

    注意到我们是如何在上述函数中调用 setMetadata(metadata) 的吗?这是为了让我们能够将元数据对象设置为状态,以便我们可以用它来渲染图像。现在让我们在 Image 元素中使用此对象。

    <Image src={metadata?.image ?? ""} alt="" />

    我们快完成了。如果你现在尝试铸造一个新的NFT,你可能会注意到网站会抛出一个错误,说它无法读取未定义的属性。我们可以通过在底部添加以下几行代码来修复这个问题。

    NewMint.getInitialProps = async ({ query }) => {
    const { mint } = query;
    if (!mint) throw { error: "No mint" };

    try {
    const mintPubkey = new PublicKey(mint);
    return { mint: mintPubkey };
    } catch {
    throws({ error: "Invalid mint" });
    }
    };

    太棒了!现在你已经添加了所有必要的代码,你应该可以铸造一个NFT,并看到该图像。这就是我看到的样子。

    🛠️小修复

    请注意网站未能准确显示内容,为了解决这个问题,我们需要前往 WalletContextProvider.tsx 并修改一些代码。

    改变

    const phantom = new PhantomWalletAdapter();

    to

    const phantom = useMemo(() => new PhantomWalletAdapter(), []);

    我们还需要给你的 autoConnect 添加一个属性。就像这样。

    <WalletProvider wallets={[phantom]} autoConnect={true}>
    <WalletModalProvider>{children}</WalletModalProvider>
    </WalletProvider>

    我们需要使用 useMemo 的原因是为了防止钱包适配器被多次构建。你可以在这里了解更多关于useMemo的信息。

    - - +
    Skip to main content

    🎨 创建铸币用户界面

    现在我们成功创建了代币和非同质化代币(NFT),让我们继续着手构建我们的铸币用户界面。这样一来,我们就能直观地与智能合约互动,并允许他人在我们的浏览器上铸造我们的NFT。是不是非常酷?你可能已经注意到,你的网站上现有一个名为 minting 的按钮,但它目前尚未实现任何功能。让我们从定义一个函数开始,然后添加逻辑来允许我们铸造NFT。如果你没有起始代码,可以在这里克隆。

    首先,我们将以下代码添加到你的 newMint.tsx 文件中。注意:不要盲目地复制粘贴代码。我只提供了必要的部分,你需要明白这些代码应放在何处。提示:应该放在 Container 元素下方。

    // 你的其余代码
    import { Button, Text, HStack } from "@chakra-ui/react";
    import { MouseEventHandler, useCallback } from "react";
    import { ArrowForwardIcon } from "@chakra-ui/icons";

    const Home: NextPage = () => {
    const handleClick: MouseEventHandler<HTMLButtonElement> = useCallback(
    async (event) => {},
    []
    );

    return (
    <MainLayout>
    {/* 你的其余代码 */}
    <Image src="" alt="" />
    <Button
    bgColor="accent"
    color="white"
    maxWidth="380px"
    onClick={handleClick}
    >
    <HStack>
    <Text>stake my buildoor</Text>
    <ArrowForwardIcon />
    </HStack>
    </Button>
    </MainLayout>
    );
    };

    完成后,我们可以进入 Connected.tsx 并添加一些代码。在 handleClick 函数上方,我们可以添加 const router = useRouter()。记得在文件顶部导入 useRouter 函数。然后,在你的 handleClick 函数中添加 router.push("/newMint")。现在它应该是这个样子。

    const handleClick: MouseEventHandler<HTMLButtonElement> = useCallback(
    async (event) => {
    if (event.defaultPrevented) return;
    if (!walletAdapter.connected || !candyMachine) return;

    try {
    setIsMinting(true);
    const nft = await metaplex
    .candyMachinesV2()
    .mint({ candyMachine });

    console.log(nft);
    router.push(`/newMint?mint=${nft.nft.address.toBase58()}`);
    } catch (error) {
    alert(error);
    } finally {
    setIsMinting(false);
    }
    },
    [metaplex, walletAdapter, candyMachine]
    );

    现在,当你点击 stake my buildoor 按钮时,将提示你从幽灵钱包批准交易。但是,你可能会注意到一旦成功批准交易,页面会刷新并导致你的钱包被登出。别担心,下一部分我们将解决这个问题。

    接下来,请前往 newMint.tsx。我们将创建一个接口来解决这个问题。将此代码添加到你的 Home 函数之上。

    import { PublicKey } from "@solana/web3.js";

    interface NewMintProps {
    mint: PublicKey;
    }

    一旦完成,你应该看到以下代码结构。

    // 你的其余代码
    import { PublicKey } from "@solana/web3.js";
    import { Metaplex, walletAdapterIdentity } from "@metaplex-foundation/js";

    interface NewMintProps {
    mint: PublicKey;
    }

    const Home: NextPage<NewMintProps> = ({ mint }) => {
    const [metadata, setMetadata] = useState<any>()
    const { connection } = useConnection()
    const walletAdapter = useWallet()
    const metaplex = useMemo(() => {
    return Metaplex.make(connection).use(walletAdapterIdentity(walletAdapter))
    }, [connection, walletAdapter])

    useEffect(() => {
    // What this does is to allow us to find the NFT object
    // based on the given mint address
    metaplex.nfts().findByMint({ mintAddress: new PublicKey(mint) })
    .then((nft) => {
    // We then fetch the NFT uri to fetch the NFT metadata
    fetch(nft.uri)
    .then((res) => res.json())
    .then((metadata) => {
    setMetadata(metadata)
    })
    })
    }, [mint, metaplex, walletAdapter])
    };

    注意到我们是如何在上述函数中调用 setMetadata(metadata) 的吗?这是为了让我们能够将元数据对象设置为状态,以便我们可以用它来渲染图像。现在让我们在 Image 元素中使用此对象。

    <Image src={metadata?.image ?? ""} alt="" />

    我们快完成了。如果你现在尝试铸造一个新的NFT,你可能会注意到网站会抛出一个错误,说它无法读取未定义的属性。我们可以通过在底部添加以下几行代码来修复这个问题。

    NewMint.getInitialProps = async ({ query }) => {
    const { mint } = query;
    if (!mint) throw { error: "No mint" };

    try {
    const mintPubkey = new PublicKey(mint);
    return { mint: mintPubkey };
    } catch {
    throws({ error: "Invalid mint" });
    }
    };

    太棒了!现在你已经添加了所有必要的代码,你应该可以铸造一个NFT,并看到该图像。这就是我看到的样子。

    🛠️小修复

    请注意网站未能准确显示内容,为了解决这个问题,我们需要前往 WalletContextProvider.tsx 并修改一些代码。

    改变

    const phantom = new PhantomWalletAdapter();

    to

    const phantom = useMemo(() => new PhantomWalletAdapter(), []);

    我们还需要给你的 autoConnect 添加一个属性。就像这样。

    <WalletProvider wallets={[phantom]} autoConnect={true}>
    <WalletModalProvider>{children}</WalletModalProvider>
    </WalletProvider>

    我们需要使用 useMemo 的原因是为了防止钱包适配器被多次构建。你可以在这里了解更多关于useMemo的信息。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/index.html b/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/index.html index 05fc08a28..8defbb328 100644 --- a/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/index.html +++ b/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module2/nfts-and-minting-with-metaplex/candy-machine-and-the-sugar-cli/index.html b/Solana-Co-Learn/module2/nfts-and-minting-with-metaplex/candy-machine-and-the-sugar-cli/index.html index 907f6f7a9..3b5750239 100644 --- a/Solana-Co-Learn/module2/nfts-and-minting-with-metaplex/candy-machine-and-the-sugar-cli/index.html +++ b/Solana-Co-Learn/module2/nfts-and-minting-with-metaplex/candy-machine-and-the-sugar-cli/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🍭 糖果机和Sugar CLI

    将自己的脸做成NFT有何不好呢?你可以永久地将自己视为一个早期的建设者,并告诉你的妈妈你已经进入了区块链世界。既然我们已经铸造了一个单独的NFT,现在我们将学习如何铸造一系列的NFT。为了实现这一目标,我们将使用Candy Machine——这是一个Solana程序,让创作者能够将他们的资产上链。尽管这不是创建系列的唯一方法,但在Solana上它已经成为一种标准,因为它拥有一些有用的功能,如防机器人保护和安全随机化。

    由于这是一个链上程序,所有的数据都存储在账户中。你首先需要为你的收藏创建一个糖果机实例。这只是一个账户,其中存储了一些关于所有者的重要信息以及糖果机在元数据字段中的配置。

    注意那个数据字段?那里存放的就是元数据,它的结构如下图所示:

    再次强调一下,这里有很多细节,我们将在适当的时候逐一解释。

    为了与糖果机程序互动,我们将使用Sugar CLI。这是一个非常棒的工具,让你能够直接从命令行与程序交互。

    🛠 安装命令行界面(CLIs)

    在我们开始之前,我们需要安装以下两样东西:

      1. Solana CLI - Sugar CLI 依赖于此。你可以在这里找到适合你操作系统的安装指南。
      1. Sugar CLI - 你可以在这里找到安装方法。

    注意 - 如果你想将CLI的安装与你的计算机隔离开来,你可以在Docker上设置Solana CLI,然后下载Sugar CLI。Docker镜像在这里。如果你不了解Docker是什么,也不要担心!

    如果安装正确,当你在终端中运行 solana --versionsugar --version 时,应该会看到版本号而不是错误信息。

    如果你还没有本地的Solana钱包,现在是配置开发网络的好时机。在终端中运行以下命令:

    solana config set --url devnet
    solana-keygen new --outfile ~/.config/solana/devnet.json
    solana airdrop 2
    solana balance

    这些命令与我们在本地客户端脚本中执行的操作相同,只不过现在是在终端中完成。这样,你就为在Solana上创建一系列NFT做好了准备。

    🍬 创建你的珍藏品

    这可能是整个构建过程中最具挑战的部分:确定你想要制作的NFT收藏品的内容。你至少需要准备5张图片,每张图片对应收藏中的一个NFT。我挑选了一些经典的pepes图像,因为它们总能引起我的共鸣。

    Solana工作空间中,创建一个新的项目文件夹,并在其中创建一个名为 assets 的文件夹。你需要将每个NFT资产与一份元数据JSON文件配对,并从零开始为每一对编号。因此,你的文件夹结构应该如下所示:

    ...
    |
    |── assets
    | |── 0.png
    | |── 0.json
    | |...
    | |── 5.png
    | |── 5.json
    |
    |── node_modules
    |── src
    |── package.json
    ....

    下面是一个JSON文件的示例:

    在实际操作中,你可以编写脚本来生成这些文件,但现在我们暂时手动完成。你可以从这些示例资产开始,然后用你自己的图片替换它们。别忘了更新JSON文件!

    你还可以选择添加与之匹配的 collection.jsoncollection.png 文件,市场将使用这些文件作为集合的名称、描述和缩略图。

    以下是模板:

    {
    "name": "Studious Crabs Collection",
    "symbol": "CRAB",
    "description": "Collection of 10 crabs seeking refuge from overfishing on the blockchain.",
    "image": "collection.png",
    "attributes": [],
    "properties": {
    "files": [
    {
    "uri": "collection.png",
    "type": "image/png"
    }
    ]
    }
    }

    拯救🦀螃蟹,使其免受🎣渔民捕捞

    现在,你的资产文件夹应该只包括商品(如果你使用Windows系统,可能还会有一个~文件夹)。

    🍭 配置你的糖果机

    接下来,我们需要创建一个糖果机的配置文件。这个文件用于在链上创建糖果机实例。Sugar CLI将指导你完成最基本的设置,无需手动操作!以下是它的样子:

    你是否听说过吃太多糖对身体不好?开发Sugar CLI的人似乎也这么认为。要设立一个糖果机,你只需运行 launch 命令,其余的工作它都会为你处理。

    🚀 发行你的NFT珍藏品

    在终端中输入 sugar launch 命令,当它询问是否要创建新的配置文件时,按下y键。回答问题后,你的项目文件夹中会生成一个 config.json 文件。

    以下是我的回答:

    ✔ What is the price of each NFT? · 0.3
    ✔ Found 10 file pairs in "assets". Is this how many NFTs you will have in your candy machine? · ye
    ✔ Found symbol "CRAB" in your metadata file. Is this value correct? · no
    ✔ What is the symbol of your collection? Hit [ENTER] for no symbol. · PEPE
    ✔ What is the seller fee basis points? · 100
    ? What is your go live date? Many common formats are supported. · now
    ✔ How many creator wallets do you have? (max limit of 4) · 1
    ✔ Enter creator wallet address #1 · B1aLAAe4vW8nSQCetXnYqJfRxzTjnbooczwkUJAr7yMS
    ✔ Enter royalty percentage share for creator #1 (e.g., 70). Total shares must add to 100. · 100
    ? Which extra features do you want to use? ·
    ✔ What is your SOL treasury address? · B1aLAAe4vW8nSQCetXnYqJfRxzTjnbooczwkUJAr7yMS
    ✔ What upload method do you want to use? · Bundlr
    ✔ Do you want to retain update authority on your NFTs? We HIGHLY recommend you choose yes. · yes
    ✔ Do you want your NFTs to remain mutable? We HIGHLY recommend you choose yes. · yes

    你可能会收到 MISSING COLLECTION FILES IN ASSETS FOLDER 的警告,不用担心,这是因为我们没有在 assets 文件夹中设置 collection.pngcollection.json 文件。继续回答 y。如果你想了解更多关于这些文件的信息,可以在此处了解更多。

    现在我们暂时不需要任何特殊功能。如果你感兴趣,可以在此处阅读更多相关信息。

    如果遇到任何问题或中途改变主意,你可以随时退出并重新开始。你还可以直接编辑 config.json 文件。Sugar CLI会显示非常有用的错误信息,所以如果遇到困难,只需仔细阅读,通常就能找到解决方案。

    如果一切顺利,最终你会看到一个绿色的“命令成功”消息。在消息上方,你会看到一个SolanEyes链接。点击该链接,你就可以在Solana网络上查看你的糖果机!复制糖果机的ID以备后用。

    如果这还不足以让你惊奇,那么你可以尝试使用 sugar mint 命令来铸造一个NFT,这简直是一种美味的体验。

    一旦你整理好你的收藏品,并在巴厘岛享受休闲时光,"糖"工具也可以帮助你执行各种操作。如果你感到好奇,可以查看这里的命令了解更多。

    🌐 为你的NFT收藏创建前端界面

    希望你已经用过晚餐,因为现在又到了享用更多糖果的时刻。

    Metaplex基金会为你提供了一个时尚的React UI模板,你可以使用它来为你的NFT收藏打造前端界面。下面,让我们开始设置:

    git clone https://github.com/metaplex-foundation/candy-machine-ui
    cd candy-machine-ui
    npm i

    虽然这里进行了很多操作,但我们不必过于担心。只需将 .env.example 文件重命名为 .env,并粘贴你之前复制的糖果机ID

    REACT_APP_CANDY_MACHINE_ID=GNfbQEfMA1u1irEFnThTcrzDyefJsoa7sndACShaS5vC

    这就是你需要做的全部工作!现在,如果你运行 npm start,你将在 localhost:3000 上看到一个精美的用户界面,可以用它来铸造你的NFT。

    对于Mac用户,如果遇到 export NODE_OPTIONS=--openssl-legacy-provider 问题,请在终端中运行。

    铸造完成后,你可以在钱包的收藏品部分查看NFT

    你会注意到铸造的NFT并不是1.png。这是因为糖果机的铸造过程默认是随机的。

    我们只是浅尝辄止地触及了Candy MachineSugar CLI的潜力。未来我们还会深入探讨更多内容——本节的目的是让你具备足够的基础知识,以便能够自主深入研究。随着我们对NFT项目的不断完善,我们将继续探索。

    🚢 挑战

    让我们再享受一会儿糖果机的乐趣吧!🍭

    通过更新 config.json 文件并运行 sugar update 命令,你可以挖掘创造力并尝试不同的糖果机配置。

    例如:

    • 修改 goLiveDate
    • 启用 gatekeeper(验证码功能)
    • 启用 whitelistMintSettings
      • 需要创建令牌
    • 使用 splToken 来代替本地的sol进行付款
      • 需要创建令牌

    想了解更多提示和文档,请访问:

    https://docs.metaplex.com/developer-tools/sugar/learning/settings

    - - +
    Skip to main content

    🍭 糖果机和Sugar CLI

    将自己的脸做成NFT有何不好呢?你可以永久地将自己视为一个早期的建设者,并告诉你的妈妈你已经进入了区块链世界。既然我们已经铸造了一个单独的NFT,现在我们将学习如何铸造一系列的NFT。为了实现这一目标,我们将使用Candy Machine——这是一个Solana程序,让创作者能够将他们的资产上链。尽管这不是创建系列的唯一方法,但在Solana上它已经成为一种标准,因为它拥有一些有用的功能,如防机器人保护和安全随机化。

    由于这是一个链上程序,所有的数据都存储在账户中。你首先需要为你的收藏创建一个糖果机实例。这只是一个账户,其中存储了一些关于所有者的重要信息以及糖果机在元数据字段中的配置。

    注意那个数据字段?那里存放的就是元数据,它的结构如下图所示:

    再次强调一下,这里有很多细节,我们将在适当的时候逐一解释。

    为了与糖果机程序互动,我们将使用Sugar CLI。这是一个非常棒的工具,让你能够直接从命令行与程序交互。

    🛠 安装命令行界面(CLIs)

    在我们开始之前,我们需要安装以下两样东西:

      1. Solana CLI - Sugar CLI 依赖于此。你可以在这里找到适合你操作系统的安装指南。
      1. Sugar CLI - 你可以在这里找到安装方法。

    注意 - 如果你想将CLI的安装与你的计算机隔离开来,你可以在Docker上设置Solana CLI,然后下载Sugar CLI。Docker镜像在这里。如果你不了解Docker是什么,也不要担心!

    如果安装正确,当你在终端中运行 solana --versionsugar --version 时,应该会看到版本号而不是错误信息。

    如果你还没有本地的Solana钱包,现在是配置开发网络的好时机。在终端中运行以下命令:

    solana config set --url devnet
    solana-keygen new --outfile ~/.config/solana/devnet.json
    solana airdrop 2
    solana balance

    这些命令与我们在本地客户端脚本中执行的操作相同,只不过现在是在终端中完成。这样,你就为在Solana上创建一系列NFT做好了准备。

    🍬 创建你的珍藏品

    这可能是整个构建过程中最具挑战的部分:确定你想要制作的NFT收藏品的内容。你至少需要准备5张图片,每张图片对应收藏中的一个NFT。我挑选了一些经典的pepes图像,因为它们总能引起我的共鸣。

    Solana工作空间中,创建一个新的项目文件夹,并在其中创建一个名为 assets 的文件夹。你需要将每个NFT资产与一份元数据JSON文件配对,并从零开始为每一对编号。因此,你的文件夹结构应该如下所示:

    ...
    |
    |── assets
    | |── 0.png
    | |── 0.json
    | |...
    | |── 5.png
    | |── 5.json
    |
    |── node_modules
    |── src
    |── package.json
    ....

    下面是一个JSON文件的示例:

    在实际操作中,你可以编写脚本来生成这些文件,但现在我们暂时手动完成。你可以从这些示例资产开始,然后用你自己的图片替换它们。别忘了更新JSON文件!

    你还可以选择添加与之匹配的 collection.jsoncollection.png 文件,市场将使用这些文件作为集合的名称、描述和缩略图。

    以下是模板:

    {
    "name": "Studious Crabs Collection",
    "symbol": "CRAB",
    "description": "Collection of 10 crabs seeking refuge from overfishing on the blockchain.",
    "image": "collection.png",
    "attributes": [],
    "properties": {
    "files": [
    {
    "uri": "collection.png",
    "type": "image/png"
    }
    ]
    }
    }

    拯救🦀螃蟹,使其免受🎣渔民捕捞

    现在,你的资产文件夹应该只包括商品(如果你使用Windows系统,可能还会有一个~文件夹)。

    🍭 配置你的糖果机

    接下来,我们需要创建一个糖果机的配置文件。这个文件用于在链上创建糖果机实例。Sugar CLI将指导你完成最基本的设置,无需手动操作!以下是它的样子:

    你是否听说过吃太多糖对身体不好?开发Sugar CLI的人似乎也这么认为。要设立一个糖果机,你只需运行 launch 命令,其余的工作它都会为你处理。

    🚀 发行你的NFT珍藏品

    在终端中输入 sugar launch 命令,当它询问是否要创建新的配置文件时,按下y键。回答问题后,你的项目文件夹中会生成一个 config.json 文件。

    以下是我的回答:

    ✔ What is the price of each NFT? · 0.3
    ✔ Found 10 file pairs in "assets". Is this how many NFTs you will have in your candy machine? · ye
    ✔ Found symbol "CRAB" in your metadata file. Is this value correct? · no
    ✔ What is the symbol of your collection? Hit [ENTER] for no symbol. · PEPE
    ✔ What is the seller fee basis points? · 100
    ? What is your go live date? Many common formats are supported. · now
    ✔ How many creator wallets do you have? (max limit of 4) · 1
    ✔ Enter creator wallet address #1 · B1aLAAe4vW8nSQCetXnYqJfRxzTjnbooczwkUJAr7yMS
    ✔ Enter royalty percentage share for creator #1 (e.g., 70). Total shares must add to 100. · 100
    ? Which extra features do you want to use? ·
    ✔ What is your SOL treasury address? · B1aLAAe4vW8nSQCetXnYqJfRxzTjnbooczwkUJAr7yMS
    ✔ What upload method do you want to use? · Bundlr
    ✔ Do you want to retain update authority on your NFTs? We HIGHLY recommend you choose yes. · yes
    ✔ Do you want your NFTs to remain mutable? We HIGHLY recommend you choose yes. · yes

    你可能会收到 MISSING COLLECTION FILES IN ASSETS FOLDER 的警告,不用担心,这是因为我们没有在 assets 文件夹中设置 collection.pngcollection.json 文件。继续回答 y。如果你想了解更多关于这些文件的信息,可以在此处了解更多。

    现在我们暂时不需要任何特殊功能。如果你感兴趣,可以在此处阅读更多相关信息。

    如果遇到任何问题或中途改变主意,你可以随时退出并重新开始。你还可以直接编辑 config.json 文件。Sugar CLI会显示非常有用的错误信息,所以如果遇到困难,只需仔细阅读,通常就能找到解决方案。

    如果一切顺利,最终你会看到一个绿色的“命令成功”消息。在消息上方,你会看到一个SolanEyes链接。点击该链接,你就可以在Solana网络上查看你的糖果机!复制糖果机的ID以备后用。

    如果这还不足以让你惊奇,那么你可以尝试使用 sugar mint 命令来铸造一个NFT,这简直是一种美味的体验。

    一旦你整理好你的收藏品,并在巴厘岛享受休闲时光,"糖"工具也可以帮助你执行各种操作。如果你感到好奇,可以查看这里的命令了解更多。

    🌐 为你的NFT收藏创建前端界面

    希望你已经用过晚餐,因为现在又到了享用更多糖果的时刻。

    Metaplex基金会为你提供了一个时尚的React UI模板,你可以使用它来为你的NFT收藏打造前端界面。下面,让我们开始设置:

    git clone https://github.com/metaplex-foundation/candy-machine-ui
    cd candy-machine-ui
    npm i

    虽然这里进行了很多操作,但我们不必过于担心。只需将 .env.example 文件重命名为 .env,并粘贴你之前复制的糖果机ID

    REACT_APP_CANDY_MACHINE_ID=GNfbQEfMA1u1irEFnThTcrzDyefJsoa7sndACShaS5vC

    这就是你需要做的全部工作!现在,如果你运行 npm start,你将在 localhost:3000 上看到一个精美的用户界面,可以用它来铸造你的NFT。

    对于Mac用户,如果遇到 export NODE_OPTIONS=--openssl-legacy-provider 问题,请在终端中运行。

    铸造完成后,你可以在钱包的收藏品部分查看NFT

    你会注意到铸造的NFT并不是1.png。这是因为糖果机的铸造过程默认是随机的。

    我们只是浅尝辄止地触及了Candy MachineSugar CLI的潜力。未来我们还会深入探讨更多内容——本节的目的是让你具备足够的基础知识,以便能够自主深入研究。随着我们对NFT项目的不断完善,我们将继续探索。

    🚢 挑战

    让我们再享受一会儿糖果机的乐趣吧!🍭

    通过更新 config.json 文件并运行 sugar update 命令,你可以挖掘创造力并尝试不同的糖果机配置。

    例如:

    • 修改 goLiveDate
    • 启用 gatekeeper(验证码功能)
    • 启用 whitelistMintSettings
      • 需要创建令牌
    • 使用 splToken 来代替本地的sol进行付款
      • 需要创建令牌

    想了解更多提示和文档,请访问:

    https://docs.metaplex.com/developer-tools/sugar/learning/settings

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module2/nfts-and-minting-with-metaplex/index.html b/Solana-Co-Learn/module2/nfts-and-minting-with-metaplex/index.html index f5fbb15d9..17c86cbda 100644 --- a/Solana-Co-Learn/module2/nfts-and-minting-with-metaplex/index.html +++ b/Solana-Co-Learn/module2/nfts-and-minting-with-metaplex/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module2/nfts-and-minting-with-metaplex/nft-your-face/index.html b/Solana-Co-Learn/module2/nfts-and-minting-with-metaplex/nft-your-face/index.html index bd2b1d918..533758fbd 100644 --- a/Solana-Co-Learn/module2/nfts-and-minting-with-metaplex/nft-your-face/index.html +++ b/Solana-Co-Learn/module2/nfts-and-minting-with-metaplex/nft-your-face/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🤨 NFT你的脸

    有什么比将你的脸做成NFT更有趣的选择呢?你可以将自己永远铭记为早期的开拓者,并骄傲地告诉你的妈妈你已经成为了区块链的一部分。

    我们将从创建一个客户端开始:

    npx create-solana-client [name] --initialize-keypair
    cd [name]

    紧接着,请执行以下命令:

    npm install @metaplex-foundation/js fs

    src 文件夹中添加两个图像文件。我们将使用其中一个作为初始图像,第二个作为更新后的图像。

    接下来是我们在 src/index.ts 中所需的导入项,这些都不是什么新鲜事:

    import { Connection, clusterApiUrl, PublicKey } from "@solana/web3.js"
    import {
    Metaplex,
    keypairIdentity,
    bundlrStorage,
    toMetaplexFile,
    NftWithToken,
    } from "@metaplex-foundation/js"
    import * as fs from "fs"

    如果我们声明一些常量,那么在创建和更新NFT之间更改变量将会变得更容易:

    const tokenName = "Token Name"
    const description = "Description"
    const symbol = "SYMBOL"
    const sellerFeeBasisPoints = 100
    const imageFile = "test.png"

    async function main() {
    ...
    }

    我们不需要创建任何辅助函数,而是可以将所有内容放在 main() 函数中。首先,我们将创建一个 Metaplex 实例:

    async function main() {
    ...

    const metaplex = Metaplex.make(connection)
    .use(keypairIdentity(user))
    .use(
    bundlrStorage({
    address: "https://devnet.bundlr.network",
    providerUrl: "https://api.devnet.solana.com",
    timeout: 60000,
    })
    )
    }

    上传图片的步骤包括:

    • 读取图像文件
    • 将其转换为Metaplex文件
    • 上传图片
    async function main() {
    ...

    // 将文件读取为缓冲区
    const buffer = fs.readFileSync("src/" + imageFile)

    // 将缓冲区转换为Metaplex文件
    const file = toMetaplexFile(buffer, imageFile)

    // 上传图像并获取图像URI
    const imageUri = await metaplex.storage().upload(file)
    console.log("图像URI:", imageUri)
    }

    最后,我们可以使用我们得到的图像URI上传元数据:

    async function main() {
    ...

    // 上传元数据并获取元数据URI(链下元数据)
    const { uri } = await metaplex
    .nfts()
    .uploadMetadata({
    name: tokenName,
    description: description,
    image: imageUri,
    })

    console.log("元数据URI:", uri)
    }

    在这里,创建一个专门的铸造NFT功能是个不错的主意,将其放在主函数之外:

    // 创建NFT
    async function createNft(
    metaplex: Metaplex,
    uri: string
    ): Promise<NftWithToken> {
    const { nft } = await metaplex
    .nfts()
    .create({
    uri: uri,
    name: tokenName,
    sellerFeeBasisPoints: sellerFeeBasisPoints,
    symbol: symbol,
    })

    console.log(
    `代币Mint地址:https://explorer.solana.com/address/${nft.address.toString()}?cluster=devnet`
    )

    return nft
    }

    现在你只需在主函数的末尾调用它即可:

    async function main() {
    ...

    await createNft(metaplex, uri)
    }

    我们已经准备好铸造我们的NFT了!在终端中运行脚本 npm run start ,然后点击Solana Explorer的URL,你应该会看到类似这样的内容:

    我们刚刚在Solana上制造了一个NFT🎉🎉🎉。这简直就像把现成的饭菜再热一热那么简单。

    🤯 更新你的NFT

    总结一下,我们来快速看一下如何更新刚刚铸造的NFT

    首先,在你的脚本顶部,将 imageFile 常量更新为你的NFT的最终图像的名称。

    唯一改变的是我们将称之为Metaplex的方法。你可以将下面的代码添加到 main 函数之外的任何位置:

    async function updateNft(
    metaplex: Metaplex,
    uri: string,
    mintAddress: PublicKey
    ) {
    // 通过薄荷地址获取 "NftWithToken" 类型
    const nft = await metaplex.nfts().findByMint({ mintAddress })

    // 省略任何保持不变的字段
    await metaplex
    .nfts()
    .update({
    nftOrSft: nft,
    name: tokenName,
    symbol: symbol,
    uri: uri,
    sellerFeeBasisPoints: sellerFeeBasisPoints,
    })

    console.log(
    `代币Mint地址:https://explorer.solana.com/address/${nft.address.toString()}?cluster=devnet`
    )
    }

    现在,在主函数中,你可以注释掉 createNFT 的调用,并使用新的 updateNFT 辅助函数:

    async function main() {

    ...

    // 你可以暂时注释掉 createNft 的调用
    // await createNft(metaplex, uri)

    // 你可以从Solana Explorer的URL中获取薄荷地址
    const mintAddress = new PublicKey("EPd324PkQx53Cx2g2B9ZfxVmu6m6gyneMaoWTy2hk2bW")
    await updateNft(metaplex, uri, mintAddress)
    }

    你可以从在铸造NFT时记录的URL中获取薄荷地址。它出现在多个位置 - URL本身、"地址"属性和元数据选项卡中。

    如此一来,你就可以轻松地更新你的NFT的图像和其他相关信息了。

    这一系列操作既简单又直观,现在你已经掌握了在Solana上创建和更新NFT的完整流程!

    - - +
    Skip to main content

    🤨 NFT你的脸

    有什么比将你的脸做成NFT更有趣的选择呢?你可以将自己永远铭记为早期的开拓者,并骄傲地告诉你的妈妈你已经成为了区块链的一部分。

    我们将从创建一个客户端开始:

    npx create-solana-client [name] --initialize-keypair
    cd [name]

    紧接着,请执行以下命令:

    npm install @metaplex-foundation/js fs

    src 文件夹中添加两个图像文件。我们将使用其中一个作为初始图像,第二个作为更新后的图像。

    接下来是我们在 src/index.ts 中所需的导入项,这些都不是什么新鲜事:

    import { Connection, clusterApiUrl, PublicKey } from "@solana/web3.js"
    import {
    Metaplex,
    keypairIdentity,
    bundlrStorage,
    toMetaplexFile,
    NftWithToken,
    } from "@metaplex-foundation/js"
    import * as fs from "fs"

    如果我们声明一些常量,那么在创建和更新NFT之间更改变量将会变得更容易:

    const tokenName = "Token Name"
    const description = "Description"
    const symbol = "SYMBOL"
    const sellerFeeBasisPoints = 100
    const imageFile = "test.png"

    async function main() {
    ...
    }

    我们不需要创建任何辅助函数,而是可以将所有内容放在 main() 函数中。首先,我们将创建一个 Metaplex 实例:

    async function main() {
    ...

    const metaplex = Metaplex.make(connection)
    .use(keypairIdentity(user))
    .use(
    bundlrStorage({
    address: "https://devnet.bundlr.network",
    providerUrl: "https://api.devnet.solana.com",
    timeout: 60000,
    })
    )
    }

    上传图片的步骤包括:

    • 读取图像文件
    • 将其转换为Metaplex文件
    • 上传图片
    async function main() {
    ...

    // 将文件读取为缓冲区
    const buffer = fs.readFileSync("src/" + imageFile)

    // 将缓冲区转换为Metaplex文件
    const file = toMetaplexFile(buffer, imageFile)

    // 上传图像并获取图像URI
    const imageUri = await metaplex.storage().upload(file)
    console.log("图像URI:", imageUri)
    }

    最后,我们可以使用我们得到的图像URI上传元数据:

    async function main() {
    ...

    // 上传元数据并获取元数据URI(链下元数据)
    const { uri } = await metaplex
    .nfts()
    .uploadMetadata({
    name: tokenName,
    description: description,
    image: imageUri,
    })

    console.log("元数据URI:", uri)
    }

    在这里,创建一个专门的铸造NFT功能是个不错的主意,将其放在主函数之外:

    // 创建NFT
    async function createNft(
    metaplex: Metaplex,
    uri: string
    ): Promise<NftWithToken> {
    const { nft } = await metaplex
    .nfts()
    .create({
    uri: uri,
    name: tokenName,
    sellerFeeBasisPoints: sellerFeeBasisPoints,
    symbol: symbol,
    })

    console.log(
    `代币Mint地址:https://explorer.solana.com/address/${nft.address.toString()}?cluster=devnet`
    )

    return nft
    }

    现在你只需在主函数的末尾调用它即可:

    async function main() {
    ...

    await createNft(metaplex, uri)
    }

    我们已经准备好铸造我们的NFT了!在终端中运行脚本 npm run start ,然后点击Solana Explorer的URL,你应该会看到类似这样的内容:

    我们刚刚在Solana上制造了一个NFT🎉🎉🎉。这简直就像把现成的饭菜再热一热那么简单。

    🤯 更新你的NFT

    总结一下,我们来快速看一下如何更新刚刚铸造的NFT

    首先,在你的脚本顶部,将 imageFile 常量更新为你的NFT的最终图像的名称。

    唯一改变的是我们将称之为Metaplex的方法。你可以将下面的代码添加到 main 函数之外的任何位置:

    async function updateNft(
    metaplex: Metaplex,
    uri: string,
    mintAddress: PublicKey
    ) {
    // 通过薄荷地址获取 "NftWithToken" 类型
    const nft = await metaplex.nfts().findByMint({ mintAddress })

    // 省略任何保持不变的字段
    await metaplex
    .nfts()
    .update({
    nftOrSft: nft,
    name: tokenName,
    symbol: symbol,
    uri: uri,
    sellerFeeBasisPoints: sellerFeeBasisPoints,
    })

    console.log(
    `代币Mint地址:https://explorer.solana.com/address/${nft.address.toString()}?cluster=devnet`
    )
    }

    现在,在主函数中,你可以注释掉 createNFT 的调用,并使用新的 updateNFT 辅助函数:

    async function main() {

    ...

    // 你可以暂时注释掉 createNft 的调用
    // await createNft(metaplex, uri)

    // 你可以从Solana Explorer的URL中获取薄荷地址
    const mintAddress = new PublicKey("EPd324PkQx53Cx2g2B9ZfxVmu6m6gyneMaoWTy2hk2bW")
    await updateNft(metaplex, uri, mintAddress)
    }

    你可以从在铸造NFT时记录的URL中获取薄荷地址。它出现在多个位置 - URL本身、"地址"属性和元数据选项卡中。

    如此一来,你就可以轻松地更新你的NFT的图像和其他相关信息了。

    这一系列操作既简单又直观,现在你已经掌握了在Solana上创建和更新NFT的完整流程!

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module2/nfts-and-minting-with-metaplex/nfts-one-solana/index.html b/Solana-Co-Learn/module2/nfts-and-minting-with-metaplex/nfts-one-solana/index.html index 21ea621b3..91648fbbf 100644 --- a/Solana-Co-Learn/module2/nfts-and-minting-with-metaplex/nfts-one-solana/index.html +++ b/Solana-Co-Learn/module2/nfts-and-minting-with-metaplex/nfts-one-solana/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🎨 Solana上的NFT

    我们来了,不过花了不了多长时间。猴子画像、猩猩、岩石,以及其他一些看起来丑陋却能卖到10万美元的动物主题头像。这就是NFT

    与以太坊不同,Solana上铸造NFT非常便宜,这使得它们更加有趣。即使在熊市的时候,在以太坊上铸造一个NFT可能只需要5美元,这感觉有些乏味。为什么我要花5美元来永久保留我的梗图呢?

    🫣 NFT就是代币

    Solana上NFT价格便宜的一个重要原因是它们并不是一些特殊的代码,它们只是普通的代币,附加了一些额外的数据。

    第一个主要的区别在于铸造账户。对于NFT来说,铸造账户:

    • 供应量为1,意味着只有一枚代币在流通。
    • 没有小数点,意味着你不能拥有0.5个代币这样的东西。
    • 没有铸造权限,意味着没有人可以铸造更多的代币。

    正如你可能猜到的,额外的数据存储在程序派生的账户中。让我们一起来认识一下这些新账户吧 :D

    🐙 主版本(Master Edition)账户

    Token Metadata程序为NFT提供了另一个特殊的账户类型,称为Master Edition账户。它不会废除铸币权,而是将铸币和冻结权限转移到Master Edition PDA

    换句话说,铸币权被转移到了Token Metadata程序的PDA上。这确保了在没有通过Token Metadata程序的情况下,任何人都不能铸造或冻结代币。

    只有程序提供的指令可以使用它,并且程序中并没有这样的指令。这样做的原因是为了让Metaplex能够部署Token Metadata程序的升级,并将现有的NFT迁移到新版本。

    🪰 版本(Editions)账户

    除了证明不可替代性,Master Edition账户还允许用户打印一个或多个NFT的副本。这使得创作者可以提供多个1/1 NFT的复制品。

    Master Edition账户包括一个可选的Max Supply属性:

    • 如果设置为0,则禁用打印功能;
    • 如果设置为None,则可以打印无限数量的副本。

    🧰 Metaplex SDK

    通过我们的新朋友Metaplex SDK,所有这些都变得简单容易。它让你能够轻松创建和更新NFT,只需提供最基本的信息,它就会自动填充其余的默认值。

    和令牌元数据一样,我们将使用相同的流程来:

    • 上传一张图片;
    • 上传元数据;
    • 然后使用元数据URI创建一个NFT

    你能想象代码会是什么样子吗?在我们开始之前,试着在脑海中描绘它,然后我们一起来编写实现吧 :)

    - - +
    Skip to main content

    🎨 Solana上的NFT

    我们来了,不过花了不了多长时间。猴子画像、猩猩、岩石,以及其他一些看起来丑陋却能卖到10万美元的动物主题头像。这就是NFT

    与以太坊不同,Solana上铸造NFT非常便宜,这使得它们更加有趣。即使在熊市的时候,在以太坊上铸造一个NFT可能只需要5美元,这感觉有些乏味。为什么我要花5美元来永久保留我的梗图呢?

    🫣 NFT就是代币

    Solana上NFT价格便宜的一个重要原因是它们并不是一些特殊的代码,它们只是普通的代币,附加了一些额外的数据。

    第一个主要的区别在于铸造账户。对于NFT来说,铸造账户:

    • 供应量为1,意味着只有一枚代币在流通。
    • 没有小数点,意味着你不能拥有0.5个代币这样的东西。
    • 没有铸造权限,意味着没有人可以铸造更多的代币。

    正如你可能猜到的,额外的数据存储在程序派生的账户中。让我们一起来认识一下这些新账户吧 :D

    🐙 主版本(Master Edition)账户

    Token Metadata程序为NFT提供了另一个特殊的账户类型,称为Master Edition账户。它不会废除铸币权,而是将铸币和冻结权限转移到Master Edition PDA

    换句话说,铸币权被转移到了Token Metadata程序的PDA上。这确保了在没有通过Token Metadata程序的情况下,任何人都不能铸造或冻结代币。

    只有程序提供的指令可以使用它,并且程序中并没有这样的指令。这样做的原因是为了让Metaplex能够部署Token Metadata程序的升级,并将现有的NFT迁移到新版本。

    🪰 版本(Editions)账户

    除了证明不可替代性,Master Edition账户还允许用户打印一个或多个NFT的副本。这使得创作者可以提供多个1/1 NFT的复制品。

    Master Edition账户包括一个可选的Max Supply属性:

    • 如果设置为0,则禁用打印功能;
    • 如果设置为None,则可以打印无限数量的副本。

    🧰 Metaplex SDK

    通过我们的新朋友Metaplex SDK,所有这些都变得简单容易。它让你能够轻松创建和更新NFT,只需提供最基本的信息,它就会自动填充其余的默认值。

    和令牌元数据一样,我们将使用相同的流程来:

    • 上传一张图片;
    • 上传元数据;
    • 然后使用元数据URI创建一个NFT

    你能想象代码会是什么样子吗?在我们开始之前,试着在脑海中描绘它,然后我们一起来编写实现吧 :)

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module2/spl-token/give-your-token-an-identity/index.html b/Solana-Co-Learn/module2/spl-token/give-your-token-an-identity/index.html index cf331dc0c..a1dfdc8b4 100644 --- a/Solana-Co-Learn/module2/spl-token/give-your-token-an-identity/index.html +++ b/Solana-Co-Learn/module2/spl-token/give-your-token-an-identity/index.html @@ -9,15 +9,15 @@ - - + +
    Skip to main content

    🧬 为你的代币赋予身份

    现在是时候让代币与它们的创造者(也就是你)相遇了。我们将在之前构建的基础上继续前进。如果需要,你可以从这个链接获取起始代码(确保你处于 solution-without-burn 分支)。

    首先,我们要添加新的依赖项:

    npm install @metaplex-foundation/js fs
    npm install @metaplex-foundation/mpl-token-metadata

    我们将借助Metaplex SDK添加元数据,并使用 fs 库来读取代币的标志图片。接下来,创建一个名为 assets 的新文件夹,并添加你的标志。这将在测试网络上进行,所以尽情玩乐吧!我选了一个比萨饼的表情符号,所以我把文件命名为pizza.png

    Metaplex将负责所有繁重的工作,所以请在index.ts文件顶部添加以下导入语句:

    import {
    Metaplex,
    keypairIdentity,
    bundlrStorage,
    toMetaplexFile,
    } from "@metaplex-foundation/js"
    import {
    DataV2,
    createCreateMetadataAccountV3Instruction,
    } from "@metaplex-foundation/mpl-token-metadata"
    import * as fs from "fs"

    现在我们已经做好了一切准备,我们将开始处理元数据部分。首先进行链下操作,然后创建代币元数据账户。

    从高层次来看,这里需要执行的步骤包括:

    1. 使用 toMetaplexFile() 方法将图像文件转换为Metaplex文件。
    2. 使用 metaplex.storage().upload 方法上传图片。
    3. 使用 metaplex.uploadMetadata() 方法上传链下元数据。
    4. 使用 findMetadataPda() 方法推导出元数据账户的程序派生地址(PDA)。
    5. 构建类型为 DataV2 的链上数据格式。
    6. 使用 createCreateMetadataAccountV2Instruction 方法创建元数据账户的构建指令(不是拼写错误哦)。
    7. 发送带有指令的交易,以创建令牌元数据账户。

    这里涉及许多步骤,但都是基础操作。花一点时间仔细阅读,你就能完全理解正在发生的事情!

    我们将创建一个单一的函数来完成所有这些操作:

    async function createTokenMetadata(
    connection: web3.Connection,
    metaplex: Metaplex,
    mint: web3.PublicKey,
    user: web3.Keypair,
    name: string,
    symbol: string,
    description: string
    ) {
    // file to buffer
    const buffer = fs.readFileSync("assets/1203.png")

    // buffer to metaplex file
    const file = toMetaplexFile(buffer, "1203.png")

    // upload image and get image uri
    const imageUri = await metaplex.storage().upload(file)
    console.log("image uri:", imageUri)

    // upload metadata and get metadata uri (off chain metadata)
    const { uri } = await metaplex
    .nfts()
    .uploadMetadata({
    name: name,
    description: description,
    image: imageUri,
    })

    console.log("metadata uri:", uri)

    // get metadata account address
    const metadataPDA = metaplex.nfts().pdas().metadata({ mint })

    // onchain metadata format
    const tokenMetadata = {
    name: name,
    symbol: symbol,
    uri: uri,
    sellerFeeBasisPoints: 0,
    creators: null,
    collection: null,
    uses: null,
    } as DataV2

    // transaction to create metadata account
    const transaction = new web3.Transaction().add(
    createCreateMetadataAccountV3Instruction(
    {
    metadata: metadataPDA,
    mint: mint,
    mintAuthority: user.publicKey,
    payer: user.publicKey,
    updateAuthority: user.publicKey,
    },
    {
    createMetadataAccountArgsV3: {
    data: tokenMetadata,
    isMutable: true,
    collectionDetails: null
    },
    }
    )
    )

    // send transaction
    const transactionSignature = await web3.sendAndConfirmTransaction(
    connection,
    transaction,
    [user]
    )

    console.log(
    `Create Metadata Account: https://explorer.solana.com/tx/${transactionSignature}?cluster=devnet`
    )
    }

    确保你更新了文件名!此外,不必担心 nfts() 的调用 - 最初,Metaplex是为NFT构建的,但最近它扩展到了可替代代币的工作。

    你会注意到我们在这里留下了许多空白的地方 - 那是因为在创建可替代代币时,我们并不需要设置这些内容。非可替代代币则需要定义更具体的行为特性。

    我可以逐个解释这个函数,但实际上我只是在重复自己。了解它的工作原理固然重要,但更重要的是知道如何使用它。你需要阅读文档来学习如何使用API,从而创建像这样的函数。

    我在讨论学会钓鱼的技能,而不仅仅是获取一条鱼。

    你的首选资源应始终是官方文档。但有时,新编写的代码可能还没有文档。所以你可以这样做 - 在代码被编写时查看它。如果你查看Metaplex的存储库,你会找到以下资源:

    这并不是什么深奥的科学,你只需要深入代码,找到你所需要的。理解代码构建的基本元素(在这种情况下是Solana指令)可能需要几次尝试,但回报是巨大的。

    通常,我会尝试以下操作:

    • Discord中搜索或询问(如MetaplexAnchor等)。
    • Stack Exchange上搜索或提问。
    • 浏览项目或程序存储库,如果你想了解如何为程序设置指令,请参考测试。
    • 或者,如果没有可参考的测试,你可以在GitHub中复制/粘贴,并希望在某处找到参考。

    希望这能给你一些关于如何成为先驱者的启示:)

    现在,让我们回到按计划进行的建设!

    还记得之前保存的代币铸造地址吗?在调用这个新函数时,我们将使用它。如果你忘记了代币铸造账户的地址,你可以随时通过浏览器查找钱包地址,并检查代币选项卡。

    下面是我们更新后的 main() 函数,在调用 createTokenMetadata 函数时的样子:

    async function main() {
    const connection = new web3.Connection(web3.clusterApiUrl("devnet"))
    const user = await initializeKeypair(connection)

    console.log("PublicKey:", user.publicKey.toBase58())

    // MAKE SURE YOU REPLACE THIS ADDRESS WITH YOURS!
    const MINT_ADDRESS = "87MGWR6EbAqegYXr3LoZmKKC9fSFXQx4EwJEAczcMpMF"

    // metaplex setup
    const metaplex = Metaplex.make(connection)
    .use(keypairIdentity(user))
    .use(
    bundlrStorage({
    address: "https://devnet.bundlr.network",
    providerUrl: "https://api.devnet.solana.com",
    timeout: 60000,
    })
    )

    // Calling the token
    await createTokenMetadata(
    connection,
    metaplex,
    new web3.PublicKey(MINT_ADDRESS),
    user,
    "Pizza", // Token name - REPLACE THIS WITH YOURS
    "PZA", // Token symbol - REPLACE THIS WITH YOURS
    "Whoever holds this token is invited to my pizza party" // Token description - REPLACE THIS WITH YOURS
    )
    }

    更新Mnint地址和代币详情,然后运行 npm run start,你会看到类似下面的输出:

    > solana-course-client@1.0.0 start
    > ts-node src/index.ts

    Current balance is 1.996472479
    PublicKey: 5y3G3Rz5vgK9rKRxu3BaC3PvhsMKGyAmtcizgrxojYAA
    image uri: https://arweave.net/7sDCnvGRJAqfgEuGOYWhIshfgTC-hNfG4NSjwsKunQs
    metadata uri: https://arweave.net/-2vGrM69PNtb2YaHnOErh1_006D28JJa825CIcEGIok
    Create Metadata Account: https://explorer.solana.com/tx/4w8XEGCJY82MnBnErW9F5r1i5UL5ffJCCujcgFeXS8TTdZ6tHBEMznWnPoQXVcsPY3WoPbL2Nb1ubXCUJWWt2GWi?cluster=devnet
    Finished successfully

    所有必要的步骤都已一次性完成!你可以随意点击Arweave链接,就像去中心化和永久的AWS S3/Google Cloud存储一样,它会展示你上传的资产是什么样子的。

    如果你回到浏览器上的代币铸造账户,你会看到一个漂亮的新图标和名称。这是我的样子:

    正如一位智者曾经说过,

    你的代币已经准备就绪!记得传播一些爱心。也许你可以给你的朋友或者Discord服务器中的其他建设者发送一些代币。在 #progress 频道分享你的地址,这样别人就可以给你空投他们的代币了。加油,你做得很好!:D

    🚢 挑战

    年轻的区块链探索者,现在是时候重新运用所学的课程概念从头开始构建了。

    你的挑战是尝试构建一个包含以下指令的单个交易:

    • 创建一个新的代币铸造;
    • 为代币铸造创建一个元数据账户;
    • 创建一个代币账户;
      • 如果可能,请尝试有条件地添加此指令;
      • 请参考 getOrCreateAssociatedTokenAccount 的实现方案;
      • 提示:链接
    • 铸造代币。

    这基本上就是你在生产环境中要完成的任务 - 将所有操作一次性地整合到一起。

    注意 这个挑战比平常更自由。挑战自己,尝试并真正努力理解每个拼图的组成部分。

    要按照我们设想的方式进行操作,你需要逐步构建每个指令,然后将它们全部添加到一个事务中。在你自己尝试解决这个问题后,你可以在此存储库的挑战分支中查看一个可能的实现。

    额外的提示:链接 - 直接查看源代码,不要依赖辅助函数。让这个挑战成为你Solana技能的飞跃!加油!

    Reference

    - - +
  • Token Program
  • 与代币交互
  • + + \ No newline at end of file diff --git a/Solana-Co-Learn/module2/spl-token/index.html b/Solana-Co-Learn/module2/spl-token/index.html index d79e14bce..5145b52a2 100644 --- a/Solana-Co-Learn/module2/spl-token/index.html +++ b/Solana-Co-Learn/module2/spl-token/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module2/spl-token/mint-token-on-solana/index.html b/Solana-Co-Learn/module2/spl-token/mint-token-on-solana/index.html index fff4b67ac..bcb245be1 100644 --- a/Solana-Co-Learn/module2/spl-token/mint-token-on-solana/index.html +++ b/Solana-Co-Learn/module2/spl-token/mint-token-on-solana/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🏧 在Solana上铸造代币

    话不多说,让我们来创造一些神奇的互联网货币吧。在我们的最终项目中,我们将创建一个代币,你将随着抵押你的社区NFT而逐渐获得它。在那之前,让我们先玩一下这个铸币过程的实际构建部分。现在是激发你的想象力,尽情享受的好时机。也许你一直想创建自己的模因币——现在是你的机会了 🚀。

    我们将从一个全新的Solana客户端开始。导航到你的Solana工作区,并运行以下命令:

    npx create-solana-client [name] --initialize-keypair
    cd [name]
    npm i

    请以你想创建的代币来命名你的客户端。我要创建Pizzacoin,因为我昨天吃了一些非常美味的披萨。现在是发挥你的创意的时候了。也许你想将时间本身进行代币化?你可以创建HokageCoin,甚至是TwitterThreadCoin。可能性无穷无尽!

    --initialize-keypair标志位完成了我们上次使用initalizeKeypair进行的所有准备工作。运行npm run start,你将获得一对新的密钥,并获得一些SOL空投。让我们打开货币印刷机,让它开始运作吧。

    图片:美国联邦储备银行行长杰罗姆·鲍威尔让印刷机开始运作。

    🎁 构建代币铸造器

    请按照以下步骤来操作:

    1. 创建一个名为Token Mint的账户
    2. 为特定钱包创建关联的token账户
    3. Mint代币发送到该钱包

    下面是src/index.ts文件中的第一步,可以在导入之后、main()函数之前加入如下代码:

    // 在文件顶部添加spl-token导入
    import * as token from "@solana/spl-token"
    import * as web3 from "@solana/web3.js"

    async function createNewMint(
    connection: web3.Connection,
    payer: web3.Keypair,
    mintAuthority: web3.PublicKey,
    freezeAuthority: web3.PublicKey,
    decimals: number
    ): Promise<web3.PublicKey> {

    const tokenMint = await token.createMint(
    connection,
    payer,
    mintAuthority,
    freezeAuthority,
    decimals
    );

    console.log(`代币铸造账户地址为 ${tokenMint}`)
    console.log(
    `Token Mint: https://explorer.solana.com/address/${tokenMint}?cluster=devnet`
    );

    return tokenMint;
    }

    这段代码应该看起来很熟悉。如果不是,请回头再阅读一遍上一节的内容 😠。

    接下来,我们需要创建关联的令牌账户,在createNewMint函数之后添加以下内容:

    async function createTokenAccount(
    connection: web3.Connection,
    payer: web3.Keypair,
    mint: web3.PublicKey,
    owner: web3.PublicKey
    ) {
    const tokenAccount = await token.getOrCreateAssociatedTokenAccount(
    connection,
    payer,
    mint,
    owner
    )

    console.log(
    `Token Account: https://explorer.solana.com/address/${tokenAccount.address}?cluster=devnet`
    )

    return tokenAccount
    }

    这里没有新的内容。但需注意的是,payerowner可能不同 - 你可以为他人的账户支付费用。这可能相当昂贵,因为你将为他们的账户支付“租金”,所以在进行此操作之前,请确保计算好费用。

    最后,铸币功能的实现:

    async function mintTokens(
    connection: web3.Connection,
    payer: web3.Keypair,
    mint: web3.PublicKey,
    destination: web3.PublicKey,
    authority: web3.Keypair,
    amount: number
    ) {
    const mintInfo = await token.getMint(connection, mint)

    const transactionSignature = await token.mintTo(
    connection,
    payer,
    mint,
    destination,
    authority,
    amount * 10 ** mintInfo.decimals
    )

    console.log(
    `铸币交易链接:https://explorer.solana.com/tx/${transactionSignature}?cluster=devnet`
    )
    }

    接下来的部分描述了如何在主函数中调用这些函数,包括创建代币Mint账户、运行程序、查看结果以及在Solana区块链上转移和销毁代币的示例代码。

    让我们在主函数中集成这些功能,以下是我编写的代码:

    async function main() {
    const connection = new web3.Connection(web3.clusterApiUrl("devnet"));
    const user = await initializeKeypair(connection);

    console.log("公钥:", user.publicKey.toBase58());

    const mint = await createNewMint(
    connection,
    user, // 我们支付费用
    user.publicKey, // 我们是铸币权限者
    user.publicKey, // 以及冻结权限者 >:)
    2 // 只有两个小数位!
    );

    const tokenAccount = await createTokenAccount(
    connection,
    user,
    mint,
    user.publicKey // 将我们的地址与代币账户关联
    );

    // 向我们的地址铸造100个代币
    await mintTokens(connection, user, mint, tokenAccount.address, user, 100);
    }

    // ... 其他代码 ...

    执行 npm run start,你应在终端中看到三个浏览器链接被记录下来。注意:请确保你已经安装了@solana/spl-token,否则会出现错误。要安装,请在终端中输入 npm uninstall @solana/spl-tokennpm install @solana/spl-token。保存代币Mint账户地址,稍后将会用到。打开最后一个链接,然后向下滚动到代币余额部分。

    恭喜你,你刚刚铸造了一些代币!这些代币可以代表任何你想要的东西。每个代币价值100美元?100分钟时间?100张猫咪表情包?100片12英寸黄油鸡肉薄饼披萨?这全都由你决定。你是唯一控制铸币账户的人,因此代币供应的价值完全取决于你,无价还是珍贵都可。

    在你开始在Solana区块链上重新塑造现代金融之前,让我们学习如何转移和销毁代币:

    async function transferTokens(
    connection: web3.Connection,
    payer: web3.Keypair,
    source: web3.PublicKey,
    destination: web3.PublicKey,
    owner: web3.PublicKey,
    amount: number,
    mint: web3.PublicKey
    ) {
    const mintInfo = await token.getMint(connection, mint)

    const transactionSignature = await token.transfer(
    connection,
    payer,
    source,
    destination,
    owner,
    amount * 10 ** mintInfo.decimals
    )

    console.log(
    `Transfer Transaction: https://explorer.solana.com/tx/${transactionSignature}?cluster=devnet`
    )
    }

    async function burnTokens(
    connection: web3.Connection,
    payer: web3.Keypair,
    account: web3.PublicKey,
    mint: web3.PublicKey,
    owner: web3.Keypair,
    amount: number
    ) {

    const mintInfo = await token.getMint(connection, mint)

    const transactionSignature = await token.burn(
    connection,
    payer,
    account,
    mint,
    owner,
    amount * 10 ** mintInfo.decimals
    )

    console.log(
    `Burn Transaction: https://explorer.solana.com/tx/${transactionSignature}?cluster=devnet`
    )
    }

    虽然这些函数看起来很长,但其实只是因为我给每个参数都单独占了一行,实际上它们只有三行代码,哈哈。

    调用它们也同样简单:

    async function main() {
    const receiver = web3.Keypair.generate().publicKey

    const receiverTokenAccount = await createTokenAccount(
    connection,
    user,
    mint,
    receiver
    )

    await transferTokens(
    connection,
    user,
    tokenAccount.address,
    receiverTokenAccount.address,
    user.publicKey,
    50,
    mint
    )

    await burnTokens(connection, user, tokenAccount.address, mint, user, 25)
    }

    现在你可以自由玩弄转账功能,向你的钱包地址发送一些代币,看看效果如何。这是我看到的界面:

    嗯...显示为未知?让我们一起来修复这个问题吧!

    - - +
    Skip to main content

    🏧 在Solana上铸造代币

    话不多说,让我们来创造一些神奇的互联网货币吧。在我们的最终项目中,我们将创建一个代币,你将随着抵押你的社区NFT而逐渐获得它。在那之前,让我们先玩一下这个铸币过程的实际构建部分。现在是激发你的想象力,尽情享受的好时机。也许你一直想创建自己的模因币——现在是你的机会了 🚀。

    我们将从一个全新的Solana客户端开始。导航到你的Solana工作区,并运行以下命令:

    npx create-solana-client [name] --initialize-keypair
    cd [name]
    npm i

    请以你想创建的代币来命名你的客户端。我要创建Pizzacoin,因为我昨天吃了一些非常美味的披萨。现在是发挥你的创意的时候了。也许你想将时间本身进行代币化?你可以创建HokageCoin,甚至是TwitterThreadCoin。可能性无穷无尽!

    --initialize-keypair标志位完成了我们上次使用initalizeKeypair进行的所有准备工作。运行npm run start,你将获得一对新的密钥,并获得一些SOL空投。让我们打开货币印刷机,让它开始运作吧。

    图片:美国联邦储备银行行长杰罗姆·鲍威尔让印刷机开始运作。

    🎁 构建代币铸造器

    请按照以下步骤来操作:

    1. 创建一个名为Token Mint的账户
    2. 为特定钱包创建关联的token账户
    3. Mint代币发送到该钱包

    下面是src/index.ts文件中的第一步,可以在导入之后、main()函数之前加入如下代码:

    // 在文件顶部添加spl-token导入
    import * as token from "@solana/spl-token"
    import * as web3 from "@solana/web3.js"

    async function createNewMint(
    connection: web3.Connection,
    payer: web3.Keypair,
    mintAuthority: web3.PublicKey,
    freezeAuthority: web3.PublicKey,
    decimals: number
    ): Promise<web3.PublicKey> {

    const tokenMint = await token.createMint(
    connection,
    payer,
    mintAuthority,
    freezeAuthority,
    decimals
    );

    console.log(`代币铸造账户地址为 ${tokenMint}`)
    console.log(
    `Token Mint: https://explorer.solana.com/address/${tokenMint}?cluster=devnet`
    );

    return tokenMint;
    }

    这段代码应该看起来很熟悉。如果不是,请回头再阅读一遍上一节的内容 😠。

    接下来,我们需要创建关联的令牌账户,在createNewMint函数之后添加以下内容:

    async function createTokenAccount(
    connection: web3.Connection,
    payer: web3.Keypair,
    mint: web3.PublicKey,
    owner: web3.PublicKey
    ) {
    const tokenAccount = await token.getOrCreateAssociatedTokenAccount(
    connection,
    payer,
    mint,
    owner
    )

    console.log(
    `Token Account: https://explorer.solana.com/address/${tokenAccount.address}?cluster=devnet`
    )

    return tokenAccount
    }

    这里没有新的内容。但需注意的是,payerowner可能不同 - 你可以为他人的账户支付费用。这可能相当昂贵,因为你将为他们的账户支付“租金”,所以在进行此操作之前,请确保计算好费用。

    最后,铸币功能的实现:

    async function mintTokens(
    connection: web3.Connection,
    payer: web3.Keypair,
    mint: web3.PublicKey,
    destination: web3.PublicKey,
    authority: web3.Keypair,
    amount: number
    ) {
    const mintInfo = await token.getMint(connection, mint)

    const transactionSignature = await token.mintTo(
    connection,
    payer,
    mint,
    destination,
    authority,
    amount * 10 ** mintInfo.decimals
    )

    console.log(
    `铸币交易链接:https://explorer.solana.com/tx/${transactionSignature}?cluster=devnet`
    )
    }

    接下来的部分描述了如何在主函数中调用这些函数,包括创建代币Mint账户、运行程序、查看结果以及在Solana区块链上转移和销毁代币的示例代码。

    让我们在主函数中集成这些功能,以下是我编写的代码:

    async function main() {
    const connection = new web3.Connection(web3.clusterApiUrl("devnet"));
    const user = await initializeKeypair(connection);

    console.log("公钥:", user.publicKey.toBase58());

    const mint = await createNewMint(
    connection,
    user, // 我们支付费用
    user.publicKey, // 我们是铸币权限者
    user.publicKey, // 以及冻结权限者 >:)
    2 // 只有两个小数位!
    );

    const tokenAccount = await createTokenAccount(
    connection,
    user,
    mint,
    user.publicKey // 将我们的地址与代币账户关联
    );

    // 向我们的地址铸造100个代币
    await mintTokens(connection, user, mint, tokenAccount.address, user, 100);
    }

    // ... 其他代码 ...

    执行 npm run start,你应在终端中看到三个浏览器链接被记录下来。注意:请确保你已经安装了@solana/spl-token,否则会出现错误。要安装,请在终端中输入 npm uninstall @solana/spl-tokennpm install @solana/spl-token。保存代币Mint账户地址,稍后将会用到。打开最后一个链接,然后向下滚动到代币余额部分。

    恭喜你,你刚刚铸造了一些代币!这些代币可以代表任何你想要的东西。每个代币价值100美元?100分钟时间?100张猫咪表情包?100片12英寸黄油鸡肉薄饼披萨?这全都由你决定。你是唯一控制铸币账户的人,因此代币供应的价值完全取决于你,无价还是珍贵都可。

    在你开始在Solana区块链上重新塑造现代金融之前,让我们学习如何转移和销毁代币:

    async function transferTokens(
    connection: web3.Connection,
    payer: web3.Keypair,
    source: web3.PublicKey,
    destination: web3.PublicKey,
    owner: web3.PublicKey,
    amount: number,
    mint: web3.PublicKey
    ) {
    const mintInfo = await token.getMint(connection, mint)

    const transactionSignature = await token.transfer(
    connection,
    payer,
    source,
    destination,
    owner,
    amount * 10 ** mintInfo.decimals
    )

    console.log(
    `Transfer Transaction: https://explorer.solana.com/tx/${transactionSignature}?cluster=devnet`
    )
    }

    async function burnTokens(
    connection: web3.Connection,
    payer: web3.Keypair,
    account: web3.PublicKey,
    mint: web3.PublicKey,
    owner: web3.Keypair,
    amount: number
    ) {

    const mintInfo = await token.getMint(connection, mint)

    const transactionSignature = await token.burn(
    connection,
    payer,
    account,
    mint,
    owner,
    amount * 10 ** mintInfo.decimals
    )

    console.log(
    `Burn Transaction: https://explorer.solana.com/tx/${transactionSignature}?cluster=devnet`
    )
    }

    虽然这些函数看起来很长,但其实只是因为我给每个参数都单独占了一行,实际上它们只有三行代码,哈哈。

    调用它们也同样简单:

    async function main() {
    const receiver = web3.Keypair.generate().publicKey

    const receiverTokenAccount = await createTokenAccount(
    connection,
    user,
    mint,
    receiver
    )

    await transferTokens(
    connection,
    user,
    tokenAccount.address,
    receiverTokenAccount.address,
    user.publicKey,
    50,
    mint
    )

    await burnTokens(connection, user, tokenAccount.address, mint, user, 25)
    }

    现在你可以自由玩弄转账功能,向你的钱包地址发送一些代币,看看效果如何。这是我看到的界面:

    嗯...显示为未知?让我们一起来修复这个问题吧!

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module2/spl-token/the-token-program/index.html b/Solana-Co-Learn/module2/spl-token/the-token-program/index.html index 5749c5b38..23d95f3fe 100644 --- a/Solana-Co-Learn/module2/spl-token/the-token-program/index.html +++ b/Solana-Co-Learn/module2/spl-token/the-token-program/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    💵 Token Program

    作为区块链最基本的承诺,这些代币也许是你安装钱包的主要原因,它们是区块链上资产最纯粹的表现形式,从合成股票到数百种狗币。

    本课将主要讲解在Solana上代币是如何工作的。如果你对其他区块链有所了解,可能会发现这里存在一些差异,所以尽量不要与你当前对代币的理解联系起来。

    探讨Solana中代币的工作原理也是一个深入了解不同程序如何使用账户的绝佳机会。你越深入了解Solana,就越能意识到账户的重要性。虽然它们在文件系统中的抽象和灵活性与文件类似,但这也意味着任何特定程序上的账户可能变得相当复杂!初开始可能会感到混乱,但随着时间的推移,它会变得更加清晰。

    Solana上的代币是通过Solana Token Program来创建和管理的,它是Solana Program Library(SPL)的一部分程序。常规代币和非同质化代币(NFTs)都属于Solana程序库中的代币类型。今天我们不会涉及NFTs,但不要担心,我们会很快介绍。

    🗃 账户关系

    首先,我们要了解一下整体情况。Token Program需要三个必要的账户:

    • Wallet Account - 就是你的钱包!
    • Mint Account - 存储关于代币铸造的元数据
    • Token Account - 与钱包关联,存储有关特定钱包的信息,例如它持有多少代币。

    现在让我们深入了解每个账户,看看它们的内部情况。

    🌌 铸币账户(Mint Account)

    铸币账户存储关于代币本身的元数据,不是关于你对代币的所有权,而是涉及代币的更广泛内容。它具有以下属性:

    • mint authority - 只有一个账户可以从铸币账户签名并铸造代币。创建铸币账户时,必须指定铸币权限,可以是你的个人钱包或其他程序。
    • supply - 存在的总代币数量。“supply”基本上是在说:“码农大神,你好!这是发行的总代币数量。”
    • decimals - 这是我们允许代币被分割的小数位数,即代币的精度。这可能会有些棘手,因为实际上链上没有小数。整个供应量表示为整数,所以你必须进行数学计算以在小数之间进行转换。例如,如果你将小数位数设置为两位,而你的供应量是一百,那么实际上你只有一个代币。供应量中只有一个代币,但你允许它被分割成较小的面额。
    • Is Initialized - 基本上指的是该账户是否已准备就绪。这与账户本身有关,而不是代币程序
    • Freeze authority - 冻结权限与mint authority类似,表示一个人或程序具有冻结(或铸造)代币的权限。

    将铸币权限设置为你的钱包是一种相当标准的做法。你可以铸造任何你想要的代币,然后撤销铸币权限,基本上意味着未来不会再有更多的供应量。或者,如果你有某种动态发行代币的方式,常见的做法是将其放入权限中,作为一个程序来管理代币的铸造。

    冻结权限的工作方式相同。

    👛 代币账户(Token Account)

    你可能已经注意到了生态系统中流通的众多不同代币。你的钱包里现在可能充满了各种各样的代币。但是,网络是如何识别你持有某些代币的呢?答案就在于存储这些数据的账户!最好的方式就是通过关联代币账户来实现。下面这张图可以帮助你理解:

    这就是数据关系和账户属性的示例图。

    Token账户必须与用户或钱包关联。一种简便的方式是创建一个PDA(程序衍生地址),其地址将铸币账户和钱包连接在一起。令牌账户PDA的种子由铸币账户的地址和钱包地址组成(其中令牌程序ID是默认存在的)。

    虽然包括了很多不同的信息,但现在你只需要理解的是,你的钱包并不直接持有具体的代币。它与一个存储了代币数量的关联账户有关。此外,还有一个铸币账户,存储了有关所有代币和铸币的更广泛信息。

    请花些时间仔细研究这个图表,对不明白的部分进行搜索(例如关联的令牌程序究竟是什么?)。处理了所有复杂的部分后,这一切将变得非常简单!

    🤑 代币铸造过程

    别再盯着图表看了,让我们来深入一些代码,了解这一切是如何实现的。

    要创建一个新的SPL-Token,首先必须创建一个Token Mint(一个存储该特定代币相关数据的账户)。

    你可以将这个过程想象为制作比萨饼。你需要食谱(关于代币的数据)、食材(铸币账户和钱包地址),还有一个人将它们组合在一起(派生一个新的PDA)。就像制作比萨一样,只要你有正确的食材并按照食谱操作,最终你就能得到一枚美味的新代币!

    由于Token ProgramSPL的一部分,你可以使用 @solana/spl-token TypeScript SDK相当轻松地进行创建交易。

    下面是一个createMint的示例:


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

    // 生成token 的 account地址,此项为可选项
    const newToken = web3.Keypair.generate();
    const tokenMint = await createMint(
    connection,
    payer,
    mintAuthority,
    freezeAuthority,
    decimals,
    newToken,
    null,
    TOKEN_PROGRAM_ID
    )

    你需要以下参数:

    • connection - 与集群的JSON-RPC连接
    • payer - 付款方交易的公钥
    • mintAuthority - 被授权铸造新代币的账户
    • freezeAuthority - 被授权冻结代币的账户。如果你不想冻结代币,请将其设置为null
    • decimals - 指定代币所需的小数精度

    可选参数:

    • newToken 生成token 的 account地址,为空,将默认生成一个
    • null 为 confirmOptions ,按照默认即可
    • TOKEN_PROGRAM_ID token 程序的ID

    完成这个步骤后,你可以继续以下步骤:

    • 创建关联的Token账户
    • 将代币铸造到某个账户中
    • 如果你想通过转账功能空投到多个账户

    你在 @solana/spl-token SDK中需要的一切都已准备好。如果你对某个具体部分感兴趣,可以查看文档

    在大多数情况下,@solana/spl-token SDK就足够了,你无需自己创建原始交易。

    此外,一个有趣的附注——如果出于某种原因,你想要在创建铸币指令的同时创建另一个指令,你可以自己创建这些指令,并将它们组合成一个事务,以确保所有操作都是原子性的。也许你正在构建一个高度保密的代币程序,你希望在铸币后立即锁定所有代币,使没有人能够转移它们。

    不用说,围绕这些代币发生了许多有趣的事情。你可以在此处了解每个功能背后的工作原理,甚至还可以查看一些关于销毁代币等的说明。:)

    参考资料

    - - +
    Skip to main content

    💵 Token Program

    作为区块链最基本的承诺,这些代币也许是你安装钱包的主要原因,它们是区块链上资产最纯粹的表现形式,从合成股票到数百种狗币。

    本课将主要讲解在Solana上代币是如何工作的。如果你对其他区块链有所了解,可能会发现这里存在一些差异,所以尽量不要与你当前对代币的理解联系起来。

    探讨Solana中代币的工作原理也是一个深入了解不同程序如何使用账户的绝佳机会。你越深入了解Solana,就越能意识到账户的重要性。虽然它们在文件系统中的抽象和灵活性与文件类似,但这也意味着任何特定程序上的账户可能变得相当复杂!初开始可能会感到混乱,但随着时间的推移,它会变得更加清晰。

    Solana上的代币是通过Solana Token Program来创建和管理的,它是Solana Program Library(SPL)的一部分程序。常规代币和非同质化代币(NFTs)都属于Solana程序库中的代币类型。今天我们不会涉及NFTs,但不要担心,我们会很快介绍。

    🗃 账户关系

    首先,我们要了解一下整体情况。Token Program需要三个必要的账户:

    • Wallet Account - 就是你的钱包!
    • Mint Account - 存储关于代币铸造的元数据
    • Token Account - 与钱包关联,存储有关特定钱包的信息,例如它持有多少代币。

    现在让我们深入了解每个账户,看看它们的内部情况。

    🌌 铸币账户(Mint Account)

    铸币账户存储关于代币本身的元数据,不是关于你对代币的所有权,而是涉及代币的更广泛内容。它具有以下属性:

    • mint authority - 只有一个账户可以从铸币账户签名并铸造代币。创建铸币账户时,必须指定铸币权限,可以是你的个人钱包或其他程序。
    • supply - 存在的总代币数量。“supply”基本上是在说:“码农大神,你好!这是发行的总代币数量。”
    • decimals - 这是我们允许代币被分割的小数位数,即代币的精度。这可能会有些棘手,因为实际上链上没有小数。整个供应量表示为整数,所以你必须进行数学计算以在小数之间进行转换。例如,如果你将小数位数设置为两位,而你的供应量是一百,那么实际上你只有一个代币。供应量中只有一个代币,但你允许它被分割成较小的面额。
    • Is Initialized - 基本上指的是该账户是否已准备就绪。这与账户本身有关,而不是代币程序
    • Freeze authority - 冻结权限与mint authority类似,表示一个人或程序具有冻结(或铸造)代币的权限。

    将铸币权限设置为你的钱包是一种相当标准的做法。你可以铸造任何你想要的代币,然后撤销铸币权限,基本上意味着未来不会再有更多的供应量。或者,如果你有某种动态发行代币的方式,常见的做法是将其放入权限中,作为一个程序来管理代币的铸造。

    冻结权限的工作方式相同。

    👛 代币账户(Token Account)

    你可能已经注意到了生态系统中流通的众多不同代币。你的钱包里现在可能充满了各种各样的代币。但是,网络是如何识别你持有某些代币的呢?答案就在于存储这些数据的账户!最好的方式就是通过关联代币账户来实现。下面这张图可以帮助你理解:

    这就是数据关系和账户属性的示例图。

    Token账户必须与用户或钱包关联。一种简便的方式是创建一个PDA(程序衍生地址),其地址将铸币账户和钱包连接在一起。令牌账户PDA的种子由铸币账户的地址和钱包地址组成(其中令牌程序ID是默认存在的)。

    虽然包括了很多不同的信息,但现在你只需要理解的是,你的钱包并不直接持有具体的代币。它与一个存储了代币数量的关联账户有关。此外,还有一个铸币账户,存储了有关所有代币和铸币的更广泛信息。

    请花些时间仔细研究这个图表,对不明白的部分进行搜索(例如关联的令牌程序究竟是什么?)。处理了所有复杂的部分后,这一切将变得非常简单!

    🤑 代币铸造过程

    别再盯着图表看了,让我们来深入一些代码,了解这一切是如何实现的。

    要创建一个新的SPL-Token,首先必须创建一个Token Mint(一个存储该特定代币相关数据的账户)。

    你可以将这个过程想象为制作比萨饼。你需要食谱(关于代币的数据)、食材(铸币账户和钱包地址),还有一个人将它们组合在一起(派生一个新的PDA)。就像制作比萨一样,只要你有正确的食材并按照食谱操作,最终你就能得到一枚美味的新代币!

    由于Token ProgramSPL的一部分,你可以使用 @solana/spl-token TypeScript SDK相当轻松地进行创建交易。

    下面是一个createMint的示例:


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

    // 生成token 的 account地址,此项为可选项
    const newToken = web3.Keypair.generate();
    const tokenMint = await createMint(
    connection,
    payer,
    mintAuthority,
    freezeAuthority,
    decimals,
    newToken,
    null,
    TOKEN_PROGRAM_ID
    )

    你需要以下参数:

    • connection - 与集群的JSON-RPC连接
    • payer - 付款方交易的公钥
    • mintAuthority - 被授权铸造新代币的账户
    • freezeAuthority - 被授权冻结代币的账户。如果你不想冻结代币,请将其设置为null
    • decimals - 指定代币所需的小数精度

    可选参数:

    • newToken 生成token 的 account地址,为空,将默认生成一个
    • null 为 confirmOptions ,按照默认即可
    • TOKEN_PROGRAM_ID token 程序的ID

    完成这个步骤后,你可以继续以下步骤:

    • 创建关联的Token账户
    • 将代币铸造到某个账户中
    • 如果你想通过转账功能空投到多个账户

    你在 @solana/spl-token SDK中需要的一切都已准备好。如果你对某个具体部分感兴趣,可以查看文档

    在大多数情况下,@solana/spl-token SDK就足够了,你无需自己创建原始交易。

    此外,一个有趣的附注——如果出于某种原因,你想要在创建铸币指令的同时创建另一个指令,你可以自己创建这些指令,并将它们组合成一个事务,以确保所有操作都是原子性的。也许你正在构建一个高度保密的代币程序,你希望在铸币后立即锁定所有代币,使没有人能够转移它们。

    不用说,围绕这些代币发生了许多有趣的事情。你可以在此处了解每个功能背后的工作原理,甚至还可以查看一些关于销毁代币等的说明。:)

    参考资料

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module2/spl-token/token-metadata/index.html b/Solana-Co-Learn/module2/spl-token/token-metadata/index.html index aab8a54c6..9e474b18b 100644 --- a/Solana-Co-Learn/module2/spl-token/token-metadata/index.html +++ b/Solana-Co-Learn/module2/spl-token/token-metadata/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🧮 令牌元数据

    Token元数据指的是代币的基本信息,例如名称、符号和标志。注意你钱包中的各种代币都拥有这些特性,除了你自己创建的代币。

    这就是所有的元数据!这适用于所有代币,不仅仅是可替代代币。在Solana上,NFT与任何其他代币无异,只不过通过属性(例如小数位)来定义它们作为NFT

    所有这些都是通过Token Metadata Program实现的 - 这是处理Solana区块链上的TokenNFT时最重要的程序之一。它的主要任务是将额外数据附加到Solana上的可替代或不可替代Token。它使用从Mint账户地址派生的程序派生地址(PDAs)来实现这个目标。

    🎭 令牌元数据账户

    一旦我们制造了一个闪亮的新代币,我们需要使其更具吸引力。我们将使用Token Metadata Program来完成这个任务,以下是生成的账户样式:

    这被称为元数据账户。它能存储有关特定代币铸造账户的各种信息。你会注意到一个 URI(统一资源标识符)属性 - 它指向链下的一个JSON文件,主要用于非同质化代币(NFT)。由于链下部分不受链上费用限制,你可以存储高质量图形和其他大型数据对象。

    元数据账户有许多属性,你不需要了解其中大部分。我们将在需要时深入探讨相关部分。现在,我们只关心链下部分,这是我们制作Pizzacoin所需的第一步。

    🖼 代币标准

    链下部分遵循Metaplex代币标准,基本上是一种格式,你需要按照这个格式来实现不同类型的代币元数据。我们在元数据账户链上部分的 Token Standard 字段中通知网络上的所有应用程序我们的代币类型。我们的选择有:

    • NonFungible:带有主版本的非同质化代币(NFTs)。
    • FungibleAsset:具有元数据和属性的代币,有时也被称为半可替代代币(例如游戏物品)。
    • Fungible:具有简单元数据的代币(如USDC或SOL这样的常规代币)。
    • NonFungibleEdition:一个具有Edition账户的非同质化代币(从主版中打印出来的,例如100个中的1个)。

    Metaplex Token标准在整个行业中得到了广泛接受。各种应用程序、交易所和钱包都希望令牌符合此标准。Token标准由Token元数据程序自动设置,不能手动更新。以下是它如何决定应用正确的标准:

    • 如果令牌拥有主版本账户,则为 NonFungible
    • 如果令牌具有Edition账户,则为 NonFungibleEdition
    • 如果代币没有(主)版账户(确保其供应量大于1)并且使用零位小数,那么它是 FungibleAsset
    • 如果代币没有(主)版账户(确保其供应量大于1)并且使用至少一位小数,那么它是 Fungible

    你现在可以忽略“Master Edition”是什么意思,Pizzacoin是完全可替代的,因此我们将专注于可替代代币。

    🧰 Metaplex SDK

    欢迎接触Solana上其中一款极为实用的SDK——Metaplex SDK。如果你之前在Solana上铸造过NFT,那么你很可能在毫不知情的情况下已经使用过Metaplex SDK了。我们将利用 @metaplex-foundation/js@metaplex-foundation/mpl-token-metadata 库来创建和我们的代币铸造项目相关联的元数据账户。现在是时候赋予Pizzacoin独一无二的身份了。

    我们首先将着手于链下部分,准备好后,再继续创建代币元数据账户。

    一般的操作流程如下:

    1. 安装Metaplex SDK——你可能会使用现有的密钥对。
    2. 上传一个图像作为代币标志——我们会使用本地文件,但SDK也支持直接从浏览器上传。
    3. 上传链下元数据(包括你上传的图像的URI),这样你就可以开始进行链上操作了。
    4. 生成元数据账户的程序派生地址(PDA,俗称“蛋”)。
    5. 创建链上的Token元数据账户,包括各种指令、交易等。

    感觉有些不确定吗?别担心,我们接下来会通过一些代码来帮助你消除疑虑 🤺。

    - - +
    Skip to main content

    🧮 令牌元数据

    Token元数据指的是代币的基本信息,例如名称、符号和标志。注意你钱包中的各种代币都拥有这些特性,除了你自己创建的代币。

    这就是所有的元数据!这适用于所有代币,不仅仅是可替代代币。在Solana上,NFT与任何其他代币无异,只不过通过属性(例如小数位)来定义它们作为NFT

    所有这些都是通过Token Metadata Program实现的 - 这是处理Solana区块链上的TokenNFT时最重要的程序之一。它的主要任务是将额外数据附加到Solana上的可替代或不可替代Token。它使用从Mint账户地址派生的程序派生地址(PDAs)来实现这个目标。

    🎭 令牌元数据账户

    一旦我们制造了一个闪亮的新代币,我们需要使其更具吸引力。我们将使用Token Metadata Program来完成这个任务,以下是生成的账户样式:

    这被称为元数据账户。它能存储有关特定代币铸造账户的各种信息。你会注意到一个 URI(统一资源标识符)属性 - 它指向链下的一个JSON文件,主要用于非同质化代币(NFT)。由于链下部分不受链上费用限制,你可以存储高质量图形和其他大型数据对象。

    元数据账户有许多属性,你不需要了解其中大部分。我们将在需要时深入探讨相关部分。现在,我们只关心链下部分,这是我们制作Pizzacoin所需的第一步。

    🖼 代币标准

    链下部分遵循Metaplex代币标准,基本上是一种格式,你需要按照这个格式来实现不同类型的代币元数据。我们在元数据账户链上部分的 Token Standard 字段中通知网络上的所有应用程序我们的代币类型。我们的选择有:

    • NonFungible:带有主版本的非同质化代币(NFTs)。
    • FungibleAsset:具有元数据和属性的代币,有时也被称为半可替代代币(例如游戏物品)。
    • Fungible:具有简单元数据的代币(如USDC或SOL这样的常规代币)。
    • NonFungibleEdition:一个具有Edition账户的非同质化代币(从主版中打印出来的,例如100个中的1个)。

    Metaplex Token标准在整个行业中得到了广泛接受。各种应用程序、交易所和钱包都希望令牌符合此标准。Token标准由Token元数据程序自动设置,不能手动更新。以下是它如何决定应用正确的标准:

    • 如果令牌拥有主版本账户,则为 NonFungible
    • 如果令牌具有Edition账户,则为 NonFungibleEdition
    • 如果代币没有(主)版账户(确保其供应量大于1)并且使用零位小数,那么它是 FungibleAsset
    • 如果代币没有(主)版账户(确保其供应量大于1)并且使用至少一位小数,那么它是 Fungible

    你现在可以忽略“Master Edition”是什么意思,Pizzacoin是完全可替代的,因此我们将专注于可替代代币。

    🧰 Metaplex SDK

    欢迎接触Solana上其中一款极为实用的SDK——Metaplex SDK。如果你之前在Solana上铸造过NFT,那么你很可能在毫不知情的情况下已经使用过Metaplex SDK了。我们将利用 @metaplex-foundation/js@metaplex-foundation/mpl-token-metadata 库来创建和我们的代币铸造项目相关联的元数据账户。现在是时候赋予Pizzacoin独一无二的身份了。

    我们首先将着手于链下部分,准备好后,再继续创建代币元数据账户。

    一般的操作流程如下:

    1. 安装Metaplex SDK——你可能会使用现有的密钥对。
    2. 上传一个图像作为代币标志——我们会使用本地文件,但SDK也支持直接从浏览器上传。
    3. 上传链下元数据(包括你上传的图像的URI),这样你就可以开始进行链上操作了。
    4. 生成元数据账户的程序派生地址(PDA,俗称“蛋”)。
    5. 创建链上的Token元数据账户,包括各种指令、交易等。

    感觉有些不确定吗?别担心,我们接下来会通过一些代码来帮助你消除疑虑 🤺。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module3/index.html b/Solana-Co-Learn/module3/index.html index 5091eb41b..9993547a9 100644 --- a/Solana-Co-Learn/module3/index.html +++ b/Solana-Co-Learn/module3/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module3/introduction-to-rust/hello-world/index.html b/Solana-Co-Learn/module3/introduction-to-rust/hello-world/index.html index 01bebce17..a45c21e5a 100644 --- a/Solana-Co-Learn/module3/introduction-to-rust/hello-world/index.html +++ b/Solana-Co-Learn/module3/introduction-to-rust/hello-world/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    📝 你好,世界

    我们将在游乐场上制作一个简单的Hello World程序,这个程序只会在交易日志中记录一条消息,相当有趣。

    📦 Rust模块系统

    Solana上编写程序,我们会像对待客户一样用到一系列的库,免去了大量手动编写样板代码的麻烦。Rust采用一种被称为“模块系统”的方式来组织代码,它和Node.js中的模块或C++中的命名空间有许多相似之处。

    下面是一个直观的可视化展示:

    这个系统的三个部分包括:

    • package - 这是一个包含一组crate和用于指定元数据及包之间依赖关系的清单文件的包。你可以将其理解为Node.js中的 package.json
    • crate(木箱) - 一个crate可以是一个库或一个可执行程序。crate的源代码通常会被细分为多个模块。这就像一个Node.js模块。
    • module - 模块用于将代码分割成逻辑单元,为组织、作用域和路径的隐私提供了独立的命名空间。它们基本上是单独的文件和文件夹。

    🛣 路径和范围

    Crate模块可以在项目中被重复使用,就像你可以在React中重用组件和在Node中重用模块一样。比较棘手的是我们需要知道它们的路径才能引用它们。

    crate结构想象成一棵树,其中crate是树的基础,模块是树枝,每个模块可能有子模块或额外的分支。

    我们所需的其中一个事物是AccountInfo子模块中的account_info结构体,以下是它的样子:

    struct是一种自定义数据类型。把它想象成一种自定义的原始数据类型,就像字符串或整数一样,但与只存储一个值的原始类型不同,struct可以包含多个值。

    Rust中, :: 用作路径分隔符,就像 ./ 一样。因此,要引用 AccountInfo 结构体,我们可以这样写:

    use solana_program::account_info::AccountInfo;

    这里的层次结构是:

    • 基础cratesolana_program
    • solana_program 包含一个名为 account_info 的模块
    • account_info 包含一个名为 AccountInfo 的结构体

    Rust文件的顶部常常会有一系列的 use 命令,这与 importrequire 语句相似。

    我们还需要一些其他项目。我们可以使用花括号从单个模块中引入多个项目,这有点像JS中的解构。

    use solana_program::{
    account_info::AccountInfo,
    entrypoint,
    entrypoint::ProgramResult,
    pubkey::Pubkey,
    msg
    };

    到目前为止,一切都很简单明了。 AccountInfo 结构体是Solana账户数据的通用描述符,它定义了账户应具备的所有属性。

    如果你以前从未使用过像TypeScriptJava这样的静态类型语言,你可能会想知道为什么我们要导入像 PubKeyAccountInfo 这样的“数据类型”。简言之,在Rust中,我们需要在声明变量时定义其类型。这有助于我们在编译或运行代码之前捕捉错误。因此,当程序在区块链上执行交易时,它不会崩溃,而是在开发过程中出错,这样你就可以更快地准备好可运行的代码。

    我会在需要的时候介绍剩下的这些项目。现在让我们继续前进!

    🏁 Solana程序入口

    回想一下我们的TypeScript客户端。我们在 index.tsmain 中有一个函数,它是我们脚本的入口点。同样的,Rust脚本也是这样的方式!但是,我们不仅仅是编写任何Rust脚本,我们正在编写一个将在Solana上运行的脚本。

    这就是我们第二个 use 语句的用途 —— 它引入了 entrypoint! 宏:一种特殊类型的 main 函数,Solana将用它来执行我们的指令。

    宏就像是代码的快捷方式 —— 它们是一种可以编写代码的代码。在编译时,entrypoint!(process_instruction); 将展开为一堆样板代码,有点像模板。你不必深入了解宏的工作原理,但你可以在这里阅读更多相关信息。

    我们的入口函数将调用 process_instruction,所以这是我们的 lib.rs 文件目前应该的样子:

    use solana_program::{
    account_info::AccountInfo,
    entrypoint,
    entrypoint::ProgramResult,
    pubkey::Pubkey,
    msg
    };

    entrypoint!(process_instruction);

    接下来是关于 process_instruction 函数的部分。

    🔨 Rust中的函数

    Rust中的 fnTypescript中的function非常相似 —— 它们只需要参数、类型和返回类型。在 entrypoint! 宏下面添加以下内容:

    pub fn process_instruction(
    // 参数及其类型
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
    // 返回类型(即函数返回的数据类型)
    ) -> ProgramResult{
    // 暂时留空函数体
    }

    我们的 process_instruction 函数需要以下参数:

    • program_id:程序账户的公钥。用于验证程序是否由正确的账户调用。类型为 &Pubkey
    • accounts:指令所涉及的账户。必须为类型 &[AccountInfo]
    • instruction_data:我们交易中的8位指令数据。必须为 &[u8] 类型。

    其中 [] 表示 AccountInfou8 是“切片”类型,类似于长度未知的数组。我们不称它们为数组,因为它们更底层 —— 在Rust中,切片是指向一块内存块的指针 🤯。

    稍后我们会讨论 & :)

    📜 Result 枚举

    现在是时候介绍我们的第三个 use 语句 - ProgramResult 了。这是一个Rust枚举,代表了Solana程序执行的结果。

    现在试着点击左侧栏上的“构建”按钮来编译脚本。你应该会得到一个警告和一个错误,错误信息是:

    error[E0308]: mismatched types
    --> /src/lib.rs:12:6
    |
    7 | pub fn process_instruction(
    | ------------------- implicitly returns `()` as its body has no tail or `return` expression
    ...
    12 | ) -> ProgramResult {
    | ^^^^^^^^^^^^^ expected enum `Result`, found `()`
    |
    = note: expected enum `Result<(), ProgramError>`
    found unit type `()`

    我想花点时间欣赏一下Rust错误信息的精确性。它准确地告诉你出了什么问题,问题在哪里,以及如何修复。如果JavaScript也这样友好,我会少失去多少头发呢 😢。

    由于我们的函数体为空,它会隐式地返回 () - 空元组。错误消息表示它期望 Result,但我们声明的返回类型是 ProgramResult。嗯,这里发生了什么呢🤔?

    这是因为SolanaProgramResult 类型是用Rust的 Result 类型实现的:

    pub type ProgramResult = Result<(), ProgramError>;

    Result 是一个标准库类型,代表两个离散的结果:

    • 成功( Ok
    • 失败 ( Err )
    pub enum Result<T, E> {
    Ok(T),
    Err(E),
    }

    把它理解成HTTP错误代码也许会更直观——200代表 Ok404代表 Err。因此,当我们返回 ProgramResult 时,实际上是在表示我们的函数可以返回 ()(一个空值)以示成功,或者通过自定义的 ProgramError 枚举来告知出现了何种问题。非常实用!

    我们所需要做的就是:

    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
    ) -> ProgramResult{
    // 成功时返回 Ok()
    Ok(())
    }

    🚀 部署你的首个程序

    我们的程序现在已经接近完成了!唯一缺失的部分就是实际上输出“Hello World”,我们可以通过使用 msg! 宏来实现这一目的。由于我们暂时不会对指令数据进行任何操作,为了避免出现“未使用的变量”警告,只需在变量名前加上下划线即可。

    以下是 process_instruction 函数的完整样式:

    pub fn process_instruction(
    _program_id: &Pubkey,
    _accounts: &[AccountInfo],
    _instruction_data: &[u8]
    ) -> ProgramResult{
    msg!("Hello World!");
    Ok(())
    }

    如果你点击构建,控制台上应该会出现绿色的“构建成功”消息。恭喜你!你已经成功编写了你的第一个Solana程序🎉。

    在这个在线环境中部署程序非常便捷。只需切换到左上角的“构建和部署”选项卡,在“资源管理器”图标下方,点击“部署”按钮即可。

    - - +
    Skip to main content

    📝 你好,世界

    我们将在游乐场上制作一个简单的Hello World程序,这个程序只会在交易日志中记录一条消息,相当有趣。

    📦 Rust模块系统

    Solana上编写程序,我们会像对待客户一样用到一系列的库,免去了大量手动编写样板代码的麻烦。Rust采用一种被称为“模块系统”的方式来组织代码,它和Node.js中的模块或C++中的命名空间有许多相似之处。

    下面是一个直观的可视化展示:

    这个系统的三个部分包括:

    • package - 这是一个包含一组crate和用于指定元数据及包之间依赖关系的清单文件的包。你可以将其理解为Node.js中的 package.json
    • crate(木箱) - 一个crate可以是一个库或一个可执行程序。crate的源代码通常会被细分为多个模块。这就像一个Node.js模块。
    • module - 模块用于将代码分割成逻辑单元,为组织、作用域和路径的隐私提供了独立的命名空间。它们基本上是单独的文件和文件夹。

    🛣 路径和范围

    Crate模块可以在项目中被重复使用,就像你可以在React中重用组件和在Node中重用模块一样。比较棘手的是我们需要知道它们的路径才能引用它们。

    crate结构想象成一棵树,其中crate是树的基础,模块是树枝,每个模块可能有子模块或额外的分支。

    我们所需的其中一个事物是AccountInfo子模块中的account_info结构体,以下是它的样子:

    struct是一种自定义数据类型。把它想象成一种自定义的原始数据类型,就像字符串或整数一样,但与只存储一个值的原始类型不同,struct可以包含多个值。

    Rust中, :: 用作路径分隔符,就像 ./ 一样。因此,要引用 AccountInfo 结构体,我们可以这样写:

    use solana_program::account_info::AccountInfo;

    这里的层次结构是:

    • 基础cratesolana_program
    • solana_program 包含一个名为 account_info 的模块
    • account_info 包含一个名为 AccountInfo 的结构体

    Rust文件的顶部常常会有一系列的 use 命令,这与 importrequire 语句相似。

    我们还需要一些其他项目。我们可以使用花括号从单个模块中引入多个项目,这有点像JS中的解构。

    use solana_program::{
    account_info::AccountInfo,
    entrypoint,
    entrypoint::ProgramResult,
    pubkey::Pubkey,
    msg
    };

    到目前为止,一切都很简单明了。 AccountInfo 结构体是Solana账户数据的通用描述符,它定义了账户应具备的所有属性。

    如果你以前从未使用过像TypeScriptJava这样的静态类型语言,你可能会想知道为什么我们要导入像 PubKeyAccountInfo 这样的“数据类型”。简言之,在Rust中,我们需要在声明变量时定义其类型。这有助于我们在编译或运行代码之前捕捉错误。因此,当程序在区块链上执行交易时,它不会崩溃,而是在开发过程中出错,这样你就可以更快地准备好可运行的代码。

    我会在需要的时候介绍剩下的这些项目。现在让我们继续前进!

    🏁 Solana程序入口

    回想一下我们的TypeScript客户端。我们在 index.tsmain 中有一个函数,它是我们脚本的入口点。同样的,Rust脚本也是这样的方式!但是,我们不仅仅是编写任何Rust脚本,我们正在编写一个将在Solana上运行的脚本。

    这就是我们第二个 use 语句的用途 —— 它引入了 entrypoint! 宏:一种特殊类型的 main 函数,Solana将用它来执行我们的指令。

    宏就像是代码的快捷方式 —— 它们是一种可以编写代码的代码。在编译时,entrypoint!(process_instruction); 将展开为一堆样板代码,有点像模板。你不必深入了解宏的工作原理,但你可以在这里阅读更多相关信息。

    我们的入口函数将调用 process_instruction,所以这是我们的 lib.rs 文件目前应该的样子:

    use solana_program::{
    account_info::AccountInfo,
    entrypoint,
    entrypoint::ProgramResult,
    pubkey::Pubkey,
    msg
    };

    entrypoint!(process_instruction);

    接下来是关于 process_instruction 函数的部分。

    🔨 Rust中的函数

    Rust中的 fnTypescript中的function非常相似 —— 它们只需要参数、类型和返回类型。在 entrypoint! 宏下面添加以下内容:

    pub fn process_instruction(
    // 参数及其类型
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
    // 返回类型(即函数返回的数据类型)
    ) -> ProgramResult{
    // 暂时留空函数体
    }

    我们的 process_instruction 函数需要以下参数:

    • program_id:程序账户的公钥。用于验证程序是否由正确的账户调用。类型为 &Pubkey
    • accounts:指令所涉及的账户。必须为类型 &[AccountInfo]
    • instruction_data:我们交易中的8位指令数据。必须为 &[u8] 类型。

    其中 [] 表示 AccountInfou8 是“切片”类型,类似于长度未知的数组。我们不称它们为数组,因为它们更底层 —— 在Rust中,切片是指向一块内存块的指针 🤯。

    稍后我们会讨论 & :)

    📜 Result 枚举

    现在是时候介绍我们的第三个 use 语句 - ProgramResult 了。这是一个Rust枚举,代表了Solana程序执行的结果。

    现在试着点击左侧栏上的“构建”按钮来编译脚本。你应该会得到一个警告和一个错误,错误信息是:

    error[E0308]: mismatched types
    --> /src/lib.rs:12:6
    |
    7 | pub fn process_instruction(
    | ------------------- implicitly returns `()` as its body has no tail or `return` expression
    ...
    12 | ) -> ProgramResult {
    | ^^^^^^^^^^^^^ expected enum `Result`, found `()`
    |
    = note: expected enum `Result<(), ProgramError>`
    found unit type `()`

    我想花点时间欣赏一下Rust错误信息的精确性。它准确地告诉你出了什么问题,问题在哪里,以及如何修复。如果JavaScript也这样友好,我会少失去多少头发呢 😢。

    由于我们的函数体为空,它会隐式地返回 () - 空元组。错误消息表示它期望 Result,但我们声明的返回类型是 ProgramResult。嗯,这里发生了什么呢🤔?

    这是因为SolanaProgramResult 类型是用Rust的 Result 类型实现的:

    pub type ProgramResult = Result<(), ProgramError>;

    Result 是一个标准库类型,代表两个离散的结果:

    • 成功( Ok
    • 失败 ( Err )
    pub enum Result<T, E> {
    Ok(T),
    Err(E),
    }

    把它理解成HTTP错误代码也许会更直观——200代表 Ok404代表 Err。因此,当我们返回 ProgramResult 时,实际上是在表示我们的函数可以返回 ()(一个空值)以示成功,或者通过自定义的 ProgramError 枚举来告知出现了何种问题。非常实用!

    我们所需要做的就是:

    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
    ) -> ProgramResult{
    // 成功时返回 Ok()
    Ok(())
    }

    🚀 部署你的首个程序

    我们的程序现在已经接近完成了!唯一缺失的部分就是实际上输出“Hello World”,我们可以通过使用 msg! 宏来实现这一目的。由于我们暂时不会对指令数据进行任何操作,为了避免出现“未使用的变量”警告,只需在变量名前加上下划线即可。

    以下是 process_instruction 函数的完整样式:

    pub fn process_instruction(
    _program_id: &Pubkey,
    _accounts: &[AccountInfo],
    _instruction_data: &[u8]
    ) -> ProgramResult{
    msg!("Hello World!");
    Ok(())
    }

    如果你点击构建,控制台上应该会出现绿色的“构建成功”消息。恭喜你!你已经成功编写了你的第一个Solana程序🎉。

    在这个在线环境中部署程序非常便捷。只需切换到左上角的“构建和部署”选项卡,在“资源管理器”图标下方,点击“部署”按钮即可。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module3/introduction-to-rust/index.html b/Solana-Co-Learn/module3/introduction-to-rust/index.html index 85eb484ab..457f4b8ed 100644 --- a/Solana-Co-Learn/module3/introduction-to-rust/index.html +++ b/Solana-Co-Learn/module3/introduction-to-rust/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module3/introduction-to-rust/interact-wit-your-deployed-program/index.html b/Solana-Co-Learn/module3/introduction-to-rust/interact-wit-your-deployed-program/index.html index 536848c59..48753de26 100644 --- a/Solana-Co-Learn/module3/introduction-to-rust/interact-wit-your-deployed-program/index.html +++ b/Solana-Co-Learn/module3/introduction-to-rust/interact-wit-your-deployed-program/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    👋 与你部署的程序互动

    我们将在Solana的游乐场上创建一个简单的Hello World程序。它只会在交易日志中记录一条消息。现在我们的程序已经部署完成了,是时候与之互动了。别忘了,在之前的阶段,你已经多次实现过这个过程!你可以像之前一样通过create-solana-client设置本地客户端,或者直接使用Solana的游乐场。

    我倾向于选择游乐场,因为那里更加方便快捷 :P

    首先,你需要获取程序ID。你可以在“程序凭证”选项卡下找到它:

    接下来我们来查看我们的TS脚本。返回“资源管理器”选项卡,然后在左侧的 Client 部分下找到并打开 client.ts。以下是我们所需的代码:

    const programId = new web3.PublicKey(
    "REPLACE_WITH_YOUR_PROGRAM_ID"
    );

    async function sayHello(
    payer: web3.Keypair
    ): Promise<web3.TransactionSignature> {
    const transaction = new web3.Transaction();

    const instruction = new web3.TransactionInstruction({
    keys: [], // 这里暂时不使用任何账户
    programId,
    // 这里不需要添加数据!
    });

    transaction.add(instruction);

    const transactionSignature = await web3.sendAndConfirmTransaction(
    pg.connection,
    transaction,
    [payer]
    );

    return transactionSignature;
    }

    async function main() {
    const transactionSignature = await sayHello(pg.wallet.keypair);

    console.log(
    `交易链接: https://explorer.solana.com/tx/${transactionSignature}?cluster=devnet`
    );
    }

    main();

    你会发现这部分代码非常熟悉。在Solana的游乐场中,获取密钥对和连接到开发网络的方式有所不同,这是这个脚本的两个变化要点。全局对象 pg 包含了这两个要素。

    运行这个脚本后,你应该能在控制台上看到已记录的交易。打开链接并向下滚动,你就会看到你的消息!

    🚢 接下来的挑战

    现在轮到你自己来构建一些东西了。由于我们从非常基础的程序开始,你所创建的程序应该与我们刚才创建的程序非常相似。尽量自己编写代码,而不是从这里复制粘贴。

    Solana Playground中创建一个新程序,使用msg!宏将自定义消息打印到程序日志中。按照我们演示的方式构建和部署你的程序。编写一个客户端脚本来调用你新部署的程序,然后使用Solana Explorer来检查程序日志中是否打印了你的消息。

    除了创建一个基础程序外,也可以花些时间去探索Rust。你可以查阅Rust书籍,并使用Rust Playground来更好地理解这门语言的工作原理。这样,当我们深入探讨更具挑战性的Solana程序主题时,你就能领先一步。

    或许,你可以尝试让程序使用一组词汇的组合来随机生成登出时的消息,而不是固定的消息?这将是一个有趣的挑战!

    - - +
    Skip to main content

    👋 与你部署的程序互动

    我们将在Solana的游乐场上创建一个简单的Hello World程序。它只会在交易日志中记录一条消息。现在我们的程序已经部署完成了,是时候与之互动了。别忘了,在之前的阶段,你已经多次实现过这个过程!你可以像之前一样通过create-solana-client设置本地客户端,或者直接使用Solana的游乐场。

    我倾向于选择游乐场,因为那里更加方便快捷 :P

    首先,你需要获取程序ID。你可以在“程序凭证”选项卡下找到它:

    接下来我们来查看我们的TS脚本。返回“资源管理器”选项卡,然后在左侧的 Client 部分下找到并打开 client.ts。以下是我们所需的代码:

    const programId = new web3.PublicKey(
    "REPLACE_WITH_YOUR_PROGRAM_ID"
    );

    async function sayHello(
    payer: web3.Keypair
    ): Promise<web3.TransactionSignature> {
    const transaction = new web3.Transaction();

    const instruction = new web3.TransactionInstruction({
    keys: [], // 这里暂时不使用任何账户
    programId,
    // 这里不需要添加数据!
    });

    transaction.add(instruction);

    const transactionSignature = await web3.sendAndConfirmTransaction(
    pg.connection,
    transaction,
    [payer]
    );

    return transactionSignature;
    }

    async function main() {
    const transactionSignature = await sayHello(pg.wallet.keypair);

    console.log(
    `交易链接: https://explorer.solana.com/tx/${transactionSignature}?cluster=devnet`
    );
    }

    main();

    你会发现这部分代码非常熟悉。在Solana的游乐场中,获取密钥对和连接到开发网络的方式有所不同,这是这个脚本的两个变化要点。全局对象 pg 包含了这两个要素。

    运行这个脚本后,你应该能在控制台上看到已记录的交易。打开链接并向下滚动,你就会看到你的消息!

    🚢 接下来的挑战

    现在轮到你自己来构建一些东西了。由于我们从非常基础的程序开始,你所创建的程序应该与我们刚才创建的程序非常相似。尽量自己编写代码,而不是从这里复制粘贴。

    Solana Playground中创建一个新程序,使用msg!宏将自定义消息打印到程序日志中。按照我们演示的方式构建和部署你的程序。编写一个客户端脚本来调用你新部署的程序,然后使用Solana Explorer来检查程序日志中是否打印了你的消息。

    除了创建一个基础程序外,也可以花些时间去探索Rust。你可以查阅Rust书籍,并使用Rust Playground来更好地理解这门语言的工作原理。这样,当我们深入探讨更具挑战性的Solana程序主题时,你就能领先一步。

    或许,你可以尝试让程序使用一组词汇的组合来随机生成登出时的消息,而不是固定的消息?这将是一个有趣的挑战!

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module3/introduction-to-rust/the-magic-internet-computer/index.html b/Solana-Co-Learn/module3/introduction-to-rust/the-magic-internet-computer/index.html index 2a40e578f..381395bc9 100644 --- a/Solana-Co-Learn/module3/introduction-to-rust/the-magic-internet-computer/index.html +++ b/Solana-Co-Learn/module3/introduction-to-rust/the-magic-internet-computer/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    ✨ 魔法互联网计算机

    我们将在游乐场中编写一个简单的Hello World程序。它仅会在交易日志中记录一条消息。至今为止,我们已经完成了许多酷炫的项目,包括建立各种类型的客户端,创建NFT收藏品,铸造SPL代币,甚至构建用户界面让其他人与之互动。然而,我们迄今为止所做的一切都是基于现有的程序。

    你现在已经做好成为一名全栈Solana开发者,并学习如何编写自己程序的准备了。

    Solana的强大之处在于能够运行任何可执行代码。Solana程序,或者说其他区块链环境中的“智能合约”,实际上是Solana生态系统的核心组成部分。这意味着Solana本质上是一个通用计算机,任何互联网用户都可以访问和运行。你可能会好奇为何这样做很重要,但确实并不感觉如此。

    区块链网络是互联网的下一次演进,这就是为什么整个行业将其称为“Web 3.0”的原因。能够安全且无需许可地运行可重复的代码,这为我们打开了一个全新的可能性世界。

    尽管如此,它并没有像“静态类型”语言那样具有魔力,因为魔力仍由像你我这样的人构建。随着开发人员和创作者构建并部署新程序,程序的数量每天都在增长。

    🤔 Rust 是什么鬼东西?

    Solana的程序是用Rust编写的,这是一种与C类似的低级编程语言,速度非常快。在我们着手标准的“Hello World”程序之前,让我稍微解释一下为什么Rust被视为相当困难。

    简单来说:Rust感觉艰难,因为它规则众多。就像那些学习曲线陡峭的视频游戏——DOTA、英雄联盟、星际争霸(甚至国际象棋或CSGO)。这些游戏有数百个独特的角色/物品/能力,每个都有自己的规则和互动方式。要想在这些游戏中获胜,你必须掌握所有的规则,并了解它们是如何相互作用的。

    Rust也是如此。它是一种强有力的语言,迫使你以不同的方式思考代码。它不是一种可以随意拿来写程序的语言,而是一种你必须深入学习和理解的语言。

    但这并不是要吓到你——学习Rust并不像学习DOTA那么难💀。我只是想告诉你我们已经弄明白了。学习Rust可以非常有趣,只是可能需要比你以往习惯的更多努力 :)

    就像玩电子游戏一样,我们会一步步来,从简单的开始,逐渐解决困难问题,不断提升自己的水平⚔️。

    我们将从构建Hello World程序所需的基本概念开始:


    🛹 Solana Playground

    info

    这里的内容可以看也行也可以不看,因为前面我们已经学会了如何在本地启动Solana节点,搭建一个本地开发环境了。

    我们将在Solana Playground上开始编写程序。这是一个基于浏览器的集成开发环境,它会处理所有设置要求,让我们可以专注于Rust

    启动它并使用原生框架创建一个新项目,我们将保持原汁原味🌼。Anchor是一个用于在Solana上构建Rust的框架,有点像React对于Web的作用。我们稍后会学习如何使用它,现在先使用原生框架。

    你会看到一个包含高级Hello World程序的lib.rs文件。忽略它,我们要做的要更简单。

    在这里,你最不想做的事情就是设置游乐场钱包。你会在左下角看到一个“未连接”按钮。

    点击它,系统会为你生成一个Solana钱包,并用devnet SOL为其充值。如果需要,你可以保存密钥对,这在测试特定密钥对部署的程序时可能很有用。对于我来说,我只会构建一个简单的程序,所以并不需要它 :)

    现在我们知道了在哪里编写代码,是时候开始学习Rust了!

    - - +
    Skip to main content

    ✨ 魔法互联网计算机

    我们将在游乐场中编写一个简单的Hello World程序。它仅会在交易日志中记录一条消息。至今为止,我们已经完成了许多酷炫的项目,包括建立各种类型的客户端,创建NFT收藏品,铸造SPL代币,甚至构建用户界面让其他人与之互动。然而,我们迄今为止所做的一切都是基于现有的程序。

    你现在已经做好成为一名全栈Solana开发者,并学习如何编写自己程序的准备了。

    Solana的强大之处在于能够运行任何可执行代码。Solana程序,或者说其他区块链环境中的“智能合约”,实际上是Solana生态系统的核心组成部分。这意味着Solana本质上是一个通用计算机,任何互联网用户都可以访问和运行。你可能会好奇为何这样做很重要,但确实并不感觉如此。

    区块链网络是互联网的下一次演进,这就是为什么整个行业将其称为“Web 3.0”的原因。能够安全且无需许可地运行可重复的代码,这为我们打开了一个全新的可能性世界。

    尽管如此,它并没有像“静态类型”语言那样具有魔力,因为魔力仍由像你我这样的人构建。随着开发人员和创作者构建并部署新程序,程序的数量每天都在增长。

    🤔 Rust 是什么鬼东西?

    Solana的程序是用Rust编写的,这是一种与C类似的低级编程语言,速度非常快。在我们着手标准的“Hello World”程序之前,让我稍微解释一下为什么Rust被视为相当困难。

    简单来说:Rust感觉艰难,因为它规则众多。就像那些学习曲线陡峭的视频游戏——DOTA、英雄联盟、星际争霸(甚至国际象棋或CSGO)。这些游戏有数百个独特的角色/物品/能力,每个都有自己的规则和互动方式。要想在这些游戏中获胜,你必须掌握所有的规则,并了解它们是如何相互作用的。

    Rust也是如此。它是一种强有力的语言,迫使你以不同的方式思考代码。它不是一种可以随意拿来写程序的语言,而是一种你必须深入学习和理解的语言。

    但这并不是要吓到你——学习Rust并不像学习DOTA那么难💀。我只是想告诉你我们已经弄明白了。学习Rust可以非常有趣,只是可能需要比你以往习惯的更多努力 :)

    就像玩电子游戏一样,我们会一步步来,从简单的开始,逐渐解决困难问题,不断提升自己的水平⚔️。

    我们将从构建Hello World程序所需的基本概念开始:


    🛹 Solana Playground

    info

    这里的内容可以看也行也可以不看,因为前面我们已经学会了如何在本地启动Solana节点,搭建一个本地开发环境了。

    我们将在Solana Playground上开始编写程序。这是一个基于浏览器的集成开发环境,它会处理所有设置要求,让我们可以专注于Rust

    启动它并使用原生框架创建一个新项目,我们将保持原汁原味🌼。Anchor是一个用于在Solana上构建Rust的框架,有点像React对于Web的作用。我们稍后会学习如何使用它,现在先使用原生框架。

    你会看到一个包含高级Hello World程序的lib.rs文件。忽略它,我们要做的要更简单。

    在这里,你最不想做的事情就是设置游乐场钱包。你会在左下角看到一个“未连接”按钮。

    点击它,系统会为你生成一个Solana钱包,并用devnet SOL为其充值。如果需要,你可以保存密钥对,这在测试特定密钥对部署的程序时可能很有用。对于我来说,我只会构建一个简单的程序,所以并不需要它 :)

    现在我们知道了在哪里编写代码,是时候开始学习Rust了!

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module3/native-solana-development/build-a-movie-review-program/index.html b/Solana-Co-Learn/module3/native-solana-development/build-a-movie-review-program/index.html index ead870968..28a62f01c 100644 --- a/Solana-Co-Learn/module3/native-solana-development/build-a-movie-review-program/index.html +++ b/Solana-Co-Learn/module3/native-solana-development/build-a-movie-review-program/index.html @@ -9,15 +9,15 @@ - - + +
    Skip to main content

    🎥 构建一个电影评论程序

    还记得我们在第一节中互动开发的电影评论节目吗?现在我们要继续深入开发它。当然,你可以随意评论任何内容,不仅限于电影,毕竟我并不是你的长辈,你自由发挥就好。

    让我们回到操场(是上一节课的操场,不是中学时的那个),开始一个全新的项目。我们将从基础的结构编写开始,具体如 lib.rs 文件:

    如果是在本地开发的话,我们需要执行cargo init <your-program-name> --lib。 下面这个是以cargo init hello --lib生成的Cargo.toml文件。

    [package]
    name = "hello"
    version = "0.1.0"
    edition = "2021"

    # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

    [dependencies]

    并且还需要添加solana-program, borsh,通过在命令行执行cargo add solana-programcargo add borsh安装。

    执行后的Cargo.toml内容是:

    [package]
    name = "hello"
    version = "0.1.0"
    edition = "2021"

    # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

    [dependencies]
    borsh = "0.10.3"
    solana-program = "1.16.10"

    还有需要配置下Cargo.toml,在文件内添加如下内容:

    [package]
    name = "hello"
    version = "0.1.0"
    edition = "2021"

    # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

    [dependencies]
    borsh = "0.10.3"
    solana-program = "1.16.10"

    # 这是你需要添加的内容
    [lib]
    crate-type = ["cdylib", "lib"]

    然后是将lib.rs里面的内容替换为下面的内容。

    use solana_program::{
    entrypoint,
    entrypoint::ProgramResult,
    pubkey::Pubkey,
    msg,
    account_info::AccountInfo,
    };

    entrypoint!(process_instruction);

    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
    ) -> ProgramResult {

    Ok(())
    }

    就像我们构建记事程序一样,我们将从定义指令结构开始,并创建用于反序列化的逻辑。

    🔪 反序列化指令数据

    我们将在一个名为 instruction.rs 的新文件中完成这个任务。

    use borsh::{BorshDeserialize};
    use solana_program::{program_error::ProgramError};

    pub enum MovieInstruction {
    AddMovieReview {
    title: String,
    rating: u8,
    description: String
    }
    }

    #[derive(BorshDeserialize)]
    struct MovieReviewPayload {
    title: String,
    rating: u8,
    description: String
    }

    我们需要引入的只有 BorshDeserialize 宏和 ProgramError 枚举。

    虽然我们只有一种指令类型,但我们仍然会使用枚举。未来我们可能会考虑添加更多的指令 :)

    你可能会好奇,为何我们需要在有效负载中指定类型。这些类型告诉Borsh如何分割字节。在切割之前,得先知道香肠有多长,记得吗?

    我们还需要为 MovieInstruction 枚举添加实现。在枚举定义下方添加以下内容。

    impl MovieInstruction {
    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {

    let (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;

    let payload = MovieReviewPayload::try_from_slice(rest).unwrap();

    Ok(match variant {
    0 => Self::AddMovieReview {
    title: payload.title,
    rating: payload.rating,
    description: payload.description },
    _ => return Err(ProgramError::InvalidInstructionData)
    })
    }
    }

    你应该能理解这里发生的一切!我们正在解析指令数据,并返回枚举的正确变体。

    注意我们在分割第一个字节时使用的 ?

    let (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;

    如果 unpack 的结果是错误,这是一种返回错误并退出 unpack 函数的简写方式。就像一个精简版的 try/catch。这在Rust中是常见的模式,你会经常看到它。

    let payload = MovieReviewPayload::try_from_slice(rest).unwrap();

    此外,我还想深入探讨一下 .unwrap();Rust中,“unwrap”意味着“给我计算的结果,如果出错就产生恐慌并停止程序。”你可能会想:“嗯,但是为什么我们需要从函数的结果中返回东西呢?难道 try_from_slice() 函数不会返回我们想要的吗?”

    不是的。Rust有一个 Option 类型:一种使用Rust类型系统来表示可能缺失的方式。这与其他语言中的 null 不同。 Option 是一种类型,可以是 SomeNoneSome 是一个值,None 是一个值的缺失。为什么呢?因为有时候你没有一个值,这是可以接受的。从文档中了解更多:

    info

    将缺失的可能性编码到类型系统中是一项重要的概念,因为它会迫使编译器强制程序员处理这种缺失的情况。

    Rust助你成为更出色的开发者!现在,你又多了解了Rust的另一部分内容🍰

    👀 将指令添加到程序中

    最后一部分的工作是将指令整合到程序中。我们将在 lib.rs 文件中完成此操作。

    pub mod instruction;
    use instruction::{MovieInstruction};

    如果你更改了枚举名称,请确保相应地更新导入。

    现在我们只需将指令数据记录到控制台。在 process_instruction 函数后添加以下代码。

    pub fn add_movie_review(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    title: String,
    rating: u8,
    description: String
    ) -> ProgramResult {

    msg!("正在添加电影评论...");
    msg!("标题: {}", title);
    msg!("评分: {}", rating);
    msg!("描述: {}", description);

    Ok(())
    }

    现在,我们可以更新 process_instruction 函数,使用 unpackadd_movie_review 函数:

    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
    ) -> ProgramResult {

    let instruction = MovieInstruction::unpack(instruction_data)?;

    match instruction {
    MovieInstruction::AddMovieReview { title, rating, description } => {
    add_movie_review(program_id, accounts, title, rating, description)
    }
    }
    }

    我们在这里做的只是解析指令数据,然后使用正确的参数调用 add_movie_review 函数。

    我们的程序现在已经完成了!请确保你点击部署按钮,并从游乐场复制程序ID

    如果你觉得这有点让人失望,那是因为我们在上一课已经逐一讲解了每个部分。现在,让我们尝试使用客户端将电影评论添加到我们的程序中。

    提交电影评论

    我们的进展飞速,走吧!

    不需要从头开始编写脚本,我相信你知道该怎么做 :)

    下面是如何设置完整脚本的步骤,包括你所需的一切:

    git clone https://github.com/all-in-one-solana/solana-movie-client
    cd solana-movie-client
    npm install

    打开 src/index.js 并将第94行的程序ID更新为从playground复制的ID。如果你对程序做了任何更改,这里还需要更新客户端。

    在终端输入 npm run start ,你应该会得到一个资源管理器链接。点击该链接,然后向下滚动到程序指令日志,你应该能看到你的电影评论!

    轻松有趣,我们可以做到,继续前进!

    🚢 挑战

    对于本课程的挑战,尝试复制一个学生介绍程序。

    该程序接收用户的姓名和短信作为指令数据,并创建一个账户以将数据存储在区块链上。

    利用你在本课程中学到的知识,构建一个学生介绍程序,使得当程序被调用时,能够将用户提供的姓名和信息打印到程序日志中。

    解决方案代码 -你可以通过构建这个前端并在Solana Explorer上检查程序日志来测试你的程序。记得用你部署的程序ID替换前端代码中的ID

    如果可以的话,尽量自己独立完成这个任务!但如果遇到困难,可以参考解决方案代码

    我对你有信心。

    - - +你可以通过构建这个前端并在Solana Explorer上检查程序日志来测试你的程序。记得用你部署的程序ID替换前端代码中的ID

    如果可以的话,尽量自己独立完成这个任务!但如果遇到困难,可以参考解决方案代码

    我对你有信心。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module3/native-solana-development/index.html b/Solana-Co-Learn/module3/native-solana-development/index.html index f688cf015..7bbe593ed 100644 --- a/Solana-Co-Learn/module3/native-solana-development/index.html +++ b/Solana-Co-Learn/module3/native-solana-development/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module3/native-solana-development/state-management/index.html b/Solana-Co-Learn/module3/native-solana-development/state-management/index.html index dac2c22d1..3d6f3d69d 100644 --- a/Solana-Co-Learn/module3/native-solana-development/state-management/index.html +++ b/Solana-Co-Learn/module3/native-solana-development/state-management/index.html @@ -9,14 +9,14 @@ - - + +
    Skip to main content

    🤠 状态管理

    你还记得我们在第一节中互动的电影评论程序吗?现在我们要在这里构建它。你想评论的不一定只是电影,我可不会限制你。状态是指存储在链上的程序数据。

    我们已经有了一个可靠的程序,它接收指令数据并做好了处理准备。要执行这些指令,我们需要学习更多关于 Rust 的知识。

    📝 将程序状态作为 Rust 数据类型

    Solana 之所以保持其速度和效率,部分原因是程序是无状态的。这意味着你无法更改程序上的数据——所有数据都存储在外部账户中,通常是由程序拥有的账户。这些账户中的大部分是 PDAProgram Derived Accounts)——现在我们将研究它们的数据存储元素,并稍后深入了解其余部分。

    正如我们将指令数据转换为 Rust 类型一样,我们也将把程序状态转换为 Rust 类型,以便更容易处理。

    回想一下 Solana 账户中的 data 字段——它存储了一个原始字节数组。我们可以通过序列化和反序列化来表示它在 Rust 类型中的形式。

    我们将再次使用 borsh macro

    use borsh::{BorshDeserialize, BorshSerialize};

    #[derive(BorshSerialize, BorshDeserialize, Debug)]
    struct NoteState {
    title: String,
    body: String,
    id: u64,
    }

    数据在传输和存储时以原始字节的形式存在,但当我们想要处理数据时,会将其转换为 Rust 类型。听起来很有道理,不是吗?

    🏠 空间与租金

    没错,Solana 也有房东:那就是将区块链状态存储在自己机器上的验证者。

    租金以 Lamports 为单位支付,其中 LamportSOL 的最小单位(0.000000001 SOL = 1 Lamport),根据账户占用的空间大小计算。下面是一个显示常见类型所占用的字节空间的表格:

    付房租有两种方式:

    • 按每个时期支付租金。这就像每月支付房租一样——只要你继续支付,你就能继续居住。如果账户没有足够的 SOL,它将被清零,数据将丢失。
    • 保持等于 2 年租金的最低余额。这样账户就免除了租金。这里的逻辑是硬件成本每 2 年下降 50%,所以如果你有足够的 SOL 支付 2 年的租金,你就再也不用担心了!

    现在要求免租金,所以只有选项 #2。这种方法的最大优点是,当你不再需要存储数据时,可以销毁账户并取回你的 SOL!区块链上的免费存储(减去交易费用)🥳。

    那么,为什么要在区块链上支付租金呢?嗯,这是一种防止人们大量创建但从未使用的账户的方式。这会浪费空间和验证者的资源。这个机制也是 Solana 上存储如此便宜的原因之一——与以太坊不同,我的那些愚蠢的 Hello World NFT 收藏会被所有验证者永久存储。

    你可以在这里阅读更多有关的内容,我觉得这真的很酷!

    📊 租金计算

    计算租金其实非常简单——有一个非常有用的功能可以帮助你。难的部分是弄清楚你需要多少空间。

    以下是我们超级笔记程序的代码示例:

    // 计算存储结构体 NoteState 所需的账户大小
    // 4字节用于存储后续动态数据(字符串)的大小
    // 8字节用于存储64位整数ID
    let account_len: usize = (4 + title.len()) + (4 + body.len()) + 8;

    // 计算所需租金
    let rent = Rent::get()?;
    let rent_lamports = rent.minimum_balance(account_len);

    首先,我们需要计算存储的数据的总长度。这可以通过将字符串的长度和8字节的id相加来实现。

    在我们的情况下,titlebody 是动态数据类型(字符串)——它们可以是任意长度。我们使用前4个字节来存储每个项目的长度,因此我们将每个字符串的长度加上4

    接着我们可以使用 Rent::get() 函数来获取账户的租金。非常直接轻松!

    📜 程序派生地址

    我们已经从指令中提取了数据,计算出了需要支付的租金,现在我们需要一个账户来存储它。PDAs!我们将从程序ID和一组种子中推导出账户地址。

    我们将在将来深入探讨PDA的工作原理,但现在你只需了解 find_program_address 功能,并且只有 program_id 中的程序才能为PDA签名。就像安全存储一样,不需要密码。

    🛫 跨程序调用

    最后一步是初始化PDA(我们才刚刚找到地址)。我们将使用跨程序调用(CPI)来完成此操作。正如其名字所示,我们将在我们的程序中与Solana网络上的另一个程序进行交互。

    CPI可以使用 invokeinvoke_signed 来实现。

    pub fn invoke(
    instruction: &Instruction,
    account_infos: &[AccountInfo],
    ) -> ProgramResult
    pub fn invoke_signed(
    instruction: &Instruction,
    account_infos: &[AccountInfo],
    signers_seeds: &[&[u8]],
    ) -> ProgramResult

    当你不需要签署交易时,使用 invoke。当你需要签署交易时,使用 invoke_signed。在我们的例子中,我们是唯一可以为PDA签署的人,因此我们将使用 invoke_signed

    这就是它的样子。你可能会想:“这到底是什么?”——别担心,我们接下来会进行练习,一切都会变得清晰的 :)

    我们在这里所做的只是使用 Rust 在程序中创建一个交易,这与我们在客户端使用 TypeScript 时的做法类似。这里有一个特殊的 signers_seeds 事项,这对于PDA是必需的。

    ✂ 账户数据的序列化和反序列化

    一旦我们创建了一个新账户,我们需要访问并更新该账户的数据字段(目前为空字节)。这就涉及将其字节数组反序列化为我们创建的类型实例,更新该实例上的字段,然后将该实例重新序列化为字节数组。

    账户数据的反序列化

    更新账户数据的第一步是将其数据字节数组反序列化为Rust类型。你可以首先借用账户上的数据字段来实现此操作,这样可以在不获取所有权的情况下访问数据。

    然后,你可以使用try_from_slice_unchecked函数来反序列化借用的账户数据字段,根据你创建的类型来表示数据格式。这将提供一个Rust类型的实例,使你可以轻松使用点符号来更新字段。如果我们采用我们一直在使用的笔记应用程序示例,它的形式如下:

    let mut account_data = try_from_slice_unchecked::<NoteState>(note_pda_account.data.borrow()).unwrap();

    account_data.title = title;
    account_data.body = body;
    account_data.id = id;

    账户数据的序列化

    一旦Rust实例更新了账户数据的合适值,你就可以将数据“保存”到账户上。

    这通过对你创建的Rust类型实例使用serialize函数完成。你需要传入账户数据的可变引用。这里的语法有些复杂,所以如果你不完全理解也不必担心。借用和引用是Rust中最复杂的概念之一。

    account_data.serialize(&mut &mut note_pda_account.data.borrow_mut()[..])?;

    上面的示例将account_data对象转换为字节数组,并设置为note_pda_account上的data属性。这实际上将更新后的account_data变量保存到新账户的数据字段中。现在,当用户获取note_pda_account并反序列化数据时,它将显示我们序列化到账户中的更新数据。

    📼 总结 - 将所有内容整合在一起

    让我们回顾一下整个过程:

    1. 用户通过发送包含标题、正文和字节ID的交易来创建笔记。
    2. 我们的程序接收指令,提取数据并将其格式化为Rust类型。
    3. 我们使用Rust类型计算账户需要多少空间以及我们需要支付多少租金。
    4. 我们从程序ID和一组种子中推导出账户的地址。
    5. 我们使用CPI创建一个空白数据的账户。
    6. 我们将账户数据反序列化为Rust类型。
    7. 我们使用指令中的数据来更新Rust类型的账户数据。
    8. 我们将Rust类型序列化为原始字节,并将其保存到账户中。

    你现在了解了如何在Solana上向账户写入数据。

    🎥 构建一个电影评论程序

    到了真正付诸实践的时候了。我们的史诗级电影评论将不再仅仅记录在控制台上,而是将它们存储在区块链上!

    我们将使用与之前相同的程序,你可以从头开始设置它,或者使用上一节的那个。

    📝 账户数据的表示

    我们需要一个新的Rust类型来表示我们要存储的数据。创建一个名为 state.rs 的新文件,并在其中定义 MovieAccountState

    use borsh::{BorshSerialize, BorshDeserialize};

    #[derive(BorshSerialize, BorshDeserialize)]
    pub struct MovieAccountState {
    pub is_initialized: bool,
    pub rating: u8,
    pub title: String,
    pub description: String,
    }

    这里我们使用了 BorshSerializeBorshDeserialize 两个trait :)

    接下来,我们需要更新 lib.rs,以便将我们需要的所有内容引入范围。将文件顶部更新为以下内容:

    use solana_program::{
    entrypoint,
    entrypoint::ProgramResult,
    pubkey::Pubkey,
    msg,
    account_info::{next_account_info, AccountInfo},
    system_instruction,
    sysvar::{rent::Rent, Sysvar},
    program::{invoke_signed},
    borsh::try_from_slice_unchecked,
    };
    use std::convert::TryInto;
    pub mod instruction;
    pub mod state;
    use instruction::MovieInstruction;
    use state::MovieAccountState;
    use borsh::BorshSerialize;

    好的,到目前为止一切顺利。当需要时,我会解释新添加的内容。现在,让我们回到我们的 add_movie_review 函数,用实际的逻辑填充它,而不仅仅是打印输出。

    ⏩ 迭代账户

    在我们的 add_movie_review 函数中传入的第二个参数是一个账户数组。我们可以遍历这些账户,获取它们的数据并执行相应的操作。我们可以借助 next_account_info 函数(需要引入use solana_program::account_info::next_account_info;)来实现这一点 - 它是一个接受迭代器并安全返回列表中下一个项的函数。我们可以像下面这样使用它:

    // 获取账户迭代器
    let account_info_iter = &mut accounts.iter();

    // 获取账户
    let initializer = next_account_info(account_info_iter)?;
    let pda_account = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    挺酷的,对吧?如果列表为空,我们将收到 ProgramError::NotEnoughAccountKeys 错误。如果我们试图访问不存在的账户,将会收到 ProgramError::MissingRequiredSignature 错误。

    🥚 生成PDA地址

    我们只需要一行代码就可以实现(继续添加到 add_movie_review 函数中):

    let (pda, bump_seed) = Pubkey::find_program_address(&[initializer.key.as_ref(), title.as_bytes().as_ref(),], program_id);

    其中的种子包括:

    1. 初始化器的公钥
    2. 电影的标题

    🧮 计算空间和租金

    我们之前已经讨论过这个了 :)

    // 计算所需的账户大小
    let account_len: usize = 1 + 1 + (4 + title.len()) + (4 + description.len());

    // 计算所需的租金
    let rent = Rent::get()?;
    let rent_lamports = rent.minimum_balance(account_len);

    你的 add_movie_review 函数现在应该看起来相当长了。我们还剩下两个小部分——创建账户和更新数据。继续前进吧!

    📝 创建账户

    是时候进行一些跨程序调用了!

    // 创建账户
    invoke_signed(
    &system_instruction::create_account(
    initializer.key,
    pda_account.key,
    rent_lamports,
    account_len.try_into().unwrap(),
    program_id,
    ),
    &[initializer.clone(), pda_account.clone(), system_program.clone()],
    &[&[initializer.key.as_ref(), title.as_bytes().as_ref(), &[bump_seed]]],
    )?;

    msg!("创建PDA: {}", pda);

    invoke_signed 是创建账户的交易。我们传入了 create_account 指令,我们所使用的账户,以及用于派生PDA地址的种子。

    我们需要做的最后一件事是更新账户数据:

    msg!("解包状态账户");
    let mut account_data = try_from_slice_unchecked::<MovieAccountState>(&pda_account.data.borrow()).unwrap();
    msg!("借用账户数据");

    account_data.title = title;
    account_data.rating = rating;
    account_data.description = description;
    account_data.is_initialized = true;

    msg!("序列化账户");
    account_data.serialize(&mut &mut pda_account.data.borrow_mut()[..])?;
    msg!("状态账户序列化");

    我们使用 try_from_slice_unchecked 将原始字节转换为Rust类型。然后我们更新数据,并将其序列化回原始字节。

    我们已经准备好升级并部署了!(可能需要几分钟时间)

    🎉 试一试

    你知道该如何操作了。复制地址,设置一个脚本来调用程序(你可以使用上次的脚本,无需更改),运行它,然后在Solana Explorer上查看这个新账户。

    如果你需要一个新的设置,可以执行以下操作:

    git clone https://github.com/all-in-one-solana/solana-movie-client
    cd solana-movie-client
    npm install

    更新 index.ts 文件中的 programId 为你程序的地址,然后运行 npm run start

    在终端中点击浏览器链接,并向下滚动到程序日志部分,你会看到类似的内容:

    我们可以看到,我们的程序通过CPI与系统程序进行交互,创建了一个账户(PDA),并将我们的评论添加到其中!如果我可以自夸一下的话,这绝对是一篇很棒的评论。 😉

    🚢 挑战

    现在轮到你独立创建一些东西了。

    回想一下,学生介绍程序需要用户提供姓名和简短留言作为输入instruction_data,并创建一个账户来将数据存储在区块链上。

    借助你在本课程中学到的知识,尝试重新创建完整的学生介绍程序。

    info

    提示: -除了接收一个名称和一个简短的消息作为指令数据外,程序还应该:

    1. 为每个学生创建一个独立的账户
    2. 在每个账户中存储 is_initialized 为布尔值,name 为字符串,msg 为字符串

    解决方案代码:

    你可以使用这个链接的前端来测试你的程序。记得用你部署的程序ID替换前端代码中的那个。

    如果可能的话,请尽量独立完成这个任务!但如果遇到困难,你可以参考解决方案代码

    - - +除了接收一个名称和一个简短的消息作为指令数据外,程序还应该:

    1. 为每个学生创建一个独立的账户
    2. 在每个账户中存储 is_initialized 为布尔值,name 为字符串,msg 为字符串

    解决方案代码:

    你可以使用这个链接的前端来测试你的程序。记得用你部署的程序ID替换前端代码中的那个。

    如果可能的话,请尽量独立完成这个任务!但如果遇到困难,你可以参考解决方案代码

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module3/native-solana-development/the-rust-layer-cake/index.html b/Solana-Co-Learn/module3/native-solana-development/the-rust-layer-cake/index.html index 5526ec17d..67d7d5ff9 100644 --- a/Solana-Co-Learn/module3/native-solana-development/the-rust-layer-cake/index.html +++ b/Solana-Co-Learn/module3/native-solana-development/the-rust-layer-cake/index.html @@ -9,14 +9,14 @@ - - + +
    Skip to main content

    🎂 Rust的分层蛋糕

    我们将在游乐场上制作一个简单的Hello World程序,仅仅会在交易日志中记录一条消息。招呼已经打过了。现在是时候学习如何处理指令数据,就像在客户端开发中一样。

    在开始构建之前,我想先给你介绍一些即将使用的概念。还记得我提到的规则、能力和互动吗?我会带你了解一下编写本地Solana程序所需的能力和规则。这里的“本地”非常重要 - 我们将在后续部分借助Anchor来处理我们现在所学的许多内容。

    我们学习原生开发的原因是因为了解底层工作原理是非常重要的。一旦你理解了事物是如何在最基本的层面上运作的,就能够借助像Anchor这样的工具来构建更强大的程序。你可以把这个过程想象成与不同类型的敌人进行首领战 , 你需要先学会如何逐一对抗每个个体怪物(以及了解你自己的能力)。

    当我刚开始学习的时候,我发现很难理解自己缺少了什么。所以我将其分解成了“层次”。每一个你学习的主题都建立在一层知识的基础之上。如果遇到不明白的地方,回到之前的层次,确保你真正理解了它们。

    Rust的分层蛋糕

    这是一个由Rust制作的蛋糕。

    caution

    注意:图层代表重量!

    👶 变量声明和可变性

    变量。你了解它们。你使用过它们。你甚至可能拼写错误过它们。在Rust中关于变量唯一的新概念就是可变性。所有变量默认都是不可变的 , 一旦声明了变量,就不能改变其值。你只需通过添加mut关键字告诉编译器你想要一个可变的变量。就是这么简单。如果我们不指定类型,编译器会根据提供的数据进行推断,并强制我们保持该类型。

    // compiler will throw error
    let age = 33;
    age = 34;

    // this is allowed
    let mut mutable_age = 33;
    mutable_age = 34;

    🍱 结构体

    结构体是自定义的数据结构,一种将数据组织在一起的方式。它们是你定义的自定义数据类型,类似于JavaScript中的对象。Rust并不是完全面向对象的 - 结构体本身除了保存有组织的数据外,无法执行任何操作。但你可以向结构体添加方法,使其表现得更像对象。

    Struct User {
    active: bool,
    email: String,
    age: u64
    }

    let mut user1 = User {
    active: true,
    email: String::from("test@test.com"),
    age: 33
    };

    user1.age = 34;

    📜 枚举、变体和匹配

    枚举很简单 - 它们就像代码中的下拉列表。它们限制你从几个可能的变体中选择一个。

    enum LightStatus {
    On,
    Off
    }
    enum LightStatus {
    On {
    color: String
    },
    Off
    }

    let light_status = LightStatus::On {
    color: String::from("red")
    };

    Rust中枚举的酷炫之处在于你可以(可选地)向其中添加数据,使其几乎像一个迷你的if语句。在这个例子中,你正在选择交通信号灯的状态。如果它是开启的,你需要指定颜色 - 是红色、黄色还是绿色?

    enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
    }

    fn value_in_cents(coin: Coin) -> u8 {
    match coin {
    Coin::Penny => 1,
    Coin::Nickel => 5,
    Coin::Dime => 10,
    Coin::Quarter => 25,
    }
    }

    当与匹配语句结合使用时,枚举非常有用。它们是一种检查变量值并根据该值执行代码的方式,与JavaScript中的switch语句类似。

    📦 实现

    结构体本身很有用,但如果你能为它们添加函数,效果将如何呢?下面我们来介绍实现(Implementations),它让你可以给结构体添加方法,使其更接近面向对象的设计。

    #[derive(Debug)]
    struct Rectangle {
    width: u32,
    height: u32,
    }

    impl Rectangle {
    fn area(&self) -> u32 {
    self.width * self.height
    }
    }

    fn main() {
    let rect1 = Rectangle {
    width: 30,
    height: 50,
    };

    println!(
    "The area of the rectangle is {} square pixels.",
    rect1.area()
    );
    }

    如果你对“向结构体添加方法”感到困惑,可以理解为赋予结构体特殊能力。例如,你可能有一个简单的user结构体,拥有速度、健康和伤害属性。通过使用 impl 关键字添加一个 wordPerMinute 方法,你就可以计算用户的打字速度⌨️。

    🎁 特征(Traits

    现在,我们来谈谈这个“蛋糕”的顶层部分 - TraitsTraits和实现类似,也是为类型增添功能。你可以把它看作类型能具备的一种能力。

    回到我们的 user 结构体例子,如果我添加了一个名为 ThreeArmstrait,用户将能够以更快的速度输入文字,因为他们将拥有额外的手臂!Traits这个概念可能有点抽象,所以我们来看一个具体的例子:

    pub triat BorshDeserialize : Sized {
    fn deserialize(buf: &mut &[u8]) -> Result<Self, Error>;
    fn try_from_slice(buf: &[u8]) -> Result<Self, Error> { ... }
    }

    如你所知,我们的指令数据以字节数组(由10组成)的形式提供,我们需要在程序中对其进行反序列化(转换成Rust类型)。我们将使用名为 BorshDeserializeTraits来完成这一任务:它包括一个 deserialize 方法,可以将数据转换为所需类型。这意味着,如果我们将 BorshDeserialize Traits 添加到指令结构体中,我们就可以使用 deserialize 方法将指令数据实例转换为Rust类型。

    如果你对这部分内容感到困惑,不妨再读一遍,我自己也花了一些时间才弄清楚。

    实际操作示例如下:

    #[derive(BorshDeserialize)]
    struct InstructionData {
    input: String,
    }
    caution

    注意:可能有一个你已经忽略的层面——宏。它们用来生成代码。

    在我们的场景中,特质和宏通常一起使用。例如,BorshDeserialize Traits有两个必须实现的函数:deserializetry_from_slice。我们可以使用 #[derive(BorshDeserialize)] 属性,让编译器在给定类型上(即指令数据结构)为我们实现这两个函数。 -整个流程是这样的:

    • 通过宏将Trait添加到结构体中
    • 编译器会查找Trait的定义
    • 编译器会为该Trait实现底层函数
    • 你的结构体现在具备了Trait的功能

    实际上,宏在编译时生成了用于反序列化字符串的函数。通过使用这个Trait,我们可以告诉Rust:“嘿,我想能反序列化字符串,请为我生成相应的代码。”

    对于我们的情况,唯一的要求是Borsh必须支持所有的结构数据类型(在我们的场景中是字符串)。如果你有一个Borsh不支持的自定义数据类型,就需要在宏中自己实现这些功能。

    如果你还未完全理解,不用担心!我自己也是在看到整个流程后才理解的,所以现在让我们一起实践一下!

    🎂 把所有元素整合在一起

    我们刚刚讨论了一系列相互关联的抽象主题。如果只描述每一层,可能难以想象整个“蛋糕”的样子,所以我们现在就将它们整合起来。

    假设我们正在构建一个链上的笔记程序,我们将保持它的简单性:你只能创建、更新和删除笔记。我们需要一条指令来完成这些操作,所以让我们创建一个枚举类型来代表它:

    enum NoteInstruction {
    CreateNote {
    id: u64,
    title: String,
    body: String
    },
    UpdateNote {
    id: u64,
    title: String,
    body: String
    },
    DeleteNote {
    id: u64
    }
    }

    每个指令变体的字节数组都有自己的数据类型,我们在这里有它们!

    既然我们知道指令数据的样子,我们需要将其从字节转换为这些类型。第一步是反序列化,我们将使用一个专门为有效负载创建的新结构体上的 BorshDeserialize Traits 来完成。

    #[derive(BorshDeserialize)]
    struct NoteInstructionPayload {
    id: u64,
    title: String,
    body: String
    }

    我们在这里处理了titlebody,这就是字节数组中的内容。Borsh的工作仅仅是添加反序列化的支持,它实际上并没有进行反序列化,而是仅提供了我们可以调用的反序列化函数。

    下一步,我们要实际使用这些函数来反序列化数据。我们将在一个实现中定义这个行为,这是一个手动的过程(至少暂时是这样)!

    impl NoteInstruction {
    // 将传入的缓冲区解包到相关的指令
    // 输入的预期格式是一个用Borsh序列化的向量
    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
    // 采用第一个字节作为变体来确定要执行哪个指令
    let (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;
    // 使用临时有效载荷结构体进行反序列化
    let payload = NoteInstructionPayload::try_from_slice(rest).unwrap();
    // 通过变体匹配,确定函数所期望的数据结构,然后返回TestStruct或错误
    Ok(match variant {
    0 => Self::CreateNote {
    title: payload.title,
    body: payload.body,
    id: payload.id
    },
    1 => Self::UpdateNote {
    title: payload.title,
    body: payload.body,
    id: payload.id
    },
    2 => Self::DeleteNote {
    id: payload.id
    },
    _ => return Err(ProgramError::InvalidInstructionData)
    })
    }
    }

    这部分内容可能一开始看起来有点吓人,但你很快就会觉得它其实非常直接和简单!让我们一起来深入分析一下 🕺💃👯‍♂️

    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {

    我们的解包函数从指令中获取字节,并返回一个NoteInstruction类型(即 Self)或一个ProgramError

    let (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;

    现在是时候从字节中解包数据并调用反序列化函数了。我们的指令数据的第一个字节是一个整数,它告诉我们正在处理哪个指令。我们这样做的方式是使用Rust的内置函数split_first。如果切片为空,ok_or将返回ProgramError枚举中的InvalidInstructionData错误。

    let payload = NoteInstructionPayload::try_from_slice(rest).unwrap();

    现在我们有了两个变量要处理:指令指示器和指令的有效载荷(数据)。Borsh在我们的有效载荷结构中添加了try_from_slice函数,使我们可以在有效载荷变量rest上调用它。这就是反序列化的过程!

    接下来的步骤包括:

    • 将指令数据定义为Rust类型中的枚举。
    • 定义负载结构体。
    • 在负载结构体上声明BorshDeserialize宏。
    • 为负载结构体创建一个实现(字节 -> 结构体)。
    • 创建unpack函数,该函数接收指令数据并对其进行反序列化。

    我们unpack函数的最后一步是将反序列化的数据转换为枚举变体(即指令数据类型)。我们将使用匹配语句来完成这个任务,通过匹配指令指示器,我们可以返回枚举的正确变体。

    Ok(match variant {
    0 => Self::CreateNote {
    title: payload.title,
    body: payload.body,
    id: payload.id
    },
    1 => Self::UpdateNote {
    title: payload.title,
    body: payload.body,
    id: payload.id
    },
    2 => Self::DeleteNote {
    id: payload.id
    },
    _ => return Err(ProgramError::InvalidInstructionData)
    })

    现在你已经知道了整个过程!理解这一切确实需要集中精力,所以如果你需要多读几遍,也完全没关系。

    这部分内容信息量较大,可能会让人觉得有些复杂。但别担心,我们会通过大量的练习来逐渐熟悉它们。随着时间的推移和反复练习,你会发现这些内容开始变得更加直观和易懂。

    🚀 程序逻辑

    我们已经解压了指令数据,准备投入实际使用。现在,我们需要针对每个指令编写相应的逻辑处理。这部分其实是最为直观和简单的!相对于复杂的反序列化处理,这一部分就像吃蛋糕一样轻松了(Anchor会为你处理大部分反序列化工作)。

    entrypoint!(process_instruction);

    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
    ) -> ProgramResult {

    // 解压指令数据
    let instruction = NoteInstruction::unpack(instruction_data)?;

    // 匹配指令并执行相应的逻辑
    match instruction {
    NoteInstruction::CreateNote { title, body, id } => {
    msg!("Instruction: CreateNote");
    create_note(accounts, title, body, id, program_id)
    }
    NoteInstruction::UpdateNote { title, body, id } => {
    msg!("Instruction: UpdateNote");
    update_note(accounts, title, body, id)
    }
    NoteInstruction::DeleteNote { id } => {
    msg!("Instruction: DeleteNote");
    delete_note(accounts, id)
    }
    }
    }

    首先,我们要做的是定义程序的入口函数。process_instruction 函数的定义与我们之前的 “Hello World” 程序一样。接着,我们将在 NoteInstruction 的实现中使用 unpack 函数来提取指令数据。然后,我们可以依靠 NoteInstruction 枚举来确定指令的具体类型。

    在本阶段,我们还没有涉及具体的逻辑处理,真正的构建将在后续阶段展开。

    📂 文件结构说明

    编写自定义程序时,将代码划分为不同的文件结构会非常有助于管理。这样做不仅方便代码重用,还能让你更快速地找到所需的内容。

    除了 lib.rs 文件外,我们还会把程序的各个部分放入不同的文件。最明显的一个例子就是 instruction.rs 文件。在这里,我们将定义指令数据类型并实现对指令数据的解包功能。

    你做得真棒👏👏👏

    我想借此机会对你的努力付出表示赞赏。你正在学习一些强大且实用的技能,这些技能不仅在 Solana 领域有用,Rust 的应用也非常广泛。尽管学习 Solana 可能会有一些困难,但请记住,这样的困难也有人曾经经历并战胜。例如,FormFunction 的创始人在大约一年前的推文中提到了他是如何找到困难的:

    FormFunction 已经筹集了超过 470 万美元,是我心目中 Solana 上最优秀的 1/1 NFT 平台。Matt 凭借坚持不懈的努力建立了一些令人难以置信的东西。试想一下,如果你掌握了这些技能,一年后你会站在哪里呢?

    - - +整个流程是这样的:

    实际上,宏在编译时生成了用于反序列化字符串的函数。通过使用这个Trait,我们可以告诉Rust:“嘿,我想能反序列化字符串,请为我生成相应的代码。”

    对于我们的情况,唯一的要求是Borsh必须支持所有的结构数据类型(在我们的场景中是字符串)。如果你有一个Borsh不支持的自定义数据类型,就需要在宏中自己实现这些功能。

    如果你还未完全理解,不用担心!我自己也是在看到整个流程后才理解的,所以现在让我们一起实践一下!

    🎂 把所有元素整合在一起

    我们刚刚讨论了一系列相互关联的抽象主题。如果只描述每一层,可能难以想象整个“蛋糕”的样子,所以我们现在就将它们整合起来。

    假设我们正在构建一个链上的笔记程序,我们将保持它的简单性:你只能创建、更新和删除笔记。我们需要一条指令来完成这些操作,所以让我们创建一个枚举类型来代表它:

    enum NoteInstruction {
    CreateNote {
    id: u64,
    title: String,
    body: String
    },
    UpdateNote {
    id: u64,
    title: String,
    body: String
    },
    DeleteNote {
    id: u64
    }
    }

    每个指令变体的字节数组都有自己的数据类型,我们在这里有它们!

    既然我们知道指令数据的样子,我们需要将其从字节转换为这些类型。第一步是反序列化,我们将使用一个专门为有效负载创建的新结构体上的 BorshDeserialize Traits 来完成。

    #[derive(BorshDeserialize)]
    struct NoteInstructionPayload {
    id: u64,
    title: String,
    body: String
    }

    我们在这里处理了titlebody,这就是字节数组中的内容。Borsh的工作仅仅是添加反序列化的支持,它实际上并没有进行反序列化,而是仅提供了我们可以调用的反序列化函数。

    下一步,我们要实际使用这些函数来反序列化数据。我们将在一个实现中定义这个行为,这是一个手动的过程(至少暂时是这样)!

    impl NoteInstruction {
    // 将传入的缓冲区解包到相关的指令
    // 输入的预期格式是一个用Borsh序列化的向量
    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
    // 采用第一个字节作为变体来确定要执行哪个指令
    let (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;
    // 使用临时有效载荷结构体进行反序列化
    let payload = NoteInstructionPayload::try_from_slice(rest).unwrap();
    // 通过变体匹配,确定函数所期望的数据结构,然后返回TestStruct或错误
    Ok(match variant {
    0 => Self::CreateNote {
    title: payload.title,
    body: payload.body,
    id: payload.id
    },
    1 => Self::UpdateNote {
    title: payload.title,
    body: payload.body,
    id: payload.id
    },
    2 => Self::DeleteNote {
    id: payload.id
    },
    _ => return Err(ProgramError::InvalidInstructionData)
    })
    }
    }

    这部分内容可能一开始看起来有点吓人,但你很快就会觉得它其实非常直接和简单!让我们一起来深入分析一下 🕺💃👯‍♂️

    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {

    我们的解包函数从指令中获取字节,并返回一个NoteInstruction类型(即 Self)或一个ProgramError

    let (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;

    现在是时候从字节中解包数据并调用反序列化函数了。我们的指令数据的第一个字节是一个整数,它告诉我们正在处理哪个指令。我们这样做的方式是使用Rust的内置函数split_first。如果切片为空,ok_or将返回ProgramError枚举中的InvalidInstructionData错误。

    let payload = NoteInstructionPayload::try_from_slice(rest).unwrap();

    现在我们有了两个变量要处理:指令指示器和指令的有效载荷(数据)。Borsh在我们的有效载荷结构中添加了try_from_slice函数,使我们可以在有效载荷变量rest上调用它。这就是反序列化的过程!

    接下来的步骤包括:

    我们unpack函数的最后一步是将反序列化的数据转换为枚举变体(即指令数据类型)。我们将使用匹配语句来完成这个任务,通过匹配指令指示器,我们可以返回枚举的正确变体。

    Ok(match variant {
    0 => Self::CreateNote {
    title: payload.title,
    body: payload.body,
    id: payload.id
    },
    1 => Self::UpdateNote {
    title: payload.title,
    body: payload.body,
    id: payload.id
    },
    2 => Self::DeleteNote {
    id: payload.id
    },
    _ => return Err(ProgramError::InvalidInstructionData)
    })

    现在你已经知道了整个过程!理解这一切确实需要集中精力,所以如果你需要多读几遍,也完全没关系。

    这部分内容信息量较大,可能会让人觉得有些复杂。但别担心,我们会通过大量的练习来逐渐熟悉它们。随着时间的推移和反复练习,你会发现这些内容开始变得更加直观和易懂。

    🚀 程序逻辑

    我们已经解压了指令数据,准备投入实际使用。现在,我们需要针对每个指令编写相应的逻辑处理。这部分其实是最为直观和简单的!相对于复杂的反序列化处理,这一部分就像吃蛋糕一样轻松了(Anchor会为你处理大部分反序列化工作)。

    entrypoint!(process_instruction);

    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
    ) -> ProgramResult {

    // 解压指令数据
    let instruction = NoteInstruction::unpack(instruction_data)?;

    // 匹配指令并执行相应的逻辑
    match instruction {
    NoteInstruction::CreateNote { title, body, id } => {
    msg!("Instruction: CreateNote");
    create_note(accounts, title, body, id, program_id)
    }
    NoteInstruction::UpdateNote { title, body, id } => {
    msg!("Instruction: UpdateNote");
    update_note(accounts, title, body, id)
    }
    NoteInstruction::DeleteNote { id } => {
    msg!("Instruction: DeleteNote");
    delete_note(accounts, id)
    }
    }
    }

    首先,我们要做的是定义程序的入口函数。process_instruction 函数的定义与我们之前的 “Hello World” 程序一样。接着,我们将在 NoteInstruction 的实现中使用 unpack 函数来提取指令数据。然后,我们可以依靠 NoteInstruction 枚举来确定指令的具体类型。

    在本阶段,我们还没有涉及具体的逻辑处理,真正的构建将在后续阶段展开。

    📂 文件结构说明

    编写自定义程序时,将代码划分为不同的文件结构会非常有助于管理。这样做不仅方便代码重用,还能让你更快速地找到所需的内容。

    除了 lib.rs 文件外,我们还会把程序的各个部分放入不同的文件。最明显的一个例子就是 instruction.rs 文件。在这里,我们将定义指令数据类型并实现对指令数据的解包功能。

    你做得真棒👏👏👏

    我想借此机会对你的努力付出表示赞赏。你正在学习一些强大且实用的技能,这些技能不仅在 Solana 领域有用,Rust 的应用也非常广泛。尽管学习 Solana 可能会有一些困难,但请记住,这样的困难也有人曾经经历并战胜。例如,FormFunction 的创始人在大约一年前的推文中提到了他是如何找到困难的:

    FormFunction 已经筹集了超过 470 万美元,是我心目中 Solana 上最优秀的 1/1 NFT 平台。Matt 凭借坚持不懈的努力建立了一些令人难以置信的东西。试想一下,如果你掌握了这些技能,一年后你会站在哪里呢?

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module3/nft-staking/build-an-nft-staking-program/index.html b/Solana-Co-Learn/module3/nft-staking/build-an-nft-staking-program/index.html index ad542b803..c2880230a 100644 --- a/Solana-Co-Learn/module3/nft-staking/build-an-nft-staking-program/index.html +++ b/Solana-Co-Learn/module3/nft-staking/build-an-nft-staking-program/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🛠️ 构建NFT质押程序

    今天,我们将编写质押程序,并实现所有必要的质押功能,暂时不涉及任何代币转账。我将陪伴你,一步一步讲解整个过程,解释每个环节,以便你了解正在进行的操作。首先,让我们进入Solana Playground,点击create a new project,并创建一个名为src的新文件夹,其中包括一个名为lib.rs的文件。

    这就是你的集成开发环境应该呈现的样子。

    目前,主要目标是编写一个程序,跟踪每个用户的质押状态。下面是一些基本步骤:

    准备就绪后,我们将继续创建剩余的文件,就像我们在之前的课程中所做的那样。让我们继续在你的src文件夹中创建以下5个文件:entrypoint.rserror.rsinstruction.rsprocessor.rsstate.rs

    现在,文件结构应该如下所示:

    我们已经准备好了!现在让我们用以下代码填充我们的lib.rs文件:

    // Lib.rs
    pub mod entrypoint;
    pub mod error;
    pub mod instruction;
    pub mod processor;
    pub mod state;

    接着,进入entrypoint.rs并添加以下代码:

    // Entrypoint.rs
    use solana_program::{
    account_info::AccountInfo,
    entrypoint,
    entrypoint::ProgramResult,
    pubkey::Pubkey
    };
    use crate::processor;

    // This macro will help process all incoming instructions
    entrypoint!(process_instruction);

    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
    ) -> ProgramResult {
    processor::process_instruction(program_id, accounts, instruction_data)?;
    Ok(())
    }

    当你运行代码时,会注意到一个错误,因为我们还没有在processor.rs中定义process_instruction函数。现在让我们创建这个函数。转到processor.rs并添加以下代码:

    // Processor.rs
    use solana_program::{
    account_info::AccountInfo,
    entrypoint::ProgramResult,
    pubkey::Pubkey
    };

    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
    ) -> ProgramResult {
    Ok(())
    }

    修复了processor.rs的错误后,你可能会注意到编译代码时仍有一些错误。这是因为在你的lib.rs中,我们导入了一些空模块。不过别担心,我们会在下一节中修复它们 😊 在开始处理process_instruction中的任何内容之前,我们需要在instruction.rs中编写我们的指令,所以让我们开始定义我们的指令。

    继续创建一个名为StakeInstruction的枚举,并向其中添加四个指令。基本上,这是定义我们的指令应执行什么操作的地方。将下面的代码复制粘贴到你的instruction.rs中:

    // Instruction.rs
    use solana_program::{ program_error::ProgramError };

    pub enum StakeInstruction {
    InitializeStakeAccount,
    Stake,
    Redeem,
    Unstake
    }

    impl StakeInstruction {
    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
    let (&variant, _rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;
    Ok(match variant {
    0 => Self::InitializeStakeAccount,
    1 => Self::Stake,
    2 => Self::Redeem,
    3 => Self::Unstake,
    _ => return Err(ProgramError::InvalidInstructionData)
    })
    }
    }

    现在让我们深入了解一下我们在这里做的事情。在instruction.rs中,我们创建了一个枚举,用来表示每个离散的指令,并创建了一个解包函数来反序列化数据,这里的数据是一个整数。

    让我们返回到 processor.rs 文件并定义我们的 process_instruction 函数:

    // processor.rs
    use solana_program::{
    account_info::{AccountInfo, next_account_info},
    entrypoint::ProgramResult,
    pubkey::Pubkey,
    };
    use crate::instruction::StakeInstruction;

    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
    ) -> ProgramResult {
    let instruction = StakeInstruction::unpack(instruction_data)?;

    match instruction {
    StakeInstruction::InitializeStakeAccount => process_initialize_stake_account(program_id, accounts),
    StakeInstruction::Stake => process_stake(program_id, accounts),
    StakeInstruction::Redeem => process_redeem(program_id, accounts),
    StakeInstruction::Unstake => process_unstake(program_id, accounts),
    }
    }

    /**
    此函数的作用是创建一个属于您和您的NFT的独特PDA账户。
    这将存储有关程序状态的信息,从而决定它是否已质押。
    **/
    fn process_initialize_stake_account(
    program_id: &Pubkey,
    accounts: &[AccountInfo]
    ) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let user = next_account_info(account_info_iter)?;
    let nft_token = next_account_info(account_info_iter)?;
    let stake_state = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    Ok(())
    }

    fn process_stake(
    program_id: &Pubkey,
    accounts: &[AccountInfo]
    ) -> ProgramResult {
    Ok(())
    }

    fn process_redeem(
    program_id: &Pubkey,
    accounts: &[AccountInfo]
    ) -> ProgramResult {
    Ok(())
    }

    fn process_unstake(
    program_id: &Pubkey,
    accounts: &[AccountInfo]
    ) -> ProgramResult {
    Ok(())
    }

    请注意,我们在 process_initialize_stake_account 函数中定义了一些变量,但它们在任何地方都没有被使用。这是因为我们需要一个结构体来表示程序的当前状态。因此,让我们转到 state.rs 文件并定义我们的结构体。

    // state.rs
    use borsh::{BorshSerialize, BorshDeserialize};
    use solana_program::{
    program_pack::{IsInitialized, Sealed},
    pubkey::Pubkey,
    clock::UnixTimestamp,
    };

    #[derive(BorshSerialize, BorshDeserialize, Debug)]
    pub struct UserStakeInfo {
    pub is_initialized: bool,
    pub token_account: Pubkey,
    pub stake_start_time: UnixTimestamp,
    pub last_stake_redeem: UnixTimestamp,
    pub user_pubkey: Pubkey,
    pub stake_state: StakeState,
    }

    impl Sealed for UserStakeInfo {}
    impl IsInitialized for UserStakeInfo {
    fn is_initialized(&self) -> bool {
    self.is_initialized
    }
    }

    #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq)]
    pub enum StakeState {
    Staked,
    Unstaked,
    }

    🚫 自定义错误

    现在我们来到 error.rs 文件,为我们的程序定义一些自定义的错误。

    // error.rs
    use solana_program::{program_error::ProgramError};
    use thiserror::Error;

    #[derive(Debug, Error)]
    pub enum StakeError {
    #[error("账户尚未初始化")]
    UninitializedAccount,

    #[error("派生的PDA与传入的PDA不符")]
    InvalidPda,

    #[error("无效的代币账户")]
    InvalidTokenAccount,

    #[error("无效的质押账户")]
    InvalidStakeAccount,
    }

    impl From<StakeError> for ProgramError {
    fn from(e: StakeError) -> Self {
    ProgramError::Custom(e as u32)
    }
    }

    太棒了,现在您已经在 error.rs 文件中创建了自定义错误的枚举。当您运行程序时,不应再出现任何错误。

    🫙 完善代码

    现在,让我们返回到 processor.rs 文件,并完成 process_initialize_stake_account 函数。

    // processor.rs
    use solana_program::{
    account_info::{ AccountInfo, next_account_info },
    entrypoint::ProgramResult,
    pubkey::Pubkey,
    msg,
    sysvar::{ rent::Rent, Sysvar },
    clock::Clock,
    program_pack::IsInitialized,
    system_instruction,
    program::invoke_signed,
    borsh::try_from_slice_unchecked,
    program_error::ProgramError
    };
    use borsh::BorshSerialize;
    use crate::instruction::StakeInstruction;
    use crate::error::StakeError;
    use crate::state::{ UserStakeInfo, StakeState };

    fn process_initialize_stake_account(
    program_id: &Pubkey,
    accounts: &[AccountInfo]
    ) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let user = next_account_info(account_info_iter)?;
    let nft_token_account = next_account_info(account_info_iter)?;
    let stake_state = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    let (stake_state_pda, bump_seed) = Pubkey::find_program_address(
    &[user.key.as_ref(), nft_token_account.key.as_ref()],
    program_id
    );

    // Check to ensure that you're using the right PDA
    if stake_state_pda != *stake_state.key {
    msg!("Invalid seeds for PDA");
    return Err(StakeError::InvalidPda.into());
    }

    let rent = Rent::get()?;
    let rent_lamports = rent.minimum_balance(UserStakeInfo::SIZE);

    msg!("Creating state account at {:?}", stake_state_pda);
    invoke_signed(
    &system_instruction::create_account(
    user.key,
    stake_state.key,
    rent_lamports,
    UserStakeInfo::SIZE.try_into().unwrap(),
    program_id
    ),
    &[user.clone(), stake_state.clone(), system_program.clone()],
    &[&[
    user.key.as_ref(),
    nft_token_account.key.as_ref(),
    &[bump_seed],
    ]],
    )?;

    // Let's create account
    let mut account_data = try_from_slice_unchecked::<UserStakeInfo>(&stake_state.data.borrow()).unwrap();

    if account_data.is_initialized() {
    msg!("Account already initialized");
    return Err(ProgramError::AccountAlreadyInitialized);
    }

    account_data.token_account = *nft_token_account.key;
    account_data.user_pubkey = *user.key;
    account_data.stake_state = StakeState::Unstaked;
    account_data.is_initialized = true;

    account_data.serialize(&mut &mut stake_state.data.borrow_mut()[..])?;

    Ok(())
    }

    让我们转到 state.rs 文件,并存储用户的质押信息,使用适当的数据大小。您可以将此代码段放在 impl Sealed 之上。

    // state.rs

    impl UserStakeInfo {
    /**
    这里是我们如何确定数据大小的方法。在您的UserStakeInfo结构体中,我们有以下数据:

    pub is_initialized: bool, // 1字节
    pub token_account: Pubkey, // 32字节
    pub stake_start_time: UnixTimestamp, // 64字节
    pub last_stake_redeem: UnixTimestamp, // 64字节
    pub user_pubkey: Pubkey, // 32字节
    pub stake_state: StakeState, // 1字节
    **/
    pub const SIZE: usize = 1 + 32 + 64 + 64 + 32 + 1;
    }

    现在我们刚刚为 process_initialize_stake_account 写了许多代码。如果您还不明白,请不要担心。我们将继续添加更多的代码来实现其他功能。现在让我们进入 process_stake 函数并使用这段代码。请记住,这只是代码的一部分,不要盲目地复制粘贴。

    // processor.rs

    fn process_stake(
    program_id: &Pubkey,
    accounts: &[AccountInfo]
    ) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let user = next_account_info(account_info_iter)?;
    let nft_token_account = next_account_info(account_info_iter)?;
    let stake_state = next_account_info(account_info_iter)?;

    let (stake_state_pda, _bump_seed) = Pubkey::find_program_address(
    &[user.key.as_ref(), nft_token_account.key.as_ref()],
    program_id,
    );

    if stake_state_pda != *stake_state.key {
    msg!("PDA种子无效");
    return Err(StakeError::InvalidPda.into());
    }

    // 创建账户
    let mut account_data = try_from_slice_unchecked::<UserStakeInfo>(&stake_state.data.borrow()).unwrap();

    if !account_data.is_initialized() {
    msg!("账户尚未初始化");
    return Err(ProgramError::UninitializedAccount.into());
    }

    let clock = Clock::get()?;

    account_data.token_account = *nft_token_account.key;
    account_data.user_pubkey = *user.key;
    account_data.stake_state = StakeState::Staked;
    account_data.stake_start_time = clock.unix_timestamp;
    account_data.last_stake_redeem = clock.unix_timestamp;
    account_data.is_initialized = true;

    account_data.serialize(&mut &mut stake_state.data.borrow_mut()[..])?;

    Ok(())
    }

    就是这样!我们现在已经完成了 process_stake 函数。接下来,我们将继续 process_redeem 函数。该部分的代码将与前两个函数非常相似。

    // process.rs

    fn process_redeem(
    program_id: &Pubkey,
    accounts: &[AccountInfo]
    ) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let user = next_account_info(account_info_iter)?;
    let nft_token_account = next_account_info(account_info_iter)?;
    let stake_state = next_account_info(account_info_iter)?;

    let (stake_state_pda, _bump_seed) = Pubkey::find_program_address(
    &[user.key.as_ref(), nft_token_account.key.as_ref()],
    program_id,
    );

    if stake_state_pda != *stake_state.key {
    msg!("Invalid seeds for PDA");
    return Err(StakeError::InvalidPda.into());
    }

    // For verification, we need to make sure it's the right signer
    if !user.is_signer {
    msg!("Missing required signature");
    return Err(ProgramError::MissingRequiredSignature);
    }

    // Let's create account
    let mut account_data = try_from_slice_unchecked::<UserStakeInfo>(&stake_state.data.borrow()).unwrap();

    if !account_data.is_initialized() {
    msg!("Account not initialized");
    return Err(ProgramError::UninitializedAccount.into());
    }

    if account_data.stake_state != StakeState::Staked {
    msg!("Stake account is not staking anything");
    return Err(ProgramError::InvalidArgument);
    }

    if *user.key != account_data.user_pubkey {
    msg!("Incorrect stake account for user");
    return Err(StakeError::InvalidStakeAccount.into());
    }

    if *nft_token_account.key != account_data.token_account {
    msg!("NFT Token account do not match");
    return Err(StakeError::InvalidTokenAccount.into());
    }

    let clock = Clock::get()?;
    let unix_time = clock.unix_timestamp - account_data.last_stake_redeem;
    let redeem_amount = unix_time;
    msg!("Redeeming {} tokens", redeem_amount);

    account_data.last_stake_redeem = clock.unix_timestamp;
    account_data.serialize(&mut &mut stake_state.data.borrow_mut()[..])?;

    Ok(())
    }

    太棒了!我们现在就快到了。接下来进入最后一个功能 process_unstake

    // process.rs

    fn process_unstake(
    program_id: &Pubkey,
    accounts: &[AccountInfo]
    ) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let user = next_account_info(account_info_iter)?;
    let nft_token_account = next_account_info(account_info_iter)?;
    let stake_state = next_account_info(account_info_iter)?;

    let (stake_state_pda, _bump_seed) = Pubkey::find_program_address(
    &[user.key.as_ref(), nft_token_account.key.as_ref()],
    program_id,
    );

    if stake_state_pda != *stake_state.key {
    msg!("Invalid seeds for PDA");
    return Err(StakeError::InvalidPda.into());
    }

    // For verification, we need to make sure it's the right signer
    if !user.is_signer {
    msg!("Missing required signature");
    return Err(ProgramError::MissingRequiredSignature);
    }

    // Let's create account
    let mut account_data = try_from_slice_unchecked::<UserStakeInfo>(&stake_state.data.borrow()).unwrap();

    if !account_data.is_initialized() {
    msg!("Account not initialized");
    return Err(ProgramError::UninitializedAccount.into());
    }

    if account_data.stake_state != StakeState::Staked {
    msg!("Stake account is not staking anything");
    return Err(ProgramError::InvalidArgument)
    }

    let clock = Clock::get()?;
    let unix_time = clock.unix_timestamp - account_data.last_stake_redeem;
    let redeem_amount = unix_time;
    msg!("Redeeming {} tokens", redeem_amount);

    msg!("Setting stake state to unstaked");
    account_data.stake_state = StakeState::Unstaked;
    account_data.serialize(&mut &mut stake_state.data.borrow_mut()[..]);

    Ok(())
    }

    LFG(Let's Go)!!! 我们终于完成了所有的函数定义。现在如果你运行程序,它应该会显示“构建成功”。太棒了!我们已经完成了第三周的任务,已经过半了!加油,继续保持这样的势头,我们一起朝着最终目标前进!如果你有任何问题或需要进一步的帮助,请随时联系我。

    - - +
    Skip to main content

    🛠️ 构建NFT质押程序

    今天,我们将编写质押程序,并实现所有必要的质押功能,暂时不涉及任何代币转账。我将陪伴你,一步一步讲解整个过程,解释每个环节,以便你了解正在进行的操作。首先,让我们进入Solana Playground,点击create a new project,并创建一个名为src的新文件夹,其中包括一个名为lib.rs的文件。

    这就是你的集成开发环境应该呈现的样子。

    目前,主要目标是编写一个程序,跟踪每个用户的质押状态。下面是一些基本步骤:

    准备就绪后,我们将继续创建剩余的文件,就像我们在之前的课程中所做的那样。让我们继续在你的src文件夹中创建以下5个文件:entrypoint.rserror.rsinstruction.rsprocessor.rsstate.rs

    现在,文件结构应该如下所示:

    我们已经准备好了!现在让我们用以下代码填充我们的lib.rs文件:

    // Lib.rs
    pub mod entrypoint;
    pub mod error;
    pub mod instruction;
    pub mod processor;
    pub mod state;

    接着,进入entrypoint.rs并添加以下代码:

    // Entrypoint.rs
    use solana_program::{
    account_info::AccountInfo,
    entrypoint,
    entrypoint::ProgramResult,
    pubkey::Pubkey
    };
    use crate::processor;

    // This macro will help process all incoming instructions
    entrypoint!(process_instruction);

    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
    ) -> ProgramResult {
    processor::process_instruction(program_id, accounts, instruction_data)?;
    Ok(())
    }

    当你运行代码时,会注意到一个错误,因为我们还没有在processor.rs中定义process_instruction函数。现在让我们创建这个函数。转到processor.rs并添加以下代码:

    // Processor.rs
    use solana_program::{
    account_info::AccountInfo,
    entrypoint::ProgramResult,
    pubkey::Pubkey
    };

    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
    ) -> ProgramResult {
    Ok(())
    }

    修复了processor.rs的错误后,你可能会注意到编译代码时仍有一些错误。这是因为在你的lib.rs中,我们导入了一些空模块。不过别担心,我们会在下一节中修复它们 😊 在开始处理process_instruction中的任何内容之前,我们需要在instruction.rs中编写我们的指令,所以让我们开始定义我们的指令。

    继续创建一个名为StakeInstruction的枚举,并向其中添加四个指令。基本上,这是定义我们的指令应执行什么操作的地方。将下面的代码复制粘贴到你的instruction.rs中:

    // Instruction.rs
    use solana_program::{ program_error::ProgramError };

    pub enum StakeInstruction {
    InitializeStakeAccount,
    Stake,
    Redeem,
    Unstake
    }

    impl StakeInstruction {
    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
    let (&variant, _rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;
    Ok(match variant {
    0 => Self::InitializeStakeAccount,
    1 => Self::Stake,
    2 => Self::Redeem,
    3 => Self::Unstake,
    _ => return Err(ProgramError::InvalidInstructionData)
    })
    }
    }

    现在让我们深入了解一下我们在这里做的事情。在instruction.rs中,我们创建了一个枚举,用来表示每个离散的指令,并创建了一个解包函数来反序列化数据,这里的数据是一个整数。

    让我们返回到 processor.rs 文件并定义我们的 process_instruction 函数:

    // processor.rs
    use solana_program::{
    account_info::{AccountInfo, next_account_info},
    entrypoint::ProgramResult,
    pubkey::Pubkey,
    };
    use crate::instruction::StakeInstruction;

    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
    ) -> ProgramResult {
    let instruction = StakeInstruction::unpack(instruction_data)?;

    match instruction {
    StakeInstruction::InitializeStakeAccount => process_initialize_stake_account(program_id, accounts),
    StakeInstruction::Stake => process_stake(program_id, accounts),
    StakeInstruction::Redeem => process_redeem(program_id, accounts),
    StakeInstruction::Unstake => process_unstake(program_id, accounts),
    }
    }

    /**
    此函数的作用是创建一个属于您和您的NFT的独特PDA账户。
    这将存储有关程序状态的信息,从而决定它是否已质押。
    **/
    fn process_initialize_stake_account(
    program_id: &Pubkey,
    accounts: &[AccountInfo]
    ) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let user = next_account_info(account_info_iter)?;
    let nft_token = next_account_info(account_info_iter)?;
    let stake_state = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    Ok(())
    }

    fn process_stake(
    program_id: &Pubkey,
    accounts: &[AccountInfo]
    ) -> ProgramResult {
    Ok(())
    }

    fn process_redeem(
    program_id: &Pubkey,
    accounts: &[AccountInfo]
    ) -> ProgramResult {
    Ok(())
    }

    fn process_unstake(
    program_id: &Pubkey,
    accounts: &[AccountInfo]
    ) -> ProgramResult {
    Ok(())
    }

    请注意,我们在 process_initialize_stake_account 函数中定义了一些变量,但它们在任何地方都没有被使用。这是因为我们需要一个结构体来表示程序的当前状态。因此,让我们转到 state.rs 文件并定义我们的结构体。

    // state.rs
    use borsh::{BorshSerialize, BorshDeserialize};
    use solana_program::{
    program_pack::{IsInitialized, Sealed},
    pubkey::Pubkey,
    clock::UnixTimestamp,
    };

    #[derive(BorshSerialize, BorshDeserialize, Debug)]
    pub struct UserStakeInfo {
    pub is_initialized: bool,
    pub token_account: Pubkey,
    pub stake_start_time: UnixTimestamp,
    pub last_stake_redeem: UnixTimestamp,
    pub user_pubkey: Pubkey,
    pub stake_state: StakeState,
    }

    impl Sealed for UserStakeInfo {}
    impl IsInitialized for UserStakeInfo {
    fn is_initialized(&self) -> bool {
    self.is_initialized
    }
    }

    #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq)]
    pub enum StakeState {
    Staked,
    Unstaked,
    }

    🚫 自定义错误

    现在我们来到 error.rs 文件,为我们的程序定义一些自定义的错误。

    // error.rs
    use solana_program::{program_error::ProgramError};
    use thiserror::Error;

    #[derive(Debug, Error)]
    pub enum StakeError {
    #[error("账户尚未初始化")]
    UninitializedAccount,

    #[error("派生的PDA与传入的PDA不符")]
    InvalidPda,

    #[error("无效的代币账户")]
    InvalidTokenAccount,

    #[error("无效的质押账户")]
    InvalidStakeAccount,
    }

    impl From<StakeError> for ProgramError {
    fn from(e: StakeError) -> Self {
    ProgramError::Custom(e as u32)
    }
    }

    太棒了,现在您已经在 error.rs 文件中创建了自定义错误的枚举。当您运行程序时,不应再出现任何错误。

    🫙 完善代码

    现在,让我们返回到 processor.rs 文件,并完成 process_initialize_stake_account 函数。

    // processor.rs
    use solana_program::{
    account_info::{ AccountInfo, next_account_info },
    entrypoint::ProgramResult,
    pubkey::Pubkey,
    msg,
    sysvar::{ rent::Rent, Sysvar },
    clock::Clock,
    program_pack::IsInitialized,
    system_instruction,
    program::invoke_signed,
    borsh::try_from_slice_unchecked,
    program_error::ProgramError
    };
    use borsh::BorshSerialize;
    use crate::instruction::StakeInstruction;
    use crate::error::StakeError;
    use crate::state::{ UserStakeInfo, StakeState };

    fn process_initialize_stake_account(
    program_id: &Pubkey,
    accounts: &[AccountInfo]
    ) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let user = next_account_info(account_info_iter)?;
    let nft_token_account = next_account_info(account_info_iter)?;
    let stake_state = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    let (stake_state_pda, bump_seed) = Pubkey::find_program_address(
    &[user.key.as_ref(), nft_token_account.key.as_ref()],
    program_id
    );

    // Check to ensure that you're using the right PDA
    if stake_state_pda != *stake_state.key {
    msg!("Invalid seeds for PDA");
    return Err(StakeError::InvalidPda.into());
    }

    let rent = Rent::get()?;
    let rent_lamports = rent.minimum_balance(UserStakeInfo::SIZE);

    msg!("Creating state account at {:?}", stake_state_pda);
    invoke_signed(
    &system_instruction::create_account(
    user.key,
    stake_state.key,
    rent_lamports,
    UserStakeInfo::SIZE.try_into().unwrap(),
    program_id
    ),
    &[user.clone(), stake_state.clone(), system_program.clone()],
    &[&[
    user.key.as_ref(),
    nft_token_account.key.as_ref(),
    &[bump_seed],
    ]],
    )?;

    // Let's create account
    let mut account_data = try_from_slice_unchecked::<UserStakeInfo>(&stake_state.data.borrow()).unwrap();

    if account_data.is_initialized() {
    msg!("Account already initialized");
    return Err(ProgramError::AccountAlreadyInitialized);
    }

    account_data.token_account = *nft_token_account.key;
    account_data.user_pubkey = *user.key;
    account_data.stake_state = StakeState::Unstaked;
    account_data.is_initialized = true;

    account_data.serialize(&mut &mut stake_state.data.borrow_mut()[..])?;

    Ok(())
    }

    让我们转到 state.rs 文件,并存储用户的质押信息,使用适当的数据大小。您可以将此代码段放在 impl Sealed 之上。

    // state.rs

    impl UserStakeInfo {
    /**
    这里是我们如何确定数据大小的方法。在您的UserStakeInfo结构体中,我们有以下数据:

    pub is_initialized: bool, // 1字节
    pub token_account: Pubkey, // 32字节
    pub stake_start_time: UnixTimestamp, // 64字节
    pub last_stake_redeem: UnixTimestamp, // 64字节
    pub user_pubkey: Pubkey, // 32字节
    pub stake_state: StakeState, // 1字节
    **/
    pub const SIZE: usize = 1 + 32 + 64 + 64 + 32 + 1;
    }

    现在我们刚刚为 process_initialize_stake_account 写了许多代码。如果您还不明白,请不要担心。我们将继续添加更多的代码来实现其他功能。现在让我们进入 process_stake 函数并使用这段代码。请记住,这只是代码的一部分,不要盲目地复制粘贴。

    // processor.rs

    fn process_stake(
    program_id: &Pubkey,
    accounts: &[AccountInfo]
    ) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let user = next_account_info(account_info_iter)?;
    let nft_token_account = next_account_info(account_info_iter)?;
    let stake_state = next_account_info(account_info_iter)?;

    let (stake_state_pda, _bump_seed) = Pubkey::find_program_address(
    &[user.key.as_ref(), nft_token_account.key.as_ref()],
    program_id,
    );

    if stake_state_pda != *stake_state.key {
    msg!("PDA种子无效");
    return Err(StakeError::InvalidPda.into());
    }

    // 创建账户
    let mut account_data = try_from_slice_unchecked::<UserStakeInfo>(&stake_state.data.borrow()).unwrap();

    if !account_data.is_initialized() {
    msg!("账户尚未初始化");
    return Err(ProgramError::UninitializedAccount.into());
    }

    let clock = Clock::get()?;

    account_data.token_account = *nft_token_account.key;
    account_data.user_pubkey = *user.key;
    account_data.stake_state = StakeState::Staked;
    account_data.stake_start_time = clock.unix_timestamp;
    account_data.last_stake_redeem = clock.unix_timestamp;
    account_data.is_initialized = true;

    account_data.serialize(&mut &mut stake_state.data.borrow_mut()[..])?;

    Ok(())
    }

    就是这样!我们现在已经完成了 process_stake 函数。接下来,我们将继续 process_redeem 函数。该部分的代码将与前两个函数非常相似。

    // process.rs

    fn process_redeem(
    program_id: &Pubkey,
    accounts: &[AccountInfo]
    ) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let user = next_account_info(account_info_iter)?;
    let nft_token_account = next_account_info(account_info_iter)?;
    let stake_state = next_account_info(account_info_iter)?;

    let (stake_state_pda, _bump_seed) = Pubkey::find_program_address(
    &[user.key.as_ref(), nft_token_account.key.as_ref()],
    program_id,
    );

    if stake_state_pda != *stake_state.key {
    msg!("Invalid seeds for PDA");
    return Err(StakeError::InvalidPda.into());
    }

    // For verification, we need to make sure it's the right signer
    if !user.is_signer {
    msg!("Missing required signature");
    return Err(ProgramError::MissingRequiredSignature);
    }

    // Let's create account
    let mut account_data = try_from_slice_unchecked::<UserStakeInfo>(&stake_state.data.borrow()).unwrap();

    if !account_data.is_initialized() {
    msg!("Account not initialized");
    return Err(ProgramError::UninitializedAccount.into());
    }

    if account_data.stake_state != StakeState::Staked {
    msg!("Stake account is not staking anything");
    return Err(ProgramError::InvalidArgument);
    }

    if *user.key != account_data.user_pubkey {
    msg!("Incorrect stake account for user");
    return Err(StakeError::InvalidStakeAccount.into());
    }

    if *nft_token_account.key != account_data.token_account {
    msg!("NFT Token account do not match");
    return Err(StakeError::InvalidTokenAccount.into());
    }

    let clock = Clock::get()?;
    let unix_time = clock.unix_timestamp - account_data.last_stake_redeem;
    let redeem_amount = unix_time;
    msg!("Redeeming {} tokens", redeem_amount);

    account_data.last_stake_redeem = clock.unix_timestamp;
    account_data.serialize(&mut &mut stake_state.data.borrow_mut()[..])?;

    Ok(())
    }

    太棒了!我们现在就快到了。接下来进入最后一个功能 process_unstake

    // process.rs

    fn process_unstake(
    program_id: &Pubkey,
    accounts: &[AccountInfo]
    ) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let user = next_account_info(account_info_iter)?;
    let nft_token_account = next_account_info(account_info_iter)?;
    let stake_state = next_account_info(account_info_iter)?;

    let (stake_state_pda, _bump_seed) = Pubkey::find_program_address(
    &[user.key.as_ref(), nft_token_account.key.as_ref()],
    program_id,
    );

    if stake_state_pda != *stake_state.key {
    msg!("Invalid seeds for PDA");
    return Err(StakeError::InvalidPda.into());
    }

    // For verification, we need to make sure it's the right signer
    if !user.is_signer {
    msg!("Missing required signature");
    return Err(ProgramError::MissingRequiredSignature);
    }

    // Let's create account
    let mut account_data = try_from_slice_unchecked::<UserStakeInfo>(&stake_state.data.borrow()).unwrap();

    if !account_data.is_initialized() {
    msg!("Account not initialized");
    return Err(ProgramError::UninitializedAccount.into());
    }

    if account_data.stake_state != StakeState::Staked {
    msg!("Stake account is not staking anything");
    return Err(ProgramError::InvalidArgument)
    }

    let clock = Clock::get()?;
    let unix_time = clock.unix_timestamp - account_data.last_stake_redeem;
    let redeem_amount = unix_time;
    msg!("Redeeming {} tokens", redeem_amount);

    msg!("Setting stake state to unstaked");
    account_data.stake_state = StakeState::Unstaked;
    account_data.serialize(&mut &mut stake_state.data.borrow_mut()[..]);

    Ok(())
    }

    LFG(Let's Go)!!! 我们终于完成了所有的函数定义。现在如果你运行程序,它应该会显示“构建成功”。太棒了!我们已经完成了第三周的任务,已经过半了!加油,继续保持这样的势头,我们一起朝着最终目标前进!如果你有任何问题或需要进一步的帮助,请随时联系我。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module3/nft-staking/how-staking-works/index.html b/Solana-Co-Learn/module3/nft-staking/how-staking-works/index.html index f2370d4ec..0c6c245a3 100644 --- a/Solana-Co-Learn/module3/nft-staking/how-staking-works/index.html +++ b/Solana-Co-Learn/module3/nft-staking/how-staking-works/index.html @@ -9,15 +9,15 @@ - - + +
    Skip to main content

    🕒 质押工作机制详解

    恭喜你已经接近第三周的完成了!现在让我们将你学到的所有知识运用到你正在进行的NFT项目(buildoors项目)的相关质押计划中。

    我们希望你能完整地构建质押计划的所有内容,除了实际的代币功能部分。这表示在你计划与代币程序交互的任何环节,我们暂时只记录一条消息或跳过它,待下周再进行深入审视。

    目前,你的主要目标是开发一个能够跟踪每个用户质押状态的程序。下面是一些基本步骤:

    你应该设计4个指令:

    • 初始化质押账户(InitializeStakeAccount): 这将创建一个新账户,用于记录每个用户/非同质化代币组合的质押过程状态信息。该PDA的种子应由用户的公钥和非同质化代币的令牌账户组成。
    • 质押: 此指令通常用于实际质押操作。但目前,我们并不进行真正的质押,只是更新“状态”账户,以显示代币已被质押,及质押时间等信息。
    • 兑换: 这里是你会根据用户的抵押时间发放奖励代币的地方。目前,只需记录他们应得的代币数量(可以暂时设定每单位时间1个代币),并更新状态以显示上次兑换代币的时间。
    • 解除质押: 此处是你赎回任何多余代币并撤销NFT质押的地方。现阶段,这只意味着更新状态,以表明NFT未被质押,并记录应得的奖励代币数量。

    这确实是一项具有挑战性的任务。在查看参考解决方案或观看视频教程之前,试着自己先设计一些内容。如果没有做得完美,也没关系,挣扎是学习过程的一部分。 提示:你可以使用solana_program::clock::Clock来获取时间。如果需要,你可以查看文档。 -如果你已经尽力尝试了,还可以随时查看解决方案代码。如果你准备继续,欢迎开始为质押功能和与程序交互的用户界面进行开发。

    - - +如果你已经尽力尝试了,还可以随时查看解决方案代码。如果你准备继续,欢迎开始为质押功能和与程序交互的用户界面进行开发。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module3/nft-staking/index.html b/Solana-Co-Learn/module3/nft-staking/index.html index 941a75f84..2a8aeddaf 100644 --- a/Solana-Co-Learn/module3/nft-staking/index.html +++ b/Solana-Co-Learn/module3/nft-staking/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module3/security-and-validation/error-handling-and-data-validation/index.html b/Solana-Co-Learn/module3/security-and-validation/error-handling-and-data-validation/index.html index a2e9555ee..06b566bc4 100644 --- a/Solana-Co-Learn/module3/security-and-validation/error-handling-and-data-validation/index.html +++ b/Solana-Co-Learn/module3/security-and-validation/error-handling-and-data-validation/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    ❗ 错误处理和数据验证

    本节课将为你介绍一些程序安全方面的基本注意事项。虽然这并非全面的概述,但它能让你像攻击者那样思考,思索重要的问题:我如何破解这个程序?

    😡 自定义错误

    Rust具有非常强大的错误处理机制。你可能已经遇到了一些要求你必须处理异常情况的编译器规则。

    下面展示了如何为我们的笔记程序创建自定义错误的方法:

    use solana_program::{program_error::ProgramError};
    use thiserror::Error;

    #[derive(Error)]
    pub enum NoteError {
    #[error("Wrong Note Owner")]
    Forbidden,

    #[error("test is too long")]
    InvalidLength
    }

    通过derive宏属性,我们可以使NoteError枚举具有默认的错误Trait实现。

    每种错误类型我们都会通过#[error("...")]标记提供相应的错误信息。

    返回自定义错误

    程序返回的错误必须是ProgramError类型。通过impl,我们可以将自定义错误与ProgramError类型进行转换。

    impl From<NoteError> for ProgramError {
    fn from(e: NoteError) -> Self {
    ProgramError::Custom(e as u32)
    }
    }

    然后,当我们需要实际返回错误时,我们使用into()方法将错误转换为ProgramError的实例。

    if pda != *note_pda.key {
    return Err(NoteError::Forbidden.into());
    }

    🔓 基本安全准则

    以下几项基本的安全措施可以增强程序的安全性:

    • 所有权检查 - 确保账户归该程序所有。
    • 签名者检查 - 验证交易的签名者。
    • 通用账户验证 - 核实账户是否符合预期。
    • 数据验证 - 检查用户输入的有效性。

    总的来说,你应该始终验证来自用户的输入。当处理用户提供的数据时,这一点尤为重要。记得 - 程序不会保存状态。它们不知道谁是所有者,也不会检查谁在调用它们,除非你明确告诉它们。

    所有权检查

    所有权检查的目的是核实账户是否归预期的程序所有。务必确保只有你能够访问它。

    用户可能会发送结构与账户数据匹配但由不同程序创建的数据。

    if note_pda.owner != program_id {
    return Err(ProgramError::InvalidNoteAccount);
    }

    签名者检查

    签名者检查是为了验证账户是否已对交易签名。

    if !initializer.is_signer {
    msg!("缺少必要的签名");
    return Err(ProgramError::MissingRequiredSignature)
    }

    数据验证

    你还应该在适当的情况下验证客户提供的指令数据。

    let (pda, bump_seed) = PubKey::find_program_address(&[initializer.key.as_ref(), title.as_bytes().as_ref(),], program_id);

    if pda != *note_pda.key() {
    msg!("Invalid seeds for PDA");
    return Err(ProgramError::InvalidArgument);
    }

    例如,如果你的程序是一个游戏,用户可能会分配角色属性点。你可能需要验证分配的积分加上现有分配是否超出了最大限制。

    if character.agility + new_agility > 100 {
    msg!("属性点数不得超过100");
    return Err(AttributeError::TooHigh.into())
    }

    整数溢出和下溢

    Rust的整数有固定的大小,所以只能容纳特定范围的数字。如果进行算术运算的结果超出了该范围,那么结果会回绕。

    为了避免整数溢出和下溢,你可以:

    • 确保逻辑上不会发生溢出或下溢。
    • 使用checked_add等已检查的数学运算符代替+
    let first_int: u8 = 5;
    let second_int: u8 = 255;
    let sum = first_int.checked_add(second_int);

    想象一下,那些没有采取最基本安全措施的程序都有哪些漏洞等待被发现,那些漏洞赏金就在眼前🥵🤑。

    - - +
    Skip to main content

    ❗ 错误处理和数据验证

    本节课将为你介绍一些程序安全方面的基本注意事项。虽然这并非全面的概述,但它能让你像攻击者那样思考,思索重要的问题:我如何破解这个程序?

    😡 自定义错误

    Rust具有非常强大的错误处理机制。你可能已经遇到了一些要求你必须处理异常情况的编译器规则。

    下面展示了如何为我们的笔记程序创建自定义错误的方法:

    use solana_program::{program_error::ProgramError};
    use thiserror::Error;

    #[derive(Error)]
    pub enum NoteError {
    #[error("Wrong Note Owner")]
    Forbidden,

    #[error("test is too long")]
    InvalidLength
    }

    通过derive宏属性,我们可以使NoteError枚举具有默认的错误Trait实现。

    每种错误类型我们都会通过#[error("...")]标记提供相应的错误信息。

    返回自定义错误

    程序返回的错误必须是ProgramError类型。通过impl,我们可以将自定义错误与ProgramError类型进行转换。

    impl From<NoteError> for ProgramError {
    fn from(e: NoteError) -> Self {
    ProgramError::Custom(e as u32)
    }
    }

    然后,当我们需要实际返回错误时,我们使用into()方法将错误转换为ProgramError的实例。

    if pda != *note_pda.key {
    return Err(NoteError::Forbidden.into());
    }

    🔓 基本安全准则

    以下几项基本的安全措施可以增强程序的安全性:

    • 所有权检查 - 确保账户归该程序所有。
    • 签名者检查 - 验证交易的签名者。
    • 通用账户验证 - 核实账户是否符合预期。
    • 数据验证 - 检查用户输入的有效性。

    总的来说,你应该始终验证来自用户的输入。当处理用户提供的数据时,这一点尤为重要。记得 - 程序不会保存状态。它们不知道谁是所有者,也不会检查谁在调用它们,除非你明确告诉它们。

    所有权检查

    所有权检查的目的是核实账户是否归预期的程序所有。务必确保只有你能够访问它。

    用户可能会发送结构与账户数据匹配但由不同程序创建的数据。

    if note_pda.owner != program_id {
    return Err(ProgramError::InvalidNoteAccount);
    }

    签名者检查

    签名者检查是为了验证账户是否已对交易签名。

    if !initializer.is_signer {
    msg!("缺少必要的签名");
    return Err(ProgramError::MissingRequiredSignature)
    }

    数据验证

    你还应该在适当的情况下验证客户提供的指令数据。

    let (pda, bump_seed) = PubKey::find_program_address(&[initializer.key.as_ref(), title.as_bytes().as_ref(),], program_id);

    if pda != *note_pda.key() {
    msg!("Invalid seeds for PDA");
    return Err(ProgramError::InvalidArgument);
    }

    例如,如果你的程序是一个游戏,用户可能会分配角色属性点。你可能需要验证分配的积分加上现有分配是否超出了最大限制。

    if character.agility + new_agility > 100 {
    msg!("属性点数不得超过100");
    return Err(AttributeError::TooHigh.into())
    }

    整数溢出和下溢

    Rust的整数有固定的大小,所以只能容纳特定范围的数字。如果进行算术运算的结果超出了该范围,那么结果会回绕。

    为了避免整数溢出和下溢,你可以:

    • 确保逻辑上不会发生溢出或下溢。
    • 使用checked_add等已检查的数学运算符代替+
    let first_int: u8 = 5;
    let second_int: u8 = 255;
    let sum = first_int.checked_add(second_int);

    想象一下,那些没有采取最基本安全措施的程序都有哪些漏洞等待被发现,那些漏洞赏金就在眼前🥵🤑。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module3/security-and-validation/index.html b/Solana-Co-Learn/module3/security-and-validation/index.html index 249ae9270..bf6c57ec1 100644 --- a/Solana-Co-Learn/module3/security-and-validation/index.html +++ b/Solana-Co-Learn/module3/security-and-validation/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module3/security-and-validation/secure-our-program/index.html b/Solana-Co-Learn/module3/security-and-validation/secure-our-program/index.html index f220f1cb9..b1060c624 100644 --- a/Solana-Co-Learn/module3/security-and-validation/secure-our-program/index.html +++ b/Solana-Co-Learn/module3/security-and-validation/secure-our-program/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🔑 保障我们程序的安全

    是时候保障我们的Solana电影数据库程序不受到干扰了。我们将加入一些基础的安全防护,进行输入验证,并增添一个 update_movie_review 指令。

    我会为你在一个点击之下就开始,你可以看一下这个Playground设置链接

    完整的文件结构如下所示:

    • lib.rs - 注册模块
    • entrypoint.rs - 程序的入口点
    • instruction.rs - 指令数据的序列化与反序列化
    • processor.rs - 处理指令的程序逻辑
    • state.rs - 状态的序列化与反序列化
    • error.rs - 自定义程序错误

    请注意与“状态管理”结束时的初始代码所存在的不同。

    processor.rs 中:

    • account_len 函数里,将 add_movie_review 更改为固定大小的1000。

    • 通过这样做,当用户更新电影评论时,我们就无需担心重新分配大小或重新计算租金。

    // 从这里
    let account_len: usize = 1 + 1 + (4 + title.len()) + (4 + description.len());

    // 变为这里
    let account_len: usize = 1000;

    state.rs 中:

    • 实现了一个检查结构体上的 is_initialized 字段的函数。
    • Sealed 接口实现了 MovieAccountState ,这样就能指定 MovieAccountState 具有已知大小,并为其提供了一些编译器优化。
    // 在 state.rs 内
    impl Sealed for MovieAccountState {}

    impl IsInitialized for MovieAccountState {
    fn is_initialized(&self) -> bool {
    self.is_initialized
    }
    }

    我们从定义一些自定义错误开始吧!

    我们在以下情况下需要一些错误定义:

    • 在尚未初始化的账户上调用更新指令
    • 提供的 PDA 与预期或派生的 PDA 不匹配
    • 输入数据超出程序允许的范围
    • 所提供的评级不在 1-5 范围内

    error.rs 中:

    • 创建 ReviewError 的枚举类型
    • 实现转换为 ProgramError 的方法
    // 在 error.rs 内
    use solana_program::program_error::ProgramError;
    use thiserror::Error;

    #[derive(Debug, Error)]
    pub enum ReviewError{
    // error 0
    #[error("uninitialized account")]
    UninitializedAccount,
    // error 1
    #[error("Derived PDA did not match the given PDA")]
    InvalidPDA,
    // error 2
    #[error("input data length is too long")]
    InvalidDataLength,
    // error 3
    #[error("rating is out of range 5 or less than 1")]
    }

    impl From<ReviewError> for ProgramError {
    fn from(e: ReviewError) -> Self {
    ProgramError::Custom(e as u32)
    }
    }

    请前往 processor.rs 并将 ReviewError 纳入使用范围。

    // 在 processor.rs 内
    use crate::error::ReviewError;

    接下来,我们将对 add_movie_review 函数增加安全检查。

    签署人检查

    • 验证交易的评论的 initializer 是否同时也是交易的签署人。
    let account_info_iter = &mut accounts.iter();

    let initializer = next_account_info(account_info_iter)?;
    let pda_account = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    // add check here
    if !initializer.is_signer {
    msg!("Missing required signature");
    return Err(ProgramError::MissingRequiredSignature)
    }

    账户验证

    • 确认用户输入的 pda_account 是否与我们期望的 pda 匹配。
    let (pda, bump_seed) = Pubkey::find_program_address(&[initializer.key.as_ref(), title.as_bytes().as_ref(),], program_id);
    if pda != *pda_account.key {
    msg!("Invalid seeds for PDA");
    return Err(ProgramError::InvalidArgument)
    }

    数据验证

    • 确保 rating 落在 15 的评分范围内。我们不想看到 069 星的评级,真有趣呢。
    if rating > 5 || rating < 1 {
    msg!("Rating cannot be higher than 5");
    return Err(ReviewError::InvalidRating.into())
    }
    • 此外,我们还需检查评论内容的长度是否超出了分配的空间。
    let total_len: usize = 1 + 1 + (4 + title.len()) + (4 + description.len());
    if total_len > 1000 {
    msg!("Data length is larger than 1000 bytes");
    return Err(ReviewError::InvalidDataLength.into())
    }

    ⬆ 更新电影评论

    现在来到了有趣的部分!我们要添加 update_movie_review 指令。

    首先,在 instruction.rs 文件中,我们将从更新 MovieInstruction 枚举开始:

    // inside instruction.rs
    pub enum MovieInstruction {
    AddMovieReview {
    title: String,
    rating: u8,
    description: String
    },
    UpdateMovieReview {
    title: String,
    rating: u8,
    description: String
    }
    }

    Payload 结构体不需要更改,因为除了变体类型,指令数据与我们用于 AddMovieReview 的相同。

    然后我们要在同一个文件的 unpack 函数中添加这个新的变体。

    // inside instruction.rs
    impl MovieInstruction {
    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
    let (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;
    let payload = MovieReviewPayload::try_from_slice(rest).unwrap();
    Ok(match variant {
    0 => Self::AddMovieReview {
    title: payload.title,
    rating: payload.rating,
    description: payload.description },
    1 => Self::UpdateMovieReview {
    title: payload.title,
    rating: payload.rating,
    description: payload.description },
    _ => return Err(ProgramError::InvalidInstructionData)
    })
    }
    }

    最后,在 process_instruction 函数的匹配语句中添加 update_movie_review

    // inside processor.rs
    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
    ) -> ProgramResult {
    // unpack instruction data
    let instruction = MovieInstruction::unpack(instruction_data)?;
    match instruction {
    MovieInstruction::AddMovieReview { title, rating, description } => {
    add_movie_review(program_id, accounts, title, rating, description)
    },
    // add UpdateMovieReview to match against our new data structure
    MovieInstruction::UpdateMovieReview { title, rating, description } => {
    // make call to update function that we'll define next
    update_movie_review(program_id, accounts, title, rating, description)
    }
    }
    }

    以下是我们要更新的所有部分的概述,以添加新的指令:

    1. instruction.rs 文件中:

      • MovieInstruction 枚举中添加新变体
      • unpack 函数中添加新变体
      • (可选)添加新的负载结构体
    2. processor.rs 文件中:

      • process_instruction 匹配语句中添加新变体

    我们现在准备好编写实际的 update_movie_review 函数了!

    从账户迭代开始:

    pub fn update_movie_review(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    _title: String,
    rating: u8,
    description: String
    ) -> ProgramResult {
    msg!("Updating movie review...");

    // Get Account iterator
    let account_info_iter = &mut accounts.iter();

    // Get accounts
    let initializer = next_account_info(account_info_iter)?;
    let pda_account = next_account_info(account_info_iter)?;

    Ok(())
    }

    现在是检查 pda_account.owner 是否与 program_id 匹配的好时机。

    if pda_account.owner != program_id {
    return Err(ProgramError::IllegalOwner)
    }

    接下来,我们将检查签署人是否与初始化者匹配。

    if !initializer.is_signer {
    msg!("Missing required signature");
    return Err(ProgramError::MissingRequiredSignature)
    }

    现在,我们可以从 pda_account 中解压数据:

    msg!("unpacking state account");
    let mut account_data = try_from_slice_unchecked::<MovieAccountState>(&pda_account.data.borrow()).unwrap();
    msg!("borrowed account data");

    对这些全新数据的最后一轮验证:

    // Derive PDA and check that it matches client
    let (pda, _bump_seed) = Pubkey::find_program_address(&[initializer.key.as_ref(), account_data.title.as_bytes().as_ref(),], program_id);

    if pda != *pda_account.key {
    msg!("Invalid seeds for PDA");
    return Err(ReviewError::InvalidPDA.into())
    }

    if !account_data.is_initialized() {
    msg!("Account is not initialized");
    return Err(ReviewError::UninitializedAccount.into());
    }

    if rating > 5 || rating < 1 {
    msg!("Rating cannot be higher than 5");
    return Err(ReviewError::InvalidRating.into())
    }

    let total_len: usize = 1 + 1 + (4 + account_data.title.len()) + (4 + description.len());
    if total_len > 1000 {
    msg!("Data length is larger than 1000 bytes");
    return Err(ReviewError::InvalidDataLength.into())
    }

    哇哦,这一大堆的检查让我觉得自己像个银行出纳员似的,真有趣。

    最后一步是更新账户信息并将其序列化到账户中。

    account_data.rating = rating;
    account_data.description = description;

    account_data.serialize(&mut &mut pda_account.data.borrow_mut()[..])?;

    太棒了!我们在程序中添加了新的指令,并确保了其安全性。现在让我们来测试一下吧!

    构建 -> 升级 -> 复制地址 -> 粘贴到前端

    git clone https://github.com/all-in-one-solana/solana-movie-frontend
    cd solana-movie-frontend
    git checkout solution-update-reviews
    npm install

    你的前端现在应该能显示评论了!你可以添加评论,也可以更新你之前的评论!

    🚢 挑战

    现在,是时候让你亲自动手构建一些内容了。你可以以之前课程中用到的学生自我介绍项目为基础进行构建。

    学生自我介绍项目是Solana上的一个有趣项目,允许学生们在线上展示自己的身份。该项目会获取用户的姓名和简短留言作为instruction_data,并创建一个专门的账户来将这些信息储存在链上。

    结合你在本课程中学到的知识,尝试对学生自我介绍项目进行扩展。你应该完成以下任务:

    1. 新增指令:允许学生更新自己的留言。

    2. 安全实现:按照本节课所学,确保项目的基本安全性。

    你可以从这里获取起始代码。

    尽量自主完成这个挑战!如果遇到任何困难,你可以参考解决方案代码。不过请注意,根据你自己实施的检查和错误处理方式,你的代码可能会与解决方案略有不同。

    祝你挑战成功,玩得开心!

    - - +
    Skip to main content

    🔑 保障我们程序的安全

    是时候保障我们的Solana电影数据库程序不受到干扰了。我们将加入一些基础的安全防护,进行输入验证,并增添一个 update_movie_review 指令。

    我会为你在一个点击之下就开始,你可以看一下这个Playground设置链接

    完整的文件结构如下所示:

    • lib.rs - 注册模块
    • entrypoint.rs - 程序的入口点
    • instruction.rs - 指令数据的序列化与反序列化
    • processor.rs - 处理指令的程序逻辑
    • state.rs - 状态的序列化与反序列化
    • error.rs - 自定义程序错误

    请注意与“状态管理”结束时的初始代码所存在的不同。

    processor.rs 中:

    • account_len 函数里,将 add_movie_review 更改为固定大小的1000。

    • 通过这样做,当用户更新电影评论时,我们就无需担心重新分配大小或重新计算租金。

    // 从这里
    let account_len: usize = 1 + 1 + (4 + title.len()) + (4 + description.len());

    // 变为这里
    let account_len: usize = 1000;

    state.rs 中:

    • 实现了一个检查结构体上的 is_initialized 字段的函数。
    • Sealed 接口实现了 MovieAccountState ,这样就能指定 MovieAccountState 具有已知大小,并为其提供了一些编译器优化。
    // 在 state.rs 内
    impl Sealed for MovieAccountState {}

    impl IsInitialized for MovieAccountState {
    fn is_initialized(&self) -> bool {
    self.is_initialized
    }
    }

    我们从定义一些自定义错误开始吧!

    我们在以下情况下需要一些错误定义:

    • 在尚未初始化的账户上调用更新指令
    • 提供的 PDA 与预期或派生的 PDA 不匹配
    • 输入数据超出程序允许的范围
    • 所提供的评级不在 1-5 范围内

    error.rs 中:

    • 创建 ReviewError 的枚举类型
    • 实现转换为 ProgramError 的方法
    // 在 error.rs 内
    use solana_program::program_error::ProgramError;
    use thiserror::Error;

    #[derive(Debug, Error)]
    pub enum ReviewError{
    // error 0
    #[error("uninitialized account")]
    UninitializedAccount,
    // error 1
    #[error("Derived PDA did not match the given PDA")]
    InvalidPDA,
    // error 2
    #[error("input data length is too long")]
    InvalidDataLength,
    // error 3
    #[error("rating is out of range 5 or less than 1")]
    }

    impl From<ReviewError> for ProgramError {
    fn from(e: ReviewError) -> Self {
    ProgramError::Custom(e as u32)
    }
    }

    请前往 processor.rs 并将 ReviewError 纳入使用范围。

    // 在 processor.rs 内
    use crate::error::ReviewError;

    接下来,我们将对 add_movie_review 函数增加安全检查。

    签署人检查

    • 验证交易的评论的 initializer 是否同时也是交易的签署人。
    let account_info_iter = &mut accounts.iter();

    let initializer = next_account_info(account_info_iter)?;
    let pda_account = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    // add check here
    if !initializer.is_signer {
    msg!("Missing required signature");
    return Err(ProgramError::MissingRequiredSignature)
    }

    账户验证

    • 确认用户输入的 pda_account 是否与我们期望的 pda 匹配。
    let (pda, bump_seed) = Pubkey::find_program_address(&[initializer.key.as_ref(), title.as_bytes().as_ref(),], program_id);
    if pda != *pda_account.key {
    msg!("Invalid seeds for PDA");
    return Err(ProgramError::InvalidArgument)
    }

    数据验证

    • 确保 rating 落在 15 的评分范围内。我们不想看到 069 星的评级,真有趣呢。
    if rating > 5 || rating < 1 {
    msg!("Rating cannot be higher than 5");
    return Err(ReviewError::InvalidRating.into())
    }
    • 此外,我们还需检查评论内容的长度是否超出了分配的空间。
    let total_len: usize = 1 + 1 + (4 + title.len()) + (4 + description.len());
    if total_len > 1000 {
    msg!("Data length is larger than 1000 bytes");
    return Err(ReviewError::InvalidDataLength.into())
    }

    ⬆ 更新电影评论

    现在来到了有趣的部分!我们要添加 update_movie_review 指令。

    首先,在 instruction.rs 文件中,我们将从更新 MovieInstruction 枚举开始:

    // inside instruction.rs
    pub enum MovieInstruction {
    AddMovieReview {
    title: String,
    rating: u8,
    description: String
    },
    UpdateMovieReview {
    title: String,
    rating: u8,
    description: String
    }
    }

    Payload 结构体不需要更改,因为除了变体类型,指令数据与我们用于 AddMovieReview 的相同。

    然后我们要在同一个文件的 unpack 函数中添加这个新的变体。

    // inside instruction.rs
    impl MovieInstruction {
    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
    let (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;
    let payload = MovieReviewPayload::try_from_slice(rest).unwrap();
    Ok(match variant {
    0 => Self::AddMovieReview {
    title: payload.title,
    rating: payload.rating,
    description: payload.description },
    1 => Self::UpdateMovieReview {
    title: payload.title,
    rating: payload.rating,
    description: payload.description },
    _ => return Err(ProgramError::InvalidInstructionData)
    })
    }
    }

    最后,在 process_instruction 函数的匹配语句中添加 update_movie_review

    // inside processor.rs
    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
    ) -> ProgramResult {
    // unpack instruction data
    let instruction = MovieInstruction::unpack(instruction_data)?;
    match instruction {
    MovieInstruction::AddMovieReview { title, rating, description } => {
    add_movie_review(program_id, accounts, title, rating, description)
    },
    // add UpdateMovieReview to match against our new data structure
    MovieInstruction::UpdateMovieReview { title, rating, description } => {
    // make call to update function that we'll define next
    update_movie_review(program_id, accounts, title, rating, description)
    }
    }
    }

    以下是我们要更新的所有部分的概述,以添加新的指令:

    1. instruction.rs 文件中:

      • MovieInstruction 枚举中添加新变体
      • unpack 函数中添加新变体
      • (可选)添加新的负载结构体
    2. processor.rs 文件中:

      • process_instruction 匹配语句中添加新变体

    我们现在准备好编写实际的 update_movie_review 函数了!

    从账户迭代开始:

    pub fn update_movie_review(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    _title: String,
    rating: u8,
    description: String
    ) -> ProgramResult {
    msg!("Updating movie review...");

    // Get Account iterator
    let account_info_iter = &mut accounts.iter();

    // Get accounts
    let initializer = next_account_info(account_info_iter)?;
    let pda_account = next_account_info(account_info_iter)?;

    Ok(())
    }

    现在是检查 pda_account.owner 是否与 program_id 匹配的好时机。

    if pda_account.owner != program_id {
    return Err(ProgramError::IllegalOwner)
    }

    接下来,我们将检查签署人是否与初始化者匹配。

    if !initializer.is_signer {
    msg!("Missing required signature");
    return Err(ProgramError::MissingRequiredSignature)
    }

    现在,我们可以从 pda_account 中解压数据:

    msg!("unpacking state account");
    let mut account_data = try_from_slice_unchecked::<MovieAccountState>(&pda_account.data.borrow()).unwrap();
    msg!("borrowed account data");

    对这些全新数据的最后一轮验证:

    // Derive PDA and check that it matches client
    let (pda, _bump_seed) = Pubkey::find_program_address(&[initializer.key.as_ref(), account_data.title.as_bytes().as_ref(),], program_id);

    if pda != *pda_account.key {
    msg!("Invalid seeds for PDA");
    return Err(ReviewError::InvalidPDA.into())
    }

    if !account_data.is_initialized() {
    msg!("Account is not initialized");
    return Err(ReviewError::UninitializedAccount.into());
    }

    if rating > 5 || rating < 1 {
    msg!("Rating cannot be higher than 5");
    return Err(ReviewError::InvalidRating.into())
    }

    let total_len: usize = 1 + 1 + (4 + account_data.title.len()) + (4 + description.len());
    if total_len > 1000 {
    msg!("Data length is larger than 1000 bytes");
    return Err(ReviewError::InvalidDataLength.into())
    }

    哇哦,这一大堆的检查让我觉得自己像个银行出纳员似的,真有趣。

    最后一步是更新账户信息并将其序列化到账户中。

    account_data.rating = rating;
    account_data.description = description;

    account_data.serialize(&mut &mut pda_account.data.borrow_mut()[..])?;

    太棒了!我们在程序中添加了新的指令,并确保了其安全性。现在让我们来测试一下吧!

    构建 -> 升级 -> 复制地址 -> 粘贴到前端

    git clone https://github.com/all-in-one-solana/solana-movie-frontend
    cd solana-movie-frontend
    git checkout solution-update-reviews
    npm install

    你的前端现在应该能显示评论了!你可以添加评论,也可以更新你之前的评论!

    🚢 挑战

    现在,是时候让你亲自动手构建一些内容了。你可以以之前课程中用到的学生自我介绍项目为基础进行构建。

    学生自我介绍项目是Solana上的一个有趣项目,允许学生们在线上展示自己的身份。该项目会获取用户的姓名和简短留言作为instruction_data,并创建一个专门的账户来将这些信息储存在链上。

    结合你在本课程中学到的知识,尝试对学生自我介绍项目进行扩展。你应该完成以下任务:

    1. 新增指令:允许学生更新自己的留言。

    2. 安全实现:按照本节课所学,确保项目的基本安全性。

    你可以从这里获取起始代码。

    尽量自主完成这个挑战!如果遇到任何困难,你可以参考解决方案代码。不过请注意,根据你自己实施的检查和错误处理方式,你的代码可能会与解决方案略有不同。

    祝你挑战成功,玩得开心!

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module4/cross-program-invocations/build-a-payment-system-with-cpis/index.html b/Solana-Co-Learn/module4/cross-program-invocations/build-a-payment-system-with-cpis/index.html index a77ffa9a6..588de1c16 100644 --- a/Solana-Co-Learn/module4/cross-program-invocations/build-a-payment-system-with-cpis/index.html +++ b/Solana-Co-Learn/module4/cross-program-invocations/build-a-payment-system-with-cpis/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    💸 使用CPI构建支付系统

    上一堂课我们已经完成了Mint账户的准备工作,热身环节到此结束,现在正式开始主要表演。

    我们将深入到审查和评论的工作流程中,并添加必要的逻辑来铸造代币。

    我们首先从电影评论开始。请转到 processor.rs 文件,并更新 add_movie_review 函数,以便接收额外的账户。

    // Inside add_movie_review
    msg!("Adding movie review...");
    msg!("Title: {}", title);
    msg!("Rating: {}", rating);
    msg!("Description: {}", description);

    let account_info_iter = &mut accounts.iter();

    let initializer = next_account_info(account_info_iter)?;
    let pda_account = next_account_info(account_info_iter)?;
    let pda_counter = next_account_info(account_info_iter)?;
    let token_mint = next_account_info(account_info_iter)?;
    let mint_auth = next_account_info(account_info_iter)?;
    let user_ata = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;
    let token_program = next_account_info(account_info_iter)?;

    新增的部分包括:

    • token_mint - 代币的铸币地址。
    • mint_auth - 代币铸造机构的地址。
    • user_ata - 用户与此代币发行机构关联的令牌账户(用于代币铸造)。
    • token_program - 代币程序的地址。

    这里并没有太多特殊之处,这些只是处理代币时所期望的账户。

    还记得我们的编程习惯吗?每次添加一个账户后,立即添加验证!以下是我们需要在 add_movie_review 函数中添加的内容:

    msg!("deriving mint authority");
    let (mint_pda, _mint_bump) = Pubkey::find_program_address(&[b"token_mint"], program_id);
    let (mint_auth_pda, mint_auth_bump) =
    Pubkey::find_program_address(&[b"token_auth"], program_id);

    if *token_mint.key != mint_pda {
    msg!("Incorrect token mint");
    return Err(ReviewError::IncorrectAccountError.into());
    }

    if *mint_auth.key != mint_auth_pda {
    msg!("Mint passed in and mint derived do not match");
    return Err(ReviewError::InvalidPDA.into());
    }

    if *user_ata.key != get_associated_token_address(initializer.key, token_mint.key) {
    msg!("Incorrect token mint");
    return Err(ReviewError::IncorrectAccountError.into());
    }

    if *token_program.key != TOKEN_PROGRAM_ID {
    msg!("Incorrect token program");
    return Err(ReviewError::IncorrectAccountError.into());
    }

    你现在已经反复实践过这样的流程,所以这些操作应该感觉得相当熟悉了 :)

    现在我们可以开始铸币了!就在程序结束之前,我们会添加如下代码: Ok(())

    msg!("Minting 10 tokens to User associated token account");
    invoke_signed(
    // Instruction
    &spl_token::instruction::mint_to(
    token_program.key,
    token_mint.key,
    user_ata.key,
    mint_auth.key,
    &[],
    10*LAMPORTS_PER_SOL,
    )?,
    // Account_infos
    &[token_mint.clone(), user_ata.clone(), mint_auth.clone()],
    // Seeds
    &[&[b"token_auth", &[mint_auth_bump]]],
    )?;

    Ok(())

    mint_to 是来自SPL令牌库的指令,所以我们还需更新顶部的导入内容:

    // Existing imports
    use spl_token::{instruction::{initialize_mint, mint_to}, ID as TOKEN_PROGRAM_ID};

    我们的评论功能已经完成了!现在每当有人留下评论时,我们就会给他们发送10个代币。

    我们将在 add_comment 函数中执行完全相同的操作: processor.rs

    // Inside add_comment
    let account_info_iter = &mut accounts.iter();

    let commenter = next_account_info(account_info_iter)?;
    let pda_review = next_account_info(account_info_iter)?;
    let pda_counter = next_account_info(account_info_iter)?;
    let pda_comment = next_account_info(account_info_iter)?;
    let token_mint = next_account_info(account_info_iter)?;
    let mint_auth = next_account_info(account_info_iter)?;
    let user_ata = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;
    let token_program = next_account_info(account_info_iter)?;

    // Mint tokens here
    msg!("deriving mint authority");
    let (mint_pda, _mint_bump) = Pubkey::find_program_address(&[b"token_mint"], program_id);
    let (mint_auth_pda, mint_auth_bump) =
    Pubkey::find_program_address(&[b"token_auth"], program_id);

    if *token_mint.key != mint_pda {
    msg!("Incorrect token mint");
    return Err(ReviewError::IncorrectAccountError.into());
    }

    if *mint_auth.key != mint_auth_pda {
    msg!("Mint passed in and mint derived do not match");
    return Err(ReviewError::InvalidPDA.into());
    }

    if *user_ata.key != get_associated_token_address(commenter.key, token_mint.key) {
    msg!("Incorrect token mint");
    return Err(ReviewError::IncorrectAccountError.into());
    }

    if *token_program.key != TOKEN_PROGRAM_ID {
    msg!("Incorrect token program");
    return Err(ReviewError::IncorrectAccountError.into());
    }
    msg!("Minting 5 tokens to User associated token account");
    invoke_signed(
    // Instruction
    &spl_token::instruction::mint_to(
    token_program.key,
    token_mint.key,
    user_ata.key,
    mint_auth.key,
    &[],
    5 * LAMPORTS_PER_SOL,
    )?,
    // Account_infos
    &[token_mint.clone(), user_ata.clone(), mint_auth.clone()],
    // Seeds
    &[&[b"token_auth", &[mint_auth_bump]]],
    )?;

    Ok(())

    注意,不要重复 Ok(()) ,因为那会导致错误。

    希望你现在能够看出这些模式的共通性了。虽然在进行本地开发时,我们需要写很多代码,但整个工作流程相当简单,并且感觉很“纯粹”。

    🚀 构建、部署和测试

    是时候赚取一些爆米花代币了 🍿

    首先,让我们开始构建和部署项目。

    cargo build-sbf
    solana program deploy <PATH>

    接下来,我们将测试初始化代币铸造流程。

    git clone https://github.com/buildspace/solana-movie-token-client
    cd solana-movie-token-client
    npm install

    和以前一样,需要进行以下操作:

    1. index.ts 中更新 PROGRAM_ID 的值。
    2. 修改第67行的连接为在线连接。
    const connection = new web3.Connection("http://localhost:8899");

    运行 npm start 后,你的 Mint 账户将会被初始化。

    最后,我们可以使用前端来发送电影评论,并因此获得一些代币。

    像往常一样,你可以继续使用之前停下的前端,或者从正确的分支创建一个新的实例。

    git clone https://github.com/buildspace/solana-movie-frontend/
    cd solana-movie-frontend
    git checkout solution-add-tokens
    npm install

    更新 PROGRAM_ID,提交评论,发表评论后,你现在应该能在 Phantom 钱包中看到你的代币了!

    🚢 挑战

    为了运用你在本课程中学到的有关 CPI 的知识,不妨考虑如何将其整合到学生介绍方案中。你可以做些类似我们演示中的事情,比如在用户自我介绍时铸造一些代币给他们。或者,如果你感到更有挑战性,思考如何将课程中学到的所有内容整合在一起,从零开始创建全新的项目。

    如果你选择做类似的演示,可以自由使用相同的脚本来调用 initialize_mint 指令,或者你可以展现创造力,从客户端初始化铸币过程,然后将铸币权限转移到程序 PDA。如果你需要查看可能的解决方案,请查看这个游乐场链接

    享受编程的乐趣,并将此视为自我提升的机会!

    - - +
    Skip to main content

    💸 使用CPI构建支付系统

    上一堂课我们已经完成了Mint账户的准备工作,热身环节到此结束,现在正式开始主要表演。

    我们将深入到审查和评论的工作流程中,并添加必要的逻辑来铸造代币。

    我们首先从电影评论开始。请转到 processor.rs 文件,并更新 add_movie_review 函数,以便接收额外的账户。

    // Inside add_movie_review
    msg!("Adding movie review...");
    msg!("Title: {}", title);
    msg!("Rating: {}", rating);
    msg!("Description: {}", description);

    let account_info_iter = &mut accounts.iter();

    let initializer = next_account_info(account_info_iter)?;
    let pda_account = next_account_info(account_info_iter)?;
    let pda_counter = next_account_info(account_info_iter)?;
    let token_mint = next_account_info(account_info_iter)?;
    let mint_auth = next_account_info(account_info_iter)?;
    let user_ata = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;
    let token_program = next_account_info(account_info_iter)?;

    新增的部分包括:

    • token_mint - 代币的铸币地址。
    • mint_auth - 代币铸造机构的地址。
    • user_ata - 用户与此代币发行机构关联的令牌账户(用于代币铸造)。
    • token_program - 代币程序的地址。

    这里并没有太多特殊之处,这些只是处理代币时所期望的账户。

    还记得我们的编程习惯吗?每次添加一个账户后,立即添加验证!以下是我们需要在 add_movie_review 函数中添加的内容:

    msg!("deriving mint authority");
    let (mint_pda, _mint_bump) = Pubkey::find_program_address(&[b"token_mint"], program_id);
    let (mint_auth_pda, mint_auth_bump) =
    Pubkey::find_program_address(&[b"token_auth"], program_id);

    if *token_mint.key != mint_pda {
    msg!("Incorrect token mint");
    return Err(ReviewError::IncorrectAccountError.into());
    }

    if *mint_auth.key != mint_auth_pda {
    msg!("Mint passed in and mint derived do not match");
    return Err(ReviewError::InvalidPDA.into());
    }

    if *user_ata.key != get_associated_token_address(initializer.key, token_mint.key) {
    msg!("Incorrect token mint");
    return Err(ReviewError::IncorrectAccountError.into());
    }

    if *token_program.key != TOKEN_PROGRAM_ID {
    msg!("Incorrect token program");
    return Err(ReviewError::IncorrectAccountError.into());
    }

    你现在已经反复实践过这样的流程,所以这些操作应该感觉得相当熟悉了 :)

    现在我们可以开始铸币了!就在程序结束之前,我们会添加如下代码: Ok(())

    msg!("Minting 10 tokens to User associated token account");
    invoke_signed(
    // Instruction
    &spl_token::instruction::mint_to(
    token_program.key,
    token_mint.key,
    user_ata.key,
    mint_auth.key,
    &[],
    10*LAMPORTS_PER_SOL,
    )?,
    // Account_infos
    &[token_mint.clone(), user_ata.clone(), mint_auth.clone()],
    // Seeds
    &[&[b"token_auth", &[mint_auth_bump]]],
    )?;

    Ok(())

    mint_to 是来自SPL令牌库的指令,所以我们还需更新顶部的导入内容:

    // Existing imports
    use spl_token::{instruction::{initialize_mint, mint_to}, ID as TOKEN_PROGRAM_ID};

    我们的评论功能已经完成了!现在每当有人留下评论时,我们就会给他们发送10个代币。

    我们将在 add_comment 函数中执行完全相同的操作: processor.rs

    // Inside add_comment
    let account_info_iter = &mut accounts.iter();

    let commenter = next_account_info(account_info_iter)?;
    let pda_review = next_account_info(account_info_iter)?;
    let pda_counter = next_account_info(account_info_iter)?;
    let pda_comment = next_account_info(account_info_iter)?;
    let token_mint = next_account_info(account_info_iter)?;
    let mint_auth = next_account_info(account_info_iter)?;
    let user_ata = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;
    let token_program = next_account_info(account_info_iter)?;

    // Mint tokens here
    msg!("deriving mint authority");
    let (mint_pda, _mint_bump) = Pubkey::find_program_address(&[b"token_mint"], program_id);
    let (mint_auth_pda, mint_auth_bump) =
    Pubkey::find_program_address(&[b"token_auth"], program_id);

    if *token_mint.key != mint_pda {
    msg!("Incorrect token mint");
    return Err(ReviewError::IncorrectAccountError.into());
    }

    if *mint_auth.key != mint_auth_pda {
    msg!("Mint passed in and mint derived do not match");
    return Err(ReviewError::InvalidPDA.into());
    }

    if *user_ata.key != get_associated_token_address(commenter.key, token_mint.key) {
    msg!("Incorrect token mint");
    return Err(ReviewError::IncorrectAccountError.into());
    }

    if *token_program.key != TOKEN_PROGRAM_ID {
    msg!("Incorrect token program");
    return Err(ReviewError::IncorrectAccountError.into());
    }
    msg!("Minting 5 tokens to User associated token account");
    invoke_signed(
    // Instruction
    &spl_token::instruction::mint_to(
    token_program.key,
    token_mint.key,
    user_ata.key,
    mint_auth.key,
    &[],
    5 * LAMPORTS_PER_SOL,
    )?,
    // Account_infos
    &[token_mint.clone(), user_ata.clone(), mint_auth.clone()],
    // Seeds
    &[&[b"token_auth", &[mint_auth_bump]]],
    )?;

    Ok(())

    注意,不要重复 Ok(()) ,因为那会导致错误。

    希望你现在能够看出这些模式的共通性了。虽然在进行本地开发时,我们需要写很多代码,但整个工作流程相当简单,并且感觉很“纯粹”。

    🚀 构建、部署和测试

    是时候赚取一些爆米花代币了 🍿

    首先,让我们开始构建和部署项目。

    cargo build-sbf
    solana program deploy <PATH>

    接下来,我们将测试初始化代币铸造流程。

    git clone https://github.com/buildspace/solana-movie-token-client
    cd solana-movie-token-client
    npm install

    和以前一样,需要进行以下操作:

    1. index.ts 中更新 PROGRAM_ID 的值。
    2. 修改第67行的连接为在线连接。
    const connection = new web3.Connection("http://localhost:8899");

    运行 npm start 后,你的 Mint 账户将会被初始化。

    最后,我们可以使用前端来发送电影评论,并因此获得一些代币。

    像往常一样,你可以继续使用之前停下的前端,或者从正确的分支创建一个新的实例。

    git clone https://github.com/buildspace/solana-movie-frontend/
    cd solana-movie-frontend
    git checkout solution-add-tokens
    npm install

    更新 PROGRAM_ID,提交评论,发表评论后,你现在应该能在 Phantom 钱包中看到你的代币了!

    🚢 挑战

    为了运用你在本课程中学到的有关 CPI 的知识,不妨考虑如何将其整合到学生介绍方案中。你可以做些类似我们演示中的事情,比如在用户自我介绍时铸造一些代币给他们。或者,如果你感到更有挑战性,思考如何将课程中学到的所有内容整合在一起,从零开始创建全新的项目。

    如果你选择做类似的演示,可以自由使用相同的脚本来调用 initialize_mint 指令,或者你可以展现创造力,从客户端初始化铸币过程,然后将铸币权限转移到程序 PDA。如果你需要查看可能的解决方案,请查看这个游乐场链接

    享受编程的乐趣,并将此视为自我提升的机会!

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module4/cross-program-invocations/index.html b/Solana-Co-Learn/module4/cross-program-invocations/index.html index e92fa4e19..fe7b24433 100644 --- a/Solana-Co-Learn/module4/cross-program-invocations/index.html +++ b/Solana-Co-Learn/module4/cross-program-invocations/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module4/cross-program-invocations/mint-token-for-users/index.html b/Solana-Co-Learn/module4/cross-program-invocations/mint-token-for-users/index.html index 4184f5524..48134c8e1 100644 --- a/Solana-Co-Learn/module4/cross-program-invocations/mint-token-for-users/index.html +++ b/Solana-Co-Learn/module4/cross-program-invocations/mint-token-for-users/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🥇 为用户铸造代币

    我们的电影评论项目进展得不错,但还没有充分体现Web3的特性。目前我们主要将Solana用作数据库。让我们通过奖励用户增加一些趣味性吧!每当用户评论一部电影或留下评论时,我们将为其铸造代币。这可以想象成StackOverflow,只不过是用代币来代替点赞。

    你可以在上一次的本地环境上继续开发,或者通过复制这个环境来创建一个新的环境。

    git clone https://github.com/buildspace/solana-movie-program/
    cd solana-movie-program
    git checkout solution-add-comments

    我们将使用SPL代币程序来实现所有这些神奇的功能,所以请更新 Cargo.toml 文件中的依赖项:

    [dependencies]
    solana-program = "~1.10.29"
    borsh = "0.9.3"
    thiserror = "1.0.31"
    spl-token = { version="3.2.0", features = [ "no-entrypoint" ] }
    spl-associated-token-account = { version="=1.0.5", features = [ "no-entrypoint" ] }

    让我们快速测试一下,看看是否能够使用这些新的依赖项正常构建:cargo build-sbf

    一切就绪,我们现在可以开始构建了!

    🤖 设置代币铸造

    我们首先要创建一个代币铸造。提醒一下:代币铸造是一个特殊的账户,用于存储我们的代币数据。

    这是一条新的指令,所以我们将按照添加评论支持时的相同步骤来操作:

    • 更新指令枚举
    • 更新unpack函数
    • 更新 process_instruction 函数

    instruction.rs开始,我们先更新枚举:

    pub enum MovieInstruction {
    AddMovieReview {
    title: String,
    rating: u8,
    description: String,
    },
    UpdateMovieReview {
    title: String,
    rating: u8,
    description: String,
    },
    AddComment {
    comment: String,
    },
    InitializeMint, // 这里新增了初始化铸币的操作
    }

    这里我们不需要任何字段——调用该函数时只需提供地址!

    接下来,我们将更新解包函数:

    impl MovieInstruction {
    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
    let (&variant, rest) = input
    .split_first()
    .ok_or(ProgramError::InvalidInstructionData)?;
    Ok(match variant {
    0 => {
    let payload = MovieReviewPayload::try_from_slice(rest).unwrap();
    Self::AddMovieReview {
    title: payload.title,
    rating: payload.rating,
    description: payload.description,
    }
    }
    1 => {
    let payload = MovieReviewPayload::try_from_slice(rest).unwrap();
    Self::UpdateMovieReview {
    title: payload.title,
    rating: payload.rating,
    description: payload.description,
    }
    }
    2 => {
    let payload = CommentPayload::try_from_slice(rest).unwrap();
    Self::AddComment {
    comment: payload.comment,
    }
    }
    // 这里新增了初始化铸币的操作
    3 => Self::InitializeMint,
    _ => return Err(ProgramError::InvalidInstructionData),
    })
    }
    }

    你会立即注意到 process_instruction 的匹配语句中存在错误,因为我们没有处理所有情况。让我们通过引入新的SPL导入并添加到匹配语句中来修复这个问题,继续往下开发。

    // Update imports at the top
    use solana_program::{
    //Existing imports within solana_program

    sysvar::{rent::Rent, Sysvar, rent::ID as RENT_PROGRAM_ID},
    native_token::LAMPORTS_PER_SOL,
    system_program::ID as SYSTEM_PROGRAM_ID
    }
    use spl_associated_token_account::get_associated_token_address;
    use spl_token::{instruction::initialize_mint, ID as TOKEN_PROGRAM_ID};

    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
    ) -> ProgramResult {
    let instruction = MovieInstruction::unpack(instruction_data)?;
    match instruction {
    MovieInstruction::AddMovieReview {
    title,
    rating,
    description,
    } => add_movie_review(program_id, accounts, title, rating, description),
    MovieInstruction::UpdateMovieReview {
    title,
    rating,
    description,
    } => update_movie_review(program_id, accounts, title, rating, description),
    MovieInstruction::AddComment { comment } => add_comment(program_id, accounts, comment),
    // New instruction handled here to initialize the mint account
    MovieInstruction::InitializeMint => initialize_token_mint(program_id, accounts),
    }
    }
    // Rest of the file remains the same

    最后,在 initialize_token_mint 功能之后,我们可以在 processor.rs 底部实施 add_comment 账户

    pub fn initialize_token_mint(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();

    // The order of accounts is not arbitrary, the client will send them in this order
    // Whoever sent in the transaction
    let initializer = next_account_info(account_info_iter)?;
    // Token mint PDA - derived on the client
    let token_mint = next_account_info(account_info_iter)?;
    // Token mint authority
    let mint_auth = next_account_info(account_info_iter)?;
    // System program to create a new account
    let system_program = next_account_info(account_info_iter)?;
    // Solana Token program address
    let token_program = next_account_info(account_info_iter)?;
    // System account to calcuate the rent
    let sysvar_rent = next_account_info(account_info_iter)?;

    // Derive the mint PDA again so we can validate it
    // The seed is just "token_mint"
    let (mint_pda, mint_bump) = Pubkey::find_program_address(&[b"token_mint"], program_id);
    // Derive the mint authority so we can validate it
    // The seed is just "token_auth"
    let (mint_auth_pda, _mint_auth_bump) =
    Pubkey::find_program_address(&[b"token_auth"], program_id);

    msg!("Token mint: {:?}", mint_pda);
    msg!("Mint authority: {:?}", mint_auth_pda);

    // Validate the important accounts passed in
    if mint_pda != *token_mint.key {
    msg!("Incorrect token mint account");
    return Err(ReviewError::IncorrectAccountError.into());
    }

    if *token_program.key != TOKEN_PROGRAM_ID {
    msg!("Incorrect token program");
    return Err(ReviewError::IncorrectAccountError.into());
    }

    if *mint_auth.key != mint_auth_pda {
    msg!("Incorrect mint auth account");
    return Err(ReviewError::IncorrectAccountError.into());
    }

    if *system_program.key != SYSTEM_PROGRAM_ID {
    msg!("Incorrect system program");
    return Err(ReviewError::IncorrectAccountError.into());
    }

    if *sysvar_rent.key != RENT_PROGRAM_ID {
    msg!("Incorrect rent program");
    return Err(ReviewError::IncorrectAccountError.into());
    }

    // Calculate the rent
    let rent = Rent::get()?;
    // We know the size of a mint account is 82 (remember it lol)
    let rent_lamports = rent.minimum_balance(82);

    // Create the token mint PDA
    invoke_signed(
    &system_instruction::create_account(
    initializer.key,
    token_mint.key,
    rent_lamports,
    82, // Size of the token mint account
    token_program.key,
    ),
    // Accounts we're reading from or writing to
    &[
    initializer.clone(),
    token_mint.clone(),
    system_program.clone(),
    ],
    // Seeds for our token mint account
    &[&[b"token_mint", &[mint_bump]]],
    )?;

    msg!("Created token mint account");

    // Initialize the mint account
    invoke_signed(
    &initialize_mint(
    token_program.key,
    token_mint.key,
    mint_auth.key,
    Option::None, // Freeze authority - we don't want anyone to be able to freeze!
    9, // Number of decimals
    )?,
    // Which accounts we're reading from or writing to
    &[token_mint.clone(), sysvar_rent.clone(), mint_auth.clone()],
    // The seeds for our token mint PDA
    &[&[b"token_mint", &[mint_bump]]],
    )?;

    msg!("Initialized token mint");

    Ok(())
    }

    在高层次上,这里的操作过程可概括为以下几个步骤:

    1. 遍历账户列表,提取必要的信息。
    2. 派生代币的mint PDA(程序派生地址)。
    3. 对传入的重要账户进行验证:
      • Token mint account - 代币铸币账户。
      • Mint authority account - 铸币权限账户。
      • System program - 系统程序。
      • Token program - 代币程序。
      • Sysvar rent - 用于计算租金的系统变量账户。
    4. 计算mint account所需的租金。
    5. 创建token mint PDA
    6. 初始化mint account

    由于我们调用了一个未声明的新错误类型,你会收到一个错误提示。解决方法是打开error.rs文件,并将IncorrectAccountError添加到ReviewError枚举中。

    #[derive(Debug, Error)]
    pub enum ReviewError {
    #[error("Account not initialized yet")]
    UninitializedAccount,

    #[error("PDA derived does not equal PDA passed in")]
    InvalidPDA,

    #[error("Input data exceeds max length")]
    InvalidDataLength,

    #[error("Rating greater than 5 or less than 1")]
    InvalidRating,

    // 新增的错误类型
    #[error("Accounts do not match")]
    IncorrectAccountError,
    }

    这个错误信息非常直观。

    然后,在文件浏览器中打开目标文件夹,并在部署文件夹中删除密钥对。

    回到你的控制台,运行:

    cargo build-sbf

    然后复制并粘贴控制台打印的部署命令。

    如果你遇到insufficient funds的问题,请直接运行solana airdrop 2

    一旦在本地部署完成,你就可以开始进行测试了!我们将使用本地客户端脚本来测试账户初始化。以下是你需要做的设置步骤:

    git clone https://github.com/buildspace/solana-movie-token-client
    cd solana-movie-token-client
    npm install

    在运行脚本之前,请:

    1. 更新index.ts中的PROGRAM_ID
    2. 将第67行的连接更改为你的本地连接:
    const connection = new web3.Connection("http://localhost:8899");
    1. 在第二个控制台窗口中运行solana logs PROGRAM_ID_HERE

    现在,你应该有一个控制台正在记录此程序的所有输出,并且已准备好运行脚本。

    如果你运行npm start,你应该能够看到有关创建铸币账户的日志信息。

    :D

    - - +
    Skip to main content

    🥇 为用户铸造代币

    我们的电影评论项目进展得不错,但还没有充分体现Web3的特性。目前我们主要将Solana用作数据库。让我们通过奖励用户增加一些趣味性吧!每当用户评论一部电影或留下评论时,我们将为其铸造代币。这可以想象成StackOverflow,只不过是用代币来代替点赞。

    你可以在上一次的本地环境上继续开发,或者通过复制这个环境来创建一个新的环境。

    git clone https://github.com/buildspace/solana-movie-program/
    cd solana-movie-program
    git checkout solution-add-comments

    我们将使用SPL代币程序来实现所有这些神奇的功能,所以请更新 Cargo.toml 文件中的依赖项:

    [dependencies]
    solana-program = "~1.10.29"
    borsh = "0.9.3"
    thiserror = "1.0.31"
    spl-token = { version="3.2.0", features = [ "no-entrypoint" ] }
    spl-associated-token-account = { version="=1.0.5", features = [ "no-entrypoint" ] }

    让我们快速测试一下,看看是否能够使用这些新的依赖项正常构建:cargo build-sbf

    一切就绪,我们现在可以开始构建了!

    🤖 设置代币铸造

    我们首先要创建一个代币铸造。提醒一下:代币铸造是一个特殊的账户,用于存储我们的代币数据。

    这是一条新的指令,所以我们将按照添加评论支持时的相同步骤来操作:

    • 更新指令枚举
    • 更新unpack函数
    • 更新 process_instruction 函数

    instruction.rs开始,我们先更新枚举:

    pub enum MovieInstruction {
    AddMovieReview {
    title: String,
    rating: u8,
    description: String,
    },
    UpdateMovieReview {
    title: String,
    rating: u8,
    description: String,
    },
    AddComment {
    comment: String,
    },
    InitializeMint, // 这里新增了初始化铸币的操作
    }

    这里我们不需要任何字段——调用该函数时只需提供地址!

    接下来,我们将更新解包函数:

    impl MovieInstruction {
    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
    let (&variant, rest) = input
    .split_first()
    .ok_or(ProgramError::InvalidInstructionData)?;
    Ok(match variant {
    0 => {
    let payload = MovieReviewPayload::try_from_slice(rest).unwrap();
    Self::AddMovieReview {
    title: payload.title,
    rating: payload.rating,
    description: payload.description,
    }
    }
    1 => {
    let payload = MovieReviewPayload::try_from_slice(rest).unwrap();
    Self::UpdateMovieReview {
    title: payload.title,
    rating: payload.rating,
    description: payload.description,
    }
    }
    2 => {
    let payload = CommentPayload::try_from_slice(rest).unwrap();
    Self::AddComment {
    comment: payload.comment,
    }
    }
    // 这里新增了初始化铸币的操作
    3 => Self::InitializeMint,
    _ => return Err(ProgramError::InvalidInstructionData),
    })
    }
    }

    你会立即注意到 process_instruction 的匹配语句中存在错误,因为我们没有处理所有情况。让我们通过引入新的SPL导入并添加到匹配语句中来修复这个问题,继续往下开发。

    // Update imports at the top
    use solana_program::{
    //Existing imports within solana_program

    sysvar::{rent::Rent, Sysvar, rent::ID as RENT_PROGRAM_ID},
    native_token::LAMPORTS_PER_SOL,
    system_program::ID as SYSTEM_PROGRAM_ID
    }
    use spl_associated_token_account::get_associated_token_address;
    use spl_token::{instruction::initialize_mint, ID as TOKEN_PROGRAM_ID};

    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
    ) -> ProgramResult {
    let instruction = MovieInstruction::unpack(instruction_data)?;
    match instruction {
    MovieInstruction::AddMovieReview {
    title,
    rating,
    description,
    } => add_movie_review(program_id, accounts, title, rating, description),
    MovieInstruction::UpdateMovieReview {
    title,
    rating,
    description,
    } => update_movie_review(program_id, accounts, title, rating, description),
    MovieInstruction::AddComment { comment } => add_comment(program_id, accounts, comment),
    // New instruction handled here to initialize the mint account
    MovieInstruction::InitializeMint => initialize_token_mint(program_id, accounts),
    }
    }
    // Rest of the file remains the same

    最后,在 initialize_token_mint 功能之后,我们可以在 processor.rs 底部实施 add_comment 账户

    pub fn initialize_token_mint(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();

    // The order of accounts is not arbitrary, the client will send them in this order
    // Whoever sent in the transaction
    let initializer = next_account_info(account_info_iter)?;
    // Token mint PDA - derived on the client
    let token_mint = next_account_info(account_info_iter)?;
    // Token mint authority
    let mint_auth = next_account_info(account_info_iter)?;
    // System program to create a new account
    let system_program = next_account_info(account_info_iter)?;
    // Solana Token program address
    let token_program = next_account_info(account_info_iter)?;
    // System account to calcuate the rent
    let sysvar_rent = next_account_info(account_info_iter)?;

    // Derive the mint PDA again so we can validate it
    // The seed is just "token_mint"
    let (mint_pda, mint_bump) = Pubkey::find_program_address(&[b"token_mint"], program_id);
    // Derive the mint authority so we can validate it
    // The seed is just "token_auth"
    let (mint_auth_pda, _mint_auth_bump) =
    Pubkey::find_program_address(&[b"token_auth"], program_id);

    msg!("Token mint: {:?}", mint_pda);
    msg!("Mint authority: {:?}", mint_auth_pda);

    // Validate the important accounts passed in
    if mint_pda != *token_mint.key {
    msg!("Incorrect token mint account");
    return Err(ReviewError::IncorrectAccountError.into());
    }

    if *token_program.key != TOKEN_PROGRAM_ID {
    msg!("Incorrect token program");
    return Err(ReviewError::IncorrectAccountError.into());
    }

    if *mint_auth.key != mint_auth_pda {
    msg!("Incorrect mint auth account");
    return Err(ReviewError::IncorrectAccountError.into());
    }

    if *system_program.key != SYSTEM_PROGRAM_ID {
    msg!("Incorrect system program");
    return Err(ReviewError::IncorrectAccountError.into());
    }

    if *sysvar_rent.key != RENT_PROGRAM_ID {
    msg!("Incorrect rent program");
    return Err(ReviewError::IncorrectAccountError.into());
    }

    // Calculate the rent
    let rent = Rent::get()?;
    // We know the size of a mint account is 82 (remember it lol)
    let rent_lamports = rent.minimum_balance(82);

    // Create the token mint PDA
    invoke_signed(
    &system_instruction::create_account(
    initializer.key,
    token_mint.key,
    rent_lamports,
    82, // Size of the token mint account
    token_program.key,
    ),
    // Accounts we're reading from or writing to
    &[
    initializer.clone(),
    token_mint.clone(),
    system_program.clone(),
    ],
    // Seeds for our token mint account
    &[&[b"token_mint", &[mint_bump]]],
    )?;

    msg!("Created token mint account");

    // Initialize the mint account
    invoke_signed(
    &initialize_mint(
    token_program.key,
    token_mint.key,
    mint_auth.key,
    Option::None, // Freeze authority - we don't want anyone to be able to freeze!
    9, // Number of decimals
    )?,
    // Which accounts we're reading from or writing to
    &[token_mint.clone(), sysvar_rent.clone(), mint_auth.clone()],
    // The seeds for our token mint PDA
    &[&[b"token_mint", &[mint_bump]]],
    )?;

    msg!("Initialized token mint");

    Ok(())
    }

    在高层次上,这里的操作过程可概括为以下几个步骤:

    1. 遍历账户列表,提取必要的信息。
    2. 派生代币的mint PDA(程序派生地址)。
    3. 对传入的重要账户进行验证:
      • Token mint account - 代币铸币账户。
      • Mint authority account - 铸币权限账户。
      • System program - 系统程序。
      • Token program - 代币程序。
      • Sysvar rent - 用于计算租金的系统变量账户。
    4. 计算mint account所需的租金。
    5. 创建token mint PDA
    6. 初始化mint account

    由于我们调用了一个未声明的新错误类型,你会收到一个错误提示。解决方法是打开error.rs文件,并将IncorrectAccountError添加到ReviewError枚举中。

    #[derive(Debug, Error)]
    pub enum ReviewError {
    #[error("Account not initialized yet")]
    UninitializedAccount,

    #[error("PDA derived does not equal PDA passed in")]
    InvalidPDA,

    #[error("Input data exceeds max length")]
    InvalidDataLength,

    #[error("Rating greater than 5 or less than 1")]
    InvalidRating,

    // 新增的错误类型
    #[error("Accounts do not match")]
    IncorrectAccountError,
    }

    这个错误信息非常直观。

    然后,在文件浏览器中打开目标文件夹,并在部署文件夹中删除密钥对。

    回到你的控制台,运行:

    cargo build-sbf

    然后复制并粘贴控制台打印的部署命令。

    如果你遇到insufficient funds的问题,请直接运行solana airdrop 2

    一旦在本地部署完成,你就可以开始进行测试了!我们将使用本地客户端脚本来测试账户初始化。以下是你需要做的设置步骤:

    git clone https://github.com/buildspace/solana-movie-token-client
    cd solana-movie-token-client
    npm install

    在运行脚本之前,请:

    1. 更新index.ts中的PROGRAM_ID
    2. 将第67行的连接更改为你的本地连接:
    const connection = new web3.Connection("http://localhost:8899");
    1. 在第二个控制台窗口中运行solana logs PROGRAM_ID_HERE

    现在,你应该有一个控制台正在记录此程序的所有输出,并且已准备好运行脚本。

    如果你运行npm start,你应该能够看到有关创建铸币账户的日志信息。

    :D

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module4/cross-program-invocations/the-cross-program-boss-fight/index.html b/Solana-Co-Learn/module4/cross-program-invocations/the-cross-program-boss-fight/index.html index a343d17e7..e70cdf146 100644 --- a/Solana-Co-Learn/module4/cross-program-invocations/the-cross-program-boss-fight/index.html +++ b/Solana-Co-Learn/module4/cross-program-invocations/the-cross-program-boss-fight/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    ⚔ 跨项目的Boss战斗

    如果你是一位玩家,可能曾经玩过那些具有庞大Boss战的游戏。这类Boss通常强大到个人难以战胜,因此你必须与朋友们联手攻击它们。就像灭霸与复仇者联盟的战斗一样。

    战胜这些Boss的秘诀在于合作。每个人共同出力,施展自己的能力。Solana为你提供了合作的超能力:可组合性是其架构的核心设计原则。

    能够释放这种力量的是什么呢?那就是跨程序调用,或者称作CPIs

    想象一下你的终极NFT质押项目。在这里,我们将进行许多与代币相关的操作(质押、赎回、解质押),无需自己从头构建,只需调用代币程序,它就会为我们处理这些操作。

    🔀 跨程序调用

    跨程序调用是一种程序直接调用另一个程序的方式。就如同任何客户端可以通过JSON RPC调用任何程序,任何程序也可以直接调用其他程序。

    CPIs将整个Solana生态系统本质上转变为一个巨大的API,作为开发者,你可以随意发挥。

    🤔 如何制作一个CPI

    你之前已经操作过几次CPI,所以这应该看起来非常熟悉!

    CPIs是通过使用 solana_program 库中的invokeinvoke_signed函数创建的。

    CPIs能够将调用者的签名权限赋予被调用者。

    • invoke将原始交易签名传递给你想要调用的程序。
    • invoke_signed允许你的程序通过所谓的PDA(程序派生地址)“签署”指令。
    // Used when there are not signatures for PDAs needed
    pub fn invoke(
    instruction: &Instruction,
    account_infos: &[AccountInfo<'_>]
    ) -> ProgramResult

    // Used when a program must provide a 'signature' for a PDA, hence the signer_seeds parameter
    pub fn invoke_signed(
    instruction: &Instruction,
    account_infos: &[AccountInfo<'_>],
    signers_seeds: &[&[&[u8]]]
    ) -> ProgramResult

    Instruction 类型的定义如下:

    • program_id - 指定要调用的程序的公钥。
    • accounts - 一个包含账户元数据的向量列表,你需要将被调用程序将要读取或写入的所有账户都包括进去。
    • data - 一个字节缓冲区,代表作为向被调用程序传递的数据的向量。

    根据你所调用的程序的不同,可能会有一个特定的 crate 包含辅助函数来创建 Instruction 对象。accountsdata 字段都是 Vec 类型,即向量。你可以使用 vec 宏,利用数组表示法构建一个向量。

    pub struct Instruction {
    pub program_id: Pubkey,
    pub accounts: Vec<AccountMeta>,
    pub data: Vec<u8>,
    }

    accounts 字段需要一个类型为AccountMeta的向量。Instruction 结构的以下字段详细展示了 AccountMeta 的内容:

    pub struct AccountMeta {
    pub pubkey: Pubkey,
    pub is_signer: bool,
    pub is_writable: bool,
    }

    例如:

    • AccountMeta::new - 表示账户可写。
    • AccountMeta::read_only - 表示账户不可写入。
    • (account1_pubkey, true) - 表示账户是签署人。
    • (account2_pubkey, false) - 表示账户不是签署人。
    use solana_program::instruction::AccountMeta;

    let accounts = vec![
    AccountMeta::new(account1_pubkey, true),
    AccountMeta::new(account2_pubkey, false),
    AccountMeta::read_only(account3_pubkey, false),
    AccountMeta::read_only(account4_pubkey, true),
    ];

    以下是一个创建 Instruction 的示例:

    • accounts - 指令所需的 AccountMeta 的向量。
    • data - 指令所需的序列化指令数据。
    • programId - 被调用的程序。
    • 使用 solana_program::instruction::Instruction 来创建新的 Instruction
    use solana_program::instruction::{AccountMeta, Instruction};

    let accounts = vec![
    AccountMeta::new(account1_pubkey, true),
    AccountMeta::new(account2_pubkey, false),
    AccountMeta::read_only(account3_pubkey, false),
    AccountMeta::read_only(account4_pubkey, true),
    ];

    struct InstructionData {
    amount: u8,
    }

    let data = BorshSerialize.try_to_vec(InstructionData { amount: 1 });

    let instruction = Instruction {
    program_id: *program_id,
    accounts,
    data,
    };

    📜 传递账户列表

    在底层,invokeinvoke_signed 实质上都是交易,所以我们需要传入一个 account_info 对象的列表。

    你可以使用在 solana_program 包中的 account_info 结构体上实现的 Clone Trait 来复制每个需要传递到CPIaccount_info 对象。

    Clone trait 会返回一个 account_info 实例的副本。

    &[first_account.clone(), second_account.clone(), third_account.clone()]

    🏒 CPIinvoke

    请记住 - 调用其实就是像传递交易一样,执行这个操作的程序并不会真正接触到它。这意味着无需包含签名,因为Solana的运行时会将原始签名传递给你的程序。

    🏑 CPIinvoke_signed

    每当我们使用PDA时,我们会使用 invoke_signed 并传入种子。

    Solana运行时将使用提供的种子和调用程序的 program_id 内部调用create_program_address,然后将结果与指令中提供的地址进行比较。如果有任何账户地址与PDA匹配,该账户上的 is_signer 标志将被设置为 true

    这就像一条效率的捷径!

    😲 最佳实践与常见陷阱

    执行CPI时,你可能会遇到一些常见错误,通常表明你在构建CPI时使用了错误的信息。

    例如,“签名者权限升级”表示你在指示中错误地代签地址。如果你在使用 invoke_signed 并收到此错误,可能是你提供的种子不正确。

    EF1M4SPfKcchb6scq297y8FPCaLvj5kGjwMzjTM68wjA's signer privilege escalated
    Program returned error: "Cross-program invocation with unauthorized signer or writable account"

    还有其他可能导致问题的情况,包括:

    • 任何可能被程序修改的账户必须指定为可写入。
    • 写入未指定为可写的账户会导致交易失败。
    • 写入不属于该程序的账户也会导致交易失败。
    • 任何可能在程序执行期间被修改的Lamport余额的账户也必须被指定为可写入。
    • 等等。
    2qoeXa9fo8xVHzd2h9mVcueh6oK3zmAiJxCTySM5rbLZ's writable privilege escalated
    Program returned error: "Cross-program invocation with unauthorized signer or writable account"

    这里的核心概念是,如果你不在交易中明确声明你要操作这些账户,那么你就不能随意对其进行操作。

    🤔 意义何在

    CPISolana生态系统的一项关键特性,它允许所有部署的程序之间互操作。这为在现有基础上构建新协议和应用提供了可能,就像搭积木一样。

    可组合性是加密货币的一个重要组成部分,而CPI则使其在Solana上成为可能。

    CPI的另一重要方面是它们允许程序为其PDAs签名。正如你可能已经注意到的,PDAs在Solana开发中被广泛使用,因为它们允许程序以特定方式控制特定地址,以便没有外部用户能够为这些地址生成有效签名的交易。

    通过这些解释,希望对Solana中的CPI技术有更深入的理解。如果你还有任何问题或需要进一步解释,请随时提问。

    - - +
    Skip to main content

    ⚔ 跨项目的Boss战斗

    如果你是一位玩家,可能曾经玩过那些具有庞大Boss战的游戏。这类Boss通常强大到个人难以战胜,因此你必须与朋友们联手攻击它们。就像灭霸与复仇者联盟的战斗一样。

    战胜这些Boss的秘诀在于合作。每个人共同出力,施展自己的能力。Solana为你提供了合作的超能力:可组合性是其架构的核心设计原则。

    能够释放这种力量的是什么呢?那就是跨程序调用,或者称作CPIs

    想象一下你的终极NFT质押项目。在这里,我们将进行许多与代币相关的操作(质押、赎回、解质押),无需自己从头构建,只需调用代币程序,它就会为我们处理这些操作。

    🔀 跨程序调用

    跨程序调用是一种程序直接调用另一个程序的方式。就如同任何客户端可以通过JSON RPC调用任何程序,任何程序也可以直接调用其他程序。

    CPIs将整个Solana生态系统本质上转变为一个巨大的API,作为开发者,你可以随意发挥。

    🤔 如何制作一个CPI

    你之前已经操作过几次CPI,所以这应该看起来非常熟悉!

    CPIs是通过使用 solana_program 库中的invokeinvoke_signed函数创建的。

    CPIs能够将调用者的签名权限赋予被调用者。

    • invoke将原始交易签名传递给你想要调用的程序。
    • invoke_signed允许你的程序通过所谓的PDA(程序派生地址)“签署”指令。
    // Used when there are not signatures for PDAs needed
    pub fn invoke(
    instruction: &Instruction,
    account_infos: &[AccountInfo<'_>]
    ) -> ProgramResult

    // Used when a program must provide a 'signature' for a PDA, hence the signer_seeds parameter
    pub fn invoke_signed(
    instruction: &Instruction,
    account_infos: &[AccountInfo<'_>],
    signers_seeds: &[&[&[u8]]]
    ) -> ProgramResult

    Instruction 类型的定义如下:

    • program_id - 指定要调用的程序的公钥。
    • accounts - 一个包含账户元数据的向量列表,你需要将被调用程序将要读取或写入的所有账户都包括进去。
    • data - 一个字节缓冲区,代表作为向被调用程序传递的数据的向量。

    根据你所调用的程序的不同,可能会有一个特定的 crate 包含辅助函数来创建 Instruction 对象。accountsdata 字段都是 Vec 类型,即向量。你可以使用 vec 宏,利用数组表示法构建一个向量。

    pub struct Instruction {
    pub program_id: Pubkey,
    pub accounts: Vec<AccountMeta>,
    pub data: Vec<u8>,
    }

    accounts 字段需要一个类型为AccountMeta的向量。Instruction 结构的以下字段详细展示了 AccountMeta 的内容:

    pub struct AccountMeta {
    pub pubkey: Pubkey,
    pub is_signer: bool,
    pub is_writable: bool,
    }

    例如:

    • AccountMeta::new - 表示账户可写。
    • AccountMeta::read_only - 表示账户不可写入。
    • (account1_pubkey, true) - 表示账户是签署人。
    • (account2_pubkey, false) - 表示账户不是签署人。
    use solana_program::instruction::AccountMeta;

    let accounts = vec![
    AccountMeta::new(account1_pubkey, true),
    AccountMeta::new(account2_pubkey, false),
    AccountMeta::read_only(account3_pubkey, false),
    AccountMeta::read_only(account4_pubkey, true),
    ];

    以下是一个创建 Instruction 的示例:

    • accounts - 指令所需的 AccountMeta 的向量。
    • data - 指令所需的序列化指令数据。
    • programId - 被调用的程序。
    • 使用 solana_program::instruction::Instruction 来创建新的 Instruction
    use solana_program::instruction::{AccountMeta, Instruction};

    let accounts = vec![
    AccountMeta::new(account1_pubkey, true),
    AccountMeta::new(account2_pubkey, false),
    AccountMeta::read_only(account3_pubkey, false),
    AccountMeta::read_only(account4_pubkey, true),
    ];

    struct InstructionData {
    amount: u8,
    }

    let data = BorshSerialize.try_to_vec(InstructionData { amount: 1 });

    let instruction = Instruction {
    program_id: *program_id,
    accounts,
    data,
    };

    📜 传递账户列表

    在底层,invokeinvoke_signed 实质上都是交易,所以我们需要传入一个 account_info 对象的列表。

    你可以使用在 solana_program 包中的 account_info 结构体上实现的 Clone Trait 来复制每个需要传递到CPIaccount_info 对象。

    Clone trait 会返回一个 account_info 实例的副本。

    &[first_account.clone(), second_account.clone(), third_account.clone()]

    🏒 CPIinvoke

    请记住 - 调用其实就是像传递交易一样,执行这个操作的程序并不会真正接触到它。这意味着无需包含签名,因为Solana的运行时会将原始签名传递给你的程序。

    🏑 CPIinvoke_signed

    每当我们使用PDA时,我们会使用 invoke_signed 并传入种子。

    Solana运行时将使用提供的种子和调用程序的 program_id 内部调用create_program_address,然后将结果与指令中提供的地址进行比较。如果有任何账户地址与PDA匹配,该账户上的 is_signer 标志将被设置为 true

    这就像一条效率的捷径!

    😲 最佳实践与常见陷阱

    执行CPI时,你可能会遇到一些常见错误,通常表明你在构建CPI时使用了错误的信息。

    例如,“签名者权限升级”表示你在指示中错误地代签地址。如果你在使用 invoke_signed 并收到此错误,可能是你提供的种子不正确。

    EF1M4SPfKcchb6scq297y8FPCaLvj5kGjwMzjTM68wjA's signer privilege escalated
    Program returned error: "Cross-program invocation with unauthorized signer or writable account"

    还有其他可能导致问题的情况,包括:

    • 任何可能被程序修改的账户必须指定为可写入。
    • 写入未指定为可写的账户会导致交易失败。
    • 写入不属于该程序的账户也会导致交易失败。
    • 任何可能在程序执行期间被修改的Lamport余额的账户也必须被指定为可写入。
    • 等等。
    2qoeXa9fo8xVHzd2h9mVcueh6oK3zmAiJxCTySM5rbLZ's writable privilege escalated
    Program returned error: "Cross-program invocation with unauthorized signer or writable account"

    这里的核心概念是,如果你不在交易中明确声明你要操作这些账户,那么你就不能随意对其进行操作。

    🤔 意义何在

    CPISolana生态系统的一项关键特性,它允许所有部署的程序之间互操作。这为在现有基础上构建新协议和应用提供了可能,就像搭积木一样。

    可组合性是加密货币的一个重要组成部分,而CPI则使其在Solana上成为可能。

    CPI的另一重要方面是它们允许程序为其PDAs签名。正如你可能已经注意到的,PDAs在Solana开发中被广泛使用,因为它们允许程序以特定方式控制特定地址,以便没有外部用户能够为这些地址生成有效签名的交易。

    通过这些解释,希望对Solana中的CPI技术有更深入的理解。如果你还有任何问题或需要进一步解释,请随时提问。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module4/index.html b/Solana-Co-Learn/module4/index.html index 36c995f17..422298390 100644 --- a/Solana-Co-Learn/module4/index.html +++ b/Solana-Co-Learn/module4/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module4/pdas/build-on-chain-comments/index.html b/Solana-Co-Learn/module4/pdas/build-on-chain-comments/index.html index 6cf688df0..736d77460 100644 --- a/Solana-Co-Learn/module4/pdas/build-on-chain-comments/index.html +++ b/Solana-Co-Learn/module4/pdas/build-on-chain-comments/index.html @@ -9,14 +9,14 @@ - - + +
    Skip to main content

    💬 链上评论功能的构建

    现在是时候充分利用PDA的功能了。我们将给我们的旧电影评论程序添加评论支持功能。

    首先,在本地环境中新建一个项目并设置好。

    cargo new --lib movie-review-comments
    cd movie-review-comments

    然后,打开 Cargo.toml 文件,这样我们就可以添加所需的依赖项和进行其他配置了:

    [package]
    name = "movie-review-comments"
    version = "0.1.0"
    edition = "2021"

    # 更多关键字和定义,请查看:https://doc.rust-lang.org/cargo/reference/manifest.html

    [features]
    no-entrypoint = []

    [dependencies]
    solana-program = "1.10.29"
    borsh = "0.9.3"
    thiserror = "1.0.31"

    [lib]
    crate-type = ["cdylib", "lib"]
    caution

    这里需要注意的是solana-program, borsh, thiserror 的版本可能会太低了,请使用cargo add <crates-name>安装。

    此外,你还需要将我们之前用过的所有文件和代码搬过来。你可以找到我们上次离开时的电影评论程序,并将文件结构和内容复制到新的本地项目中。

    完成这些操作后,可以通过构建程序来确认一切是否准备就绪:

    cargo build-sbf

    首次运行可能会花费几分钟。如果一切顺利,你应该会看到一个显示“完成”的绿色消息。

    我们现在已经准备好开始组合构建项目了!

    info

    开始前的提示

    请注意,这是一堂较为深入的课程。我们将编写大量代码,这可能会让你觉得有些压力重重。但当你编写实际的程序时,不必进行如此繁琐的工作,速度会快得多。下周我们将深入学习如何使用Anchor,这会让整个过程变得更简单。我们现在选择采用原生方式,以便深入了解这些概念并为你奠定坚实的基础。

    🤓 数据结构化

    在存储数据时,决定如何摆放和连接物品是非常关键的。想象一下,我们需要为每个电影评论存储其下的评论。那么这在链上会是什么样子呢?当我们在客户端上阅读时,又该如何找到特定评论的评论呢?这就涉及到了数据映射。

    在这里并没有一成不变的“规则”,你需要用上计算机工程师的智慧来弄明白该如何做,就像设计数据库模式一样。通常,我们期望的结构具备以下特性:

    • 结构不要过于复杂
    • 能让数据容易检索

    具体的实现方式可能因情况而异,但有些常见的模式是你会经常看到的。一旦你明白了如何组织和连接存储数据的方法,你就能找出最适合你情况的最佳解决方案。

    存储评论

    我们首先需要决定评论将存储在何处。你可能还记得,在 add_movie_review 中,我们为每个电影评论创建了一个新的PDA。因此,我们是否可以简单地将一个大的评论数组添加到PDA中,然后就大功告成了呢?答案是否定的。由于账户的空间有限,所以我们很快就会用完空间。

    那么让我们按照电影评论的方式来进行。我们将为每条评论创建一个新的PDA,这样我们就可以存储尽可能多的评论了!为了将评论与它们所属的评论连接起来,我们将使用电影评论的PDA地址作为评论账户的种子。

    阅读评论

    我们的结构将为每个电影评论提供理论上无限数量的评论。然而,对于每个电影评论,没有任何特性来区分评论之间的关系。我们该如何知道每个电影评论有多少条评论呢?

    我们可以创建另一个账户来存储这个信息!并且,我们还可以使用一个编号系统来跟踪评论账户。

    是否感到困惑?我当时确实觉得很复杂。以下是一个方便的图表,有助于你形象地理解这个结构:

    对于每一篇电影评论,我们将拥有一个评论计数器PDA和许多评论PDA。我还列出了每个PDA的种子 - 这是我们获取账户的方式。

    这样,如果我想要获取评论#5,就知道可以在从电影评论PDA和5派生的账户中找到它。

    📦 构建基本组件

    我们想要创建两个新账户来存储数据。下面是我们在程序中需要完成的所有步骤:

    • 定义结构体,用于表示评论计数器和评论账户
    • 更新现有的 MovieAccountState,增加一个鉴别器字段(稍后将详细解释)
    • 添加一个指令变体,用来表示 add_comment 指令
    • 更新现有的 add_movie_review 指令,包括创建评论计数器账户的部分
    • 创建一个新的 add_comment 指令

    首先,我们从为新账户创建结构体开始。我们需要定义每个账户中存储的数据。打开 state.rs 文件并将其更新为以下内容:

    use borsh::{BorshSerialize, BorshDeserialize};
    use solana_program::{
    // 引入 Pubkey
    pubkey::Pubkey,
    program_pack::{IsInitialized, Sealed},
    };

    #[derive(BorshSerialize, BorshDeserialize)]
    pub struct MovieAccountState {
    // 新增了两个字段 - discriminator 和 reviewer
    pub discriminator: String,
    pub is_initialized: bool,
    pub reviewer: Pubkey,
    pub rating: u8,
    pub title: String,
    pub description: String,
    }

    // 新结构体,记录评论总数
    #[derive(BorshSerialize, BorshDeserialize)]
    pub struct MovieCommentCounter {
    pub discriminator: String,
    pub is_initialized: bool,
    pub counter: u64,
    }

    // 新结构体,存储单个评论
    #[derive(BorshSerialize, BorshDeserialize)]
    pub struct MovieComment {
    pub discriminator: String,
    pub is_initialized: bool,
    pub review: Pubkey,
    pub commenter: Pubkey,
    pub comment: String,
    pub count: u64,
    }

    impl Sealed for MovieAccountState {}

    impl IsInitialized for MovieAccountState {
    fn is_initialized(&self) -> bool {
    self.is_initialized
    }
    }

    这些新结构体都需要可序列化,所以我们在这里使用了 Borsh 派生宏。我们还添加了一个 is_initialized 字段,用于确认该账户是否已准备好使用。

    由于现在我们在程序中有多种类型的账户,所以我们需要一种方式来区分这些不同的账户。当我们在客户端上执行时,我们将获取我们电影评论程序的所有账户。这就是 getProgramAccounts 的作用。我们可以通过指定账户数据的前 8 个字节来过滤账户列表。

    我们选择使用字符串作为鉴别器,因为我们可以事先确定鉴别器的内容,这样在过滤时我们就知道要在客户端上寻找什么。

    最后,我们需要为这些新结构体实现 IsInitialized 接口。我只是从 MovieAccountState 中复制/粘贴了实现代码,并将其放在了一旁:

    impl IsInitialized for MovieCommentCounter {
    fn is_initialized(&self) -> bool {
    self.is_initialized
    }
    }

    impl IsInitialized for MovieComment {
    fn is_initialized(&self) -> bool {
    self.is_initialized
    }
    }

    📏 定义账户大小

    如果你查看位于 processor.rs 中的 add_movie_review,你会发现我们在创建账户时计算账户的大小。这样做并不是特别实用,因为这个计算是不可复用的。所以现在我们将针对这些账户进行实现,代码如下:

    impl MovieAccountState {
    pub const DISCRIMINATOR: &'static str = "review";

    pub fn get_account_size(title: String, description: String) -> usize {
    // 4个字节存储后续动态数据(字符串)的大小
    (4 + MovieAccountState::DISCRIMINATOR.len())
    + 1 // 1个字节用于is_initialized(布尔值)
    + 32 // 32个字节用于电影评论账户密钥
    + 1 // 1个字节用于评分
    + (4 + title.len()) // 4个字节存储后续动态数据(字符串)的大小
    + (4 + description.len()) // 同上
    }
    }

    impl MovieComment {
    pub const DISCRIMINATOR: &'static str = "comment";

    pub fn get_account_size(comment: String) -> usize {
    (4 + MovieComment::DISCRIMINATOR.len())
    + 1 // 1个字节用于is_initialized(布尔值)
    + 32 // 32个字节用于电影评论账户密钥
    + 32 // 32个字节用于评论者密钥的大小
    + (4 + comment.len()) // 4个字节存储后续动态数据(字符串)的大小
    + 8 // 8个字节用于计数(u64)
    }
    }

    impl MovieCommentCounter {
    pub const DISCRIMINATOR: &'static str = "counter";
    pub const SIZE: usize = (4 + MovieCommentCounter::DISCRIMINATOR.len()) + 1 + 8;
    }

    impl Sealed for MovieCommentCounter{}

    由于电影评论账户和电影评论都有动态内容,所以我们需要函数来获取它们的大小。代码注释解释了每个字节的用途。

    MovieCommentCounter 的大小始终保持不变,因此我们可以声明一个常量代替函数。

    在这里,我们也看到了我们的鉴别器!由于它不会改变,我们使用 'static 关键字来创建一个静态常量,在整个程序的运行期间保持不变。代码注释解释了每个字节的用途。

    最后,由于我们正在进行实现,我还包括了 MovieCommentCounterSealed 实现。提醒一下,当结构体的大小已知时, Sealed 特性可以让编译器进行一些优化。由于 MovieCommentCounter 有已知的固定大小,所以我们需要实现它!

    至此,你已完成了 state.rs 的整体结构,它的大纲应该如下图所示:

    总的来说,对于每个账户状态,我们有:

    • 一个用来表示账户数据的结构体
    • 一个函数实现,用于告知我们账户是否已准备好
    • 一个函数实现,用于计算每个账户内容的大小
    • 一个静态常量,用于区分账户
    • 如果账户大小不是动态的,则可以选择实现一个 Sealed 方案。

    👨‍🏫 更新我们的指令

    现在我们已经完成了所有状态的处理,可以开始更新我们的指令处理程序,并实现实际的评论逻辑。

    首先从指令处理程序开始,我们需要更新指令枚举,以支持在 instruction.rs 中的评论功能:

    pub enum MovieInstruction {
    AddMovieReview {
    title: String,
    rating: u8,
    description: String
    },
    UpdateMovieReview {
    title: String,
    rating: u8,
    description: String
    },
    AddComment {
    comment: String
    }
    }

    用于表示指令数据的结构体非常简洁:

    #[derive(BorshDeserialize)]
    struct CommentPayload {
    comment: String
    }

    此外,我们还需要稍微重构一下 unpack 函数的实现。由于以前的添加和更新指令的有效载荷是相同的,我们可以在匹配语句之前对其进行反序列化。但现在,我们引入了带有不同类型有效载荷的评论功能,所以我们将把反序列化操作移到匹配语句中。具体如下:

    impl MovieInstruction {
    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
    let (&variant, rest) = input
    .split_first()
    .ok_or(ProgramError::InvalidInstructionData)?;

    Ok(match variant {
    0 => {
    let payload = MovieReviewPayload::try_from_slice(rest)
    .map_err(|_| ProgramError::from(Error::ParseMovieReviewPayloadFailed))?;

    Self::AddMovieReview {
    title: payload.title,
    rating: payload.rating,
    description: payload.description,
    }
    }
    1 => {
    let payload = MovieReviewPayload::try_from_slice(rest)
    .map_err(|_| ProgramError::from(Error::ParseMovieReviewPayloadFailed))?;

    Self::UpdateMovieReview {
    title: payload.title,
    rating: payload.rating,
    description: payload.description,
    }
    }
    2 => {
    // 评论载荷使用自己的反序列化器,因为数据类型不同
    let payload = CommentPayload::try_from_slice(rest)
    .map_err(|_| ProgramError::from(Error::ParseMovieCommentPayloadFailed))?;

    Self::AddComment {
    comment: payload.comment,
    }
    }
    _ => return Err(ProgramError::InvalidInstructionData),
    })
    }
    }

    现在你应该对这部分内容感到非常熟悉了 :)

    最后一部分是更新 process_instruction 中的 match 语句:

    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
    ) -> ProgramResult {
    let instruction = MovieInstruction::unpack(instruction_data)?;
    match instruction {
    MovieInstruction::AddMovieReview { title, rating, description } => {
    add_movie_review(program_id, accounts, title, rating, description)
    },

    MovieInstruction::UpdateMovieReview { title, rating, description } => {
    update_movie_review(program_id, accounts, title, rating, description)
    },

    MovieInstruction::AddComment { comment } => {
    add_comment(program_id, accounts, comment)
    }
    }
    }

    总结一下,我们所做的工作是:

    • 更新指令枚举以包括新的评论指令
    • 添加指令有效载荷的结构体以便我们进行反序列化操作
    • 更新了 unpack 函数,以涵盖新的指令类型
    • 更新了 match 语句,以便在 process_instruction 函数中处理新的指令

    你可能会在这里遇到一个错误,因为 add_comment 还不存在,你可以暂时添加一个空函数来解决这个问题:

    pub fn add_comment(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    comment: String
    ) -> ProgramResult {
    Ok(())
    }

    🎬 为创建评论计数器账户更新 add_movie_review

    由于每个电影评论都需要一个计数器账户,因此我们需要在 add_movie_review 函数中增加逻辑来创建该计数器账户。

    首先,在 processor.rs 文件的 add_movie_review 函数中,我们要新增一个 pda_counter,代表将要初始化的新评论计数器账户和电影评论账户。

    let account_info_iter = &mut accounts.iter();

    let initializer = next_account_info(account_info_iter)?;
    let pda_account = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;
    // 用于存储评论计数的新账户
    let pda_counter = next_account_info(account_info_iter)?;

    在创建PDA时验证它是个好习惯,这样就能确保你永远不会忘记。请在 pda_account 验证后添加以下内容:

    let (counter_pda, counter_bump_seed) = Pubkey::find_program_address(
    &[pda.as_ref(), "comment".as_ref()],
    program_id
    )

    if counter_pda != *pda_counter.key {
    msg!("计数器PDA的种子无效");
    return Err(ProgramError::InvalidArgument)
    }

    还记得我们将账户大小移至 state.rs 吗?好,现在我们需要用它来计算账户的大小。将以下内容替换到 total_len 调用处:

    let account_len: usize = 1000;

    if MovieAccountState::get_account_size(title.clone(), description.clone()) > account_len {
    msg!("数据长度大于1000字节");
    return Err(ReviewError::InvalidDataLength.into());
    }

    我们还增加了一个 discriminator 字段,所以我们需要更新 account_data 段的数据结构体:

    account_data.discriminator = MovieAccountState::DISCRIMINATOR.to_string();
    account_data.reviewer = *initializer.key;
    account_data.title = title;
    account_data.rating = rating;
    account_data.description = description;
    account_data.is_initialized = true;

    最后,在 add_movie_review 函数中增加逻辑来初始化评论计数器账户:

    msg!("创建评论计数器");
    let rent = Rent::get()?;
    let counter_rent_lamports = rent.minimum_balance(MovieCommentCounter::SIZE);

    // 推导地址并验证传入的PDA种子是否正确
    let (counter, counter_bump) =
    Pubkey::find_program_address(&[pda.as_ref(), "comment".as_ref()], program_id);
    if counter != *pda_counter.key {
    msg!("PDA的种子无效");
    return Err(ProgramError::InvalidArgument);
    }

    // 创建评论计数器账户
    invoke_signed(
    &system_instruction::create_account(
    initializer.key, // 租金支付者
    pda_counter.key, // 要创建账户的地址
    counter_rent_lamports, // 存入账户的租金数量
    MovieCommentCounter::SIZE.try_into().unwrap(), // 账户的大小
    program_id,
    ),
    &[
    // 将要读/写的账户列表
    initializer.clone(),
    pda_counter.clone(),
    system_program.clone(),
    ],
    // PDA的种子
    // PDA账户
    // 字符串"comment"
    &[&[pda.as_ref(), "comment".as_ref(), &[counter_bump]]],
    )?;
    msg!("评论计数器已创建");

    // 反序列化新创建的计数器账户
    let mut counter_data =
    try_from_slice_unchecked::<MovieCommentCounter>(&pda_counter.data.borrow()).unwrap();

    msg!("检查计数器账户是否已初始化");
    if counter_data.is_initialized() {
    msg!("账户已初始化");
    return Err(ProgramError::AccountAlreadyInitialized);
    }

    counter_data.discriminator = MovieCommentCounter::DISCRIMINATOR.to_string();
    counter_data.counter = 0;
    counter_data.is_initialized = true;
    msg!("评论计数: {}", counter_data.counter);
    counter_data.serialize(&mut &mut pda_counter.data.borrow_mut()[..])?;

    msg!("评论计数器已初始化");
    Ok(())

    简要回顾一下这段复杂代码在做什么:

    • 计算评论计数器账户所需的租金。
    • 验证PDA的种子是否正确。
    • 使用 invoke_signed 创建评论计数器账户。
    • 从新创建的账户中反序列化数据。
    • 检查账户是否已初始化。
    • 设置数据并初始化账户。
    • 序列化数据。

    请仔细查看评论,每一行代码都有相应的解释。

    现在,每当创建新的评论时,都会初始化两个账户:

    • 第一个是存储评论内容的审核账户。这与我们开始的程序版本相同。
    • 第二个账户是用于存储评论计数器的。

    💬 添加评论支持

    最后的一块拼图是在 processor.rs 文件底部实现 add_comment 函数。

    这是我们在这个函数中需要执行的步骤:

    • 遍历传入的程序账户。
    • 计算新评论账户所需的租金免税金额。
    • 使用评论地址和当前评论计数作为种子,推导评论账户的PDA。
    • 调用系统程序创建新的评论账户。
    • 为新创建的账户设置适当的值。
    • 将账户数据序列化并从函数中返回。
    pub fn add_comment(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    comment: String
    ) -> ProgramResult {
    msg!("正在添加评论...");
    msg!("评论内容:{}", comment);

    let account_info_iter = &mut accounts.iter();

    let commenter = next_account_info(account_info_iter)?;
    let pda_review = next_account_info(account_info_iter)?;
    let pda_counter = next_account_info(account_info_iter)?;
    let pda_comment = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    let mut counter_data = try_from_slice_unchecked::<MovieCommentCounter>(&pda_counter.data.borrow()).unwrap();

    let account_len = MovieComment::get_account_size(comment.clone());

    let rent = Rent::get()?;
    let rent_lamports = rent.minimum_balance(account_len);

    let (pda, bump_seed) = Pubkey::find_program_address(&[pda_review.key.as_ref(), counter_data.counter.to_be_bytes().as_ref(),], program_id);
    if pda != *pda_comment.key {
    msg!("Invalid seeds for PDA");
    return Err(ReviewError::InvalidPDA.into())
    }

    invoke_signed(
    &system_instruction::create_account(
    commenter.key,
    pda_comment.key,
    rent_lamports,
    account_len.try_into().unwrap(),
    program_id,
    ),
    &[commenter.clone(), pda_comment.clone(), system_program.clone()],
    &[&[pda_review.key.as_ref(), counter_data.counter.to_be_bytes().as_ref(), &[bump_seed]]],
    )?;

    msg!("Created Comment Account");

    let mut comment_data = try_from_slice_unchecked::<MovieComment>(&pda_comment.data.borrow()).unwrap();

    msg!("checking if comment account is already initialized");
    if comment_data.is_initialized() {
    msg!("Account already initialized");
    return Err(ProgramError::AccountAlreadyInitialized);
    }

    comment_data.discriminator = MovieComment::DISCRIMINATOR.to_string();
    comment_data.review = *pda_review.key;
    comment_data.commenter = *commenter.key;
    comment_data.comment = comment;
    comment_data.is_initialized = true;
    comment_data.serialize(&mut &mut pda_comment.data.borrow_mut()[..])?;

    msg!("Comment Count: {}", counter_data.counter);
    counter_data.counter += 1;
    counter_data.serialize(&mut &mut pda_counter.data.borrow_mut()[..])?;

    Ok(())
    }

    这段代码大量重复了我们之前所了解的操作,所以我不再赘述。

    我们经历了许多改变。点击这里查看最终版本,以便你可以比较并检查是否有问题。

    🚀 部署程序

    我们准备好部署了!

    本地部署与在游乐场上点击部署按钮的操作略有不同。

    首先,你需要构建程序:

    cargo build-sbf

    接下来,我们可以部署。请确保替换 <PATH> 为你的路径:

    solana program deploy <PATH>

    测试非常简单,只需设置以下前端:

    git clone https://github.com/buildspace/solana-movie-frontend/
    cd solana-movie-frontend
    git checkout solution-add-comments

    在你可以发表一些高质量的电影评论之前,你需要:

    • utils/constants.ts 文件中更新程序地址。
    • 将端点设置在 WalletContextProvider.tsxhttp://127.0.0.1:8899
    • Phantom网络更改为localhost
    • 使用 solana airdrop 2 PHANTOM_WALLET_ADDRESS 获取本地主机SOL

    你会看到在 localhost:3000 上,通过运行 npm run dev,评论的魔法就开始了!

    info

    热门提示 - 本地程序日志 -遇到错误吗?有什么异常吗?你可以在本地主机上查看程序日志:

    solana logs PROGRAM_ID

    🚢 挑战

    现在轮到你独立地构建一些东西了,你可以在之前课程中使用过的学生介绍程序的基础上进行扩展。

    利用你在本课程中学到的知识,尝试将所学应用到学生介绍计划中。你的扩展应该让其他用户能够对介绍进行回复。

    要进行测试,你需要获取此前端的 solution-paging-account-data 分支,并添加一个用于显示和提交评论的组件,或者你可以编写一个向程序发送交易的脚本。

    起始代码:

    如果你没有保存之前的starter代码,可以随意使用此存储库starter 分支。

    解决方案代码

    尽量自己完成这个任务!但如果遇到困难,可以参考 solution-add-replies 分支。

    - - +遇到错误吗?有什么异常吗?你可以在本地主机上查看程序日志:

    solana logs PROGRAM_ID

    🚢 挑战

    现在轮到你独立地构建一些东西了,你可以在之前课程中使用过的学生介绍程序的基础上进行扩展。

    利用你在本课程中学到的知识,尝试将所学应用到学生介绍计划中。你的扩展应该让其他用户能够对介绍进行回复。

    要进行测试,你需要获取此前端的 solution-paging-account-data 分支,并添加一个用于显示和提交评论的组件,或者你可以编写一个向程序发送交易的脚本。

    起始代码:

    如果你没有保存之前的starter代码,可以随意使用此存储库starter 分支。

    解决方案代码

    尽量自己完成这个任务!但如果遇到困难,可以参考 solution-add-replies 分支。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module4/pdas/index.html b/Solana-Co-Learn/module4/pdas/index.html index b326659de..82f5db330 100644 --- a/Solana-Co-Learn/module4/pdas/index.html +++ b/Solana-Co-Learn/module4/pdas/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module4/pdas/pda-deep-dive/index.html b/Solana-Co-Learn/module4/pdas/pda-deep-dive/index.html index 1c3b37ebd..e66581d44 100644 --- a/Solana-Co-Learn/module4/pdas/pda-deep-dive/index.html +++ b/Solana-Co-Learn/module4/pdas/pda-deep-dive/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🧐 PDA深入探究

    程序派生地址(Program Derived Address,PDA), 让我们一起深入了解它们的工作原理。

    PDA主要具有两个核心功能:

    • 提供一种确定性的方法来查找程序拥有的账户地址。
    • 授权派生自PDA的程序代表其签署,就像用户使用私钥签署一样。

    换言之,它们是Solana网络上用于存储的安全键值存储解决方案。

    🔎 寻找 PDAs (程序派生地址)

    到现在为止,每次我们需要派生一个地址时,都使用了一个方便的函数。那么,这个函数到底做了什么呢?要了解答案,我们需要理解Solana密钥对是如何生成的。

    回想一下密钥对的作用。它是一种证明你是你声称的人的方式。我们通过数字签名系统实现了这一点。Solana的密钥对基于所谓的Ed25519椭圆曲线(你不必担心这是什么)。

    由于PDAs由程序控制,所以它们不需要私钥。因此,我们使用不在Ed25519曲线上的地址来创建PDAs。这实际上意味着它们是没有相应私钥的公钥。

    就是这样。你不需要理解Ed25519,甚至不需要知道数字签名算法是什么。你只需要知道PDA看起来像普通的Solana地址,并且由程序控制。如果你想进一步了解,可以观看Computerphile关于数字签名的精彩视频。

    要在Solana程序中找到一个PDA,我们将使用 find_program_address 函数。

    seeds”是用于派生PDAfind_program_address 函数的可选输入。例如,seeds可以是任意组合:

    • 指令数据
    • 硬编码的值
    • 其他账户的公钥

    find_program_address 函数提供了一个额外的seeds,称为“bump seed”,以确保结果不在Ed25519曲线上。

    一旦找到有效的PDA,该函数将返回两个值:

    • PDA
    • 用于派生PDABump
    let (pda, bump_seed) = Pubkey::find_program_address(&[user.key.as_ref(), user_input.as_bytes().as_ref(), "SEED".as_bytes()], program_id);

    🍳 find_program_address函数内部解析

    find_program_address 是一个冒牌货 - 它实际上将输入 seedsprogram_id 传递给 try_find_program_address 函数。

    pub fn find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> (Pubkey, u8) {
    Self::try_find_program_address(seeds, program_id)
    .unwrap_or_else(|| panic!("Unable to find a viable program address bump seed"));
    }

    然后, try_find_program_address 函数引入了 bump_seed

    bump_seed 是一个 u8 变量,其值范围在0255之间。它被附加到可选的输入seeds中,然后传递给 create_program_address 函数。

    pub fn try_find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> Option<(Pubkey, u8)> {

    let mut bump_seed = [std::u8::MAX];
    for _ in 0..std::u8::MAX {
    {
    let mut seeds_with_bump = seeds.to_vec();
    seeds_with_bump.push(&bump_seed);
    match Self::create_program_address(&seeds_with_bump, program_id) {
    Ok(address) => return Some((address, bump_seed[0])),
    Err(PubkeyError::InvalidSeeds) => (),
    _ => break,
    }
    }
    bump_seed[0] -= 1;
    }
    None

    }

    create_program_address 函数对seedsprogram_id 执行一系列哈希操作。这些操作计算出一个密钥,然后验证计算出的密钥是否位于Ed25519椭圆曲线上。

    如果找到一个有效的PDA(即一个不在曲线上的地址),则返回该PDA。否则,返回一个错误。

    pub fn create_program_address(
    seeds: &[&[u8]],
    program_id: &Pubkey,
    ) -> Result<Pubkey, PubkeyError> {

    let mut hasher = crate::hash::Hasher::default();
    for seed in seeds.iter() {
    hasher.hash(seed);
    }
    hasher.hashv(&[program_id.as_ref(), PDA_MARKER]);
    let hash = hasher.result();

    if bytes_are_curve_point(hash) {
    return Err(PubkeyError::InvalidSeeds);
    }

    Ok(Pubkey::new(hash.as_ref()))

    }

    总结一下:

    • 该函数会将输入seedsprogram_id 一并交给 try_find_program_address 函数处理。
    • try_find_program_address 函数在输入seeds中添加一个从255开始的 bump_seed,然后连续调用 create_program_address 函数,直到找到有效的PDA
    • 一旦找到了,就会返回找到的 PDA 和用于派生 PDAbump_seed

    无需深究所有细节!关键在于理解调用 find_program_address 函数时在高层次上到底发生了什么。

    🤔 有关程序派生地址(PDA)的一些说明

    • 对于相同的输入seeds,不同的凸起值会生成不同的有效PDA
    • find_program_address 返回的 bump_seed 总是找到的第一个有效的PDA
    • 这个 bump_seed 通常被称作“标准Bump”(canonical bump)。
    • 该函数只返回一个程序派生地址和用于派生该地址的增量seeds,不会做其他事情。
    • 该函数不会初始化新的账户,也不会返回与存储数据相关的PDA

    🗺 PDA账户中数据的组织和存储

    由于程序本质上没有状态,所有程序状态都由外部账户来管理。这意味着我们必须通过一系列映射来保持事务的联系。

    你如何将seedsPDA账户相映射,将高度取决于你的具体程序设计。虽然这不是一门关于系统设计或架构的课程,但以下几个指导方针值得注意:

    • 要使用在PDA派生过程中可知的seeds
    • 请细心思考如何将数据分组到一个账户中。
    • 要谨慎地考虑每个账户中数据结构的使用。
    • 通常来说,简单就是最好的。

    这些内容确实很多!不过再次强调,你不必记住此处解释的所有内容。下一步我们将构建一个链上评论系统,让我们一起探索这些理论如何在实际操作中发挥作用!

    - - +
    Skip to main content

    🧐 PDA深入探究

    程序派生地址(Program Derived Address,PDA), 让我们一起深入了解它们的工作原理。

    PDA主要具有两个核心功能:

    • 提供一种确定性的方法来查找程序拥有的账户地址。
    • 授权派生自PDA的程序代表其签署,就像用户使用私钥签署一样。

    换言之,它们是Solana网络上用于存储的安全键值存储解决方案。

    🔎 寻找 PDAs (程序派生地址)

    到现在为止,每次我们需要派生一个地址时,都使用了一个方便的函数。那么,这个函数到底做了什么呢?要了解答案,我们需要理解Solana密钥对是如何生成的。

    回想一下密钥对的作用。它是一种证明你是你声称的人的方式。我们通过数字签名系统实现了这一点。Solana的密钥对基于所谓的Ed25519椭圆曲线(你不必担心这是什么)。

    由于PDAs由程序控制,所以它们不需要私钥。因此,我们使用不在Ed25519曲线上的地址来创建PDAs。这实际上意味着它们是没有相应私钥的公钥。

    就是这样。你不需要理解Ed25519,甚至不需要知道数字签名算法是什么。你只需要知道PDA看起来像普通的Solana地址,并且由程序控制。如果你想进一步了解,可以观看Computerphile关于数字签名的精彩视频。

    要在Solana程序中找到一个PDA,我们将使用 find_program_address 函数。

    seeds”是用于派生PDAfind_program_address 函数的可选输入。例如,seeds可以是任意组合:

    • 指令数据
    • 硬编码的值
    • 其他账户的公钥

    find_program_address 函数提供了一个额外的seeds,称为“bump seed”,以确保结果不在Ed25519曲线上。

    一旦找到有效的PDA,该函数将返回两个值:

    • PDA
    • 用于派生PDABump
    let (pda, bump_seed) = Pubkey::find_program_address(&[user.key.as_ref(), user_input.as_bytes().as_ref(), "SEED".as_bytes()], program_id);

    🍳 find_program_address函数内部解析

    find_program_address 是一个冒牌货 - 它实际上将输入 seedsprogram_id 传递给 try_find_program_address 函数。

    pub fn find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> (Pubkey, u8) {
    Self::try_find_program_address(seeds, program_id)
    .unwrap_or_else(|| panic!("Unable to find a viable program address bump seed"));
    }

    然后, try_find_program_address 函数引入了 bump_seed

    bump_seed 是一个 u8 变量,其值范围在0255之间。它被附加到可选的输入seeds中,然后传递给 create_program_address 函数。

    pub fn try_find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> Option<(Pubkey, u8)> {

    let mut bump_seed = [std::u8::MAX];
    for _ in 0..std::u8::MAX {
    {
    let mut seeds_with_bump = seeds.to_vec();
    seeds_with_bump.push(&bump_seed);
    match Self::create_program_address(&seeds_with_bump, program_id) {
    Ok(address) => return Some((address, bump_seed[0])),
    Err(PubkeyError::InvalidSeeds) => (),
    _ => break,
    }
    }
    bump_seed[0] -= 1;
    }
    None

    }

    create_program_address 函数对seedsprogram_id 执行一系列哈希操作。这些操作计算出一个密钥,然后验证计算出的密钥是否位于Ed25519椭圆曲线上。

    如果找到一个有效的PDA(即一个不在曲线上的地址),则返回该PDA。否则,返回一个错误。

    pub fn create_program_address(
    seeds: &[&[u8]],
    program_id: &Pubkey,
    ) -> Result<Pubkey, PubkeyError> {

    let mut hasher = crate::hash::Hasher::default();
    for seed in seeds.iter() {
    hasher.hash(seed);
    }
    hasher.hashv(&[program_id.as_ref(), PDA_MARKER]);
    let hash = hasher.result();

    if bytes_are_curve_point(hash) {
    return Err(PubkeyError::InvalidSeeds);
    }

    Ok(Pubkey::new(hash.as_ref()))

    }

    总结一下:

    • 该函数会将输入seedsprogram_id 一并交给 try_find_program_address 函数处理。
    • try_find_program_address 函数在输入seeds中添加一个从255开始的 bump_seed,然后连续调用 create_program_address 函数,直到找到有效的PDA
    • 一旦找到了,就会返回找到的 PDA 和用于派生 PDAbump_seed

    无需深究所有细节!关键在于理解调用 find_program_address 函数时在高层次上到底发生了什么。

    🤔 有关程序派生地址(PDA)的一些说明

    • 对于相同的输入seeds,不同的凸起值会生成不同的有效PDA
    • find_program_address 返回的 bump_seed 总是找到的第一个有效的PDA
    • 这个 bump_seed 通常被称作“标准Bump”(canonical bump)。
    • 该函数只返回一个程序派生地址和用于派生该地址的增量seeds,不会做其他事情。
    • 该函数不会初始化新的账户,也不会返回与存储数据相关的PDA

    🗺 PDA账户中数据的组织和存储

    由于程序本质上没有状态,所有程序状态都由外部账户来管理。这意味着我们必须通过一系列映射来保持事务的联系。

    你如何将seedsPDA账户相映射,将高度取决于你的具体程序设计。虽然这不是一门关于系统设计或架构的课程,但以下几个指导方针值得注意:

    • 要使用在PDA派生过程中可知的seeds
    • 请细心思考如何将数据分组到一个账户中。
    • 要谨慎地考虑每个账户中数据结构的使用。
    • 通常来说,简单就是最好的。

    这些内容确实很多!不过再次强调,你不必记住此处解释的所有内容。下一步我们将构建一个链上评论系统,让我们一起探索这些理论如何在实际操作中发挥作用!

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module4/ship-a-staking-app/build-a-staking-ui/index.html b/Solana-Co-Learn/module4/ship-a-staking-app/build-a-staking-ui/index.html index 95ab91373..8346e0042 100644 --- a/Solana-Co-Learn/module4/ship-a-staking-app/build-a-staking-ui/index.html +++ b/Solana-Co-Learn/module4/ship-a-staking-app/build-a-staking-ui/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    构建一个质押用户界面

    让我们开始吧,我们要在我们的buildoors NFT项目上取得一些进展。在这个核心环节,我们希望完成三件事情:

    1. 为质押页面构建用户界面

    这就是我们的目标:

    请在前端项目的根目录下创建一个新的 utils 文件夹。然后创建一个名为 instructions.ts 的文件,并从NFT质押项目中复制/粘贴整个 instructions.ts 文件。由于代码较长超过200行,所以在这里就不粘贴了。😬

    请注意,“STAKING 4 DAYS”和“READY TO STAKE”这两个方块不会同时显示,只会显示与当前NFT质押状态相关的方块。

    如果需要,可以使用模拟数据来使界面大致符合你的要求。不过请注意,你的界面无需完全复制这个样子,可以根据需要进行个性化定制。

    1. 将实际质押功能添加到程序中

    别忘了,我们已经做了一些工作来存储状态,但程序还没有实际进行NFT质押或铸造BLD代币。我们将解决这个问题!

    1. 一旦程序完全准备就绪,就可以回到用户界面并使其工作起来。

    具体而言,“claim $BLD”,“stake buildoor”和“unstake buildoor”按钮应调用质押程序的相关指令。


    像往常一样,你可以独立尝试。请注意,这不是一项简单的任务,可能需要几个小时甚至更长时间。

    一旦你完成了,或者感觉快要困顿了,可以随时观看接下来的视频教程。在下一课中,我们将展示一种可能的解决方案。

    添加样式

    当我们回到用户界面构建时,首先要做的是在应用文件(//pages/_app.tsx)中为主题添加一些颜色。代码如下:

    const colors = {
    background: "#1F1F1F",
    accent: "#833BBE",
    bodyText: "rgba(255, 255, 255, 0.75)",
    secondaryPurple: "#CB8CFF",
    containerBg: "rgba(255, 255, 255, 0.1)",
    containerBgSecondary: "rgba(255, 255, 255, 0.05)",
    buttonGreen: "#7EFFA7",
    }

    新建薄荷路由

    我们将要在NewMint文件(//pages/newMint.tsx)中实现handleClick函数,这个函数将在质押后路由到新页面。

    首先,我们来初始化路由,命名为useRouter,并且别忘了检查那些可能遗漏的导入。

    const router = useRouter()

    接下来我们来实现这个异步事件处理函数,并路由到我们新命名为stake的页面。我们还将传递图片,因为我们已经从图片源获取了它,所以不需要再次加载。

    const handleClick: MouseEventHandler<HTMLButtonElement> = useCallback(
    async (event) => {
    router.push(`/stake?mint=${mint}&imageSrc=${metadata?.image}`)
    },
    [router, mint, metadata]
    )

    呀,当前这是一条无效的路径,会导致一个错误,所以让我们创建这个实际的页面。这将是一个新的文件,位于页面目录下(//pages/stake.tsx)。

    质押着陆页面,左半部分

    让我们为Stake创建一个NextPage,并确保已经导入了'next'库。

    const Stake: NextPage<StakeProps> = ({ mint, imageSrc }) => {
    return(
    <div></div>
    )
    }

    现在,让我们定义一些重要的属性。

    interface StakeProps {
    mint: PublicKey
    imageSrc: string
    }

    好的,我们继续前进。不妨快速检查一下npm run dev,确保前端正常渲染。

    如果你一直在忙着制造糖果,你可能会想要重置你的糖果机。🍬📠

    一切进展顺利。

    稍事休息,我们来谈谈前端的最佳实践。在顶级目录下创建一个env.local文件,并使用格式NEXT_PUBLIC_<变量名>来命名你的变量。这样它就会被注入到浏览器端的代码中,你就可以在文件中使用它。然后可以继续替换代码中的硬编码键值。

    回到质押页面,让我们开始构建我们实际想要在页面上显示的内容。我们将使用一些来自Chakra的组件,请确保你的导入正在自动完成,或者手动添加它们。如果你是前端专家,可以自由设计,否则可以跟随我的精美像素设计。👾👾👾

    有一些与我们之前为其他页面做的事情相似的部分,以下是需要注意的几点:

    1. 这里有一个与isStaking相关的押注检查,它会决定页面上显示"STAKING"还是"UNSTAKED"。你需要一个useState,并初始设置为false

    const [isStaking, setIsStaking] = useState(false)

    1. 我们想要显示抵押者的等级,所以需要另一个useState

    const [level, setLevel] = useState(1)

    再次运行npm run dev…哦对,我们需要一些属性,这样在我们首次访问时页面就可以显示一张图片。所以,让我们确保在文件底部调用了getInitialProps函数:

    Stake.getInitialProps = async ({ query }: any) => {
    const { mint, imageSrc } = query

    if (!mint || !imageSrc) throw { error: "no mint" }

    try {
    const mintPubkey = new PublicKey(mint)
    return { mint: mintPubkey, imageSrc: imageSrc }
    } catch {
    throw { error: "invalid mint" }
    }
    }

    质押页面,右半部分 && 质押选项展示组件

    好的,左半部分的工作已经完成,现在我们来专注于右侧。我们需要一个名为 VStack 的容器,在其中包括一些用于展示所需内容的独立逻辑。因此,让我们创建一个独立组件,命名为 StakeOptionsDisplay//components/StakeOptionsDisplay.tsx)。

    首先,我们从一个明显的检查开始,检查是否正在抵押,并在VStack中构建起来。

    export const StakeOptionsDisplay = ({
    isStaking,

    }: {
    isStaking: boolean

    }) => {
    return(
    )
    }

    在你遵循设计规范的同时,我们将在各个部分检查以下属性:

    1. isStaking会显示抵押的天数,或者显示"准备抵押"
    2. 已质押的天数,作为数字
    3. 总收入,作为数字
    4. 可申领的,作为数字

    以下是渲染的最终产品,适合那些喜欢粘贴前端代码的人 :P

    return (
    <VStack
    bgColor="containerBg"
    borderRadius="20px"
    padding="20px 40px"
    spacing={5}
    >
    <Text
    bgColor="containerBgSecondary"
    padding="4px 8px"
    borderRadius="20px"
    color="bodyText"
    as="b"
    fontSize="sm"
    >
    {isStaking
    ? `正在质押 ${daysStaked}${daysStaked === 1 ? "" : "S"}`
    : "准备质押"}
    </Text>
    <VStack spacing={-1}>
    <Text color="white" as="b" fontSize="4xl">
    {isStaking ? `${totalEarned} $BLD` : "0 $BLD"}
    </Text>
    <Text color="bodyText">
    {isStaking ? `${claimable} $BLD 已赚取` : "通过质押赚取 $BLD"}
    </Text>
    </VStack>
    <Button
    onClick={isStaking ? handleClaim : handleStake}
    bgColor="buttonGreen"
    width="200px"
    >
    <Text as="b">{isStaking ? "申领 $BLD" : "质押buildoor"}</Text>
    </Button>
    {isStaking ? <Button onClick={handleUnstake}>取消质押</Button> : null}
    </VStack>
    )

    如你所见,我们需要构建handleStakehandleClaimhandleUnstake的功能,稍后我们将回到这些。

    ...接着回到质押文件(//pages/stake.tsx)导入该组件和所需的属性。

    装备和战利品箱组件

    最后,我们来为装备和战利品箱构建另一个组件,可以称之为 ItemBox//components/ItemBox.tsx)。

    这是一个相对简单的案例,你只需按照视频操作,并可以随时与此代码进行比较。

    import { Center } from "@chakra-ui/react"
    import { ReactNode } from "react"

    export const ItemBox = ({
    children,
    bgColor,
    }: {
    children: ReactNode
    bgColor?: string
    }) => {
    return (
    <Center
    height="120px"
    width="120px"
    bgColor={bgColor || "containerBg"}
    borderRadius="10px"
    >
    {children}
    </Center>
    )
    }

    就这样,随意调整,根据你的喜好进行设计。接下来,我们将深入质押计划,并添加与代币相关的内容。

    做得很好,我们知道事情变得更复杂了,还有许多细致的工作要做——慢慢来,检查代码,如果有什么不明白的地方,在Discord上与我们联系。

    - - +
    Skip to main content

    构建一个质押用户界面

    让我们开始吧,我们要在我们的buildoors NFT项目上取得一些进展。在这个核心环节,我们希望完成三件事情:

    1. 为质押页面构建用户界面

    这就是我们的目标:

    请在前端项目的根目录下创建一个新的 utils 文件夹。然后创建一个名为 instructions.ts 的文件,并从NFT质押项目中复制/粘贴整个 instructions.ts 文件。由于代码较长超过200行,所以在这里就不粘贴了。😬

    请注意,“STAKING 4 DAYS”和“READY TO STAKE”这两个方块不会同时显示,只会显示与当前NFT质押状态相关的方块。

    如果需要,可以使用模拟数据来使界面大致符合你的要求。不过请注意,你的界面无需完全复制这个样子,可以根据需要进行个性化定制。

    1. 将实际质押功能添加到程序中

    别忘了,我们已经做了一些工作来存储状态,但程序还没有实际进行NFT质押或铸造BLD代币。我们将解决这个问题!

    1. 一旦程序完全准备就绪,就可以回到用户界面并使其工作起来。

    具体而言,“claim $BLD”,“stake buildoor”和“unstake buildoor”按钮应调用质押程序的相关指令。


    像往常一样,你可以独立尝试。请注意,这不是一项简单的任务,可能需要几个小时甚至更长时间。

    一旦你完成了,或者感觉快要困顿了,可以随时观看接下来的视频教程。在下一课中,我们将展示一种可能的解决方案。

    添加样式

    当我们回到用户界面构建时,首先要做的是在应用文件(//pages/_app.tsx)中为主题添加一些颜色。代码如下:

    const colors = {
    background: "#1F1F1F",
    accent: "#833BBE",
    bodyText: "rgba(255, 255, 255, 0.75)",
    secondaryPurple: "#CB8CFF",
    containerBg: "rgba(255, 255, 255, 0.1)",
    containerBgSecondary: "rgba(255, 255, 255, 0.05)",
    buttonGreen: "#7EFFA7",
    }

    新建薄荷路由

    我们将要在NewMint文件(//pages/newMint.tsx)中实现handleClick函数,这个函数将在质押后路由到新页面。

    首先,我们来初始化路由,命名为useRouter,并且别忘了检查那些可能遗漏的导入。

    const router = useRouter()

    接下来我们来实现这个异步事件处理函数,并路由到我们新命名为stake的页面。我们还将传递图片,因为我们已经从图片源获取了它,所以不需要再次加载。

    const handleClick: MouseEventHandler<HTMLButtonElement> = useCallback(
    async (event) => {
    router.push(`/stake?mint=${mint}&imageSrc=${metadata?.image}`)
    },
    [router, mint, metadata]
    )

    呀,当前这是一条无效的路径,会导致一个错误,所以让我们创建这个实际的页面。这将是一个新的文件,位于页面目录下(//pages/stake.tsx)。

    质押着陆页面,左半部分

    让我们为Stake创建一个NextPage,并确保已经导入了'next'库。

    const Stake: NextPage<StakeProps> = ({ mint, imageSrc }) => {
    return(
    <div></div>
    )
    }

    现在,让我们定义一些重要的属性。

    interface StakeProps {
    mint: PublicKey
    imageSrc: string
    }

    好的,我们继续前进。不妨快速检查一下npm run dev,确保前端正常渲染。

    如果你一直在忙着制造糖果,你可能会想要重置你的糖果机。🍬📠

    一切进展顺利。

    稍事休息,我们来谈谈前端的最佳实践。在顶级目录下创建一个env.local文件,并使用格式NEXT_PUBLIC_<变量名>来命名你的变量。这样它就会被注入到浏览器端的代码中,你就可以在文件中使用它。然后可以继续替换代码中的硬编码键值。

    回到质押页面,让我们开始构建我们实际想要在页面上显示的内容。我们将使用一些来自Chakra的组件,请确保你的导入正在自动完成,或者手动添加它们。如果你是前端专家,可以自由设计,否则可以跟随我的精美像素设计。👾👾👾

    有一些与我们之前为其他页面做的事情相似的部分,以下是需要注意的几点:

    1. 这里有一个与isStaking相关的押注检查,它会决定页面上显示"STAKING"还是"UNSTAKED"。你需要一个useState,并初始设置为false

    const [isStaking, setIsStaking] = useState(false)

    1. 我们想要显示抵押者的等级,所以需要另一个useState

    const [level, setLevel] = useState(1)

    再次运行npm run dev…哦对,我们需要一些属性,这样在我们首次访问时页面就可以显示一张图片。所以,让我们确保在文件底部调用了getInitialProps函数:

    Stake.getInitialProps = async ({ query }: any) => {
    const { mint, imageSrc } = query

    if (!mint || !imageSrc) throw { error: "no mint" }

    try {
    const mintPubkey = new PublicKey(mint)
    return { mint: mintPubkey, imageSrc: imageSrc }
    } catch {
    throw { error: "invalid mint" }
    }
    }

    质押页面,右半部分 && 质押选项展示组件

    好的,左半部分的工作已经完成,现在我们来专注于右侧。我们需要一个名为 VStack 的容器,在其中包括一些用于展示所需内容的独立逻辑。因此,让我们创建一个独立组件,命名为 StakeOptionsDisplay//components/StakeOptionsDisplay.tsx)。

    首先,我们从一个明显的检查开始,检查是否正在抵押,并在VStack中构建起来。

    export const StakeOptionsDisplay = ({
    isStaking,

    }: {
    isStaking: boolean

    }) => {
    return(
    )
    }

    在你遵循设计规范的同时,我们将在各个部分检查以下属性:

    1. isStaking会显示抵押的天数,或者显示"准备抵押"
    2. 已质押的天数,作为数字
    3. 总收入,作为数字
    4. 可申领的,作为数字

    以下是渲染的最终产品,适合那些喜欢粘贴前端代码的人 :P

    return (
    <VStack
    bgColor="containerBg"
    borderRadius="20px"
    padding="20px 40px"
    spacing={5}
    >
    <Text
    bgColor="containerBgSecondary"
    padding="4px 8px"
    borderRadius="20px"
    color="bodyText"
    as="b"
    fontSize="sm"
    >
    {isStaking
    ? `正在质押 ${daysStaked}${daysStaked === 1 ? "" : "S"}`
    : "准备质押"}
    </Text>
    <VStack spacing={-1}>
    <Text color="white" as="b" fontSize="4xl">
    {isStaking ? `${totalEarned} $BLD` : "0 $BLD"}
    </Text>
    <Text color="bodyText">
    {isStaking ? `${claimable} $BLD 已赚取` : "通过质押赚取 $BLD"}
    </Text>
    </VStack>
    <Button
    onClick={isStaking ? handleClaim : handleStake}
    bgColor="buttonGreen"
    width="200px"
    >
    <Text as="b">{isStaking ? "申领 $BLD" : "质押buildoor"}</Text>
    </Button>
    {isStaking ? <Button onClick={handleUnstake}>取消质押</Button> : null}
    </VStack>
    )

    如你所见,我们需要构建handleStakehandleClaimhandleUnstake的功能,稍后我们将回到这些。

    ...接着回到质押文件(//pages/stake.tsx)导入该组件和所需的属性。

    装备和战利品箱组件

    最后,我们来为装备和战利品箱构建另一个组件,可以称之为 ItemBox//components/ItemBox.tsx)。

    这是一个相对简单的案例,你只需按照视频操作,并可以随时与此代码进行比较。

    import { Center } from "@chakra-ui/react"
    import { ReactNode } from "react"

    export const ItemBox = ({
    children,
    bgColor,
    }: {
    children: ReactNode
    bgColor?: string
    }) => {
    return (
    <Center
    height="120px"
    width="120px"
    bgColor={bgColor || "containerBg"}
    borderRadius="10px"
    >
    {children}
    </Center>
    )
    }

    就这样,随意调整,根据你的喜好进行设计。接下来,我们将深入质押计划,并添加与代币相关的内容。

    做得很好,我们知道事情变得更复杂了,还有许多细致的工作要做——慢慢来,检查代码,如果有什么不明白的地方,在Discord上与我们联系。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module4/ship-a-staking-app/build-a-token-minter/index.html b/Solana-Co-Learn/module4/ship-a-staking-app/build-a-token-minter/index.html index 21dc8d391..2d30a3274 100644 --- a/Solana-Co-Learn/module4/ship-a-staking-app/build-a-token-minter/index.html +++ b/Solana-Co-Learn/module4/ship-a-staking-app/build-a-token-minter/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    构建一个代币铸造器

    铸币、质押等等

    很好,我们已经走了很长的路,现在让我们重新关注NFT质押计划。今天,我们将为质押者添加铸造奖励代币和执行质押操作所需的所有功能。有别于以前使用Solana Playground的方式,我们将在本地完成所有操作。可以从以下起始库开始:solutions-sans-tokens分支

    你会注意到这里有些不同。现在有一个名为“TS”的文件夹,其中包含了我们之前在Solana Playground的客户端项目中的全部内容。

    在你的前端项目中,需要在根目录下创建一个新的 utils 文件夹。接着创建一个名为 instructions.ts 的文件,并从NFT质押项目中复制/粘贴整个 instructions.ts 文件。由于代码超过200行,我在此不做展示。😬唯一重要的修改是在 /<project-name>/src/ts/src/utils/constants.ts 中,PROGRAM_ID 从项目的密钥对中读取。

    const string = fs.readFileSync(
    "../target/deploy/solana_nft_staking_program-keypair.json",
    "utf8"
    )

    ...

    export const PROGRAM_ID = Keypair.fromSecretKey(secretKey).publicKey

    准备好了!我们可以开始了。首先切换到TS目录,然后运行 npm run start。确保你已经完成了 cargo build-sbfsolana program deploy,并且你的集群设置是正确的。如果一切正常,它应该能够启动并运行。在控制台上,你应该能看到 stakesredeemsunstakes 的输出。请耐心等待,年轻的练剑师,这可能需要一两分钟的时间。

    假设一切顺利🎉,我们可以跳转到处理器文件(//src/processor.rs)。

    首先,我们需要处理一些导入:

    use mpl_token_metadata::ID as mpl_metadata_program_id;
    use spl_token::ID as spl_token_program_id;

    此外,在 solana_program::program::{invoke_signed} 的导入中添加 invoke

    现在,转到 process_stake 函数,我们将在这里进行第一次修改。

    习惯于此吧,我们经常会发现自己需要在许多地方添加账户。所以,现在是时候添加一些账户,以便我们能够真正借助令牌程序工作了。

    let nft_mint = next_account_info(account_info_iter)?;
    let nft_edition = next_account_info(account_info_iter)?;
    let stake_state = next_account_info(account_info_iter)?;
    let program_authority = next_account_info(account_info_iter)?;
    let token_program = next_account_info(account_info_iter)?;
    let metadata_program = next_account_info(account_info_iter)?;

    委托和冻结 —— 质押

    下一步,我们需要将程序设置为NFT的代理,委托NFT的权限,以便程序能够代表我们发起交易。

    msg!("Approving delegation");
    invoke(
    &spl_token::instruction::approve(
    &spl_token_program_id,
    nft_token_account.key,
    program_authority.key,
    user.key,
    &[user.key],
    1,
    )?,
    &[
    nft_token_account.clone(),
    program_authority.clone(),
    user.clone(),
    token_program.clone(),
    ],
    )?;

    现在,我们可以开始实际冻结代币的过程。我们不是真正改变代币的所有权,而是将其冻结,使在质押期间无法对代币进行任何操作。首先,我们需要为程序权限派生PDA(程序派生地址)。简单来说,我们会使用PDA作为代币铸造的授权实体,从而能够冻结账户。

    别忘了检查并确保PDA已经被提取。

    let (delegated_auth_pda, delegate_bump) =
    Pubkey::find_program_address(&[b"authority"], program_id);

    if delegated_auth_pda != *program_authority.key {
    msg!("Invalid seeds for PDA");
    return Err(StakeError::InvalidPda.into());
    }

    回到冻结操作本身,与委托批准不同,这里使用invoke_signed以从我们的程序进行签名。

    msg!("freezing NFT token account");
    invoke_signed(
    &mpl_token_metadata::instruction::freeze_delegated_account(
    mpl_metadata_program_id,
    *program_authority.key,
    *nft_token_account.key,
    *nft_edition.key,
    *nft_mint.key,
    ),
    &[
    program_authority.clone(),
    nft_token_account.clone(),
    nft_edition.clone(),
    nft_mint.clone(),
    metadata_program.clone(),
    ],
    &[&[b"authority", &[delegate_bump]]],
    )?;

    我们的程序的PDA现在具备了冻结令牌的权限。🧊

    接下来,我们将转到TypeScript文件(//ts/src/utils/instruction.rs),并向createStakingInstruction函数中添加更多的账户,确保其正常工作。

    我们需要确保新添加的账户与//src/processor.rs文件中process_stake函数的账户相匹配:

    nftMint: PublicKey,
    nftEdition: PublicKey,
    tokenProgram: PublicKey,
    metadataProgram: PublicKey,

    然后,我们将所有这些按照正确的顺序添加到TransactionInstruction中的账户列表。顺序非常重要。

    首先,取得授权账户:

    const [delegateAuthority] = PublicKey.findProgramAddressSync(
    [Buffer.from("authority")],
    programId
    )

    总共有5个新账户,你需要再次确保它们的顺序,并检查哪些是可写的,哪些是签名者。

    ...
    {
    pubkey: nftMint,
    isWritable: false,
    isSigner: false,
    },
    {
    pubkey: nftEdition,
    isWritable: false,
    isSigner: false,
    },
    ...
    {
    pubkey: delegateAuthority,
    isWritable: true,
    isSigner: false,
    },
    {
    pubkey: tokenProgram,
    isWritable: false,
    isSigner: false,
    },
    {
    pubkey: metadataProgram,
    isWritable: false,
    isSigner: false,
    },

    测试我们的质押功能

    接下来,进入索引文件(//ts/src/index.rs),在创建stakeInstruction的地方,在testStaking函数中添加与之匹配的相同账户。

    下面是四个附加项:

    nft.mintAddress,
    nft.masterEditionAddress,
    TOKEN_PROGRAM_ID,
    METADATA_PROGRAM_ID,
    import { TOKEN_PROGRAM_ID } from "@solana/spl-token"
    import { PROGRAM_ID as METADATA_PROGRAM_ID } from "@metaplex-foundation/mpl-token-metadata"

    现在是时候测试我们的进展了:

    1. 使用cargo build-sbf重新构建程序,然后使用solana program deploy {path}进行更新。
    2. 确保你处于ts目录下,并执行npm run start

    假设没有出错,那我们就回到processor.rs文件,并向process_redeem函数添加相似的代码。

    委派和冻结 -- 兑换

    首先,你猜对了,我们要添加账户——一共有4个!

    let stake_mint = next_account_info(account_info_iter)?;
    let stake_authority = next_account_info(account_info_iter)?;
    let user_stake_ata = next_account_info(account_info_iter)?;
    let token_program = next_account_info(account_info_iter)?;

    接下来,我们将验证一些新账户。首先,我们要推导出stake_auth_pda,然后用自定义错误验证PDA

    let (stake_auth_pda, auth_bump) = Pubkey::find_program_address(&[b"mint"], program_id);

    if *stake_authority.key != stake_auth_pda {
    msg!("Invalid stake mint authority!");
    return Err(StakeError::InvalidPda.into());
    }

    向下滚动一些,我们要调用一个invoke_signed来调用令牌程序,以铸造代币,等我们了解了redeem_amount之后。我们需要指令的各种键,然后是所需的账户,最后是授权的种子。别忘了使用?来传播错误,否则红色波浪线将始终困扰你。

    invoke_signed(
    &spl_token::instruction::mint_to(
    token_program.key,
    stake_mint.key,
    user_stake_ata.key,
    stake_authority.key,
    &[stake_authority.key],
    redeem_amount.try_into().unwrap(),
    )?,
    &[
    stake_mint.clone(),
    user_stake_ata.clone(),
    stake_authority.clone(),
    token_program.clone(),
    ],
    &[&[b"mint", &[auth_bump]]],
    )?;

    这应该在此文件中处理铸币操作,但我们必须在客户端上添加新账户。

    我们回到之前的instruction.ts文件,向下滚动到createRedeemInstruction,并添加以下账户。

    mint: PublicKey,
    userStakeATA: PublicKey,
    tokenProgram: PublicKey,

    现在请记住,一些账户是派生的,如权威账户,所以我们不需要手动添加它。

    然后跳到TransactionInstruction本身,首先推导出mintAuth

    const [mintAuth] = PublicKey.findProgramAddressSync(
    [Buffer.from("mint")],
    programId
    )

    接下来进入return new TransactionInstruction,添加相关账户,并标明它们是否可写和/或可签。以下是我们需要添加的4个账户 - 请记住,顺序很重要。

    {
    pubkey: mint,
    isWritable: true,
    isSigner: false,
    },
    {
    pubkey: mintAuth,
    isWritable: false,
    isSigner: false,
    },
    {
    pubkey: userStakeATA,
    isWritable: true,
    isSigner: false,
    },
    {
    pubkey: tokenProgram,
    isSigner: false,
    isWritable: false,
    },

    这应该包括了我们兑换所需的所有内容。我们最后需要回到同一个index.ts文件,并确保我们正确调用它,但这部分有些复杂,所以让我们先回到processor.rs并完成process_unstake函数。

    委托和冻结——解除质押

    解除质押过程基本上是将我们刚刚进行的质押和赎回步骤结合在一起,因此需要用到我们刚才操作过的所有账户。

    以下是我们需要添加的所有账户:

    let nft_mint = next_account_info(account_info_iter)?;
    let nft_edition = next_account_info(account_info_iter)?;
    ... (stake_state 应该在我们之前的代码中)
    let program_authority = next_account_info(account_info_iter)?;
    let stake_mint = next_account_info(account_info_iter)?;
    let stake_authority = next_account_info(account_info_iter)?;
    let user_stake_ata = next_account_info(account_info_iter)?;
    let token_program = next_account_info(account_info_iter)?;
    let metadata_program = next_account_info(account_info_iter)?;

    我们可以向下滚动,并复制粘贴 process_stakeprocess_redeem 函数中的一些验证:

    let (delegated_auth_pda, delegate_bump) =
    Pubkey::find_program_address(&[b"authority"], program_id);
    if delegated_auth_pda != *program_authority.key {
    msg!("Invalid seeds for PDA");
    return Err(StakeError::InvalidPda.into());
    }

    let (stake_auth_pda, auth_bump) = Pubkey::find_program_address(&[b"mint"], program_id);
    if *stake_authority.key != stake_auth_pda {
    msg!("Invalid stake mint authority!");
    return Err(StakeError::InvalidPda.into());
    }

    好的,这是相当新的部分,我们要“解冻”NFT代币账户。如果你还记得,我们之前冻结了它,现在我们要解冻它。这段代码与上面的冻结代码完全相反,我们只需更改辅助函数,使用 thaw_delegated_account

    msg!("thawing NFT token account");
    invoke_signed(
    &mpl_token_metadata::instruction::thaw_delegated_account(
    mpl_metadata_program_id,
    *program_authority.key,
    *nft_token_account.key,
    *nft_edition.key,
    *nft_mint.key,
    ),
    &[
    program_authority.clone(),
    nft_token_account.clone(),
    nft_edition.clone(),
    nft_mint.clone(),
    metadata_program.clone(),
    ],
    &[&[b"authority", &[delegate_bump]]],
    )?;

    接下来,我们需要撤销委托权限。与授权委托类似,但并不完全相同。我们可以移除 program_authority 字段,因为它不是必需的,并从批准助手函数中移除 amount

    msg!("Revoke delegation");
    invoke(
    &spl_token::instruction::revoke(
    &spl_token_program_id,
    nft_token_account.key,
    user.key,
    &[user.key],
    )?,
    &[
    nft_token_account.clone(),
    user.clone(),
    token_program.clone(),
    ],
    )?;

    最后,我们将从赎回函数中复制 invoke_signed,粘贴到 redeem_amount 下面。

    invoke_signed(
    &spl_token::instruction::mint_to(
    token_program.key,
    stake_mint.key,
    user_stake_ata.key,
    stake_authority.key,
    &[stake_authority.key],
    redeem_amount.try_into().unwrap(),
    )?,
    &[
    stake_mint.clone(),
    user_stake_ata.clone(),
    stake_authority.clone(),
    token_program.clone(),
    ],
    &[&[b"mint", &[auth_bump]]],
    )?;

    哦,还有一件事,我们实际上没有设置 redeem_amount,之前只是用了 unix_time。所以,改成 100 * unit_time,我们以后可以调整。确保在上述两个函数中都进行更改。

    这里应该就是了,回到客户端的文件上,添加所有的账户。向下滚动到 createUnstakeInstruction,将以下内容作为参数添加进去。

    nftMint: PublicKey,
    nftEdition: PublicKey,
    stakeMint: PublicKey,
    userStakeATA: PublicKey,
    tokenProgram: PublicKey,
    metadataProgram: PublicKey,

    再次提醒,有一些账户是自动派生的,所以我们不需要手动添加。

    接下来我们推导出 delegateAuthoritymintAuth,这与上面的代码完全相同。

    const [delegateAuthority] = PublicKey.findProgramAddressSync(
    [Buffer.from("authority")],
    programId
    )

    const [mintAuth] = PublicKey.findProgramAddressSync(
    [Buffer.from("mint")],
    programId
    )

    最后,我们将它们全部添加到指令中。这是很多账户,所以我们在这里全部发布,而不仅仅是我们要添加的那些。让你的眼睛不再在函数和文件之间来回移动。

    {
    pubkey: nftHolder,
    isWritable: false,
    isSigner: true,
    },
    {
    pubkey: nftTokenAccount,
    isWritable: true,
    isSigner: false,
    },
    {
    pubkey: nftMint,
    isWritable: false,
    isSigner: false,
    },
    {
    pubkey: nftEdition,
    isWritable: false,
    isSigner: false,
    },
    {
    pubkey: stakeAccount,
    isWritable: true,
    isSigner: false,
    },
    {
    pubkey: delegateAuthority,
    isWritable: true,
    isSigner: false,
    },
    {
    pubkey: stakeMint,
    isWritable: true,
    isSigner: false,
    },
    {
    pubkey: mintAuth,
    isWritable: false,
    isSigner: false,
    },
    {
    pubkey: userStakeATA,
    isWritable: true,
    isSigner: false,
    },
    {
    pubkey: tokenProgram,
    isWritable: false,
    isSigner: false,
    },
    {
    pubkey: metadataProgram,
    isWritable: false,
    isSigner: false,
    },

    测试我们的功能

    好的,好的,我知道你已经迫不及待了,我们快到终点了。现在让我们回到index.ts文件中,调用并测试所有的函数。对于testRedeem函数,我们需要代币的铸币地址和用户的代币账户,以及createUnstakeInstruction

    首先,在testRedeem函数的参数中添加以下内容:

    stakeMint: web3.PublicKey,
    userStakeATA: web3.PublicKey

    然后,将它们添加到下方的createRedeemInstruction中:

    stakeMint,
    userStakeATA,
    TOKEN_PROGRAM_ID,
    PROGRAM_ID

    testUnstaking函数也进行上述相同的操作。

    接着,在createUnstakingInstruction中添加以下内容:

    nft.mintAddress,
    nft.masterEditionAddress,
    stakeMint,
    userStakeATA,
    TOKEN_PROGRAM_ID,
    METADATA_PROGRAM_ID,

    现在向下滚动到main()函数的调用位置,你会注意到testRedeemtestUnstaking都是红色的,因为它们缺少一些参数。

    首先,我们要声明stakeMint,目前我们将其硬编码,以及userStakeATA,该函数会创建ATA(如果ATA还不存在的话)。

    const stakeMint = new web3.PublicKey(
    "EMPTY FOR A MINUTE"
    )

    const userStakeATA = await getOrCreateAssociatedTokenAccount(
    connection,
    user,
    stakeMint,
    user.publicKey
    )

    ...现在,将调用更改为接收额外的参数:

    await testRedeem(connection, user, nft, stakeMint, userStakeATA.address)
    await testUnstaking(connection, user, nft, stakeMint, userStakeATA.address)

    前端编辑以测试功能

    我们暂时要切换到前端Buildoors项目中的index.ts文件(//tokens/bld/index.ts)。在这里,我们使用createBldToken函数创建BLD令牌。

    在该函数内部,我们称token.CreateMint的第三个参数为铸币授权,它掌管着铸币过程。最初,它是一个payer.publicKey,用于初始调用。我们很快就会更改铸币授权。

    首先,我们要向createBldToken函数添加一个参数:

    programId: web3.PublicKey

    然后向下滚动到主函数中的调用位置,并为await createBldToken调用添加第三个参数。

    new web3.PublicKey("USE YOUR PROGRAM ID")

    如果你找不到程序ID,你可以重新部署,控制台会显示你所需的程序ID

    向上滚动,超过const tokenMint,找到mintAuth。你可以在Anchor NFT质押计划中找到授权的具体信息。

    const [mintAuth] = await web3.PublicKey.findProgramAddress(
    [Buffer.from("mint")],
    programId
    )

    滚动回到下面,在transactionSignature创建后,我们将设置新的铸币权限(这是我们上面提到的更改)。

    await token.setAuthority(
    connection,
    payer,
    tokenMint,
    payer.publicKey,
    token.AuthorityType.MintTokens,
    mintAuth
    )

    现在,我们可以使用新的认证重新创建BLD令牌,并将其添加到上面的stakeMint中。

    const stakeMint = new web3.PublicKey(
    "EMPTY FOR A MINUTE"
    )

    最后,全面测试一切

    现在,请切换到主目录并运行 npm run create-bld-token。确保你已经将环境设置为devnet

    核实你的构建脚本,它应该如下所示:

    "creat-bld-token": "ts-node tokens/bld/index.ts"

    一旦成功执行完毕,你可以从tokens/bld目录中的cache.json文件中获取新的密钥。

    现在我们终于回到了NFT质押计划,并且可以在stakeMint创建中使用这个密钥:

    const stakeMint = new web3.PublicKey(
    "MINT KEY FROM CACHE.JSON"
    )

    现在应该一切准备就绪,并可以正常工作。返回到ts目录,并使用npm run start进行全面测试。如果一切顺利,你的控制台将确认初始化、质押、赎回和解质押都已成功完成。

    确实涉及了许多细节。深呼吸,给自己一些喘息的空间。这是一项充满挑战性的任务,不妨再回头看一遍,复习一下,甚至再次实践,不管需要付出多少努力。只要你能掌握这些内容,你就将成为一名出色的Solana开发者。

    - - +
    Skip to main content

    构建一个代币铸造器

    铸币、质押等等

    很好,我们已经走了很长的路,现在让我们重新关注NFT质押计划。今天,我们将为质押者添加铸造奖励代币和执行质押操作所需的所有功能。有别于以前使用Solana Playground的方式,我们将在本地完成所有操作。可以从以下起始库开始:solutions-sans-tokens分支

    你会注意到这里有些不同。现在有一个名为“TS”的文件夹,其中包含了我们之前在Solana Playground的客户端项目中的全部内容。

    在你的前端项目中,需要在根目录下创建一个新的 utils 文件夹。接着创建一个名为 instructions.ts 的文件,并从NFT质押项目中复制/粘贴整个 instructions.ts 文件。由于代码超过200行,我在此不做展示。😬唯一重要的修改是在 /<project-name>/src/ts/src/utils/constants.ts 中,PROGRAM_ID 从项目的密钥对中读取。

    const string = fs.readFileSync(
    "../target/deploy/solana_nft_staking_program-keypair.json",
    "utf8"
    )

    ...

    export const PROGRAM_ID = Keypair.fromSecretKey(secretKey).publicKey

    准备好了!我们可以开始了。首先切换到TS目录,然后运行 npm run start。确保你已经完成了 cargo build-sbfsolana program deploy,并且你的集群设置是正确的。如果一切正常,它应该能够启动并运行。在控制台上,你应该能看到 stakesredeemsunstakes 的输出。请耐心等待,年轻的练剑师,这可能需要一两分钟的时间。

    假设一切顺利🎉,我们可以跳转到处理器文件(//src/processor.rs)。

    首先,我们需要处理一些导入:

    use mpl_token_metadata::ID as mpl_metadata_program_id;
    use spl_token::ID as spl_token_program_id;

    此外,在 solana_program::program::{invoke_signed} 的导入中添加 invoke

    现在,转到 process_stake 函数,我们将在这里进行第一次修改。

    习惯于此吧,我们经常会发现自己需要在许多地方添加账户。所以,现在是时候添加一些账户,以便我们能够真正借助令牌程序工作了。

    let nft_mint = next_account_info(account_info_iter)?;
    let nft_edition = next_account_info(account_info_iter)?;
    let stake_state = next_account_info(account_info_iter)?;
    let program_authority = next_account_info(account_info_iter)?;
    let token_program = next_account_info(account_info_iter)?;
    let metadata_program = next_account_info(account_info_iter)?;

    委托和冻结 —— 质押

    下一步,我们需要将程序设置为NFT的代理,委托NFT的权限,以便程序能够代表我们发起交易。

    msg!("Approving delegation");
    invoke(
    &spl_token::instruction::approve(
    &spl_token_program_id,
    nft_token_account.key,
    program_authority.key,
    user.key,
    &[user.key],
    1,
    )?,
    &[
    nft_token_account.clone(),
    program_authority.clone(),
    user.clone(),
    token_program.clone(),
    ],
    )?;

    现在,我们可以开始实际冻结代币的过程。我们不是真正改变代币的所有权,而是将其冻结,使在质押期间无法对代币进行任何操作。首先,我们需要为程序权限派生PDA(程序派生地址)。简单来说,我们会使用PDA作为代币铸造的授权实体,从而能够冻结账户。

    别忘了检查并确保PDA已经被提取。

    let (delegated_auth_pda, delegate_bump) =
    Pubkey::find_program_address(&[b"authority"], program_id);

    if delegated_auth_pda != *program_authority.key {
    msg!("Invalid seeds for PDA");
    return Err(StakeError::InvalidPda.into());
    }

    回到冻结操作本身,与委托批准不同,这里使用invoke_signed以从我们的程序进行签名。

    msg!("freezing NFT token account");
    invoke_signed(
    &mpl_token_metadata::instruction::freeze_delegated_account(
    mpl_metadata_program_id,
    *program_authority.key,
    *nft_token_account.key,
    *nft_edition.key,
    *nft_mint.key,
    ),
    &[
    program_authority.clone(),
    nft_token_account.clone(),
    nft_edition.clone(),
    nft_mint.clone(),
    metadata_program.clone(),
    ],
    &[&[b"authority", &[delegate_bump]]],
    )?;

    我们的程序的PDA现在具备了冻结令牌的权限。🧊

    接下来,我们将转到TypeScript文件(//ts/src/utils/instruction.rs),并向createStakingInstruction函数中添加更多的账户,确保其正常工作。

    我们需要确保新添加的账户与//src/processor.rs文件中process_stake函数的账户相匹配:

    nftMint: PublicKey,
    nftEdition: PublicKey,
    tokenProgram: PublicKey,
    metadataProgram: PublicKey,

    然后,我们将所有这些按照正确的顺序添加到TransactionInstruction中的账户列表。顺序非常重要。

    首先,取得授权账户:

    const [delegateAuthority] = PublicKey.findProgramAddressSync(
    [Buffer.from("authority")],
    programId
    )

    总共有5个新账户,你需要再次确保它们的顺序,并检查哪些是可写的,哪些是签名者。

    ...
    {
    pubkey: nftMint,
    isWritable: false,
    isSigner: false,
    },
    {
    pubkey: nftEdition,
    isWritable: false,
    isSigner: false,
    },
    ...
    {
    pubkey: delegateAuthority,
    isWritable: true,
    isSigner: false,
    },
    {
    pubkey: tokenProgram,
    isWritable: false,
    isSigner: false,
    },
    {
    pubkey: metadataProgram,
    isWritable: false,
    isSigner: false,
    },

    测试我们的质押功能

    接下来,进入索引文件(//ts/src/index.rs),在创建stakeInstruction的地方,在testStaking函数中添加与之匹配的相同账户。

    下面是四个附加项:

    nft.mintAddress,
    nft.masterEditionAddress,
    TOKEN_PROGRAM_ID,
    METADATA_PROGRAM_ID,
    import { TOKEN_PROGRAM_ID } from "@solana/spl-token"
    import { PROGRAM_ID as METADATA_PROGRAM_ID } from "@metaplex-foundation/mpl-token-metadata"

    现在是时候测试我们的进展了:

    1. 使用cargo build-sbf重新构建程序,然后使用solana program deploy {path}进行更新。
    2. 确保你处于ts目录下,并执行npm run start

    假设没有出错,那我们就回到processor.rs文件,并向process_redeem函数添加相似的代码。

    委派和冻结 -- 兑换

    首先,你猜对了,我们要添加账户——一共有4个!

    let stake_mint = next_account_info(account_info_iter)?;
    let stake_authority = next_account_info(account_info_iter)?;
    let user_stake_ata = next_account_info(account_info_iter)?;
    let token_program = next_account_info(account_info_iter)?;

    接下来,我们将验证一些新账户。首先,我们要推导出stake_auth_pda,然后用自定义错误验证PDA

    let (stake_auth_pda, auth_bump) = Pubkey::find_program_address(&[b"mint"], program_id);

    if *stake_authority.key != stake_auth_pda {
    msg!("Invalid stake mint authority!");
    return Err(StakeError::InvalidPda.into());
    }

    向下滚动一些,我们要调用一个invoke_signed来调用令牌程序,以铸造代币,等我们了解了redeem_amount之后。我们需要指令的各种键,然后是所需的账户,最后是授权的种子。别忘了使用?来传播错误,否则红色波浪线将始终困扰你。

    invoke_signed(
    &spl_token::instruction::mint_to(
    token_program.key,
    stake_mint.key,
    user_stake_ata.key,
    stake_authority.key,
    &[stake_authority.key],
    redeem_amount.try_into().unwrap(),
    )?,
    &[
    stake_mint.clone(),
    user_stake_ata.clone(),
    stake_authority.clone(),
    token_program.clone(),
    ],
    &[&[b"mint", &[auth_bump]]],
    )?;

    这应该在此文件中处理铸币操作,但我们必须在客户端上添加新账户。

    我们回到之前的instruction.ts文件,向下滚动到createRedeemInstruction,并添加以下账户。

    mint: PublicKey,
    userStakeATA: PublicKey,
    tokenProgram: PublicKey,

    现在请记住,一些账户是派生的,如权威账户,所以我们不需要手动添加它。

    然后跳到TransactionInstruction本身,首先推导出mintAuth

    const [mintAuth] = PublicKey.findProgramAddressSync(
    [Buffer.from("mint")],
    programId
    )

    接下来进入return new TransactionInstruction,添加相关账户,并标明它们是否可写和/或可签。以下是我们需要添加的4个账户 - 请记住,顺序很重要。

    {
    pubkey: mint,
    isWritable: true,
    isSigner: false,
    },
    {
    pubkey: mintAuth,
    isWritable: false,
    isSigner: false,
    },
    {
    pubkey: userStakeATA,
    isWritable: true,
    isSigner: false,
    },
    {
    pubkey: tokenProgram,
    isSigner: false,
    isWritable: false,
    },

    这应该包括了我们兑换所需的所有内容。我们最后需要回到同一个index.ts文件,并确保我们正确调用它,但这部分有些复杂,所以让我们先回到processor.rs并完成process_unstake函数。

    委托和冻结——解除质押

    解除质押过程基本上是将我们刚刚进行的质押和赎回步骤结合在一起,因此需要用到我们刚才操作过的所有账户。

    以下是我们需要添加的所有账户:

    let nft_mint = next_account_info(account_info_iter)?;
    let nft_edition = next_account_info(account_info_iter)?;
    ... (stake_state 应该在我们之前的代码中)
    let program_authority = next_account_info(account_info_iter)?;
    let stake_mint = next_account_info(account_info_iter)?;
    let stake_authority = next_account_info(account_info_iter)?;
    let user_stake_ata = next_account_info(account_info_iter)?;
    let token_program = next_account_info(account_info_iter)?;
    let metadata_program = next_account_info(account_info_iter)?;

    我们可以向下滚动,并复制粘贴 process_stakeprocess_redeem 函数中的一些验证:

    let (delegated_auth_pda, delegate_bump) =
    Pubkey::find_program_address(&[b"authority"], program_id);
    if delegated_auth_pda != *program_authority.key {
    msg!("Invalid seeds for PDA");
    return Err(StakeError::InvalidPda.into());
    }

    let (stake_auth_pda, auth_bump) = Pubkey::find_program_address(&[b"mint"], program_id);
    if *stake_authority.key != stake_auth_pda {
    msg!("Invalid stake mint authority!");
    return Err(StakeError::InvalidPda.into());
    }

    好的,这是相当新的部分,我们要“解冻”NFT代币账户。如果你还记得,我们之前冻结了它,现在我们要解冻它。这段代码与上面的冻结代码完全相反,我们只需更改辅助函数,使用 thaw_delegated_account

    msg!("thawing NFT token account");
    invoke_signed(
    &mpl_token_metadata::instruction::thaw_delegated_account(
    mpl_metadata_program_id,
    *program_authority.key,
    *nft_token_account.key,
    *nft_edition.key,
    *nft_mint.key,
    ),
    &[
    program_authority.clone(),
    nft_token_account.clone(),
    nft_edition.clone(),
    nft_mint.clone(),
    metadata_program.clone(),
    ],
    &[&[b"authority", &[delegate_bump]]],
    )?;

    接下来,我们需要撤销委托权限。与授权委托类似,但并不完全相同。我们可以移除 program_authority 字段,因为它不是必需的,并从批准助手函数中移除 amount

    msg!("Revoke delegation");
    invoke(
    &spl_token::instruction::revoke(
    &spl_token_program_id,
    nft_token_account.key,
    user.key,
    &[user.key],
    )?,
    &[
    nft_token_account.clone(),
    user.clone(),
    token_program.clone(),
    ],
    )?;

    最后,我们将从赎回函数中复制 invoke_signed,粘贴到 redeem_amount 下面。

    invoke_signed(
    &spl_token::instruction::mint_to(
    token_program.key,
    stake_mint.key,
    user_stake_ata.key,
    stake_authority.key,
    &[stake_authority.key],
    redeem_amount.try_into().unwrap(),
    )?,
    &[
    stake_mint.clone(),
    user_stake_ata.clone(),
    stake_authority.clone(),
    token_program.clone(),
    ],
    &[&[b"mint", &[auth_bump]]],
    )?;

    哦,还有一件事,我们实际上没有设置 redeem_amount,之前只是用了 unix_time。所以,改成 100 * unit_time,我们以后可以调整。确保在上述两个函数中都进行更改。

    这里应该就是了,回到客户端的文件上,添加所有的账户。向下滚动到 createUnstakeInstruction,将以下内容作为参数添加进去。

    nftMint: PublicKey,
    nftEdition: PublicKey,
    stakeMint: PublicKey,
    userStakeATA: PublicKey,
    tokenProgram: PublicKey,
    metadataProgram: PublicKey,

    再次提醒,有一些账户是自动派生的,所以我们不需要手动添加。

    接下来我们推导出 delegateAuthoritymintAuth,这与上面的代码完全相同。

    const [delegateAuthority] = PublicKey.findProgramAddressSync(
    [Buffer.from("authority")],
    programId
    )

    const [mintAuth] = PublicKey.findProgramAddressSync(
    [Buffer.from("mint")],
    programId
    )

    最后,我们将它们全部添加到指令中。这是很多账户,所以我们在这里全部发布,而不仅仅是我们要添加的那些。让你的眼睛不再在函数和文件之间来回移动。

    {
    pubkey: nftHolder,
    isWritable: false,
    isSigner: true,
    },
    {
    pubkey: nftTokenAccount,
    isWritable: true,
    isSigner: false,
    },
    {
    pubkey: nftMint,
    isWritable: false,
    isSigner: false,
    },
    {
    pubkey: nftEdition,
    isWritable: false,
    isSigner: false,
    },
    {
    pubkey: stakeAccount,
    isWritable: true,
    isSigner: false,
    },
    {
    pubkey: delegateAuthority,
    isWritable: true,
    isSigner: false,
    },
    {
    pubkey: stakeMint,
    isWritable: true,
    isSigner: false,
    },
    {
    pubkey: mintAuth,
    isWritable: false,
    isSigner: false,
    },
    {
    pubkey: userStakeATA,
    isWritable: true,
    isSigner: false,
    },
    {
    pubkey: tokenProgram,
    isWritable: false,
    isSigner: false,
    },
    {
    pubkey: metadataProgram,
    isWritable: false,
    isSigner: false,
    },

    测试我们的功能

    好的,好的,我知道你已经迫不及待了,我们快到终点了。现在让我们回到index.ts文件中,调用并测试所有的函数。对于testRedeem函数,我们需要代币的铸币地址和用户的代币账户,以及createUnstakeInstruction

    首先,在testRedeem函数的参数中添加以下内容:

    stakeMint: web3.PublicKey,
    userStakeATA: web3.PublicKey

    然后,将它们添加到下方的createRedeemInstruction中:

    stakeMint,
    userStakeATA,
    TOKEN_PROGRAM_ID,
    PROGRAM_ID

    testUnstaking函数也进行上述相同的操作。

    接着,在createUnstakingInstruction中添加以下内容:

    nft.mintAddress,
    nft.masterEditionAddress,
    stakeMint,
    userStakeATA,
    TOKEN_PROGRAM_ID,
    METADATA_PROGRAM_ID,

    现在向下滚动到main()函数的调用位置,你会注意到testRedeemtestUnstaking都是红色的,因为它们缺少一些参数。

    首先,我们要声明stakeMint,目前我们将其硬编码,以及userStakeATA,该函数会创建ATA(如果ATA还不存在的话)。

    const stakeMint = new web3.PublicKey(
    "EMPTY FOR A MINUTE"
    )

    const userStakeATA = await getOrCreateAssociatedTokenAccount(
    connection,
    user,
    stakeMint,
    user.publicKey
    )

    ...现在,将调用更改为接收额外的参数:

    await testRedeem(connection, user, nft, stakeMint, userStakeATA.address)
    await testUnstaking(connection, user, nft, stakeMint, userStakeATA.address)

    前端编辑以测试功能

    我们暂时要切换到前端Buildoors项目中的index.ts文件(//tokens/bld/index.ts)。在这里,我们使用createBldToken函数创建BLD令牌。

    在该函数内部,我们称token.CreateMint的第三个参数为铸币授权,它掌管着铸币过程。最初,它是一个payer.publicKey,用于初始调用。我们很快就会更改铸币授权。

    首先,我们要向createBldToken函数添加一个参数:

    programId: web3.PublicKey

    然后向下滚动到主函数中的调用位置,并为await createBldToken调用添加第三个参数。

    new web3.PublicKey("USE YOUR PROGRAM ID")

    如果你找不到程序ID,你可以重新部署,控制台会显示你所需的程序ID

    向上滚动,超过const tokenMint,找到mintAuth。你可以在Anchor NFT质押计划中找到授权的具体信息。

    const [mintAuth] = await web3.PublicKey.findProgramAddress(
    [Buffer.from("mint")],
    programId
    )

    滚动回到下面,在transactionSignature创建后,我们将设置新的铸币权限(这是我们上面提到的更改)。

    await token.setAuthority(
    connection,
    payer,
    tokenMint,
    payer.publicKey,
    token.AuthorityType.MintTokens,
    mintAuth
    )

    现在,我们可以使用新的认证重新创建BLD令牌,并将其添加到上面的stakeMint中。

    const stakeMint = new web3.PublicKey(
    "EMPTY FOR A MINUTE"
    )

    最后,全面测试一切

    现在,请切换到主目录并运行 npm run create-bld-token。确保你已经将环境设置为devnet

    核实你的构建脚本,它应该如下所示:

    "creat-bld-token": "ts-node tokens/bld/index.ts"

    一旦成功执行完毕,你可以从tokens/bld目录中的cache.json文件中获取新的密钥。

    现在我们终于回到了NFT质押计划,并且可以在stakeMint创建中使用这个密钥:

    const stakeMint = new web3.PublicKey(
    "MINT KEY FROM CACHE.JSON"
    )

    现在应该一切准备就绪,并可以正常工作。返回到ts目录,并使用npm run start进行全面测试。如果一切顺利,你的控制台将确认初始化、质押、赎回和解质押都已成功完成。

    确实涉及了许多细节。深呼吸,给自己一些喘息的空间。这是一项充满挑战性的任务,不妨再回头看一遍,复习一下,甚至再次实践,不管需要付出多少努力。只要你能掌握这些内容,你就将成为一名出色的Solana开发者。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module4/ship-a-staking-app/index.html b/Solana-Co-Learn/module4/ship-a-staking-app/index.html index 6ddea277d..e2503af43 100644 --- a/Solana-Co-Learn/module4/ship-a-staking-app/index.html +++ b/Solana-Co-Learn/module4/ship-a-staking-app/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module4/ship-a-staking-app/put-it-all-together/index.html b/Solana-Co-Learn/module4/ship-a-staking-app/put-it-all-together/index.html index d282cf89a..b428c63f7 100644 --- a/Solana-Co-Learn/module4/ship-a-staking-app/put-it-all-together/index.html +++ b/Solana-Co-Learn/module4/ship-a-staking-app/put-it-all-together/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    将所有部分整合到一起

    前端质押部分

    你能感受到吗?终点线就在眼前了...至少对于这个核心部分来说是这样的。😆

    我们将集中精力使程序前端的质押和解质押指令正常运行。

    首先,在你的前端项目的根目录下创建一个名为 utils 的新文件夹。然后,创建一个名为 instructions.ts 的文件,并从NFT质押项目中复制/粘贴整个 instructions.ts 文件。由于代码超过200行,所以我不会在这里粘贴。😬

    下一步我们将进入 StakeOptionsDisplay 文件(//components/StakeOptionsDisplay.rs)。你会注意到我们有三个空函数:handleStakehandleUnstakehandleClaim。这将是本节的重点。

    和往常一样,先让我们准备好钱包和网络连接。

    const walletAdapter = useWallet()
    const { connection } = useConnection()

    我们先确认下钱包是否已连接。

    if (!walletAdapter.connected || !walletAdapter.publicKey) {
    alert("Please connect your wallet")
    return
    }

    如果一切正常,我们可以开始创建质押指示。

    const stakeInstruction = createStakingInstruction(
    walletAdapter.publicKey,
    nftTokenAccount,
    nftData.mint.address,
    nftData.edition.address,
    TOKEN_PROGRAM_ID, // 需要导入
    METADATA_PROGRAM_ID, // 需要导入
    PROGRAM_ID // 需要从constants.ts导入
    )

    因此,进入 utils 文件夹,添加一个名为 constants.ts 的文件,并加入以下内容:

    import { PublicKey } from "@solana/web3.js"

    export const PROGRAM_ID = new PublicKey(
    process.env.NEXT_PUBLIC_STAKE_PROGRAM_ID ?? ""
    )

    这是我们在上述指示中使用的程序ID。确保你的env.local文件中有正确的程序ID

    stake 指令应该准备就绪了,接下来我们要创建一笔交易,添加指令,然后发送。

    const transaction = new Transaction().add(stakeInstruction)

    const signature = await walletAdapter.sendTransaction(transaction, connection)

    由于这是一个等待操作,确保在 handleStake 回调中添加 async 关键字。实际上,这三个函数都应该是异步回调函数。

    我们可以进行检查以确认是否已完成,因此让我们获取最新的区块哈希并确认交易。

    const latestBlockhash = await connection.getLatestBlockhash()

    await connection.confirmTransaction(
    {
    blockhash: latestBlockhash.blockhash,
    lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
    signature: signature,
    },
    "finalized"
    )
    } catch (error) {
    console.log(error)
    }

    await checkStakingStatus()

    确认交易后,我们可以检查是否仍在质押,因此让我们将此功能添加到 handleStake 代码块的顶部。

    const checkStakingStatus = useCallback(async () => {
    if (!walletAdapter.publicKey || !nftTokenAccount) {
    return
    }

    我们还需要将 walletAdapterconnection 添加为 handleStake 回调的依赖项。

    我们需要添加一些状态字段,所以向上滚动并添加质押状态的相关字段。

    const [isStaking, setIsStaking] = useState(isStaked)

    我们还要将参数 StakeOptionsDisplayisStaking 改为 isStaked,否则我们的状态无法正常工作。

    同时,我们还需要在 utils 中创建一个名为 accounts.ts 的新文件,并从我们的NFT质押程序utils文件夹中复制文件过来。可能还需要安装我们的borsh库。

    我们之所以要复制这些内容,是因为每次检查状态时,我们都要查看抵押账户的状态,并确认抵押的价值。

    接下来,在 checkStakingStatus 的回调函数中,我们要调用 getStakeAccount

    const account = await getStakeAccount(
    connection,
    walletAdapter.publicKey,
    nftTokenAccount
    )

    setIsStaking(account.state === 0)
    } catch (e) {
    console.log("error:", e)
    }

    既然我们要发送多个交易,请继续设置一个辅助函数来确认我们的交易。我们可以将上述代码粘贴进去。

    const sendAndConfirmTransaction = useCallback(
    async (transaction: Transaction) => {
    try {
    const signature = await walletAdapter.sendTransaction(
    transaction,
    connection
    )
    const latestBlockhash = await connection.getLatestBlockhash()
    await connection.confirmTransaction(
    {
    blockhash: latestBlockhash.blockhash,
    lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
    signature: signature,
    },
    "finalized"
    )
    } catch (error) {
    console.log(error)
    }

    await checkStakingStatus()
    },
    [walletAdapter, connection]
    )

    现在,在 handleStake 函数中只需调用 sendAndConfirmTransaction 即可。

    前端索赔/兑换

    现在就可以进行解除质押和领取奖励了。这两者实际上是相同的操作,不过增加了一个复杂的环节:我们是否需要为用户创建代币账户,用于存放他们即将获得的奖励代币。

    下面我们将解决 handleClaim 函数。

    首先,使用与之前相同的警报检查钱包适配器是否已连接并具有公钥。

    接着我们要检查奖励的关联令牌账户是否存在:

    const userStakeATA = await getAssociatedTokenAddress(
    STAKE_MINT,
    walletAdapter.publicKey
    )

    请快速查看我们创建的 constants.ts 文件,并为薄荷地址添加以下代码,因为我们需要 STAKE_MINT 的值:

    export const STAKE_MINT = new PublicKey(
    process.env.NEXT_PUBLIC_STAKE_MINT_ADDRESS ?? ""
    )

    当我们拥有了ATA后,我们需要调用 getAccountInfo 函数,它会返回一个账户或null

    const account = await connection.getAccountInfo(userStakeATA)

    随后,我们创建交易并检查是否存在一个账户,如果没有,我们调用 createAssociatedTokenAccountInstruction 函数;否则,我们调用 createRedeemInstruction 函数。

    const transaction = new Transaction()

    if (!account) {
    transaction.add(
    createAssociatedTokenAccountInstruction(
    walletAdapter.publicKey,
    userStakeATA,
    walletAdapter.publicKey,
    STAKE_MINT
    )
    )
    }

    transaction.add(
    createRedeemInstruction(
    walletAdapter.publicKey,
    nftTokenAccount,
    nftData.mint.address,
    userStakeATA,
    TOKEN_PROGRAM_ID,
    PROGRAM_ID
    )
    )

    现在我们可以调用上面创建的辅助事务确认函数。

    await sendAndConfirmTransaction(transaction)
    }, [walletAdapter, connection, nftData, nftTokenAccount])

    最后,别忘了将依赖项 walletAdapterconnection 添加到回调函数中。

    前端解除质押操作

    对于 handleUnstake 函数,我们要确保与其他函数一样使用异步处理。你可以直接从 handleClaim 复制以下内容:

    if (
    !walletAdapter.connected ||
    !walletAdapter.publicKey ||
    !nftTokenAccount
    ) {
    alert("请连接您的钱包")
    return
    }

    const userStakeATA = await getAssociatedTokenAddress(
    STAKE_MINT,
    walletAdapter.publicKey
    )

    const account = await connection.getAccountInfo(userStakeATA)

    const transaction = new Transaction()

    if (!account) {
    transaction.add(
    createAssociatedTokenAccountInstruction(
    walletAdapter.publicKey,
    userStakeATA,
    walletAdapter.publicKey,
    STAKE_MINT
    )
    )
    }

    接下来,我们将向交易中添加指令,并再次调用辅助函数:

    transaction.add(
    createUnstakeInstruction(
    walletAdapter.publicKey,
    nftTokenAccount,
    nftData.address,
    nftData.edition.address,
    STAKE_MINT,
    userStakeATA,
    TOKEN_PROGRAM_ID,
    METADATA_PROGRAM_ID,
    PROGRAM_ID
    )
    )

    await sendAndConfirmTransaction(transaction)
    }

    页面编辑的股份部分

    我们继续转到 stake.tsx 文件(位于 //pages/stake.tsx)并进行一些与上述内容相关的修改。

    首先,根据我们之前的编辑,我们需要将 isStaking 的使用更改为 isStaked。这项修改应在 <StakeOptionsDisplay> 组件中进行。我们还需要添加一个名为 nftData 的字段,并将其赋值为 nftData,我们还需要一个状态来存储这个值。

    const [nftData, setNftData] = useState<any>()`

    目前,我们还没有实际的数据。我们将使用一个 useEffect 钩子,在其中调用 metaplex,并通过铸币地址找到 NFT 数据。

    useEffect(() => {
    const metaplex = Metaplex.make(connection).use(
    walletAdapterIdentity(walletAdapter)
    )

    try {
    metaplex
    .nfts()
    .findByMint({ mintAddress: mint })
    .then((nft) => {
    console.log("在质押页面上的 NFT 数据:", nft)
    setNftData(nft)
    })
    } catch (e) {
    console.log("获取 NFT 时发生错误:", e)
    }
    }, [connection, walletAdapter])

    不要忘了像我们之前所做的那样,获取一个连接和钱包适配器。

    现在一切准备就绪,可以进行测试了。运行 npm run dev,然后在浏览器中打开本地主机。赶快试试,点击按钮吧!🔘 ⏏️ 🆒

    还需要进行一些编辑

    似乎还有几个方面可能需要改进。让我们回到 StakeOptionsDisplay 文件,并在 handleStake 函数之前添加以下的 useEffect 钩子。

    useEffect(() => {
    checkStakingStatus()

    if (nftData) {
    connection
    .getTokenLargestAccounts(nftData.mint.address)
    .then((accounts) => setNftTokenAccount(accounts.value[0].address))
    }
    }, [nftData, walletAdapter, connection])

    这是一个快速检查,确认我们是否有 NFT 数据,如果有的话,就为 NFT 代币账户设置值。这是一个 NFT,只有一个,所以它会是第一个地址,因此索引值为 '0'

    此外,在所有三个回调函数中,我们还需要将 nftData 添加为依赖项。

    最后,在 handleStake 中,在创建交易之前添加以下代码:

    const [stakeAccount] = PublicKey.findProgramAddressSync(
    [walletAdapter.publicKey.toBuffer(), nftTokenAccount.toBuffer()],
    PROGRAM_ID
    )

    const transaction = new Transaction()

    const account = await connection.getAccountInfo(stakeAccount)
    if (!account) {
    transaction.add(
    createInitializeStakeAccountInstruction(
    walletAdapter.publicKey,
    nftTokenAccount,
    PROGRAM_ID
    )
    )
    }

    我们需要一个质押账户,也就是一个程序驱动的账户(PDA),用于在程序中存储有关你的质押状态的数据。如果我们没有这样的账户,上述代码会为我们初始化它。

    终于,我们完成了核心部分 4。这最后的部分有些杂乱,为确保没有遗漏任何东西,可以将整个 StakeOptionsDisplay 文件粘贴下来进行仔细检查。

    如果你想进一步改进代码或有任何其他问题,请随时提出。

    import { VStack, Text, Button } from "@chakra-ui/react"
    import { useConnection, useWallet } from "@solana/wallet-adapter-react"
    import { PublicKey, Transaction } from "@solana/web3.js"
    import { useCallback, useEffect, useState } from "react"
    import {
    createInitializeStakeAccountInstruction,
    createRedeemInstruction,
    createStakingInstruction,
    createUnstakeInstruction,
    } from "../utils/instructions"
    import {
    TOKEN_PROGRAM_ID,
    getAssociatedTokenAddress,
    createAssociatedTokenAccountInstruction,
    } from "@solana/spl-token"
    import { PROGRAM_ID as METADATA_PROGRAM_ID } from "@metaplex-foundation/mpl-token-metadata"
    import { PROGRAM_ID, STAKE_MINT } from "../utils/constants"
    import { getStakeAccount } from "../utils/accounts"

    export const StakeOptionsDisplay = ({
    nftData,
    isStaked,
    daysStaked,
    totalEarned,
    claimable,
    }: {
    nftData: any
    isStaked: boolean
    daysStaked: number
    totalEarned: number
    claimable: number
    }) => {
    const walletAdapter = useWallet()
    const { connection } = useConnection()

    const [isStaking, setIsStaking] = useState(isStaked)
    const [nftTokenAccount, setNftTokenAccount] = useState<PublicKey>()

    const checkStakingStatus = useCallback(async () => {
    if (!walletAdapter.publicKey || !nftTokenAccount) {
    return
    }

    try {
    const account = await getStakeAccount(
    connection,
    walletAdapter.publicKey,
    nftTokenAccount
    )

    console.log("stake account:", account)

    setIsStaking(account.state === 0)
    } catch (e) {
    console.log("error:", e)
    }
    }, [walletAdapter, connection, nftTokenAccount])

    useEffect(() => {
    checkStakingStatus()

    if (nftData) {
    connection
    .getTokenLargestAccounts(nftData.mint.address)
    .then((accounts) => setNftTokenAccount(accounts.value[0].address))
    }
    }, [nftData, walletAdapter, connection])

    const handleStake = useCallback(async () => {
    if (
    !walletAdapter.connected ||
    !walletAdapter.publicKey ||
    !nftTokenAccount
    ) {
    alert("Please connect your wallet")
    return
    }

    const [stakeAccount] = PublicKey.findProgramAddressSync(
    [walletAdapter.publicKey.toBuffer(), nftTokenAccount.toBuffer()],
    PROGRAM_ID
    )

    const transaction = new Transaction()

    const account = await connection.getAccountInfo(stakeAccount)
    if (!account) {
    transaction.add(
    createInitializeStakeAccountInstruction(
    walletAdapter.publicKey,
    nftTokenAccount,
    PROGRAM_ID
    )
    )
    }

    const stakeInstruction = createStakingInstruction(
    walletAdapter.publicKey,
    nftTokenAccount,
    nftData.mint.address,
    nftData.edition.address,
    TOKEN_PROGRAM_ID,
    METADATA_PROGRAM_ID,
    PROGRAM_ID
    )

    transaction.add(stakeInstruction)

    await sendAndConfirmTransaction(transaction)
    }, [walletAdapter, connection, nftData, nftTokenAccount])

    const sendAndConfirmTransaction = useCallback(
    async (transaction: Transaction) => {
    try {
    const signature = await walletAdapter.sendTransaction(
    transaction,
    connection
    )
    const latestBlockhash = await connection.getLatestBlockhash()
    await connection.confirmTransaction(
    {
    blockhash: latestBlockhash.blockhash,
    lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
    signature: signature,
    },
    "finalized"
    )
    } catch (error) {
    console.log(error)
    }

    await checkStakingStatus()
    },
    [walletAdapter, connection]
    )

    const handleUnstake = useCallback(async () => {
    if (
    !walletAdapter.connected ||
    !walletAdapter.publicKey ||
    !nftTokenAccount
    ) {
    alert("Please connect your wallet")
    return
    }

    const userStakeATA = await getAssociatedTokenAddress(
    STAKE_MINT,
    walletAdapter.publicKey
    )

    const account = await connection.getAccountInfo(userStakeATA)

    const transaction = new Transaction()

    if (!account) {
    transaction.add(
    createAssociatedTokenAccountInstruction(
    walletAdapter.publicKey,
    userStakeATA,
    walletAdapter.publicKey,
    STAKE_MINT
    )
    )
    }

    transaction.add(
    createUnstakeInstruction(
    walletAdapter.publicKey,
    nftTokenAccount,
    nftData.address,
    nftData.edition.address,
    STAKE_MINT,
    userStakeATA,
    TOKEN_PROGRAM_ID,
    METADATA_PROGRAM_ID,
    PROGRAM_ID
    )
    )

    await sendAndConfirmTransaction(transaction)
    }, [walletAdapter, connection, nftData, nftTokenAccount])

    const handleClaim = useCallback(async () => {
    if (
    !walletAdapter.connected ||
    !walletAdapter.publicKey ||
    !nftTokenAccount
    ) {
    alert("Please connect your wallet")
    return
    }

    const userStakeATA = await getAssociatedTokenAddress(
    STAKE_MINT,
    walletAdapter.publicKey
    )

    const account = await connection.getAccountInfo(userStakeATA)

    const transaction = new Transaction()

    if (!account) {
    transaction.add(
    createAssociatedTokenAccountInstruction(
    walletAdapter.publicKey,
    userStakeATA,
    walletAdapter.publicKey,
    STAKE_MINT
    )
    )
    }

    transaction.add(
    createRedeemInstruction(
    walletAdapter.publicKey,
    nftTokenAccount,
    nftData.mint.address,
    userStakeATA,
    TOKEN_PROGRAM_ID,
    PROGRAM_ID
    )
    )

    await sendAndConfirmTransaction(transaction)
    }, [walletAdapter, connection, nftData, nftTokenAccount])

    return (
    <VStack
    bgColor="containerBg"
    borderRadius="20px"
    padding="20px 40px"
    spacing={5}
    >
    <Text
    bgColor="containerBgSecondary"
    padding="4px 8px"
    borderRadius="20px"
    color="bodyText"
    as="b"
    fontSize="sm"
    >
    {isStaking
    ? `STAKING ${daysStaked} DAY${daysStaked === 1 ? "" : "S"}`
    : "READY TO STAKE"}
    </Text>
    <VStack spacing={-1}>
    <Text color="white" as="b" fontSize="4xl">
    {isStaking ? `${totalEarned} $BLD` : "0 $BLD"}
    </Text>
    <Text color="bodyText">
    {isStaking ? `${claimable} $BLD earned` : "earn $BLD by staking"}
    </Text>
    </VStack>
    <Button
    onClick={isStaking ? handleClaim : handleStake}
    bgColor="buttonGreen"
    width="200px"
    >
    <Text as="b">{isStaking ? "claim $BLD" : "stake buildoor"}</Text>
    </Button>
    {isStaking ? <Button onClick={handleUnstake}>unstake</Button> : null}
    </VStack>
    )
    }
    - - +
    Skip to main content

    将所有部分整合到一起

    前端质押部分

    你能感受到吗?终点线就在眼前了...至少对于这个核心部分来说是这样的。😆

    我们将集中精力使程序前端的质押和解质押指令正常运行。

    首先,在你的前端项目的根目录下创建一个名为 utils 的新文件夹。然后,创建一个名为 instructions.ts 的文件,并从NFT质押项目中复制/粘贴整个 instructions.ts 文件。由于代码超过200行,所以我不会在这里粘贴。😬

    下一步我们将进入 StakeOptionsDisplay 文件(//components/StakeOptionsDisplay.rs)。你会注意到我们有三个空函数:handleStakehandleUnstakehandleClaim。这将是本节的重点。

    和往常一样,先让我们准备好钱包和网络连接。

    const walletAdapter = useWallet()
    const { connection } = useConnection()

    我们先确认下钱包是否已连接。

    if (!walletAdapter.connected || !walletAdapter.publicKey) {
    alert("Please connect your wallet")
    return
    }

    如果一切正常,我们可以开始创建质押指示。

    const stakeInstruction = createStakingInstruction(
    walletAdapter.publicKey,
    nftTokenAccount,
    nftData.mint.address,
    nftData.edition.address,
    TOKEN_PROGRAM_ID, // 需要导入
    METADATA_PROGRAM_ID, // 需要导入
    PROGRAM_ID // 需要从constants.ts导入
    )

    因此,进入 utils 文件夹,添加一个名为 constants.ts 的文件,并加入以下内容:

    import { PublicKey } from "@solana/web3.js"

    export const PROGRAM_ID = new PublicKey(
    process.env.NEXT_PUBLIC_STAKE_PROGRAM_ID ?? ""
    )

    这是我们在上述指示中使用的程序ID。确保你的env.local文件中有正确的程序ID

    stake 指令应该准备就绪了,接下来我们要创建一笔交易,添加指令,然后发送。

    const transaction = new Transaction().add(stakeInstruction)

    const signature = await walletAdapter.sendTransaction(transaction, connection)

    由于这是一个等待操作,确保在 handleStake 回调中添加 async 关键字。实际上,这三个函数都应该是异步回调函数。

    我们可以进行检查以确认是否已完成,因此让我们获取最新的区块哈希并确认交易。

    const latestBlockhash = await connection.getLatestBlockhash()

    await connection.confirmTransaction(
    {
    blockhash: latestBlockhash.blockhash,
    lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
    signature: signature,
    },
    "finalized"
    )
    } catch (error) {
    console.log(error)
    }

    await checkStakingStatus()

    确认交易后,我们可以检查是否仍在质押,因此让我们将此功能添加到 handleStake 代码块的顶部。

    const checkStakingStatus = useCallback(async () => {
    if (!walletAdapter.publicKey || !nftTokenAccount) {
    return
    }

    我们还需要将 walletAdapterconnection 添加为 handleStake 回调的依赖项。

    我们需要添加一些状态字段,所以向上滚动并添加质押状态的相关字段。

    const [isStaking, setIsStaking] = useState(isStaked)

    我们还要将参数 StakeOptionsDisplayisStaking 改为 isStaked,否则我们的状态无法正常工作。

    同时,我们还需要在 utils 中创建一个名为 accounts.ts 的新文件,并从我们的NFT质押程序utils文件夹中复制文件过来。可能还需要安装我们的borsh库。

    我们之所以要复制这些内容,是因为每次检查状态时,我们都要查看抵押账户的状态,并确认抵押的价值。

    接下来,在 checkStakingStatus 的回调函数中,我们要调用 getStakeAccount

    const account = await getStakeAccount(
    connection,
    walletAdapter.publicKey,
    nftTokenAccount
    )

    setIsStaking(account.state === 0)
    } catch (e) {
    console.log("error:", e)
    }

    既然我们要发送多个交易,请继续设置一个辅助函数来确认我们的交易。我们可以将上述代码粘贴进去。

    const sendAndConfirmTransaction = useCallback(
    async (transaction: Transaction) => {
    try {
    const signature = await walletAdapter.sendTransaction(
    transaction,
    connection
    )
    const latestBlockhash = await connection.getLatestBlockhash()
    await connection.confirmTransaction(
    {
    blockhash: latestBlockhash.blockhash,
    lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
    signature: signature,
    },
    "finalized"
    )
    } catch (error) {
    console.log(error)
    }

    await checkStakingStatus()
    },
    [walletAdapter, connection]
    )

    现在,在 handleStake 函数中只需调用 sendAndConfirmTransaction 即可。

    前端索赔/兑换

    现在就可以进行解除质押和领取奖励了。这两者实际上是相同的操作,不过增加了一个复杂的环节:我们是否需要为用户创建代币账户,用于存放他们即将获得的奖励代币。

    下面我们将解决 handleClaim 函数。

    首先,使用与之前相同的警报检查钱包适配器是否已连接并具有公钥。

    接着我们要检查奖励的关联令牌账户是否存在:

    const userStakeATA = await getAssociatedTokenAddress(
    STAKE_MINT,
    walletAdapter.publicKey
    )

    请快速查看我们创建的 constants.ts 文件,并为薄荷地址添加以下代码,因为我们需要 STAKE_MINT 的值:

    export const STAKE_MINT = new PublicKey(
    process.env.NEXT_PUBLIC_STAKE_MINT_ADDRESS ?? ""
    )

    当我们拥有了ATA后,我们需要调用 getAccountInfo 函数,它会返回一个账户或null

    const account = await connection.getAccountInfo(userStakeATA)

    随后,我们创建交易并检查是否存在一个账户,如果没有,我们调用 createAssociatedTokenAccountInstruction 函数;否则,我们调用 createRedeemInstruction 函数。

    const transaction = new Transaction()

    if (!account) {
    transaction.add(
    createAssociatedTokenAccountInstruction(
    walletAdapter.publicKey,
    userStakeATA,
    walletAdapter.publicKey,
    STAKE_MINT
    )
    )
    }

    transaction.add(
    createRedeemInstruction(
    walletAdapter.publicKey,
    nftTokenAccount,
    nftData.mint.address,
    userStakeATA,
    TOKEN_PROGRAM_ID,
    PROGRAM_ID
    )
    )

    现在我们可以调用上面创建的辅助事务确认函数。

    await sendAndConfirmTransaction(transaction)
    }, [walletAdapter, connection, nftData, nftTokenAccount])

    最后,别忘了将依赖项 walletAdapterconnection 添加到回调函数中。

    前端解除质押操作

    对于 handleUnstake 函数,我们要确保与其他函数一样使用异步处理。你可以直接从 handleClaim 复制以下内容:

    if (
    !walletAdapter.connected ||
    !walletAdapter.publicKey ||
    !nftTokenAccount
    ) {
    alert("请连接您的钱包")
    return
    }

    const userStakeATA = await getAssociatedTokenAddress(
    STAKE_MINT,
    walletAdapter.publicKey
    )

    const account = await connection.getAccountInfo(userStakeATA)

    const transaction = new Transaction()

    if (!account) {
    transaction.add(
    createAssociatedTokenAccountInstruction(
    walletAdapter.publicKey,
    userStakeATA,
    walletAdapter.publicKey,
    STAKE_MINT
    )
    )
    }

    接下来,我们将向交易中添加指令,并再次调用辅助函数:

    transaction.add(
    createUnstakeInstruction(
    walletAdapter.publicKey,
    nftTokenAccount,
    nftData.address,
    nftData.edition.address,
    STAKE_MINT,
    userStakeATA,
    TOKEN_PROGRAM_ID,
    METADATA_PROGRAM_ID,
    PROGRAM_ID
    )
    )

    await sendAndConfirmTransaction(transaction)
    }

    页面编辑的股份部分

    我们继续转到 stake.tsx 文件(位于 //pages/stake.tsx)并进行一些与上述内容相关的修改。

    首先,根据我们之前的编辑,我们需要将 isStaking 的使用更改为 isStaked。这项修改应在 <StakeOptionsDisplay> 组件中进行。我们还需要添加一个名为 nftData 的字段,并将其赋值为 nftData,我们还需要一个状态来存储这个值。

    const [nftData, setNftData] = useState<any>()`

    目前,我们还没有实际的数据。我们将使用一个 useEffect 钩子,在其中调用 metaplex,并通过铸币地址找到 NFT 数据。

    useEffect(() => {
    const metaplex = Metaplex.make(connection).use(
    walletAdapterIdentity(walletAdapter)
    )

    try {
    metaplex
    .nfts()
    .findByMint({ mintAddress: mint })
    .then((nft) => {
    console.log("在质押页面上的 NFT 数据:", nft)
    setNftData(nft)
    })
    } catch (e) {
    console.log("获取 NFT 时发生错误:", e)
    }
    }, [connection, walletAdapter])

    不要忘了像我们之前所做的那样,获取一个连接和钱包适配器。

    现在一切准备就绪,可以进行测试了。运行 npm run dev,然后在浏览器中打开本地主机。赶快试试,点击按钮吧!🔘 ⏏️ 🆒

    还需要进行一些编辑

    似乎还有几个方面可能需要改进。让我们回到 StakeOptionsDisplay 文件,并在 handleStake 函数之前添加以下的 useEffect 钩子。

    useEffect(() => {
    checkStakingStatus()

    if (nftData) {
    connection
    .getTokenLargestAccounts(nftData.mint.address)
    .then((accounts) => setNftTokenAccount(accounts.value[0].address))
    }
    }, [nftData, walletAdapter, connection])

    这是一个快速检查,确认我们是否有 NFT 数据,如果有的话,就为 NFT 代币账户设置值。这是一个 NFT,只有一个,所以它会是第一个地址,因此索引值为 '0'

    此外,在所有三个回调函数中,我们还需要将 nftData 添加为依赖项。

    最后,在 handleStake 中,在创建交易之前添加以下代码:

    const [stakeAccount] = PublicKey.findProgramAddressSync(
    [walletAdapter.publicKey.toBuffer(), nftTokenAccount.toBuffer()],
    PROGRAM_ID
    )

    const transaction = new Transaction()

    const account = await connection.getAccountInfo(stakeAccount)
    if (!account) {
    transaction.add(
    createInitializeStakeAccountInstruction(
    walletAdapter.publicKey,
    nftTokenAccount,
    PROGRAM_ID
    )
    )
    }

    我们需要一个质押账户,也就是一个程序驱动的账户(PDA),用于在程序中存储有关你的质押状态的数据。如果我们没有这样的账户,上述代码会为我们初始化它。

    终于,我们完成了核心部分 4。这最后的部分有些杂乱,为确保没有遗漏任何东西,可以将整个 StakeOptionsDisplay 文件粘贴下来进行仔细检查。

    如果你想进一步改进代码或有任何其他问题,请随时提出。

    import { VStack, Text, Button } from "@chakra-ui/react"
    import { useConnection, useWallet } from "@solana/wallet-adapter-react"
    import { PublicKey, Transaction } from "@solana/web3.js"
    import { useCallback, useEffect, useState } from "react"
    import {
    createInitializeStakeAccountInstruction,
    createRedeemInstruction,
    createStakingInstruction,
    createUnstakeInstruction,
    } from "../utils/instructions"
    import {
    TOKEN_PROGRAM_ID,
    getAssociatedTokenAddress,
    createAssociatedTokenAccountInstruction,
    } from "@solana/spl-token"
    import { PROGRAM_ID as METADATA_PROGRAM_ID } from "@metaplex-foundation/mpl-token-metadata"
    import { PROGRAM_ID, STAKE_MINT } from "../utils/constants"
    import { getStakeAccount } from "../utils/accounts"

    export const StakeOptionsDisplay = ({
    nftData,
    isStaked,
    daysStaked,
    totalEarned,
    claimable,
    }: {
    nftData: any
    isStaked: boolean
    daysStaked: number
    totalEarned: number
    claimable: number
    }) => {
    const walletAdapter = useWallet()
    const { connection } = useConnection()

    const [isStaking, setIsStaking] = useState(isStaked)
    const [nftTokenAccount, setNftTokenAccount] = useState<PublicKey>()

    const checkStakingStatus = useCallback(async () => {
    if (!walletAdapter.publicKey || !nftTokenAccount) {
    return
    }

    try {
    const account = await getStakeAccount(
    connection,
    walletAdapter.publicKey,
    nftTokenAccount
    )

    console.log("stake account:", account)

    setIsStaking(account.state === 0)
    } catch (e) {
    console.log("error:", e)
    }
    }, [walletAdapter, connection, nftTokenAccount])

    useEffect(() => {
    checkStakingStatus()

    if (nftData) {
    connection
    .getTokenLargestAccounts(nftData.mint.address)
    .then((accounts) => setNftTokenAccount(accounts.value[0].address))
    }
    }, [nftData, walletAdapter, connection])

    const handleStake = useCallback(async () => {
    if (
    !walletAdapter.connected ||
    !walletAdapter.publicKey ||
    !nftTokenAccount
    ) {
    alert("Please connect your wallet")
    return
    }

    const [stakeAccount] = PublicKey.findProgramAddressSync(
    [walletAdapter.publicKey.toBuffer(), nftTokenAccount.toBuffer()],
    PROGRAM_ID
    )

    const transaction = new Transaction()

    const account = await connection.getAccountInfo(stakeAccount)
    if (!account) {
    transaction.add(
    createInitializeStakeAccountInstruction(
    walletAdapter.publicKey,
    nftTokenAccount,
    PROGRAM_ID
    )
    )
    }

    const stakeInstruction = createStakingInstruction(
    walletAdapter.publicKey,
    nftTokenAccount,
    nftData.mint.address,
    nftData.edition.address,
    TOKEN_PROGRAM_ID,
    METADATA_PROGRAM_ID,
    PROGRAM_ID
    )

    transaction.add(stakeInstruction)

    await sendAndConfirmTransaction(transaction)
    }, [walletAdapter, connection, nftData, nftTokenAccount])

    const sendAndConfirmTransaction = useCallback(
    async (transaction: Transaction) => {
    try {
    const signature = await walletAdapter.sendTransaction(
    transaction,
    connection
    )
    const latestBlockhash = await connection.getLatestBlockhash()
    await connection.confirmTransaction(
    {
    blockhash: latestBlockhash.blockhash,
    lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
    signature: signature,
    },
    "finalized"
    )
    } catch (error) {
    console.log(error)
    }

    await checkStakingStatus()
    },
    [walletAdapter, connection]
    )

    const handleUnstake = useCallback(async () => {
    if (
    !walletAdapter.connected ||
    !walletAdapter.publicKey ||
    !nftTokenAccount
    ) {
    alert("Please connect your wallet")
    return
    }

    const userStakeATA = await getAssociatedTokenAddress(
    STAKE_MINT,
    walletAdapter.publicKey
    )

    const account = await connection.getAccountInfo(userStakeATA)

    const transaction = new Transaction()

    if (!account) {
    transaction.add(
    createAssociatedTokenAccountInstruction(
    walletAdapter.publicKey,
    userStakeATA,
    walletAdapter.publicKey,
    STAKE_MINT
    )
    )
    }

    transaction.add(
    createUnstakeInstruction(
    walletAdapter.publicKey,
    nftTokenAccount,
    nftData.address,
    nftData.edition.address,
    STAKE_MINT,
    userStakeATA,
    TOKEN_PROGRAM_ID,
    METADATA_PROGRAM_ID,
    PROGRAM_ID
    )
    )

    await sendAndConfirmTransaction(transaction)
    }, [walletAdapter, connection, nftData, nftTokenAccount])

    const handleClaim = useCallback(async () => {
    if (
    !walletAdapter.connected ||
    !walletAdapter.publicKey ||
    !nftTokenAccount
    ) {
    alert("Please connect your wallet")
    return
    }

    const userStakeATA = await getAssociatedTokenAddress(
    STAKE_MINT,
    walletAdapter.publicKey
    )

    const account = await connection.getAccountInfo(userStakeATA)

    const transaction = new Transaction()

    if (!account) {
    transaction.add(
    createAssociatedTokenAccountInstruction(
    walletAdapter.publicKey,
    userStakeATA,
    walletAdapter.publicKey,
    STAKE_MINT
    )
    )
    }

    transaction.add(
    createRedeemInstruction(
    walletAdapter.publicKey,
    nftTokenAccount,
    nftData.mint.address,
    userStakeATA,
    TOKEN_PROGRAM_ID,
    PROGRAM_ID
    )
    )

    await sendAndConfirmTransaction(transaction)
    }, [walletAdapter, connection, nftData, nftTokenAccount])

    return (
    <VStack
    bgColor="containerBg"
    borderRadius="20px"
    padding="20px 40px"
    spacing={5}
    >
    <Text
    bgColor="containerBgSecondary"
    padding="4px 8px"
    borderRadius="20px"
    color="bodyText"
    as="b"
    fontSize="sm"
    >
    {isStaking
    ? `STAKING ${daysStaked} DAY${daysStaked === 1 ? "" : "S"}`
    : "READY TO STAKE"}
    </Text>
    <VStack spacing={-1}>
    <Text color="white" as="b" fontSize="4xl">
    {isStaking ? `${totalEarned} $BLD` : "0 $BLD"}
    </Text>
    <Text color="bodyText">
    {isStaking ? `${claimable} $BLD earned` : "earn $BLD by staking"}
    </Text>
    </VStack>
    <Button
    onClick={isStaking ? handleClaim : handleStake}
    bgColor="buttonGreen"
    width="200px"
    >
    <Text as="b">{isStaking ? "claim $BLD" : "stake buildoor"}</Text>
    </Button>
    {isStaking ? <Button onClick={handleUnstake}>unstake</Button> : null}
    </VStack>
    )
    }
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module4/testing/index.html b/Solana-Co-Learn/module4/testing/index.html index 4331647ed..5750ea769 100644 --- a/Solana-Co-Learn/module4/testing/index.html +++ b/Solana-Co-Learn/module4/testing/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module4/testing/testing-solana-programs/index.html b/Solana-Co-Learn/module4/testing/testing-solana-programs/index.html index 4e07c34a5..6084fb397 100644 --- a/Solana-Co-Learn/module4/testing/testing-solana-programs/index.html +++ b/Solana-Co-Learn/module4/testing/testing-solana-programs/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🧪 测试 Solana 程序

    上节课的内容为准备 Mint 账户奠定了基础。预备阶段已完结,现在是真正行动的时候了。通过强大的测试流程,我们可以在问题真正显现之前捕捉开发人员引入的错误,从而最大限度地减少生产环境中的 bug 数量。

    本课程将涉及两种类型的测试:单元测试和集成测试。

    • 单元测试更小、更专注,一次只测试一个隔离的模块,并能测试私有接口。
    • 集成测试与你的库完全外部无关,使用你的代码的方式与使用其他外部代码相同,仅使用公共接口,并可能在每个测试中涉及多个模块。

    🔢 单元测试

    单元测试的目的是隔离其他代码,测试每个代码单元的工作情况,以快速确认代码是否按预期执行。

    Rust 中,单元测试通常与它们所测试的代码放置在同一文件中,并在名为 tests 的模块内声明,该模块带有 cfg(test) 的注解。

    • 通过 #[test] 属性在 tests 模块中定义测试。
    • cfg 属性代表配置,指示 Rust 只有在特定配置下才包含随后的代码。
    • #[cfg(test)] 注解告诉 Cargo 只有在运行 cargo test-sbf 时才编译测试代码。
    • 运行 cargo test-sbf 时,该模块中标记为测试的每个函数都会执行。

    你还可以在模块中创建非测试的辅助函数,例如:

    // 示例测试模块,包含一个单元测试
    #[cfg(test)]
    mod tests {
    #[test]
    fn it_works() {
    let result = 2 + 2;
    assert_eq!(result, 4);
    }

    fn helper_function() {
    doSomething()
    }
    }

    ❓ 如何构建单元测试

    使用 solana_sdk 来创建 Solana 程序的单元测试。这个 crateRust 中与 Typescript@solana/web3.js 包相对应。

    solana_program_test 也用于测试 Solana 程序,并包含一个基于 BanksClient 的测试框架。

    在下面的代码片段中,我们为我们的 program_id 创建了一个公钥,然后初始化了一个 ProgramTest 对象。

    banks_client 返回的 ProgramTest 将作为我们进入测试环境的接口。

    其中,payer 变量是使用 SOL 生成的新密钥对,将用于签名和支付交易。

    接着,我们创建一个第二个 Keypair,并使用合适的参数构建我们的 Transaction

    最后,我们使用 ProgramTest::new 调用时返回的 banks_client 来处理此交易,并检查返回值是否等于 Ok(_)

    该函数使用 #[tokio::test] 属性进行注解。

    Tokio 是一个用于编写异步代码的 Rust crate,该属性仅将我们的测试函数标记为 async

    // 位于 processor.rs 内部的测试模块
    #[cfg(test)]
    mod tests {
    use {
    super::*,
    assert_matches::*,
    solana_program::instruction::{AccountMeta, Instruction},
    solana_program_test::*,
    solana_sdk::{signature::Signer, transaction::Transaction, signer::keypair::Keypair},
    };

    #[tokio::test]
    async fn it_works() {
    let program_id = Pubkey::new_unique();

    let (mut banks_client, payer, recent_blockhash) = ProgramTest::new(
    "program_name",
    program_id,
    processor!(process_instruction),
    )
    .start()
    .await;

    let test_acct = Keypair::new();

    let mut transaction = Transaction::new_with_payer(
    &[Instruction {
    program_id,
    accounts: vec![
    AccountMeta::new(payer.pubkey(), true),
    AccountMeta::new(test_acct.pubkey(), true)
    ],
    data: vec![1, 2, 3],
    }],
    Some(&payer.pubkey()),
    );
    transaction.sign(&[&payer, &test_acct], recent_blockhash);

    assert_matches!(banks_client.process_transaction(transaction).await, Ok(_));
    }
    }

    集成测试

    集成测试旨在完全与其测试的代码分离,以验证不同代码部分是否能够协同工作。

    这些测试通过公共接口与你的代码进行交互,确保其他人能够按预期的方式访问它。虽然在单独运行时正常工作的代码单元可能在集成时出现问题,因此对集成代码的测试覆盖范围同样重要。

    ❓ 如何构建集成测试

    1. 创建集成测试目录: 在项目目录的顶层创建一个 tests 目录,在这个目录下创建任意数量的测试文件,每个文件都作为独立的集成测试。

    2. 独立测试: tests 目录中的每个文件都是一个独立的crate,因此我们需要将我们想要测试的代码库引入每个文件的作用域。

    3. 编写集成测试: 集成测试示例如下:

      // Example of integration test inside /tests/integration_test.rs file
      use example_lib;

      #[test]
      fn it_adds_two() {
      assert_eq!(4, example_lib::add_two(2));
      }
    4. 运行集成测试: 运行 cargo test-bpf 命令即可执行编写的测试。

    5. 输出包括三个部分: 单元测试、集成测试和文档测试。

    🔌 使用Typescript进行集成测试

    除了Rust集成测试外,还可以通过将程序部署到Devnet或本地验证器,并从你创建的客户端向其发送交易来进行测试。

    使用Typescript编写客户端测试脚本的步骤:

    1. 安装测试框架: 使用 npm install mocha chai 安装 MochaChai

    2. 更新package.json文件: 这会告诉编译器在运行命令 npm run test 时执行 /test 目录中的TypeScript文件或文件。

    3. 编写测试代码: 使用“describe”关键字声明,并用it指定每个测试。

      describe("begin tests", async () => {
      // First Mocha test
      it('first test', async () => {
      // Initialization code here to send the transaction
      ...
      // Fetch account info and deserialize
      const acct_info = await connection.getAccountInfo(pda)
      const acct = acct_struct.decode(acct_info.data)

      // Compare the value in the account to what you expect it to be
      chai.expect(acct.num).to.equal(1)
      }
      })
    4. 运行测试: 执行 npm run test 将执行所有测试,并返回每个测试是否通过或失败的结果。

    通过将测试集成到你的开发过程中,你可以确保代码质量和稳定性,同时减少未来可能出现的问题。在Solana开发中,这样的测试流程更是不可或缺的一环。

    > scripts@1.0.0 test
    > mocha -r ts-node/register ./test/*.ts

    ✔ first test (1308ms)
    ✔ second test

    2 passing (1s)

    ❌ 错误代码

    程序错误通常显示为程序返回的错误枚举中错误的十进制索引的十六进制形式。

    例如,当你在向SPL代币程序发送交易时遇到错误,错误代码 0x01 的十进制等价物就是1

    通过查看Token程序的源代码,我们能发现程序错误枚举中该索引位置的错误为 InsufficientFunds

    要翻译任何返回自定义程序错误代码的程序,你需要能访问其源代码。

    📜 程序日志

    Solana提供了非常简单的创建新自定义日志的方法,只需使用 msg!() 宏。

    Rust中编写单元测试时,请注意测试本身不能使用 msg!() 宏来记录信息。

    你需要使用Rust的本地 println!() 宏。

    程序代码中的该语句仍然有效,只是你不能在测试中使用它进行日志记录。

    🧮 计算预算

    开发区块链上的程序会遇到一些特殊限制,其中之一就是Solana上的计算预算。

    计算预算的目的在于防止程序滥用资源。

    当程序消耗完整个预算或超出限制时,运行时会终止程序并返回错误。

    默认情况下,计算预算被设置为200k计算单元乘以指令数量,最多不超过1.4M计算单元。

    基础费用为5,000 Lamports。一个微Lamport相当于0.000001 Lamports

    你可以使用 ComputeBudgetProgram.setComputeUnitLimit({ units: number }) 来设置新的计算预算。

    ComputeBudgetProgram.setComputeUnitPrice({ microLamports: number }) 可以将交易费用提高到基本费用(5,000 Lamports)之上。

    • 以微Lamports为单位的价值将乘以CU预算,从而确定Lamports中的优先费用。
    • 例如,如果你的CU预算为1M CU,并且你每CU增加了1Lamport,那么优先费用将为1 Lamport(1M * 0.000001)
    • 总费用将达到5001 Lamports

    要更改交易的计算预算,你必须将交易的前三条指令之一设置为预算设置指令。

    const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({
    units: 1000000
    });

    const addPriorityFee = ComputeBudgetProgram.setComputeUnitPrice({
    microLamports: 1
    });

    const transaction = new Transaction()
    .add(modifyComputeUnits)
    .add(addPriorityFee)
    .add(
    SystemProgram.transfer({
    fromPubkey: payer.publicKey,
    toPubkey: toAccount,
    lamports: 10000000,
    })
    );

    你还可以使用 sol_log_compute_units() 函数来打印当前指令中程序剩余的计算单元数量。

    use solana_program::log::sol_log_compute_units;

    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
    ) -> ProgramResult {

    sol_log_compute_units();

    ...
    }

    📦 堆栈大小

    每个程序在执行过程中都可以访问4KB的堆栈帧大小。在Rust中,所有值默认都在栈上分配。

    在像Rust这样的系统编程语言中,一个值是在栈上还是堆上分配的区别可能很重要,特别是在受限制的环境中,如区块链工作场景。

    当你处理更大、更复杂的程序时,可能会开始遇到使用完整4KB内存的问题。

    这通常称为"堆栈溢出"或"栈溢出"。

    程序可能会以两种方式达到堆栈限制:

    • 一些依赖的包可能包含违反堆栈帧限制的功能;
    • 或者程序本身在运行时达到堆栈限制。

    以下是当堆栈违规由依赖包引起时,可能出现的错误消息示例。

    Error: Function _ZN16curve25519_dalek7edwards21EdwardsBasepointTable6create17h178b3d2411f7f082E Stack offset of -30728 exceeded max offset of -4096 by 26632 bytes, please minimize large stack variables

    如果一个程序在运行时达到了4KB的堆栈限制,它将停止运行并返回一个错误:AccessViolation

    Program failed to complete: Access violation in stack frame 3 at address 0x200003f70 of size 8 by instruction #5128

    为了解决这个问题,你可以重构代码以更节省内存,或者将一部分内存分配到堆上。

    所有程序都可以访问一个32KB的运行时堆,这可以帮助你在堆栈上节省一些内存。

    为了实现这一点,你需要使用Box结构体。

    一个 box 是一个指向堆上类型为 T 的值的智能指针。

    你可以使用解引用运算符来访问封装的值。

    在下面的示例中,从Pubkey::create_program_address返回的值(一个公钥)将存储在堆上,而authority_pubkey变量则会持有指向堆上存储公钥位置的指针。

    let authority_pubkey = Box::new(Pubkey::create_program_address(authority_signer_seeds, program_id)?);

    if *authority_pubkey != *authority_info.key {
    msg!("Derived lending market authority {} does not match the lending market authority provided {}");
    return Err();
    }

    通过这样的调整,代码不仅可以避免堆栈溢出问题,还能使整体结构更加清晰合理。

    - - +
    Skip to main content

    🧪 测试 Solana 程序

    上节课的内容为准备 Mint 账户奠定了基础。预备阶段已完结,现在是真正行动的时候了。通过强大的测试流程,我们可以在问题真正显现之前捕捉开发人员引入的错误,从而最大限度地减少生产环境中的 bug 数量。

    本课程将涉及两种类型的测试:单元测试和集成测试。

    • 单元测试更小、更专注,一次只测试一个隔离的模块,并能测试私有接口。
    • 集成测试与你的库完全外部无关,使用你的代码的方式与使用其他外部代码相同,仅使用公共接口,并可能在每个测试中涉及多个模块。

    🔢 单元测试

    单元测试的目的是隔离其他代码,测试每个代码单元的工作情况,以快速确认代码是否按预期执行。

    Rust 中,单元测试通常与它们所测试的代码放置在同一文件中,并在名为 tests 的模块内声明,该模块带有 cfg(test) 的注解。

    • 通过 #[test] 属性在 tests 模块中定义测试。
    • cfg 属性代表配置,指示 Rust 只有在特定配置下才包含随后的代码。
    • #[cfg(test)] 注解告诉 Cargo 只有在运行 cargo test-sbf 时才编译测试代码。
    • 运行 cargo test-sbf 时,该模块中标记为测试的每个函数都会执行。

    你还可以在模块中创建非测试的辅助函数,例如:

    // 示例测试模块,包含一个单元测试
    #[cfg(test)]
    mod tests {
    #[test]
    fn it_works() {
    let result = 2 + 2;
    assert_eq!(result, 4);
    }

    fn helper_function() {
    doSomething()
    }
    }

    ❓ 如何构建单元测试

    使用 solana_sdk 来创建 Solana 程序的单元测试。这个 crateRust 中与 Typescript@solana/web3.js 包相对应。

    solana_program_test 也用于测试 Solana 程序,并包含一个基于 BanksClient 的测试框架。

    在下面的代码片段中,我们为我们的 program_id 创建了一个公钥,然后初始化了一个 ProgramTest 对象。

    banks_client 返回的 ProgramTest 将作为我们进入测试环境的接口。

    其中,payer 变量是使用 SOL 生成的新密钥对,将用于签名和支付交易。

    接着,我们创建一个第二个 Keypair,并使用合适的参数构建我们的 Transaction

    最后,我们使用 ProgramTest::new 调用时返回的 banks_client 来处理此交易,并检查返回值是否等于 Ok(_)

    该函数使用 #[tokio::test] 属性进行注解。

    Tokio 是一个用于编写异步代码的 Rust crate,该属性仅将我们的测试函数标记为 async

    // 位于 processor.rs 内部的测试模块
    #[cfg(test)]
    mod tests {
    use {
    super::*,
    assert_matches::*,
    solana_program::instruction::{AccountMeta, Instruction},
    solana_program_test::*,
    solana_sdk::{signature::Signer, transaction::Transaction, signer::keypair::Keypair},
    };

    #[tokio::test]
    async fn it_works() {
    let program_id = Pubkey::new_unique();

    let (mut banks_client, payer, recent_blockhash) = ProgramTest::new(
    "program_name",
    program_id,
    processor!(process_instruction),
    )
    .start()
    .await;

    let test_acct = Keypair::new();

    let mut transaction = Transaction::new_with_payer(
    &[Instruction {
    program_id,
    accounts: vec![
    AccountMeta::new(payer.pubkey(), true),
    AccountMeta::new(test_acct.pubkey(), true)
    ],
    data: vec![1, 2, 3],
    }],
    Some(&payer.pubkey()),
    );
    transaction.sign(&[&payer, &test_acct], recent_blockhash);

    assert_matches!(banks_client.process_transaction(transaction).await, Ok(_));
    }
    }

    集成测试

    集成测试旨在完全与其测试的代码分离,以验证不同代码部分是否能够协同工作。

    这些测试通过公共接口与你的代码进行交互,确保其他人能够按预期的方式访问它。虽然在单独运行时正常工作的代码单元可能在集成时出现问题,因此对集成代码的测试覆盖范围同样重要。

    ❓ 如何构建集成测试

    1. 创建集成测试目录: 在项目目录的顶层创建一个 tests 目录,在这个目录下创建任意数量的测试文件,每个文件都作为独立的集成测试。

    2. 独立测试: tests 目录中的每个文件都是一个独立的crate,因此我们需要将我们想要测试的代码库引入每个文件的作用域。

    3. 编写集成测试: 集成测试示例如下:

      // Example of integration test inside /tests/integration_test.rs file
      use example_lib;

      #[test]
      fn it_adds_two() {
      assert_eq!(4, example_lib::add_two(2));
      }
    4. 运行集成测试: 运行 cargo test-bpf 命令即可执行编写的测试。

    5. 输出包括三个部分: 单元测试、集成测试和文档测试。

    🔌 使用Typescript进行集成测试

    除了Rust集成测试外,还可以通过将程序部署到Devnet或本地验证器,并从你创建的客户端向其发送交易来进行测试。

    使用Typescript编写客户端测试脚本的步骤:

    1. 安装测试框架: 使用 npm install mocha chai 安装 MochaChai

    2. 更新package.json文件: 这会告诉编译器在运行命令 npm run test 时执行 /test 目录中的TypeScript文件或文件。

    3. 编写测试代码: 使用“describe”关键字声明,并用it指定每个测试。

      describe("begin tests", async () => {
      // First Mocha test
      it('first test', async () => {
      // Initialization code here to send the transaction
      ...
      // Fetch account info and deserialize
      const acct_info = await connection.getAccountInfo(pda)
      const acct = acct_struct.decode(acct_info.data)

      // Compare the value in the account to what you expect it to be
      chai.expect(acct.num).to.equal(1)
      }
      })
    4. 运行测试: 执行 npm run test 将执行所有测试,并返回每个测试是否通过或失败的结果。

    通过将测试集成到你的开发过程中,你可以确保代码质量和稳定性,同时减少未来可能出现的问题。在Solana开发中,这样的测试流程更是不可或缺的一环。

    > scripts@1.0.0 test
    > mocha -r ts-node/register ./test/*.ts

    ✔ first test (1308ms)
    ✔ second test

    2 passing (1s)

    ❌ 错误代码

    程序错误通常显示为程序返回的错误枚举中错误的十进制索引的十六进制形式。

    例如,当你在向SPL代币程序发送交易时遇到错误,错误代码 0x01 的十进制等价物就是1

    通过查看Token程序的源代码,我们能发现程序错误枚举中该索引位置的错误为 InsufficientFunds

    要翻译任何返回自定义程序错误代码的程序,你需要能访问其源代码。

    📜 程序日志

    Solana提供了非常简单的创建新自定义日志的方法,只需使用 msg!() 宏。

    Rust中编写单元测试时,请注意测试本身不能使用 msg!() 宏来记录信息。

    你需要使用Rust的本地 println!() 宏。

    程序代码中的该语句仍然有效,只是你不能在测试中使用它进行日志记录。

    🧮 计算预算

    开发区块链上的程序会遇到一些特殊限制,其中之一就是Solana上的计算预算。

    计算预算的目的在于防止程序滥用资源。

    当程序消耗完整个预算或超出限制时,运行时会终止程序并返回错误。

    默认情况下,计算预算被设置为200k计算单元乘以指令数量,最多不超过1.4M计算单元。

    基础费用为5,000 Lamports。一个微Lamport相当于0.000001 Lamports

    你可以使用 ComputeBudgetProgram.setComputeUnitLimit({ units: number }) 来设置新的计算预算。

    ComputeBudgetProgram.setComputeUnitPrice({ microLamports: number }) 可以将交易费用提高到基本费用(5,000 Lamports)之上。

    • 以微Lamports为单位的价值将乘以CU预算,从而确定Lamports中的优先费用。
    • 例如,如果你的CU预算为1M CU,并且你每CU增加了1Lamport,那么优先费用将为1 Lamport(1M * 0.000001)
    • 总费用将达到5001 Lamports

    要更改交易的计算预算,你必须将交易的前三条指令之一设置为预算设置指令。

    const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({
    units: 1000000
    });

    const addPriorityFee = ComputeBudgetProgram.setComputeUnitPrice({
    microLamports: 1
    });

    const transaction = new Transaction()
    .add(modifyComputeUnits)
    .add(addPriorityFee)
    .add(
    SystemProgram.transfer({
    fromPubkey: payer.publicKey,
    toPubkey: toAccount,
    lamports: 10000000,
    })
    );

    你还可以使用 sol_log_compute_units() 函数来打印当前指令中程序剩余的计算单元数量。

    use solana_program::log::sol_log_compute_units;

    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
    ) -> ProgramResult {

    sol_log_compute_units();

    ...
    }

    📦 堆栈大小

    每个程序在执行过程中都可以访问4KB的堆栈帧大小。在Rust中,所有值默认都在栈上分配。

    在像Rust这样的系统编程语言中,一个值是在栈上还是堆上分配的区别可能很重要,特别是在受限制的环境中,如区块链工作场景。

    当你处理更大、更复杂的程序时,可能会开始遇到使用完整4KB内存的问题。

    这通常称为"堆栈溢出"或"栈溢出"。

    程序可能会以两种方式达到堆栈限制:

    • 一些依赖的包可能包含违反堆栈帧限制的功能;
    • 或者程序本身在运行时达到堆栈限制。

    以下是当堆栈违规由依赖包引起时,可能出现的错误消息示例。

    Error: Function _ZN16curve25519_dalek7edwards21EdwardsBasepointTable6create17h178b3d2411f7f082E Stack offset of -30728 exceeded max offset of -4096 by 26632 bytes, please minimize large stack variables

    如果一个程序在运行时达到了4KB的堆栈限制,它将停止运行并返回一个错误:AccessViolation

    Program failed to complete: Access violation in stack frame 3 at address 0x200003f70 of size 8 by instruction #5128

    为了解决这个问题,你可以重构代码以更节省内存,或者将一部分内存分配到堆上。

    所有程序都可以访问一个32KB的运行时堆,这可以帮助你在堆栈上节省一些内存。

    为了实现这一点,你需要使用Box结构体。

    一个 box 是一个指向堆上类型为 T 的值的智能指针。

    你可以使用解引用运算符来访问封装的值。

    在下面的示例中,从Pubkey::create_program_address返回的值(一个公钥)将存储在堆上,而authority_pubkey变量则会持有指向堆上存储公钥位置的指针。

    let authority_pubkey = Box::new(Pubkey::create_program_address(authority_signer_seeds, program_id)?);

    if *authority_pubkey != *authority_info.key {
    msg!("Derived lending market authority {} does not match the lending market authority provided {}");
    return Err();
    }

    通过这样的调整,代码不仅可以避免堆栈溢出问题,还能使整体结构更加清晰合理。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module4/testing/writing-tests-in-rust/index.html b/Solana-Co-Learn/module4/testing/writing-tests-in-rust/index.html index b2a37fa5e..f2f6f0cc4 100644 --- a/Solana-Co-Learn/module4/testing/writing-tests-in-rust/index.html +++ b/Solana-Co-Learn/module4/testing/writing-tests-in-rust/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    😳 使用Rust编写测试

    上一节课我们已经为MINT账户做好了准备。预热阶段已经结束,现在是正式开始的时候了。让我们为我们心爱的电影评论程序编写测试吧。

    设置 - 入门:https://github.com/buildspace/solana-movie-program/tree/solution-add-tokens

    • Cargo.toml 文件中添加:
    [dev-dependencies]
    assert_matches = "1.4.0"
    solana-program-test = "~1.10.29"
    solana-sdk = "~1.10.29"

    初始化测试框架

    • processor.rs 文件底部添加:
    // Inside processor.rs
    #[cfg(test)]
    mod tests {
    use {
    super::*,
    assert_matches::*,
    solana_program::{
    instruction::{AccountMeta, Instruction},
    system_program::ID as SYSTEM_PROGRAM_ID,
    },
    solana_program_test::*,
    solana_sdk::{
    signature::Signer,
    transaction::Transaction,
    sysvar::rent::ID as SYSVAR_RENT_ID
    },
    spl_associated_token_account::{
    get_associated_token_address,
    instruction::create_associated_token_account,
    },
    spl_token:: ID as TOKEN_PROGRAM_ID,
    };
    }

    辅助函数

    • 创建用于初始化铸币的辅助函数。
    • 在测试模块中添加一个函数,这样你可以在需要时调用它。
    // 在测试模块内部
    fn create_init_mint_ix(payer: Pubkey, program_id: Pubkey) -> (Pubkey, Pubkey, Instruction) {
    // 导出用于token mint授权的PDA
    let (mint, _bump_seed) = Pubkey::find_program_address(&[b"token_mint"], &program_id);
    let (mint_auth, _bump_seed) = Pubkey::find_program_address(&[b"token_auth"], &program_id);

    let init_mint_ix = Instruction {
    program_id: program_id,
    accounts: vec![
    AccountMeta::new_readonly(payer, true),
    AccountMeta::new(mint, false),
    AccountMeta::new(mint_auth, false),
    AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
    AccountMeta::new_readonly(TOKEN_PROGRAM_ID, false),
    AccountMeta::new_readonly(SYSVAR_RENT_ID, false)
    ],
    data: vec![3]
    };

    (mint, mint_auth, init_mint_ix)
    }

    初始化铸币测试

    • 测试 initialize_token_mint 指令。
    • 我们的辅助函数将返回一个元组。
    • 我们可以使用解构来获取我们所需的值:
      • mint pubkey
      • mint_auth pubkey
      • 相应的Instruction
    • 一旦指令组装完成,我们可以将其添加到 Transaction 中,并使用从 ProgramTest 构造函数生成的 banks_client 来处理它。
    • 使用 assert_matches! 宏来确认测试是否通过。
    // 第一个单元测试
    #[tokio::test]
    async fn test_initialize_mint_instruction() {
    let program_id = Pubkey::new_unique();
    let (mut banks_client, payer, recent_blockhash) = ProgramTest::new(
    "pda_local",
    program_id,
    processor!(process_instruction),
    )
    .start()
    .await;

    // 调用辅助函数
    let (_mint, _mint_auth, init_mint_ix) = create_init_mint_ix(payer.pubkey(), program_id);

    // 创建具有指令、帐户和输入数据的交易对象
    let mut transaction = Transaction::new_with_payer(
    &[init_mint_ix,],
    Some(&payer.pubkey()),
    );
    transaction.sign(&[&payer], recent_blockhash);

    // 处理交易并比较结果
    assert_matches!(banks_client.process_transaction(transaction).await, Ok(_));
    }

    添加电影评论测试

    • 测试 add_movie_review 指令设置:
    // 第二个单元测试
    #[tokio::test]
    async fn test_add_movie_review_instruction() {
    let program_id = Pubkey::new_unique();
    let (mut banks_client, payer, recent_blockhash) = ProgramTest::new(
    "pda_local",
    program_id,
    processor!(process_instruction),
    )
    .start()
    .await;

    // 调用辅助函数
    let (mint, mint_auth, init_mint_ix) = create_init_mint_ix(payer.pubkey(), program_id);

    }
    • 在第二个测试中推导出PDA
      • 导出评论,
      • 评论计数器,
      • 用户关联的令牌账户地址。
    // 创建评论PDA
    let title: String = "Captain America".to_owned();
    const RATING: u8 = 3;
    let review: String = "Liked the movie".to_owned();
    let (review_pda, _bump_seed) =
    Pubkey::find_program_address(&[payer.pubkey().as_ref(), title.as_bytes()], &program_id);

    // 创建评论计数器PDA
    let (comment_pda, _bump_seed) =
    Pubkey::find_program_address(&[review_pda.as_ref(), b"comment"], &program_id);

    // 创建与token mint关联的用户令牌账户
    let init_ata_ix: Instruction = create_associated_token_account(
    &payer.pubkey(),
    &payer.pubkey(),
    &mint,
    );

    let user_ata: Pubkey =
    get_associated_token_address(&payer.pubkey(), &mint);
    • 构建交易(仍在第二次测试中):
    // 将数据连接到单个缓冲区
    let mut data_vec = vec![0];
    data_vec.append(&mut (title.len().try_into().unwrap().to_le_bytes().try_into().unwrap()));
    data_vec.append(&mut title.into_bytes());
    data_vec.push(RATING);
    data_vec.append(&mut (review.len().try_into().unwrap().to_le_bytes().try_into().unwrap()));
    data_vec.append(&mut review.into_bytes());

    // 创建具有指令、帐户和输入数据的交易对象
    let mut transaction = Transaction::new_with_payer(
    &[
    init_mint_ix,
    init_ata_ix,
    Instruction {
    program_id: program_id,
    accounts: vec![
    AccountMeta::new_readonly(payer.pubkey(), true),
    AccountMeta::new(review_pda, false),
    AccountMeta::new(comment_pda, false),
    AccountMeta::new(mint, false),
    AccountMeta::new_readonly(mint_auth, false),
    AccountMeta::new(user_ata, false),
    AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
    AccountMeta::new_readonly(TOKEN_PROGRAM_ID, false),
    ],
    data: data_vec,
    },
    ],
    Some(&payer.pubkey()),
    );
    transaction.sign(&[&payer], recent_blockhash);

    // 处理交易并比较结果
    assert_matches!(banks_client.process_transaction(transaction).await, Ok(_));
    • 使用 cargo test-sbf 命令运行这些测试

    🚢 挑战

    既然你已经掌握了如何在Rust中编写单元测试,那就不妨继续添加一些你认为对电影评论或学生介绍程序功能至关重要的测试。

    如果你想进一步挑战自己,还可以尝试添加一些TypeScript的集成测试。虽然我们没有一起走过这些步骤,但尝试一下肯定不会错!

    随着你在项目中的进展,一些挑战可能会变得更加开放,从而让你能够根据自己的需求推动自己前进。不要滥用这个机会,而是把它看作提升学习效果的机会。

    - - +
    Skip to main content

    😳 使用Rust编写测试

    上一节课我们已经为MINT账户做好了准备。预热阶段已经结束,现在是正式开始的时候了。让我们为我们心爱的电影评论程序编写测试吧。

    设置 - 入门:https://github.com/buildspace/solana-movie-program/tree/solution-add-tokens

    • Cargo.toml 文件中添加:
    [dev-dependencies]
    assert_matches = "1.4.0"
    solana-program-test = "~1.10.29"
    solana-sdk = "~1.10.29"

    初始化测试框架

    • processor.rs 文件底部添加:
    // Inside processor.rs
    #[cfg(test)]
    mod tests {
    use {
    super::*,
    assert_matches::*,
    solana_program::{
    instruction::{AccountMeta, Instruction},
    system_program::ID as SYSTEM_PROGRAM_ID,
    },
    solana_program_test::*,
    solana_sdk::{
    signature::Signer,
    transaction::Transaction,
    sysvar::rent::ID as SYSVAR_RENT_ID
    },
    spl_associated_token_account::{
    get_associated_token_address,
    instruction::create_associated_token_account,
    },
    spl_token:: ID as TOKEN_PROGRAM_ID,
    };
    }

    辅助函数

    • 创建用于初始化铸币的辅助函数。
    • 在测试模块中添加一个函数,这样你可以在需要时调用它。
    // 在测试模块内部
    fn create_init_mint_ix(payer: Pubkey, program_id: Pubkey) -> (Pubkey, Pubkey, Instruction) {
    // 导出用于token mint授权的PDA
    let (mint, _bump_seed) = Pubkey::find_program_address(&[b"token_mint"], &program_id);
    let (mint_auth, _bump_seed) = Pubkey::find_program_address(&[b"token_auth"], &program_id);

    let init_mint_ix = Instruction {
    program_id: program_id,
    accounts: vec![
    AccountMeta::new_readonly(payer, true),
    AccountMeta::new(mint, false),
    AccountMeta::new(mint_auth, false),
    AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
    AccountMeta::new_readonly(TOKEN_PROGRAM_ID, false),
    AccountMeta::new_readonly(SYSVAR_RENT_ID, false)
    ],
    data: vec![3]
    };

    (mint, mint_auth, init_mint_ix)
    }

    初始化铸币测试

    • 测试 initialize_token_mint 指令。
    • 我们的辅助函数将返回一个元组。
    • 我们可以使用解构来获取我们所需的值:
      • mint pubkey
      • mint_auth pubkey
      • 相应的Instruction
    • 一旦指令组装完成,我们可以将其添加到 Transaction 中,并使用从 ProgramTest 构造函数生成的 banks_client 来处理它。
    • 使用 assert_matches! 宏来确认测试是否通过。
    // 第一个单元测试
    #[tokio::test]
    async fn test_initialize_mint_instruction() {
    let program_id = Pubkey::new_unique();
    let (mut banks_client, payer, recent_blockhash) = ProgramTest::new(
    "pda_local",
    program_id,
    processor!(process_instruction),
    )
    .start()
    .await;

    // 调用辅助函数
    let (_mint, _mint_auth, init_mint_ix) = create_init_mint_ix(payer.pubkey(), program_id);

    // 创建具有指令、帐户和输入数据的交易对象
    let mut transaction = Transaction::new_with_payer(
    &[init_mint_ix,],
    Some(&payer.pubkey()),
    );
    transaction.sign(&[&payer], recent_blockhash);

    // 处理交易并比较结果
    assert_matches!(banks_client.process_transaction(transaction).await, Ok(_));
    }

    添加电影评论测试

    • 测试 add_movie_review 指令设置:
    // 第二个单元测试
    #[tokio::test]
    async fn test_add_movie_review_instruction() {
    let program_id = Pubkey::new_unique();
    let (mut banks_client, payer, recent_blockhash) = ProgramTest::new(
    "pda_local",
    program_id,
    processor!(process_instruction),
    )
    .start()
    .await;

    // 调用辅助函数
    let (mint, mint_auth, init_mint_ix) = create_init_mint_ix(payer.pubkey(), program_id);

    }
    • 在第二个测试中推导出PDA
      • 导出评论,
      • 评论计数器,
      • 用户关联的令牌账户地址。
    // 创建评论PDA
    let title: String = "Captain America".to_owned();
    const RATING: u8 = 3;
    let review: String = "Liked the movie".to_owned();
    let (review_pda, _bump_seed) =
    Pubkey::find_program_address(&[payer.pubkey().as_ref(), title.as_bytes()], &program_id);

    // 创建评论计数器PDA
    let (comment_pda, _bump_seed) =
    Pubkey::find_program_address(&[review_pda.as_ref(), b"comment"], &program_id);

    // 创建与token mint关联的用户令牌账户
    let init_ata_ix: Instruction = create_associated_token_account(
    &payer.pubkey(),
    &payer.pubkey(),
    &mint,
    );

    let user_ata: Pubkey =
    get_associated_token_address(&payer.pubkey(), &mint);
    • 构建交易(仍在第二次测试中):
    // 将数据连接到单个缓冲区
    let mut data_vec = vec![0];
    data_vec.append(&mut (title.len().try_into().unwrap().to_le_bytes().try_into().unwrap()));
    data_vec.append(&mut title.into_bytes());
    data_vec.push(RATING);
    data_vec.append(&mut (review.len().try_into().unwrap().to_le_bytes().try_into().unwrap()));
    data_vec.append(&mut review.into_bytes());

    // 创建具有指令、帐户和输入数据的交易对象
    let mut transaction = Transaction::new_with_payer(
    &[
    init_mint_ix,
    init_ata_ix,
    Instruction {
    program_id: program_id,
    accounts: vec![
    AccountMeta::new_readonly(payer.pubkey(), true),
    AccountMeta::new(review_pda, false),
    AccountMeta::new(comment_pda, false),
    AccountMeta::new(mint, false),
    AccountMeta::new_readonly(mint_auth, false),
    AccountMeta::new(user_ata, false),
    AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
    AccountMeta::new_readonly(TOKEN_PROGRAM_ID, false),
    ],
    data: data_vec,
    },
    ],
    Some(&payer.pubkey()),
    );
    transaction.sign(&[&payer], recent_blockhash);

    // 处理交易并比较结果
    assert_matches!(banks_client.process_transaction(transaction).await, Ok(_));
    • 使用 cargo test-sbf 命令运行这些测试

    🚢 挑战

    既然你已经掌握了如何在Rust中编写单元测试,那就不妨继续添加一些你认为对电影评论或学生介绍程序功能至关重要的测试。

    如果你想进一步挑战自己,还可以尝试添加一些TypeScript的集成测试。虽然我们没有一起走过这些步骤,但尝试一下肯定不会错!

    随着你在项目中的进展,一些挑战可能会变得更加开放,从而让你能够根据自己的需求推动自己前进。不要滥用这个机会,而是把它看作提升学习效果的机会。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module5/a-full-stack-anchor-app/build-the-front-end/index.html b/Solana-Co-Learn/module5/a-full-stack-anchor-app/build-the-front-end/index.html index 44ee46a10..f25490333 100644 --- a/Solana-Co-Learn/module5/a-full-stack-anchor-app/build-the-front-end/index.html +++ b/Solana-Co-Learn/module5/a-full-stack-anchor-app/build-the-front-end/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🏬 前端开发

    既然程序已经运行起来了,现在我们来进入前端代码的部分,为Anchor做适当的调整。整个设置过程只需一分钟,稍作等待,我们还会有一些修改要做。

    首先,我们需要从程序中引入IDL文件。你可以直接将整个文件复制并粘贴到utils文件夹中,包括JSONTypeScript格式。

    然后,创建一个名为WorkspaceProvider.ts的新组件文件。为了节省时间,我们可以直接从我们之前构建的电影评论前端中复制粘贴这段代码,然后将所有的"电影评论"实例替换为"Anchor NFT质押"。你会注意到我们正在从常量文件夹中导入PROGRAM_IDs,所以请进入该文件夹并确保程序ID是我们Anchor NFT质押程序的新ID(而非我们Solana原生程序的ID)。

    import { createContext, useContext } from "react"
    import {
    Program,
    AnchorProvider,
    Idl,
    setProvider,
    } from "@project-serum/anchor"
    import { AnchorNftStaking, IDL } from "../utils/anchor_nft_staking"
    import { Connection } from "@solana/web3.js"
    import { useAnchorWallet, useConnection } from "@solana/wallet-adapter-react"
    import { PROGRAM_ID } from "../utils/constants"

    const WorkspaceContext = createContext({})
    const programId = PROGRAM_ID

    interface Workspace {
    connection?: Connection
    provider?: AnchorProvider
    program?: Program<AnchorNftStaking>
    }

    const WorkspaceProvider = ({ children }: any) => {
    const wallet = useAnchorWallet() || MockWallet
    const { connection } = useConnection()

    const provider = new AnchorProvider(connection, wallet, {})
    setProvider(provider)

    const program = new Program(IDL as Idl, programId)
    const workspace = {
    connection,
    provider,
    program,
    }

    return (
    <WorkspaceContext.Provider value={workspace}>
    {children}
    </WorkspaceContext.Provider>
    )
    }

    const useWorkspace = (): Workspace => {
    return useContext(WorkspaceContext)
    }

    import { Keypair } from "@solana/web3.js"

    const MockWallet = {
    publicKey: Keypair.generate().publicKey,
    signTransaction: () => Promise.reject(),
    signAllTransactions: () => Promise.reject(),
    }

    export { WorkspaceProvider, useWorkspace }

    另外,请从电影评论项目中复制模拟钱包文件,或者创建一个名为MockWallet.ts的新组件,并粘贴下面的代码。

    import { Keypair } from "@solana/web3.js"

    const MockWallet = {
    publicKey: Keypair.generate().publicKey,
    signTransaction: () => Promise.reject(),
    signAllTransactions: () => Promise.reject(),
    }

    export default MockWallet

    确保已经安装了项目serum,可以通过运行npm install @project-serum/anchor来安装。

    现在执行npm run dev,打开本地主机看看是否一切正常。如果没问题,我们就继续进行下去。

    既然进口和额外组件已经准备好了,我们来仔细检查文件,找出我们在使用Anchor时可以进一步简化的地方。

    请跳转到文件(/pages/_app.tsx),并添加我们的新WorkspaceProvider组件,同时确保已经正确导入。

    function MyApp({ Component, pageProps }: AppProps) {
    return (
    <ChakraProvider theme={theme}>
    <WalletContextProvider>
    <WorkspaceProvider>
    <Component {...pageProps} />
    </WorkspaceProvider>
    </WalletContextProvider>
    </ChakraProvider>
    )
    }

    跳转到组件文件夹中的StakeOptionsDisplay.ts文件。

    首先,我们导入Anchor

    import * as anchor from '@project-serum/anchor'

    在声明两个状态变量之后,我们来定义工作空间。

    let workspace = useWorkspace()

    接下来,在checkStakingStatus函数里添加一个额外的检查,连同我们的其他检查一起,确保!workspace.program的存在。

    if (
    !walletAdapter.connected ||
    !walletAdapter.publicKey ||
    !nftTokenAccount ||
    !workspace.program
    )

    现在,跳转到/utils/accounts.ts文件。你可以删除所有的borsh代码,并将getStakeAccount代码替换为以下代码。这就是使用Anchor工作的美妙之处之一,我们不需要担心序列化和反序列化的问题。

    export async function getStakeAccount(
    program: any,
    user: PublicKey,
    tokenAccount: PublicKey
    ): Promise<StakeAccount> {
    const [pda] = PublicKey.findProgramAddressSync(
    [user.toBuffer(), tokenAccount.toBuffer()],
    program.programId
    )

    const account = await program.account.userStakeInfo.fetch(pda)
    return account
    }

    现在,一切都已经比以前简单得多了,不是吗?

    回到StakeOptionsDisplay文件中的checkStakingStatus函数,在被称为getStakeAccount的地方,将第一个参数从connection更改为workspace.program

    打开浏览器,确保本地主机上的所有功能正常运行。

    再回到StakeOptionsDisplay文件,向下滚动到handleStake函数。

    再次,首先添加一个检查!workspace.program的步骤。很快,我们也将其添加到handleUnstakehandleClaim函数中。

    你现在可以放心地从我们之前的工作中删除所有这些代码。

    const account = await connection.getAccountInfo(stakeAccount)
    if (!account) {
    transaction.add(
    createInitializeStakeAccountInstruction(
    walletAdapter.publicKey,
    nftTokenAccount,
    PROGRAM_ID
    )
    )
    }

    const stakeInstruction = createStakingInstruction(
    walletAdapter.publicKey,
    nftTokenAccount,
    nftData.mint.address,
    nftData.edition.address,
    TOKEN_PROGRAM_ID,
    METADATA_PROGRAM_ID,
    PROGRAM_ID
    )

    ... 简单地用以下内容替换它:

    transaction.add(
    await workspace.program.methods
    .stake()
    .accounts({
    nftTokenAccount: nftTokenAccount,
    nftMint: nftData.mint.address,
    nftEdition: nftData.edition.address,
    metadataProgram: METADATA_PROGRAM_ID,
    })
    .instruction()
    )

    这也意味着我们在instructions.ts文件中创建的一大堆代码现在已经不再需要了。再次返回浏览器进行测试。

    假如一切都运行正常,我们接下来将处理handleUnstake代码部分。

    由于现在程序已经处理了所有的事情,我们将放弃下面这段代码:

    const account = await connection.getAccountInfo(userStakeATA)

    const transaction = new Transaction()

    if (!account) {
    transaction.add(
    createAssociatedTokenAccountInstruction(
    walletAdapter.publicKey,
    userStakeATA,
    walletAdapter.publicKey,
    STAKE_MINT
    )
    )
    }

    然后在transaction.add内部去掉createUnstakeInstruction,并用以下代码替换:

    transaction.add(
    await workspace.program.methods
    .unstake()
    .accounts({
    nftTokenAccount: nftTokenAccount,
    nftMint: nftData.mint.address,
    nftEdition: nftData.edition.address,
    metadataProgram: METADATA_PROGRAM_ID,
    stakeMint: STAKE_MINT,
    userStakeAta: userStakeATA,
    })
    .instruction()
    )

    你会注意到这些账户与handleStake中的相同,只是多了一些用于股份铸币和用户ATA的账户。

    最后,转到handleClaim,按照相同的模式进行删除,新的transaction.add应该如下所示:

    transaction.add(
    await workspace.program.methods
    .redeem()
    .accounts({
    nftTokenAccount: nftTokenAccount,
    stakeMint: STAKE_MINT,
    userStakeAta: userStakeATA,
    })
    .instruction()
    )

    现在你可以直接删除整个instructions.ts文件。太棒了!!! :)

    你可以自由地清理未使用的导入,整理你的文件。

    还有一件事需要我们注意,在令牌目录中,我们已经创建了奖励令牌,现在需要使用新的程序ID对其进行重新初始化。在bld/index.ts文件中,当调用await createBldToken时,需要将其替换为新的程序ID。然后重新运行npm run create-bld-token脚本。如果我们不这样做,我们的兑换将无法正常工作。

    这将创建一个新的Mint程序ID,你需要将其添加到你的环境变量中。

    就这样,我们的前端功能已经有一些正在运作了。下周,我们将更深入地使用Anchor进行开发,目前我们只是想展示一下使用Anchor有多么容易,并让基本功能开始运行。

    - - +
    Skip to main content

    🏬 前端开发

    既然程序已经运行起来了,现在我们来进入前端代码的部分,为Anchor做适当的调整。整个设置过程只需一分钟,稍作等待,我们还会有一些修改要做。

    首先,我们需要从程序中引入IDL文件。你可以直接将整个文件复制并粘贴到utils文件夹中,包括JSONTypeScript格式。

    然后,创建一个名为WorkspaceProvider.ts的新组件文件。为了节省时间,我们可以直接从我们之前构建的电影评论前端中复制粘贴这段代码,然后将所有的"电影评论"实例替换为"Anchor NFT质押"。你会注意到我们正在从常量文件夹中导入PROGRAM_IDs,所以请进入该文件夹并确保程序ID是我们Anchor NFT质押程序的新ID(而非我们Solana原生程序的ID)。

    import { createContext, useContext } from "react"
    import {
    Program,
    AnchorProvider,
    Idl,
    setProvider,
    } from "@project-serum/anchor"
    import { AnchorNftStaking, IDL } from "../utils/anchor_nft_staking"
    import { Connection } from "@solana/web3.js"
    import { useAnchorWallet, useConnection } from "@solana/wallet-adapter-react"
    import { PROGRAM_ID } from "../utils/constants"

    const WorkspaceContext = createContext({})
    const programId = PROGRAM_ID

    interface Workspace {
    connection?: Connection
    provider?: AnchorProvider
    program?: Program<AnchorNftStaking>
    }

    const WorkspaceProvider = ({ children }: any) => {
    const wallet = useAnchorWallet() || MockWallet
    const { connection } = useConnection()

    const provider = new AnchorProvider(connection, wallet, {})
    setProvider(provider)

    const program = new Program(IDL as Idl, programId)
    const workspace = {
    connection,
    provider,
    program,
    }

    return (
    <WorkspaceContext.Provider value={workspace}>
    {children}
    </WorkspaceContext.Provider>
    )
    }

    const useWorkspace = (): Workspace => {
    return useContext(WorkspaceContext)
    }

    import { Keypair } from "@solana/web3.js"

    const MockWallet = {
    publicKey: Keypair.generate().publicKey,
    signTransaction: () => Promise.reject(),
    signAllTransactions: () => Promise.reject(),
    }

    export { WorkspaceProvider, useWorkspace }

    另外,请从电影评论项目中复制模拟钱包文件,或者创建一个名为MockWallet.ts的新组件,并粘贴下面的代码。

    import { Keypair } from "@solana/web3.js"

    const MockWallet = {
    publicKey: Keypair.generate().publicKey,
    signTransaction: () => Promise.reject(),
    signAllTransactions: () => Promise.reject(),
    }

    export default MockWallet

    确保已经安装了项目serum,可以通过运行npm install @project-serum/anchor来安装。

    现在执行npm run dev,打开本地主机看看是否一切正常。如果没问题,我们就继续进行下去。

    既然进口和额外组件已经准备好了,我们来仔细检查文件,找出我们在使用Anchor时可以进一步简化的地方。

    请跳转到文件(/pages/_app.tsx),并添加我们的新WorkspaceProvider组件,同时确保已经正确导入。

    function MyApp({ Component, pageProps }: AppProps) {
    return (
    <ChakraProvider theme={theme}>
    <WalletContextProvider>
    <WorkspaceProvider>
    <Component {...pageProps} />
    </WorkspaceProvider>
    </WalletContextProvider>
    </ChakraProvider>
    )
    }

    跳转到组件文件夹中的StakeOptionsDisplay.ts文件。

    首先,我们导入Anchor

    import * as anchor from '@project-serum/anchor'

    在声明两个状态变量之后,我们来定义工作空间。

    let workspace = useWorkspace()

    接下来,在checkStakingStatus函数里添加一个额外的检查,连同我们的其他检查一起,确保!workspace.program的存在。

    if (
    !walletAdapter.connected ||
    !walletAdapter.publicKey ||
    !nftTokenAccount ||
    !workspace.program
    )

    现在,跳转到/utils/accounts.ts文件。你可以删除所有的borsh代码,并将getStakeAccount代码替换为以下代码。这就是使用Anchor工作的美妙之处之一,我们不需要担心序列化和反序列化的问题。

    export async function getStakeAccount(
    program: any,
    user: PublicKey,
    tokenAccount: PublicKey
    ): Promise<StakeAccount> {
    const [pda] = PublicKey.findProgramAddressSync(
    [user.toBuffer(), tokenAccount.toBuffer()],
    program.programId
    )

    const account = await program.account.userStakeInfo.fetch(pda)
    return account
    }

    现在,一切都已经比以前简单得多了,不是吗?

    回到StakeOptionsDisplay文件中的checkStakingStatus函数,在被称为getStakeAccount的地方,将第一个参数从connection更改为workspace.program

    打开浏览器,确保本地主机上的所有功能正常运行。

    再回到StakeOptionsDisplay文件,向下滚动到handleStake函数。

    再次,首先添加一个检查!workspace.program的步骤。很快,我们也将其添加到handleUnstakehandleClaim函数中。

    你现在可以放心地从我们之前的工作中删除所有这些代码。

    const account = await connection.getAccountInfo(stakeAccount)
    if (!account) {
    transaction.add(
    createInitializeStakeAccountInstruction(
    walletAdapter.publicKey,
    nftTokenAccount,
    PROGRAM_ID
    )
    )
    }

    const stakeInstruction = createStakingInstruction(
    walletAdapter.publicKey,
    nftTokenAccount,
    nftData.mint.address,
    nftData.edition.address,
    TOKEN_PROGRAM_ID,
    METADATA_PROGRAM_ID,
    PROGRAM_ID
    )

    ... 简单地用以下内容替换它:

    transaction.add(
    await workspace.program.methods
    .stake()
    .accounts({
    nftTokenAccount: nftTokenAccount,
    nftMint: nftData.mint.address,
    nftEdition: nftData.edition.address,
    metadataProgram: METADATA_PROGRAM_ID,
    })
    .instruction()
    )

    这也意味着我们在instructions.ts文件中创建的一大堆代码现在已经不再需要了。再次返回浏览器进行测试。

    假如一切都运行正常,我们接下来将处理handleUnstake代码部分。

    由于现在程序已经处理了所有的事情,我们将放弃下面这段代码:

    const account = await connection.getAccountInfo(userStakeATA)

    const transaction = new Transaction()

    if (!account) {
    transaction.add(
    createAssociatedTokenAccountInstruction(
    walletAdapter.publicKey,
    userStakeATA,
    walletAdapter.publicKey,
    STAKE_MINT
    )
    )
    }

    然后在transaction.add内部去掉createUnstakeInstruction,并用以下代码替换:

    transaction.add(
    await workspace.program.methods
    .unstake()
    .accounts({
    nftTokenAccount: nftTokenAccount,
    nftMint: nftData.mint.address,
    nftEdition: nftData.edition.address,
    metadataProgram: METADATA_PROGRAM_ID,
    stakeMint: STAKE_MINT,
    userStakeAta: userStakeATA,
    })
    .instruction()
    )

    你会注意到这些账户与handleStake中的相同,只是多了一些用于股份铸币和用户ATA的账户。

    最后,转到handleClaim,按照相同的模式进行删除,新的transaction.add应该如下所示:

    transaction.add(
    await workspace.program.methods
    .redeem()
    .accounts({
    nftTokenAccount: nftTokenAccount,
    stakeMint: STAKE_MINT,
    userStakeAta: userStakeATA,
    })
    .instruction()
    )

    现在你可以直接删除整个instructions.ts文件。太棒了!!! :)

    你可以自由地清理未使用的导入,整理你的文件。

    还有一件事需要我们注意,在令牌目录中,我们已经创建了奖励令牌,现在需要使用新的程序ID对其进行重新初始化。在bld/index.ts文件中,当调用await createBldToken时,需要将其替换为新的程序ID。然后重新运行npm run create-bld-token脚本。如果我们不这样做,我们的兑换将无法正常工作。

    这将创建一个新的Mint程序ID,你需要将其添加到你的环境变量中。

    就这样,我们的前端功能已经有一些正在运作了。下周,我们将更深入地使用Anchor进行开发,目前我们只是想展示一下使用Anchor有多么容易,并让基本功能开始运行。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module5/a-full-stack-anchor-app/index.html b/Solana-Co-Learn/module5/a-full-stack-anchor-app/index.html index 2213261c8..dbfab670f 100644 --- a/Solana-Co-Learn/module5/a-full-stack-anchor-app/index.html +++ b/Solana-Co-Learn/module5/a-full-stack-anchor-app/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module5/a-full-stack-anchor-app/redeeming-with-anchor/index.html b/Solana-Co-Learn/module5/a-full-stack-anchor-app/redeeming-with-anchor/index.html index f4a7331ff..ba9f6e80c 100644 --- a/Solana-Co-Learn/module5/a-full-stack-anchor-app/redeeming-with-anchor/index.html +++ b/Solana-Co-Learn/module5/a-full-stack-anchor-app/redeeming-with-anchor/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    💸 使用Anchor赎回

    lib.rs文件中找到Redeem结构体。由于它与Stake非常相似,我们可以直接粘贴该代码,并根据需要进行调整。

    我们不需要的是nft_mintnft_editionprogram_authority。我们要更改nft_token_account的约束条件,将令牌授权改为'user',因为我们并没有传入mint

    对于stake_state账户,由于不再需要初始化,所以我们只要设定种子和bump,并使其可变化。我们还可以为其增加一些手动约束。

    constraint = *user.key == stake_state.user_pubkey,
    constraint = nft_token_account.key() == stake_state.token_account

    接下来,我们要添加几个账户。其中一个是stake_mint,它需要可变。这是奖励铸币的账户。

    #[account(mut)]
    pub stake_mint: Account<'info, Mint>,

    另一个是stake_authority,它将是另一个未经检查的账户,所以让我们添加这个检查。

    #[account(seeds = ["mint".as_bytes().as_ref()], bump)]

    用户的user_stake_ata是一个TokenAccount,具有以下限制条件。

    #[account(
    init_if_needed,
    payer=user,
    associated_token::mint=stake_mint,
    associated_token::authority=user
    )]
    pub user_stake_ata: Account<'info, TokenAccount>,

    关联的 associated_token_program 是一个 AssociatedToken

    pub associated_token_program: Program<'info, AssociatedToken>,

    最后,将metadata_program替换为rent

    pub rent: Sysvar<'info, Rent>,

    然后,将我们的账户总数增加到10个。以下是所有代码的片段。

    #[derive(Accounts)]
    pub struct Redeem<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
    mut,
    token::authority=user
    )]
    pub nft_token_account: Account<'info, TokenAccount>,
    #[account(
    mut,
    seeds = [user.key().as_ref(), nft_token_account.key().as_ref()],
    bump,
    constraint = *user.key == stake_state.user_pubkey,
    constraint = nft_token_account.key() == stake_state.token_account
    )]
    pub stake_state: Account<'info, UserStakeInfo>,
    #[account(mut)]
    pub stake_mint: Account<'info, Mint>,
    /// CHECK: manual check
    #[account(seeds = ["mint".as_bytes().as_ref()], bump)]
    pub stake_authority: UncheckedAccount<'info>,
    #[account(
    init_if_needed,
    payer=user,
    associated_token::mint=stake_mint,
    associated_token::authority=user
    )]
    pub user_stake_ata: Account<'info, TokenAccount>,
    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
    pub rent: Sysvar<'info, Rent>,
    }

    回到测试文件中,编写一个简单的测试以确保函数被触发。

    it("Redeems", async () => {
    await program.methods
    .redeem()
    .accounts({
    nftTokenAccount: nft.tokenAddress,
    stakeMint: mint,
    userStakeAta: tokenAddress,
    })
    .rpc()

    ...然后运行 anchor test ,如果一切正常并且两个测试通过,我们就进入函数并编写赎回逻辑。

    接下来,让我们进行一些检查,确认它是否已初始化,以及确保它已经抵押。我们需要在文件底部为这两种情况增加自定义错误。

    require!(
    ctx.accounts.stake_state.is_initialized,
    StakeError::UninitializedAccount
    );

    require!(
    ctx.accounts.stake_state.stake_state == StakeState::Staked,
    StakeError::InvalidStakeState
    );

    ...

    #[msg("State account is uninitialized")]
    UninitializedAccount,

    #[msg("Stake state is invalid")]
    InvalidStakeState,

    之后,让我们获取时钟。

    let clock = Clock::get()?;

    现在,我们可以添加一些消息来跟踪事物的进展,并声明我们的时间和兑换金额。

    msg!(
    "Stake last redeem: {:?}",
    ctx.accounts.stake_state.last_stake_redeem
    );

    msg!("Current time: {:?}", clock.unix_timestamp);
    let unix_time = clock.unix_timestamp - ctx.accounts.stake_state.last_stake_redeem;
    msg!("Seconds since last redeem: {}", unix_time);
    let redeem_amount = (10 * i64::pow(10, 2) * unix_time) / (24 * 60 * 60);
    msg!("Elligible redeem amount: {}", redeem_amount);

    好了,现在我们将实际铸造奖励。首先,我们要使用我们的程序创建CpiContext,然后在MintTo对象中传递账户信息。最后,添加种子和金额。

    msg!("Minting staking rewards");
    token::mint_to(
    CpiContext::new_with_signer(
    ctx.accounts.token_program.to_account_info(),
    MintTo {
    mint: ctx.accounts.stake_mint.to_account_info(),
    to: ctx.accounts.user_stake_ata.to_account_info(),
    authority: ctx.accounts.stake_authority.to_account_info(),
    },
    &[&[
    b"mint".as_ref(),
    &[*ctx.bumps.get("stake_authority").unwrap()],
    ]],
    ),
    redeem_amount.try_into().unwrap(),
    )?;

    一切准备就绪后,我们需要设置最后的赎回时间。如果不设置,用户可能会获得比实际应得的更多奖励。

    ctx.accounts.stake_state.last_stake_redeem = clock.unix_timestamp;
    msg!(
    "Updated last stake redeem time: {:?}",
    ctx.accounts.stake_state.last_stake_redeem
    );

    重新进入兑换测试,并添加以下内容。

    const account = await program.account.userStakeInfo.fetch(stakeStatePda)
    expect(account.stakeState === "Unstaked")
    const tokenAccount = await getAccount(provider.connection, tokenAddress)

    你可以继续添加更多的测试来确保其稳定性。目前我们只想先确保基本功能的实现和测试。假如一切顺利,我们可以继续进行解除质押的指令。

    - - +
    Skip to main content

    💸 使用Anchor赎回

    lib.rs文件中找到Redeem结构体。由于它与Stake非常相似,我们可以直接粘贴该代码,并根据需要进行调整。

    我们不需要的是nft_mintnft_editionprogram_authority。我们要更改nft_token_account的约束条件,将令牌授权改为'user',因为我们并没有传入mint

    对于stake_state账户,由于不再需要初始化,所以我们只要设定种子和bump,并使其可变化。我们还可以为其增加一些手动约束。

    constraint = *user.key == stake_state.user_pubkey,
    constraint = nft_token_account.key() == stake_state.token_account

    接下来,我们要添加几个账户。其中一个是stake_mint,它需要可变。这是奖励铸币的账户。

    #[account(mut)]
    pub stake_mint: Account<'info, Mint>,

    另一个是stake_authority,它将是另一个未经检查的账户,所以让我们添加这个检查。

    #[account(seeds = ["mint".as_bytes().as_ref()], bump)]

    用户的user_stake_ata是一个TokenAccount,具有以下限制条件。

    #[account(
    init_if_needed,
    payer=user,
    associated_token::mint=stake_mint,
    associated_token::authority=user
    )]
    pub user_stake_ata: Account<'info, TokenAccount>,

    关联的 associated_token_program 是一个 AssociatedToken

    pub associated_token_program: Program<'info, AssociatedToken>,

    最后,将metadata_program替换为rent

    pub rent: Sysvar<'info, Rent>,

    然后,将我们的账户总数增加到10个。以下是所有代码的片段。

    #[derive(Accounts)]
    pub struct Redeem<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
    mut,
    token::authority=user
    )]
    pub nft_token_account: Account<'info, TokenAccount>,
    #[account(
    mut,
    seeds = [user.key().as_ref(), nft_token_account.key().as_ref()],
    bump,
    constraint = *user.key == stake_state.user_pubkey,
    constraint = nft_token_account.key() == stake_state.token_account
    )]
    pub stake_state: Account<'info, UserStakeInfo>,
    #[account(mut)]
    pub stake_mint: Account<'info, Mint>,
    /// CHECK: manual check
    #[account(seeds = ["mint".as_bytes().as_ref()], bump)]
    pub stake_authority: UncheckedAccount<'info>,
    #[account(
    init_if_needed,
    payer=user,
    associated_token::mint=stake_mint,
    associated_token::authority=user
    )]
    pub user_stake_ata: Account<'info, TokenAccount>,
    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
    pub rent: Sysvar<'info, Rent>,
    }

    回到测试文件中,编写一个简单的测试以确保函数被触发。

    it("Redeems", async () => {
    await program.methods
    .redeem()
    .accounts({
    nftTokenAccount: nft.tokenAddress,
    stakeMint: mint,
    userStakeAta: tokenAddress,
    })
    .rpc()

    ...然后运行 anchor test ,如果一切正常并且两个测试通过,我们就进入函数并编写赎回逻辑。

    接下来,让我们进行一些检查,确认它是否已初始化,以及确保它已经抵押。我们需要在文件底部为这两种情况增加自定义错误。

    require!(
    ctx.accounts.stake_state.is_initialized,
    StakeError::UninitializedAccount
    );

    require!(
    ctx.accounts.stake_state.stake_state == StakeState::Staked,
    StakeError::InvalidStakeState
    );

    ...

    #[msg("State account is uninitialized")]
    UninitializedAccount,

    #[msg("Stake state is invalid")]
    InvalidStakeState,

    之后,让我们获取时钟。

    let clock = Clock::get()?;

    现在,我们可以添加一些消息来跟踪事物的进展,并声明我们的时间和兑换金额。

    msg!(
    "Stake last redeem: {:?}",
    ctx.accounts.stake_state.last_stake_redeem
    );

    msg!("Current time: {:?}", clock.unix_timestamp);
    let unix_time = clock.unix_timestamp - ctx.accounts.stake_state.last_stake_redeem;
    msg!("Seconds since last redeem: {}", unix_time);
    let redeem_amount = (10 * i64::pow(10, 2) * unix_time) / (24 * 60 * 60);
    msg!("Elligible redeem amount: {}", redeem_amount);

    好了,现在我们将实际铸造奖励。首先,我们要使用我们的程序创建CpiContext,然后在MintTo对象中传递账户信息。最后,添加种子和金额。

    msg!("Minting staking rewards");
    token::mint_to(
    CpiContext::new_with_signer(
    ctx.accounts.token_program.to_account_info(),
    MintTo {
    mint: ctx.accounts.stake_mint.to_account_info(),
    to: ctx.accounts.user_stake_ata.to_account_info(),
    authority: ctx.accounts.stake_authority.to_account_info(),
    },
    &[&[
    b"mint".as_ref(),
    &[*ctx.bumps.get("stake_authority").unwrap()],
    ]],
    ),
    redeem_amount.try_into().unwrap(),
    )?;

    一切准备就绪后,我们需要设置最后的赎回时间。如果不设置,用户可能会获得比实际应得的更多奖励。

    ctx.accounts.stake_state.last_stake_redeem = clock.unix_timestamp;
    msg!(
    "Updated last stake redeem time: {:?}",
    ctx.accounts.stake_state.last_stake_redeem
    );

    重新进入兑换测试,并添加以下内容。

    const account = await program.account.userStakeInfo.fetch(stakeStatePda)
    expect(account.stakeState === "Unstaked")
    const tokenAccount = await getAccount(provider.connection, tokenAddress)

    你可以继续添加更多的测试来确保其稳定性。目前我们只想先确保基本功能的实现和测试。假如一切顺利,我们可以继续进行解除质押的指令。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module5/a-full-stack-anchor-app/staking-with-anchor/index.html b/Solana-Co-Learn/module5/a-full-stack-anchor-app/staking-with-anchor/index.html index 825fbc43d..00c745de3 100644 --- a/Solana-Co-Learn/module5/a-full-stack-anchor-app/staking-with-anchor/index.html +++ b/Solana-Co-Learn/module5/a-full-stack-anchor-app/staking-with-anchor/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🥩 使用Anchor进行NFT的质押

    现在是时候将你的NFT质押计划及用户界面迁移到Anchor上了!你一直辛苦开发的buildoor项目已经相当出色,但将其迁移到Anchor上将使未来的工作变得更加简洁。请利用你所掌握的知识,完成下述任务:

    • 使用Anchor从头开始重新编写代码。
    • 增加一些可靠的测试覆盖,以确保你能够严密捕捉任何安全风险。
    • Anchor的方法构建器来替换复杂的UI代码。

    这项任务可能有些复杂,需要你投入一些时间独立进行尝试。如果几个小时后你感到困惑,随时可以观看我们提供的视频演示解决方案。

    我们来共同完成这个任务,并查看我们的成果。我们不是在增加新功能,而是要完全用Anchor替换我们一直在努力开发的质押计划。

    首先,通过运行 anchor init anchor-nft-staking 创建一个新项目,或者可以选择自己的名字。然后打开 Anchor.toml 文件,将种子设置为 true,集群设置为 devnet

    接下来,跳转到 /programs/anchor-nft-staking/Cargo.toml,并添加以下依赖项。

    anchor-lang = { version="0.25.0", features = ["init-if-needed"] }
    anchor-spl = "0.25.0"
    mpl-token-metadata = { version="1.4.1", features=["no-entrypoint"] }

    好的,打开 lib.rs 文件,我们来构建基本的框架。

    我们需要添加以下导入。随着我们的工作进展,每个导入的必要性将逐渐显现。

    use anchor_lang::solana_program::program::invoke_signed;
    use anchor_spl::token;
    use anchor_spl::{
    associated_token::AssociatedToken,
    token::{Approve, Mint, MintTo, Revoke, Token, TokenAccount},
    };
    use mpl_token_metadata::{
    instruction::{freeze_delegated_account, thaw_delegated_account},
    ID as MetadataTokenId,
    };

    首先,我们将默认函数的名称更改为 stake,并更改其相关上下文为类型Stake

    然后添加名为 redeem 的函数,上下文类型为 Redeem

    最后,对于 unstake,使用上下文类型 Unstake 进行相同操作。

    接下来我们要构建的是Stake的结构。我们需要一个PDA来存储UserStakeInfo,并且需要一个StakeState枚举来表示PDA的其中一个字段。

    #[account]
    pub struct UserStakeInfo {
    pub token_account: Pubkey,
    pub stake_start_time: i64,
    pub last_stake_redeem: i64,
    pub user_pubkey: Pubkey,
    pub stake_state: StakeState,
    pub is_initialized: bool,
    }

    #[derive(Debug, PartialEq, AnchorDeserialize, AnchorSerialize, Clone)]
    pub enum StakeState {
    Unstaked,
    Staked,
    }

    StakeState添加一个默认值,设为未抵押状态。

    由于我们将使用的元数据程序相对较新,锚定程序中还没有相应的类型。为了像其他程序(例如系统程序、令牌程序等)一样使用它,我们将为其创建一个结构体,并添加一个名为 id 的实现,返回一个 Pubkey,它对应于 MetadataTokenId

    #[derive(Clone)]
    pub struct Metadata;

    impl anchor_lang::Id for Metadata {
    fn id() -> Pubkey {
    MetadataTokenId
    }
    }

    好的,现在我们可以开始处理质押部分。下面是结构体所需的九个账户,以及一些值得注意的事项。

    首先,你会看到 nft_edition 是一个 Unchecked 账户,这是因为系统中还未为这种类型的账户创建。所有未经核实的账户都需附带一条备注,以便系统知道你将进行手动安全检查。你会在下方看到 CHECK: Manual validation

    需要提醒的是,每个账户上的属性都是一种安全检查,以确保账户是正确的类型并能执行特定功能。由于用户需要付费,并且NFT代币账户将被修改,所以两者都具有mut属性。某些账户还需要种子,如下所示。

    至于其他没有任何属性的账户,它们在Anchor中都有自己必需的安全检查,所以我们不需要添加任何属性。

    #[derive(Accounts)]
    pub struct Stake<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
    mut,
    associated_token::mint=nft_mint,
    associated_token::authority=user
    )]
    pub nft_token_account: Account<'info, TokenAccount>,
    pub nft_mint: Account<'info, Mint>,
    /// CHECK: Manual validation
    #[account(owner=MetadataTokenId)]
    pub nft_edition: UncheckedAccount<'info>,
    #[account(
    init_if_needed,
    payer=user,
    space = std::mem::size_of::<UserStakeInfo>() + 8,
    seeds = [user.key().as_ref(), nft_token_account.key().as_ref()],
    bump
    )]
    pub stake_state: Account<'info, UserStakeInfo>,
    /// CHECK: Manual validation
    #[account(mut, seeds=["authority".as_bytes().as_ref()], bump)]
    pub program_authority: UncheckedAccount<'info>,
    pub token_program: Program<'info, Token>,
    pub system_program: Program<'info, System>,
    pub metadata_program: Program<'info, Metadata>,
    }

    在继续操作之前,先运行anchor build,这样我们的第一个构建就可以开始了。请记住,这是我们的第一次构建,它会生成我们的程序ID

    在构建的同时,在tests目录中创建一个名为utils的新文件夹。在其中创建一个名为setupNft.ts的文件,并将下面的代码粘贴进去。

    import {
    bundlrStorage,
    keypairIdentity,
    Metaplex,
    } from "@metaplex-foundation/js"
    import { createMint, getAssociatedTokenAddress } from "@solana/spl-token"
    import * as anchor from "@project-serum/anchor"

    export const setupNft = async (program, payer) => {
    const metaplex = Metaplex.make(program.provider.connection)
    .use(keypairIdentity(payer))
    .use(bundlrStorage())

    const nft = await metaplex
    .nfts()
    .create({
    uri: "",
    name: "Test nft",
    sellerFeeBasisPoints: 0,
    })

    console.log("nft metadata pubkey: ", nft.metadataAddress.toBase58())
    console.log("nft token address: ", nft.tokenAddress.toBase58())
    const [delegatedAuthPda] = await anchor.web3.PublicKey.findProgramAddress(
    [Buffer.from("authority")],
    program.programId
    )
    const [stakeStatePda] = await anchor.web3.PublicKey.findProgramAddress(
    [payer.publicKey.toBuffer(), nft.tokenAddress.toBuffer()],
    program.programId
    )

    console.log("delegated authority pda: ", delegatedAuthPda.toBase58())
    console.log("stake state pda: ", stakeStatePda.toBase58())
    const [mintAuth] = await anchor.web3.PublicKey.findProgramAddress(
    [Buffer.from("mint")],
    program.programId
    )

    const mint = await createMint(
    program.provider.connection,
    payer,
    mintAuth,
    null,
    2
    )
    console.log("Mint pubkey: ", mint.toBase58())

    const tokenAddress = await getAssociatedTokenAddress(mint, payer.publicKey)

    return {
    nft: nft,
    delegatedAuthPda: delegatedAuthPda,
    stakeStatePda: stakeStatePda,
    mint: mint,
    mintAuth: mintAuth,
    tokenAddress: tokenAddress,
    }
    }

    然后,运行npm install @metaplex-foundation/js

    最后,转到anchor-nft-staking.ts目录。这是Anchor创建的默认文件。

    你需要将提供者的默认行分为两部分,以便在以后需要时能够访问提供者。

    const provider = anchor.AnchorProvider.env();
    anchor.setProvider(provider);

    让我们引入钱包,这将使我们能够公开付款人为交易签名。

    const wallet = anchor.workspace.AnchorNftStaking.provider.wallet;

    检查你的编译情况,如果一切顺利,请运行 anchor deploy。如果出现问题,你可能需要为自己空投一些SOL。

    编译完成后,运行 anchor keys list 并获取程序ID,然后将其放入 lib.rsAnchor.toml 文件中。如果编译花费一些时间,你可能需要回到这一步。

    回到测试文件。

    让我们定义一些测试中需要使用的变量类型。

    let delegatedAuthPda: anchor.web3.PublicKey;
    let stakeStatePda: anchor.web3.PublicKey;
    let nft: any;
    let mintAuth: anchor.web3.PublicKey;
    let mint: anchor.web3.PublicKey;
    let tokenAddress: anchor.web3.PublicKey;

    现在,我们添加一个 before 函数,该函数会在测试运行之前被调用。注意";"语法,它会解构返回值并为所有这些值进行设置。

    before(async () => {
    ;({ nft, delegatedAuthPda, stakeStatePda, mint, mintAuth, tokenAddress } =
    await setupNft(program, wallet.payer));
    });

    转到默认测试,将其更改为 it("Stakes"。首先,我们只是确认函数被成功调用。我们还没有构建实际的抵押函数,所以暂时不会进行任何逻辑测试。

    it("Stakes", async () => {
    // 在此添加你的测试。
    await program.methods
    .stake()
    .accounts({
    nftTokenAccount: nft.tokenAddress,
    nftMint: nft.mintAddress,
    nftEdition: nft.masterEditionAddress,
    metadataProgram: METADATA_PROGRAM_ID,
    })
    .rpc();
    });

    现在,运行 anchor test。如果它通过了,这意味着我们通过了在Stake结构中创建账户的验证。

    回到逻辑部分,下面是抵押工作所需的逐步操作。我们需要获取时钟访问权限,确保抵押状态已初始化,并确认尚未抵押。

    stake函数中,我们首先获取时钟。

    let clock = Clock::get().unwrap();

    接下来,我们创建一个CPI来委托该程序作为冻结或解冻我们的NFT的权限。首先,我们设置CPI,然后确定我们要使用的账户,最后设定权限。

    msg!("Approving delegate");

    let cpi_approve_program = ctx.accounts.token_program.to_account_info();
    let cpi_approve_accounts = Approve {
    to: ctx.accounts.nft_token_account.to_account_info(),
    delegate: ctx.accounts.program_authority.to_account_info(),
    authority: ctx.accounts.user.to_account_info(),
    };

    let cpi_approve_ctx = CpiContext::new(cpi_approve_program, cpi_approve_accounts);
    token::approve(cpi_approve_ctx, 1)?;

    然后我们开始冻结代币。首先设置权限提升,然后调用invoke_signed函数,传入所有必要的账户和账户信息数组,最后是种子和提升值。

    msg!("Freezing token account");
    let authority_bump = *ctx.bumps.get("program_authority").unwrap();
    invoke_signed(
    &freeze_delegated_account(
    ctx.accounts.metadata_program.key(),
    ctx.accounts.program_authority.key(),
    ctx.accounts.nft_token_account.key(),
    ctx.accounts.nft_edition.key(),
    ctx.accounts.nft_mint.key(),
    ),
    &[
    ctx.accounts.program_authority.to_account_info(),
    ctx.accounts.nft_token_account.to_account_info(),
    ctx.accounts.nft_edition.to_account_info(),
    ctx.accounts.nft_mint.to_account_info(),
    ctx.accounts.metadata_program.to_account_info(),
    ],
    &[&[b"authority", &[authority_bump]]],
    )?;

    现在,我们在股权账户上设置数据。

    ctx.accounts.stake_state.token_account = ctx.accounts.nft_token_account.key();
    ctx.accounts.stake_state.user_pubkey = ctx.accounts.user.key();
    ctx.accounts.stake_state.stake_state = StakeState::Staked;
    ctx.accounts.stake_state.stake_start_time = clock.unix_timestamp;
    ctx.accounts.stake_state.last_stake_redeem = clock.unix_timestamp;
    ctx.accounts.stake_state.is_initialized = true;

    哎呀,让我们跳到文件开始部分并添加一个安全检查,我们还需要一个自定义错误。下面是两段代码,但是将自定义错误代码放在文件底部,这样不会影响逻辑和结构的阅读。

    require!(
    ctx.accounts.stake_state.stake_state == StakeState::Unstaked,
    StakeError::AlreadyStaked
    );
    #[error_code]
    pub enum StakeError {
    #[msg("NFT already staked")]
    AlreadyStaked,
    }

    在再次测试之前,不要忘记充实你的SOL余额。

    好的,就这样,让我们回到测试中,为我们的质押测试添加一些功能,以检查质押状态是否正确。

    const account = await program.account.userStakeInfo.fetch(stakeStatePda);
    expect(account.stakeState === "Staked");

    再次运行测试,希望一切都顺利!🤞

    就这样,我们的第一个指令已经落地生效。在接下来的部分,我们将处理其余两个指令,然后终于开始处理客户端交易的事宜。

    - - +
    Skip to main content

    🥩 使用Anchor进行NFT的质押

    现在是时候将你的NFT质押计划及用户界面迁移到Anchor上了!你一直辛苦开发的buildoor项目已经相当出色,但将其迁移到Anchor上将使未来的工作变得更加简洁。请利用你所掌握的知识,完成下述任务:

    • 使用Anchor从头开始重新编写代码。
    • 增加一些可靠的测试覆盖,以确保你能够严密捕捉任何安全风险。
    • Anchor的方法构建器来替换复杂的UI代码。

    这项任务可能有些复杂,需要你投入一些时间独立进行尝试。如果几个小时后你感到困惑,随时可以观看我们提供的视频演示解决方案。

    我们来共同完成这个任务,并查看我们的成果。我们不是在增加新功能,而是要完全用Anchor替换我们一直在努力开发的质押计划。

    首先,通过运行 anchor init anchor-nft-staking 创建一个新项目,或者可以选择自己的名字。然后打开 Anchor.toml 文件,将种子设置为 true,集群设置为 devnet

    接下来,跳转到 /programs/anchor-nft-staking/Cargo.toml,并添加以下依赖项。

    anchor-lang = { version="0.25.0", features = ["init-if-needed"] }
    anchor-spl = "0.25.0"
    mpl-token-metadata = { version="1.4.1", features=["no-entrypoint"] }

    好的,打开 lib.rs 文件,我们来构建基本的框架。

    我们需要添加以下导入。随着我们的工作进展,每个导入的必要性将逐渐显现。

    use anchor_lang::solana_program::program::invoke_signed;
    use anchor_spl::token;
    use anchor_spl::{
    associated_token::AssociatedToken,
    token::{Approve, Mint, MintTo, Revoke, Token, TokenAccount},
    };
    use mpl_token_metadata::{
    instruction::{freeze_delegated_account, thaw_delegated_account},
    ID as MetadataTokenId,
    };

    首先,我们将默认函数的名称更改为 stake,并更改其相关上下文为类型Stake

    然后添加名为 redeem 的函数,上下文类型为 Redeem

    最后,对于 unstake,使用上下文类型 Unstake 进行相同操作。

    接下来我们要构建的是Stake的结构。我们需要一个PDA来存储UserStakeInfo,并且需要一个StakeState枚举来表示PDA的其中一个字段。

    #[account]
    pub struct UserStakeInfo {
    pub token_account: Pubkey,
    pub stake_start_time: i64,
    pub last_stake_redeem: i64,
    pub user_pubkey: Pubkey,
    pub stake_state: StakeState,
    pub is_initialized: bool,
    }

    #[derive(Debug, PartialEq, AnchorDeserialize, AnchorSerialize, Clone)]
    pub enum StakeState {
    Unstaked,
    Staked,
    }

    StakeState添加一个默认值,设为未抵押状态。

    由于我们将使用的元数据程序相对较新,锚定程序中还没有相应的类型。为了像其他程序(例如系统程序、令牌程序等)一样使用它,我们将为其创建一个结构体,并添加一个名为 id 的实现,返回一个 Pubkey,它对应于 MetadataTokenId

    #[derive(Clone)]
    pub struct Metadata;

    impl anchor_lang::Id for Metadata {
    fn id() -> Pubkey {
    MetadataTokenId
    }
    }

    好的,现在我们可以开始处理质押部分。下面是结构体所需的九个账户,以及一些值得注意的事项。

    首先,你会看到 nft_edition 是一个 Unchecked 账户,这是因为系统中还未为这种类型的账户创建。所有未经核实的账户都需附带一条备注,以便系统知道你将进行手动安全检查。你会在下方看到 CHECK: Manual validation

    需要提醒的是,每个账户上的属性都是一种安全检查,以确保账户是正确的类型并能执行特定功能。由于用户需要付费,并且NFT代币账户将被修改,所以两者都具有mut属性。某些账户还需要种子,如下所示。

    至于其他没有任何属性的账户,它们在Anchor中都有自己必需的安全检查,所以我们不需要添加任何属性。

    #[derive(Accounts)]
    pub struct Stake<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
    mut,
    associated_token::mint=nft_mint,
    associated_token::authority=user
    )]
    pub nft_token_account: Account<'info, TokenAccount>,
    pub nft_mint: Account<'info, Mint>,
    /// CHECK: Manual validation
    #[account(owner=MetadataTokenId)]
    pub nft_edition: UncheckedAccount<'info>,
    #[account(
    init_if_needed,
    payer=user,
    space = std::mem::size_of::<UserStakeInfo>() + 8,
    seeds = [user.key().as_ref(), nft_token_account.key().as_ref()],
    bump
    )]
    pub stake_state: Account<'info, UserStakeInfo>,
    /// CHECK: Manual validation
    #[account(mut, seeds=["authority".as_bytes().as_ref()], bump)]
    pub program_authority: UncheckedAccount<'info>,
    pub token_program: Program<'info, Token>,
    pub system_program: Program<'info, System>,
    pub metadata_program: Program<'info, Metadata>,
    }

    在继续操作之前,先运行anchor build,这样我们的第一个构建就可以开始了。请记住,这是我们的第一次构建,它会生成我们的程序ID

    在构建的同时,在tests目录中创建一个名为utils的新文件夹。在其中创建一个名为setupNft.ts的文件,并将下面的代码粘贴进去。

    import {
    bundlrStorage,
    keypairIdentity,
    Metaplex,
    } from "@metaplex-foundation/js"
    import { createMint, getAssociatedTokenAddress } from "@solana/spl-token"
    import * as anchor from "@project-serum/anchor"

    export const setupNft = async (program, payer) => {
    const metaplex = Metaplex.make(program.provider.connection)
    .use(keypairIdentity(payer))
    .use(bundlrStorage())

    const nft = await metaplex
    .nfts()
    .create({
    uri: "",
    name: "Test nft",
    sellerFeeBasisPoints: 0,
    })

    console.log("nft metadata pubkey: ", nft.metadataAddress.toBase58())
    console.log("nft token address: ", nft.tokenAddress.toBase58())
    const [delegatedAuthPda] = await anchor.web3.PublicKey.findProgramAddress(
    [Buffer.from("authority")],
    program.programId
    )
    const [stakeStatePda] = await anchor.web3.PublicKey.findProgramAddress(
    [payer.publicKey.toBuffer(), nft.tokenAddress.toBuffer()],
    program.programId
    )

    console.log("delegated authority pda: ", delegatedAuthPda.toBase58())
    console.log("stake state pda: ", stakeStatePda.toBase58())
    const [mintAuth] = await anchor.web3.PublicKey.findProgramAddress(
    [Buffer.from("mint")],
    program.programId
    )

    const mint = await createMint(
    program.provider.connection,
    payer,
    mintAuth,
    null,
    2
    )
    console.log("Mint pubkey: ", mint.toBase58())

    const tokenAddress = await getAssociatedTokenAddress(mint, payer.publicKey)

    return {
    nft: nft,
    delegatedAuthPda: delegatedAuthPda,
    stakeStatePda: stakeStatePda,
    mint: mint,
    mintAuth: mintAuth,
    tokenAddress: tokenAddress,
    }
    }

    然后,运行npm install @metaplex-foundation/js

    最后,转到anchor-nft-staking.ts目录。这是Anchor创建的默认文件。

    你需要将提供者的默认行分为两部分,以便在以后需要时能够访问提供者。

    const provider = anchor.AnchorProvider.env();
    anchor.setProvider(provider);

    让我们引入钱包,这将使我们能够公开付款人为交易签名。

    const wallet = anchor.workspace.AnchorNftStaking.provider.wallet;

    检查你的编译情况,如果一切顺利,请运行 anchor deploy。如果出现问题,你可能需要为自己空投一些SOL。

    编译完成后,运行 anchor keys list 并获取程序ID,然后将其放入 lib.rsAnchor.toml 文件中。如果编译花费一些时间,你可能需要回到这一步。

    回到测试文件。

    让我们定义一些测试中需要使用的变量类型。

    let delegatedAuthPda: anchor.web3.PublicKey;
    let stakeStatePda: anchor.web3.PublicKey;
    let nft: any;
    let mintAuth: anchor.web3.PublicKey;
    let mint: anchor.web3.PublicKey;
    let tokenAddress: anchor.web3.PublicKey;

    现在,我们添加一个 before 函数,该函数会在测试运行之前被调用。注意";"语法,它会解构返回值并为所有这些值进行设置。

    before(async () => {
    ;({ nft, delegatedAuthPda, stakeStatePda, mint, mintAuth, tokenAddress } =
    await setupNft(program, wallet.payer));
    });

    转到默认测试,将其更改为 it("Stakes"。首先,我们只是确认函数被成功调用。我们还没有构建实际的抵押函数,所以暂时不会进行任何逻辑测试。

    it("Stakes", async () => {
    // 在此添加你的测试。
    await program.methods
    .stake()
    .accounts({
    nftTokenAccount: nft.tokenAddress,
    nftMint: nft.mintAddress,
    nftEdition: nft.masterEditionAddress,
    metadataProgram: METADATA_PROGRAM_ID,
    })
    .rpc();
    });

    现在,运行 anchor test。如果它通过了,这意味着我们通过了在Stake结构中创建账户的验证。

    回到逻辑部分,下面是抵押工作所需的逐步操作。我们需要获取时钟访问权限,确保抵押状态已初始化,并确认尚未抵押。

    stake函数中,我们首先获取时钟。

    let clock = Clock::get().unwrap();

    接下来,我们创建一个CPI来委托该程序作为冻结或解冻我们的NFT的权限。首先,我们设置CPI,然后确定我们要使用的账户,最后设定权限。

    msg!("Approving delegate");

    let cpi_approve_program = ctx.accounts.token_program.to_account_info();
    let cpi_approve_accounts = Approve {
    to: ctx.accounts.nft_token_account.to_account_info(),
    delegate: ctx.accounts.program_authority.to_account_info(),
    authority: ctx.accounts.user.to_account_info(),
    };

    let cpi_approve_ctx = CpiContext::new(cpi_approve_program, cpi_approve_accounts);
    token::approve(cpi_approve_ctx, 1)?;

    然后我们开始冻结代币。首先设置权限提升,然后调用invoke_signed函数,传入所有必要的账户和账户信息数组,最后是种子和提升值。

    msg!("Freezing token account");
    let authority_bump = *ctx.bumps.get("program_authority").unwrap();
    invoke_signed(
    &freeze_delegated_account(
    ctx.accounts.metadata_program.key(),
    ctx.accounts.program_authority.key(),
    ctx.accounts.nft_token_account.key(),
    ctx.accounts.nft_edition.key(),
    ctx.accounts.nft_mint.key(),
    ),
    &[
    ctx.accounts.program_authority.to_account_info(),
    ctx.accounts.nft_token_account.to_account_info(),
    ctx.accounts.nft_edition.to_account_info(),
    ctx.accounts.nft_mint.to_account_info(),
    ctx.accounts.metadata_program.to_account_info(),
    ],
    &[&[b"authority", &[authority_bump]]],
    )?;

    现在,我们在股权账户上设置数据。

    ctx.accounts.stake_state.token_account = ctx.accounts.nft_token_account.key();
    ctx.accounts.stake_state.user_pubkey = ctx.accounts.user.key();
    ctx.accounts.stake_state.stake_state = StakeState::Staked;
    ctx.accounts.stake_state.stake_start_time = clock.unix_timestamp;
    ctx.accounts.stake_state.last_stake_redeem = clock.unix_timestamp;
    ctx.accounts.stake_state.is_initialized = true;

    哎呀,让我们跳到文件开始部分并添加一个安全检查,我们还需要一个自定义错误。下面是两段代码,但是将自定义错误代码放在文件底部,这样不会影响逻辑和结构的阅读。

    require!(
    ctx.accounts.stake_state.stake_state == StakeState::Unstaked,
    StakeError::AlreadyStaked
    );
    #[error_code]
    pub enum StakeError {
    #[msg("NFT already staked")]
    AlreadyStaked,
    }

    在再次测试之前,不要忘记充实你的SOL余额。

    好的,就这样,让我们回到测试中,为我们的质押测试添加一些功能,以检查质押状态是否正确。

    const account = await program.account.userStakeInfo.fetch(stakeStatePda);
    expect(account.stakeState === "Staked");

    再次运行测试,希望一切都顺利!🤞

    就这样,我们的第一个指令已经落地生效。在接下来的部分,我们将处理其余两个指令,然后终于开始处理客户端交易的事宜。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module5/a-full-stack-anchor-app/unstaking-with-anchor/index.html b/Solana-Co-Learn/module5/a-full-stack-anchor-app/unstaking-with-anchor/index.html index e8b52f5cd..6856b9459 100644 --- a/Solana-Co-Learn/module5/a-full-stack-anchor-app/unstaking-with-anchor/index.html +++ b/Solana-Co-Learn/module5/a-full-stack-anchor-app/unstaking-with-anchor/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🍖 解除与Anchor的质押

    现在赎回和质押都已完成,让我们开始解除质押。解除质押账户结构包括了总共14个账户,这些是赎回和质押组合在一起的结果,具体如下所示。请确保顺序相同。

    #[derive(Accounts)]
    pub struct Unstake<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
    mut,
    token::authority=user
    )]
    pub nft_token_account: Account<'info, TokenAccount>,
    pub nft_mint: Account<'info, Mint>,
    /// CHECK: Manual validation
    #[account(owner=MetadataTokenId)]
    pub nft_edition: UncheckedAccount<'info>,
    #[account(
    mut,
    seeds = [user.key().as_ref(), nft_token_account.key().as_ref()],
    bump,
    constraint = *user.key == stake_state.user_pubkey,
    constraint = nft_token_account.key() == stake_state.token_account
    )]
    pub stake_state: Account<'info, UserStakeInfo>,
    /// CHECK: manual check
    #[account(mut, seeds=["authority".as_bytes().as_ref()], bump)]
    pub program_authority: UncheckedAccount<'info>,
    #[account(mut)]
    pub stake_mint: Account<'info, Mint>,
    /// CHECK: manual check
    #[account(seeds = ["mint".as_bytes().as_ref()], bump)]
    pub stake_authority: UncheckedAccount<'info>,
    #[account(
    init_if_needed,
    payer=user,
    associated_token::mint=stake_mint,
    associated_token::authority=user
    )]
    pub user_stake_ata: Account<'info, TokenAccount>,
    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
    pub rent: Sysvar<'info, Rent>,
    pub metadata_program: Program<'info, Metadata>,
    }

    这个过程相当简单,我们来编写一些基础测试,以确保其正常工作。我们需要添加那六个不会自动推断的账户。

    it("Unstakes", async () => {
    await program.methods
    .unstake()
    .accounts({
    nftTokenAccount: nft.tokenAddress,
    nftMint: nft.mintAddress,
    nftEdition: nft.masterEditionAddress,
    metadataProgram: METADATA_PROGRAM_ID,
    stakeMint: mint,
    userStakeAta: tokenAddress,
    })
    .rpc()
    });

    运行 anchor test 来确保我们的账户验证设置正确。

    回到实际功能本身,这个功能会比前两个稍微复杂一些。它与兑换过程非常相似,您可以先粘贴兑换的代码,从而节省一些敲键的时间。

    我们将从相同的两个require语句开始。在这两个语句之后,我们需要“解冻”我们的账户。这段代码与冻结账户的invoke_signed非常相似,我们只需要反向执行几个步骤。

    假如您已经粘贴了兑换的代码,在声明时钟之前,可以加入以下内容。您会注意到它几乎完全相同,但我们显然是在调用解冻函数。

    msg!("Thawing token account");
    let authority_bump = *ctx.bumps.get("program_authority").unwrap();
    invoke_signed(
    &thaw_delegated_account(
    ctx.accounts.metadata_program.key(),
    ctx.accounts.program_authority.key(),
    ctx.accounts.nft_token_account.key(),
    ctx.accounts.nft_edition.key(),
    ctx.accounts.nft_mint.key(),
    ),
    &[
    ctx.accounts.program_authority.to_account_info(),
    ctx.accounts.nft_token_account.to_account_info(),
    ctx.accounts.nft_edition.to_account_info(),
    ctx.accounts.nft_mint.to_account_info(),
    ctx.accounts.metadata_program.to_account_info(),
    ],
    &[&[b"authority", &[authority_bump]]],
    )?;

    接下来我们需要撤销委托。这里同样可以复制之前批准委托时的代码,只需将方法从approve改为revoke,并更改对象。确保还要更改变量名。看一下下方的代码,基本上我们只是将approve替换为revoke

    msg!("Revoking delegate");

    let cpi_revoke_program = ctx.accounts.token_program.to_account_info();
    let cpi_revoke_accounts = Revoke {
    source: ctx.accounts.nft_token_account.to_account_info(),
    authority: ctx.accounts.user.to_account_info(),
    };

    let cpi_revoke_ctx = CpiContext::new(cpi_revoke_program, cpi_revoke_accounts);
    token::revoke(cpi_revoke_ctx)?;

    剩下的代码与兑换函数保持一致(即刚刚粘贴的部分),所以所有的兑换都将执行。接下来,我们需要更改质押状态,在底部添加以下代码行。

    ctx.accounts.stake_state.stake_state = StakeState::Unstaked;

    测试部分已经完成,我们可以通过添加以下检查来确认功能正常运行。

    const account = await program.account.userStakeInfo.fetch(stakeStatePda)
    expect(account.stakeState === "Unstaked")

    再次提醒,我们可以增加更多测试以确保一切按照预期进行。这部分我会留给您来处理。

    至此,我们的教程就到此为止了。希望您现在能明白为什么与Anchor合作会更加方便,也能节省许多时间。下一步是进入前端开发阶段!

    - - +
    Skip to main content

    🍖 解除与Anchor的质押

    现在赎回和质押都已完成,让我们开始解除质押。解除质押账户结构包括了总共14个账户,这些是赎回和质押组合在一起的结果,具体如下所示。请确保顺序相同。

    #[derive(Accounts)]
    pub struct Unstake<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
    mut,
    token::authority=user
    )]
    pub nft_token_account: Account<'info, TokenAccount>,
    pub nft_mint: Account<'info, Mint>,
    /// CHECK: Manual validation
    #[account(owner=MetadataTokenId)]
    pub nft_edition: UncheckedAccount<'info>,
    #[account(
    mut,
    seeds = [user.key().as_ref(), nft_token_account.key().as_ref()],
    bump,
    constraint = *user.key == stake_state.user_pubkey,
    constraint = nft_token_account.key() == stake_state.token_account
    )]
    pub stake_state: Account<'info, UserStakeInfo>,
    /// CHECK: manual check
    #[account(mut, seeds=["authority".as_bytes().as_ref()], bump)]
    pub program_authority: UncheckedAccount<'info>,
    #[account(mut)]
    pub stake_mint: Account<'info, Mint>,
    /// CHECK: manual check
    #[account(seeds = ["mint".as_bytes().as_ref()], bump)]
    pub stake_authority: UncheckedAccount<'info>,
    #[account(
    init_if_needed,
    payer=user,
    associated_token::mint=stake_mint,
    associated_token::authority=user
    )]
    pub user_stake_ata: Account<'info, TokenAccount>,
    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
    pub rent: Sysvar<'info, Rent>,
    pub metadata_program: Program<'info, Metadata>,
    }

    这个过程相当简单,我们来编写一些基础测试,以确保其正常工作。我们需要添加那六个不会自动推断的账户。

    it("Unstakes", async () => {
    await program.methods
    .unstake()
    .accounts({
    nftTokenAccount: nft.tokenAddress,
    nftMint: nft.mintAddress,
    nftEdition: nft.masterEditionAddress,
    metadataProgram: METADATA_PROGRAM_ID,
    stakeMint: mint,
    userStakeAta: tokenAddress,
    })
    .rpc()
    });

    运行 anchor test 来确保我们的账户验证设置正确。

    回到实际功能本身,这个功能会比前两个稍微复杂一些。它与兑换过程非常相似,您可以先粘贴兑换的代码,从而节省一些敲键的时间。

    我们将从相同的两个require语句开始。在这两个语句之后,我们需要“解冻”我们的账户。这段代码与冻结账户的invoke_signed非常相似,我们只需要反向执行几个步骤。

    假如您已经粘贴了兑换的代码,在声明时钟之前,可以加入以下内容。您会注意到它几乎完全相同,但我们显然是在调用解冻函数。

    msg!("Thawing token account");
    let authority_bump = *ctx.bumps.get("program_authority").unwrap();
    invoke_signed(
    &thaw_delegated_account(
    ctx.accounts.metadata_program.key(),
    ctx.accounts.program_authority.key(),
    ctx.accounts.nft_token_account.key(),
    ctx.accounts.nft_edition.key(),
    ctx.accounts.nft_mint.key(),
    ),
    &[
    ctx.accounts.program_authority.to_account_info(),
    ctx.accounts.nft_token_account.to_account_info(),
    ctx.accounts.nft_edition.to_account_info(),
    ctx.accounts.nft_mint.to_account_info(),
    ctx.accounts.metadata_program.to_account_info(),
    ],
    &[&[b"authority", &[authority_bump]]],
    )?;

    接下来我们需要撤销委托。这里同样可以复制之前批准委托时的代码,只需将方法从approve改为revoke,并更改对象。确保还要更改变量名。看一下下方的代码,基本上我们只是将approve替换为revoke

    msg!("Revoking delegate");

    let cpi_revoke_program = ctx.accounts.token_program.to_account_info();
    let cpi_revoke_accounts = Revoke {
    source: ctx.accounts.nft_token_account.to_account_info(),
    authority: ctx.accounts.user.to_account_info(),
    };

    let cpi_revoke_ctx = CpiContext::new(cpi_revoke_program, cpi_revoke_accounts);
    token::revoke(cpi_revoke_ctx)?;

    剩下的代码与兑换函数保持一致(即刚刚粘贴的部分),所以所有的兑换都将执行。接下来,我们需要更改质押状态,在底部添加以下代码行。

    ctx.accounts.stake_state.stake_state = StakeState::Unstaked;

    测试部分已经完成,我们可以通过添加以下检查来确认功能正常运行。

    const account = await program.account.userStakeInfo.fetch(stakeStatePda)
    expect(account.stakeState === "Unstaked")

    再次提醒,我们可以增加更多测试以确保一切按照预期进行。这部分我会留给您来处理。

    至此,我们的教程就到此为止了。希望您现在能明白为什么与Anchor合作会更加方便,也能节省许多时间。下一步是进入前端开发阶段!

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module5/anchor-on-the-front-end/anchor-into-typescript/index.html b/Solana-Co-Learn/module5/anchor-on-the-front-end/anchor-into-typescript/index.html index 33a3a3ec9..29da993c5 100644 --- a/Solana-Co-Learn/module5/anchor-on-the-front-end/anchor-into-typescript/index.html +++ b/Solana-Co-Learn/module5/anchor-on-the-front-end/anchor-into-typescript/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🐹 Anchor 到 Typescript

    要使用前端与程序进行交互,我们需要创建一个 Anchor Program 对象。

    Program 对象提供了一个自定义的 API,通过结合程序 IDLProvider 来与特定程序进行交互。

    创建 Program 对象,我们需要以下内容:

    • Connection - 集群连接
    • Wallet - 用于支付和签署交易的默认密钥对
    • Provider - 将 ConnectionWallet 封装到一个 Solana 集群中
    • IDL - 表示程序结构的文件

    接下来,让我们逐项审视,以更好地理解所有事物之间的联系。

    IDL(接口描述语言)

    当构建一个 Anchor 程序时,Anchor 会生成一个名为 IDL 的 JSON 文件。

    IDL 文件包含程序的结构,并由客户端用于了解如何与特定程序进行交互。

    以下是使用 IDL 编写计数器程序的示例:

    {
    "version": "0.1.0",
    "name": "counter",
    "instructions": [
    {
    "name": "initialize",
    "accounts": [
    { "name": "counter", "isMut": true, "isSigner": true },
    { "name": "user", "isMut": true, "isSigner": true },
    { "name": "systemProgram", "isMut": false, "isSigner": false }
    ],
    "args": []
    },
    {
    "name": "increment",
    "accounts": [
    { "name": "counter", "isMut": true, "isSigner": false },
    { "name": "user", "isMut": false, "isSigner": true }
    ],
    "args": []
    }
    ],
    "accounts": [
    {
    "name": "Counter",
    "type": {
    "kind": "struct",
    "fields": [{ "name": "count", "type": "u64" }]
    }
    }
    ]
    }

    Provider 供应商

    在使用 IDL 创建 Program 对象之前,我们首先需要创建一个 AnchorProvider 对象。

    Provider 对象代表了两个主要部分的结合:

    • Connection - 连接到 Solana 集群(例如 localhostdevnetmainnet
    • Wallet - 用于支付和签署交易的指定地址

    接着,Provider 就能够代表 WalletSolana 区块链发送交易,并在发送的交易中加入钱包的签名。

    当使用 Solana 钱包提供商的前端时,所有的外部交易仍然需要通过提示用户进行批准。

    AnchorProvider 构造函数接受三个参数:

    • connection - 连接到 Solana 集群的 Connection
    • wallet - Wallet 对象
    • opts - 可选参数,用于指定确认选项,如果未提供,则使用默认设置
    /**
    * 用于发送由供应商支付和签署的交易的网络和钱包上下文。
    */
    export class AnchorProvider implements Provider {
    readonly publicKey: PublicKey;

    /**
    * @param connection 程序部署的集群连接。
    * @param wallet 用于支付和签署所有交易的钱包。
    * @param opts 默认使用的交易确认选项。
    */
    constructor(
    readonly connection: Connection,
    readonly wallet: Wallet,
    readonly opts: ConfirmOptions
    ) {
    this.publicKey = wallet.publicKey;
    }
    ...
    }
    caution

    请注意,来自 @solana/wallet-adapter-reactuseWallet 钩子提供的 Wallet 对象与 Anchor Provider 期望的 Wallet 对象不兼容。

    因此,让我们来比较一下来自 useAnchorWalletAnchorWallet 和来自 useWalletWalletContextState

    WalletContextState 提供了更多的功能,但是我们需要使用 AnchorWallet 来设置 Provider 对象。

    export interface AnchorWallet {
    publicKey: PublicKey;
    signTransaction(transaction: Transaction): Promise<Transaction>;
    signAllTransactions(transactions: Transaction[]): Promise<Transaction[]>;
    }
    export interface WalletContextState {
    autoConnect: boolean;
    wallets: Wallet[];
    wallet: Wallet | null;
    publicKey: PublicKey | null;
    connecting: boolean;
    connected: boolean;
    disconnecting: boolean;
    select(walletName: WalletName): void;
    connect(): Promise<void>;
    disconnect(): Promise<void>;
    sendTransaction(transaction: Transaction, connection: Connection, options?: SendTransactionOptions): Promise<TransactionSignature>;
    signTransaction: SignerWalletAdapterProps['signTransaction'] | undefined;
    signAllTransactions: SignerWalletAdapterProps['signAllTransactions'] | undefined;
    signMessage: MessageSignerWalletAdapterProps['signMessage'] | undefined;
    }

    此外,您可以使用以下方式:

    • 使用 useAnchorWallet 钩子来获取兼容的 AnchorWallet
    • 使用 useConnection 钩子连接到 Solana 集群
    • 通过 AnchorProvider 对象的构造函数创建 Provider
    • 使用 setProvider 来设置客户端的默认提供程序
    import { useAnchorWallet, useConnection } from "@solana/wallet-adapter-react"
    import { AnchorProvider, setProvider } from "@project-serum/anchor"

    const { connection } = useConnection()
    const wallet = useAnchorWallet()

    const provider = new AnchorProvider(connection, wallet, {})
    setProvider(provider)

    程序

    最后一步是创建一个 Program 对象,代表了以下两个事物的组合:

    • IDL:展示了程序的结构。
    • Provider:负责与集群建立连接并签署 WalletConnection

    首先,你需要导入程序的 IDL,并明确指定programId,这个programId通常会包含在IDL中,当然也可以单独声明。

    在创建程序对象时,如果没有特定地指定提供程序,系统将会使用默认提供程序。

    程序的最终设置应该如下:

    import idl from "./idl.json";
    import { useAnchorWallet, useConnection } from "@solana/wallet-adapter-react";
    import { Program, Idl, AnchorProvider, setProvider } from "@project-serum/anchor";

    const { connection } = useConnection();
    const wallet = useAnchorWallet();

    const provider = new AnchorProvider(connection, wallet, {});
    setProvider(provider);

    const programId = new PublicKey(idl.metadata.address);
    const program = new Program(idl as Idl, programId);

    摘要

    让我们简要总结一下步骤:

    • 导入程序的 IDL
    • 使用 useConnection 钩子与集群建立连接。
    • 使用 useAnchorWallet 钩子获取兼容的 AnchorWallet
    • 通过 AnchorProvider 构造函数创建 Provider 对象。
    • 使用 setProvider 设置默认的 Provider
    • 指定 programId,可以从 IDL 中选择,也可以直接指定。
    • 使用 Program 构造函数创建 Program 对象。

    Anchor MethodsBuilder 使用

    一旦 Program 对象设置完成,我们就可以利用 AnchorMethodsBuilder 来根据程序中的指令构建交易。

    MethodsBuilder 利用 IDL,为调用程序指令提供了一种简化格式,基本格式如下:

    • program:由 programId 指定的被调用程序,来自 Program 对象。
    • methods:包括 IDL 的所有指令,用于构建程序中所有的 API
    • instructionName:从 IDL 中调用的特定指令的名称。
    • args:传递给指令的参数,包括在指令名称后的括号中所需的任何指令数据。
    • accounts:需要作为输入提供的一份指令所需的账户列表。
    • signers:任何需要输入的额外签署人信息。
    • rpc:创建并发送带有指定指令的已签名交易,并返回一个 TransactionSignature

    如果指示中没有除使用 Wallet 指定的 Provider 之外的其他签署人,你可以省略 .signers([]) 行。

    // 发送交易
    const transactionSignature = await program.methods
    .instructionName(instructionDataInputs)
    .accounts({})
    .signers([])
    .rpc();

    你还可以通过将 .rpc() 更改为 .transaction() 来直接构建交易,以及通过以下方式创建 Transaction 对象:

    // 创建交易
    const transaction = await program.methods
    .instructionName(instructionDataInputs)
    .accounts({})
    .transaction();

    // 发送交易
    await sendTransaction(transaction, connection);

    同样,你还可以使用相同的格式来构建一个使用 .instruction 的指令,然后手动将指令添加到新的交易中。

    // 创建第一条指令
    const instructionOne = await program.methods
    .instructionOneName(instructionOneDataInputs)
    .accounts({})
    .instruction();

    // 创建第二条指令
    const instructionTwo = await program.methods
    .instructionTwoName(instructionTwoDataInputs)
    .accounts({})
    .instruction();

    // 将两个指令添加到一个交易中
    const transaction = new Transaction().add(instructionOne, instructionTwo);

    // 发送交易
    await sendTransaction(transaction, connection);

    总的来说,Anchor MethodsBuilder 为与链上程序交互提供了一种更简洁且灵活的方式。你可以构建指令、交易,或者使用相同的格式构建和发送交易,无需手动序列化或反序列化账户或指令数据。

    发送交易

    可以使用由 @solana/wallet-adapter-react 提供的 useWallet() 钩子中的 sendTransaction 方法,通过钱包适配器发送交易。

    sendTransaction 方法会在发送之前提示连接的钱包批准和签署交易。你还可以通过包括 { signers: [] } 来添加额外的签名:

    import { useWallet } from "@solana/wallet-adapter-react";

    const { sendTransaction } = useWallet();

    ...

    sendTransaction(transaction, connection);

    或者:

    sendTransaction(transaction, connection, { signers: [] });

    获取程序账户

    你还可以使用 program 对象来获取程序账户类型。通过 fetch() 来获取单个账户,通过 all() 来获取指定类型的所有账户,或者使用 memcmp 来筛选要获取的账户。

    const account = await program.account.accountType.fetch(publickey);

    const accounts = (await program.account.accountType.all());

    const accounts =
    (await program.account.accountType.all([
    {
    memcmp: {
    offset: 8,
    bytes: publicKey.toBase58(),
    },
    },
    ]));

    示例摘要

    创建一个计数器账户,并在单个事务中递增它。此外,还可以获取计数器账户。

    const counter = Keypair.generate();
    const transaction = new anchor.web3.Transaction();

    const initializeInstruction = await program.methods
    .initialize()
    .accounts({
    counter: counter.publicKey,
    })
    .instruction();

    const incrementInstruction = await program.methods
    .increment()
    .accounts({
    counter: counter.publicKey
    })
    .instruction();

    transaction.add(initializeInstruction, incrementInstruction);

    const transactionSignature = await sendTransaction(
    transaction,
    connection,
    {
    signers: [counter],
    }
    ).then((transactionSignature) => {
    return transactionSignature
    })

    const latestBlockHash = await connection.getLatestBlockhash()
    await connection.confirmTransaction({
    blockhash: latestBlockHash.blockhash,
    lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
    signature: transactionSignature,
    })

    const counterAccount = await program.account.counter.fetch(counter.publicKey)
    - - +
    Skip to main content

    🐹 Anchor 到 Typescript

    要使用前端与程序进行交互,我们需要创建一个 Anchor Program 对象。

    Program 对象提供了一个自定义的 API,通过结合程序 IDLProvider 来与特定程序进行交互。

    创建 Program 对象,我们需要以下内容:

    • Connection - 集群连接
    • Wallet - 用于支付和签署交易的默认密钥对
    • Provider - 将 ConnectionWallet 封装到一个 Solana 集群中
    • IDL - 表示程序结构的文件

    接下来,让我们逐项审视,以更好地理解所有事物之间的联系。

    IDL(接口描述语言)

    当构建一个 Anchor 程序时,Anchor 会生成一个名为 IDL 的 JSON 文件。

    IDL 文件包含程序的结构,并由客户端用于了解如何与特定程序进行交互。

    以下是使用 IDL 编写计数器程序的示例:

    {
    "version": "0.1.0",
    "name": "counter",
    "instructions": [
    {
    "name": "initialize",
    "accounts": [
    { "name": "counter", "isMut": true, "isSigner": true },
    { "name": "user", "isMut": true, "isSigner": true },
    { "name": "systemProgram", "isMut": false, "isSigner": false }
    ],
    "args": []
    },
    {
    "name": "increment",
    "accounts": [
    { "name": "counter", "isMut": true, "isSigner": false },
    { "name": "user", "isMut": false, "isSigner": true }
    ],
    "args": []
    }
    ],
    "accounts": [
    {
    "name": "Counter",
    "type": {
    "kind": "struct",
    "fields": [{ "name": "count", "type": "u64" }]
    }
    }
    ]
    }

    Provider 供应商

    在使用 IDL 创建 Program 对象之前,我们首先需要创建一个 AnchorProvider 对象。

    Provider 对象代表了两个主要部分的结合:

    • Connection - 连接到 Solana 集群(例如 localhostdevnetmainnet
    • Wallet - 用于支付和签署交易的指定地址

    接着,Provider 就能够代表 WalletSolana 区块链发送交易,并在发送的交易中加入钱包的签名。

    当使用 Solana 钱包提供商的前端时,所有的外部交易仍然需要通过提示用户进行批准。

    AnchorProvider 构造函数接受三个参数:

    • connection - 连接到 Solana 集群的 Connection
    • wallet - Wallet 对象
    • opts - 可选参数,用于指定确认选项,如果未提供,则使用默认设置
    /**
    * 用于发送由供应商支付和签署的交易的网络和钱包上下文。
    */
    export class AnchorProvider implements Provider {
    readonly publicKey: PublicKey;

    /**
    * @param connection 程序部署的集群连接。
    * @param wallet 用于支付和签署所有交易的钱包。
    * @param opts 默认使用的交易确认选项。
    */
    constructor(
    readonly connection: Connection,
    readonly wallet: Wallet,
    readonly opts: ConfirmOptions
    ) {
    this.publicKey = wallet.publicKey;
    }
    ...
    }
    caution

    请注意,来自 @solana/wallet-adapter-reactuseWallet 钩子提供的 Wallet 对象与 Anchor Provider 期望的 Wallet 对象不兼容。

    因此,让我们来比较一下来自 useAnchorWalletAnchorWallet 和来自 useWalletWalletContextState

    WalletContextState 提供了更多的功能,但是我们需要使用 AnchorWallet 来设置 Provider 对象。

    export interface AnchorWallet {
    publicKey: PublicKey;
    signTransaction(transaction: Transaction): Promise<Transaction>;
    signAllTransactions(transactions: Transaction[]): Promise<Transaction[]>;
    }
    export interface WalletContextState {
    autoConnect: boolean;
    wallets: Wallet[];
    wallet: Wallet | null;
    publicKey: PublicKey | null;
    connecting: boolean;
    connected: boolean;
    disconnecting: boolean;
    select(walletName: WalletName): void;
    connect(): Promise<void>;
    disconnect(): Promise<void>;
    sendTransaction(transaction: Transaction, connection: Connection, options?: SendTransactionOptions): Promise<TransactionSignature>;
    signTransaction: SignerWalletAdapterProps['signTransaction'] | undefined;
    signAllTransactions: SignerWalletAdapterProps['signAllTransactions'] | undefined;
    signMessage: MessageSignerWalletAdapterProps['signMessage'] | undefined;
    }

    此外,您可以使用以下方式:

    • 使用 useAnchorWallet 钩子来获取兼容的 AnchorWallet
    • 使用 useConnection 钩子连接到 Solana 集群
    • 通过 AnchorProvider 对象的构造函数创建 Provider
    • 使用 setProvider 来设置客户端的默认提供程序
    import { useAnchorWallet, useConnection } from "@solana/wallet-adapter-react"
    import { AnchorProvider, setProvider } from "@project-serum/anchor"

    const { connection } = useConnection()
    const wallet = useAnchorWallet()

    const provider = new AnchorProvider(connection, wallet, {})
    setProvider(provider)

    程序

    最后一步是创建一个 Program 对象,代表了以下两个事物的组合:

    • IDL:展示了程序的结构。
    • Provider:负责与集群建立连接并签署 WalletConnection

    首先,你需要导入程序的 IDL,并明确指定programId,这个programId通常会包含在IDL中,当然也可以单独声明。

    在创建程序对象时,如果没有特定地指定提供程序,系统将会使用默认提供程序。

    程序的最终设置应该如下:

    import idl from "./idl.json";
    import { useAnchorWallet, useConnection } from "@solana/wallet-adapter-react";
    import { Program, Idl, AnchorProvider, setProvider } from "@project-serum/anchor";

    const { connection } = useConnection();
    const wallet = useAnchorWallet();

    const provider = new AnchorProvider(connection, wallet, {});
    setProvider(provider);

    const programId = new PublicKey(idl.metadata.address);
    const program = new Program(idl as Idl, programId);

    摘要

    让我们简要总结一下步骤:

    • 导入程序的 IDL
    • 使用 useConnection 钩子与集群建立连接。
    • 使用 useAnchorWallet 钩子获取兼容的 AnchorWallet
    • 通过 AnchorProvider 构造函数创建 Provider 对象。
    • 使用 setProvider 设置默认的 Provider
    • 指定 programId,可以从 IDL 中选择,也可以直接指定。
    • 使用 Program 构造函数创建 Program 对象。

    Anchor MethodsBuilder 使用

    一旦 Program 对象设置完成,我们就可以利用 AnchorMethodsBuilder 来根据程序中的指令构建交易。

    MethodsBuilder 利用 IDL,为调用程序指令提供了一种简化格式,基本格式如下:

    • program:由 programId 指定的被调用程序,来自 Program 对象。
    • methods:包括 IDL 的所有指令,用于构建程序中所有的 API
    • instructionName:从 IDL 中调用的特定指令的名称。
    • args:传递给指令的参数,包括在指令名称后的括号中所需的任何指令数据。
    • accounts:需要作为输入提供的一份指令所需的账户列表。
    • signers:任何需要输入的额外签署人信息。
    • rpc:创建并发送带有指定指令的已签名交易,并返回一个 TransactionSignature

    如果指示中没有除使用 Wallet 指定的 Provider 之外的其他签署人,你可以省略 .signers([]) 行。

    // 发送交易
    const transactionSignature = await program.methods
    .instructionName(instructionDataInputs)
    .accounts({})
    .signers([])
    .rpc();

    你还可以通过将 .rpc() 更改为 .transaction() 来直接构建交易,以及通过以下方式创建 Transaction 对象:

    // 创建交易
    const transaction = await program.methods
    .instructionName(instructionDataInputs)
    .accounts({})
    .transaction();

    // 发送交易
    await sendTransaction(transaction, connection);

    同样,你还可以使用相同的格式来构建一个使用 .instruction 的指令,然后手动将指令添加到新的交易中。

    // 创建第一条指令
    const instructionOne = await program.methods
    .instructionOneName(instructionOneDataInputs)
    .accounts({})
    .instruction();

    // 创建第二条指令
    const instructionTwo = await program.methods
    .instructionTwoName(instructionTwoDataInputs)
    .accounts({})
    .instruction();

    // 将两个指令添加到一个交易中
    const transaction = new Transaction().add(instructionOne, instructionTwo);

    // 发送交易
    await sendTransaction(transaction, connection);

    总的来说,Anchor MethodsBuilder 为与链上程序交互提供了一种更简洁且灵活的方式。你可以构建指令、交易,或者使用相同的格式构建和发送交易,无需手动序列化或反序列化账户或指令数据。

    发送交易

    可以使用由 @solana/wallet-adapter-react 提供的 useWallet() 钩子中的 sendTransaction 方法,通过钱包适配器发送交易。

    sendTransaction 方法会在发送之前提示连接的钱包批准和签署交易。你还可以通过包括 { signers: [] } 来添加额外的签名:

    import { useWallet } from "@solana/wallet-adapter-react";

    const { sendTransaction } = useWallet();

    ...

    sendTransaction(transaction, connection);

    或者:

    sendTransaction(transaction, connection, { signers: [] });

    获取程序账户

    你还可以使用 program 对象来获取程序账户类型。通过 fetch() 来获取单个账户,通过 all() 来获取指定类型的所有账户,或者使用 memcmp 来筛选要获取的账户。

    const account = await program.account.accountType.fetch(publickey);

    const accounts = (await program.account.accountType.all());

    const accounts =
    (await program.account.accountType.all([
    {
    memcmp: {
    offset: 8,
    bytes: publicKey.toBase58(),
    },
    },
    ]));

    示例摘要

    创建一个计数器账户,并在单个事务中递增它。此外,还可以获取计数器账户。

    const counter = Keypair.generate();
    const transaction = new anchor.web3.Transaction();

    const initializeInstruction = await program.methods
    .initialize()
    .accounts({
    counter: counter.publicKey,
    })
    .instruction();

    const incrementInstruction = await program.methods
    .increment()
    .accounts({
    counter: counter.publicKey
    })
    .instruction();

    transaction.add(initializeInstruction, incrementInstruction);

    const transactionSignature = await sendTransaction(
    transaction,
    connection,
    {
    signers: [counter],
    }
    ).then((transactionSignature) => {
    return transactionSignature
    })

    const latestBlockHash = await connection.getLatestBlockhash()
    await connection.confirmTransaction({
    blockhash: latestBlockHash.blockhash,
    lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
    signature: transactionSignature,
    })

    const counterAccount = await program.account.counter.fetch(counter.publicKey)
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module5/anchor-on-the-front-end/build-a-better-movie-review-program/index.html b/Solana-Co-Learn/module5/anchor-on-the-front-end/build-a-better-movie-review-program/index.html index 65d3d20c4..924f3c515 100644 --- a/Solana-Co-Learn/module5/anchor-on-the-front-end/build-a-better-movie-review-program/index.html +++ b/Solana-Co-Learn/module5/anchor-on-the-front-end/build-a-better-movie-review-program/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🎥 打造一款优秀的电影评论程序

    让我们开始行动,释放所有的魔力吧!

    我们将会将电影评论前端适配为使用Anchor IDL

    获取起始代码

    git clone https://github.com/buildspace/anchor-solana-movie-review-frontend
    cd anchor-solana-movie-review-frontend
    git checkout starter-add-tokens
    npm i
    • 请注意,起始代码在Anchor设置完成之前是无法运行的。
    • ./context/Anchor/MockWallet.ts 中,我们有一个临时的 AnchorWallet,在钱包连接之前可以使用。
    import { Keypair } from "@solana/web3.js"

    const MockWallet = {
    publicKey: Keypair.generate().publicKey,
    signTransaction: () => Promise.reject(),
    signAllTransactions: () => Promise.reject(),
    }

    export default MockWallet

    Anchor 的设置

    • 位于 ./context/Anchor/index.tsx 文件中。
    • 创建 WorkspaceProvider 的上下文,并提供一个名为 useWorkspace 的钩子。
      • 我们将使用 useWorkspace 钩子来访问我们组件中的 program 对象。
      • 这样做的好处是只需要进行一次设置。
    import { createContext, useContext } from "react"
    import {
    Program,
    AnchorProvider,
    Idl,
    setProvider,
    } from "@project-serum/anchor"
    import { MovieReview, IDL } from "./movie_review"
    import { Connection, PublicKey } from "@solana/web3.js"
    import { useAnchorWallet, useConnection } from "@solana/wallet-adapter-react"
    import MockWallet from "./MockWallet"
    const WorkspaceContext = createContext({})
    const programId = new PublicKey("BouTUP7a3MZLtXqMAm1NrkJSKwAjmid8abqiNjUyBJSr")

    interface WorkSpace {
    connection?: Connection
    provider?: AnchorProvider
    program?: Program<MovieReview>
    }

    const WorkspaceProvider = ({ children }: any) => {
    const wallet = useAnchorWallet() || MockWallet
    const { connection } = useConnection()

    const provider = new AnchorProvider(connection, wallet, {})

    setProvider(provider)
    const program = new Program(IDL as Idl, programId)
    const workspace = {
    connection,
    provider,
    program,
    }

    return (
    <WorkspaceContext.Provider value={workspace}>
    {children}
    </WorkspaceContext.Provider>
    )
    }

    const useWorkspace = (): WorkSpace => {
    return useContext(WorkspaceContext)
    }

    export { WorkspaceProvider, useWorkspace }
    • ..pages/_app.tsx 文件中。
    • 将整个应用程序包裹在 WorkspaceProvider 中。
    • 现在我们可以在不同的组件中使用 useWorkspace 钩子来访问 program 对象。
    import "../styles/globals.css"
    import type { AppProps } from "next/app"
    import { ChakraProvider } from "@chakra-ui/react"
    import WalletContextProvider from "../context/WalletContextProvider"
    import { WorkspaceProvider } from "../context/Anchor"

    function MyApp({ Component, pageProps }: AppProps) {
    return (
    <WalletContextProvider>
    <ChakraProvider>
    <WorkspaceProvider>
    <Component {...pageProps} />
    </WorkspaceProvider>
    </ChakraProvider>
    </WalletContextProvider>
    )
    }

    export default MyApp

    Form.tsx

    • handleSubmit 函数中:
      • 可以实现根据情况选择调用 addMovieReviewupdateMovieReview 指令。

    在使用Anchor时,利用IDL (Interface Description Language) 的特性可以推断PDA(程序派生地址)账户和其他账户(如系统程序或代币程序),因此无需显式传递这些信息。

    import { FC } from "react"
    import { useState } from "react"
    import {
    Box,
    Button,
    FormControl,
    FormLabel,
    Input,
    NumberDecrementStepper,
    NumberIncrementStepper,
    NumberInput,
    NumberInputField,
    NumberInputStepper,
    Textarea,
    Switch,
    } from "@chakra-ui/react"
    import * as anchor from "@project-serum/anchor"
    import { getAssociatedTokenAddress } from "@solana/spl-token"
    import { useConnection, useWallet } from "@solana/wallet-adapter-react"
    import { useWorkspace } from "../context/Anchor"

    export const Form: FC = () => {
    const [title, setTitle] = useState("")
    const [rating, setRating] = useState(0)
    const [description, setDescription] = useState("")
    const [toggle, setToggle] = useState(true)

    const { connection } = useConnection()
    const { publicKey, sendTransaction } = useWallet()

    const workspace = useWorkspace()
    const program = workspace.program

    const handleSubmit = async (event: any) => {
    event.preventDefault()

    if (!publicKey || !program) {
    alert("Please connect your wallet!")
    return
    }

    const [mintPDA] = await anchor.web3.PublicKey.findProgramAddress(
    [Buffer.from("mint")],
    program.programId
    )

    const tokenAddress = await getAssociatedTokenAddress(mintPDA, publicKey)

    const transaction = new anchor.web3.Transaction()

    if (toggle) {
    const instruction = await program.methods
    .addMovieReview(title, description, rating)
    .accounts({
    tokenAccount: tokenAddress,
    })
    .instruction()

    transaction.add(instruction)
    } else {
    const instruction = await program.methods
    .updateMovieReview(title, description, rating)
    .instruction()

    transaction.add(instruction)
    }

    try {
    let txid = await sendTransaction(transaction, connection)
    alert(
    `Transaction submitted: https://explorer.solana.com/tx/${txid}?cluster=devnet`
    )
    console.log(
    `Transaction submitted: https://explorer.solana.com/tx/${txid}?cluster=devnet`
    )
    } catch (e) {
    console.log(JSON.stringify(e))
    alert(JSON.stringify(e))
    }
    }

    return (
    <Box
    p={4}
    display={{ md: "flex" }}
    maxWidth="32rem"
    borderWidth={1}
    margin={2}
    justifyContent="center"
    >
    <form onSubmit={handleSubmit}>
    <FormControl isRequired>
    <FormLabel color="gray.200">Movie Title</FormLabel>
    <Input
    id="title"
    color="gray.400"
    onChange={(event) => setTitle(event.currentTarget.value)}
    />
    </FormControl>
    <FormControl isRequired>
    <FormLabel color="gray.200">Add your review</FormLabel>
    <Textarea
    id="review"
    color="gray.400"
    onChange={(event) => setDescription(event.currentTarget.value)}
    />
    </FormControl>
    <FormControl isRequired>
    <FormLabel color="gray.200">Rating</FormLabel>
    <NumberInput
    max={5}
    min={1}
    onChange={(valueString) => setRating(parseInt(valueString))}
    >
    <NumberInputField id="amount" color="gray.400" />
    <NumberInputStepper color="gray.400">
    <NumberIncrementStepper />
    <NumberDecrementStepper />
    </NumberInputStepper>
    </NumberInput>
    </FormControl>
    <FormControl display="center" alignItems="center">
    <FormLabel color="gray.100" mt={2}>
    Update
    </FormLabel>
    <Switch
    id="update"
    onChange={(event) => setToggle((prevCheck) => !prevCheck)}
    />
    </FormControl>
    <Button width="full" mt={4} type="submit">
    Submit Review
    </Button>
    </form>
    </Box>
    )
    }

    MovieList.tsx

    • fetchMyReviews
      • 为连接的钱包的评论实施 movieAccountState 账户过滤器
    • fetchAccounts
      • 执行获取所有账户
    • 实现评论的分页
    import { Card } from "./Card"
    import { FC, useEffect, useState } from "react"
    import {
    Button,
    Center,
    HStack,
    Input,
    Spacer,
    Heading,
    } from "@chakra-ui/react"
    import { useWorkspace } from "../context/Anchor"
    import { useWallet } from "@solana/wallet-adapter-react"
    import { useDisclosure } from "@chakra-ui/react"
    import { ReviewDetail } from "./ReviewDetail"

    export const MovieList: FC = () => {
    const { program } = useWorkspace()
    const [movies, setMovies] = useState<any | null>(null)
    const [page, setPage] = useState(1)
    const [search, setSearch] = useState("")
    const [result, setResult] = useState<any | null>(null)
    const [selectedMovie, setSelectedMovie] = useState<any | null>(null)
    const { isOpen, onOpen, onClose } = useDisclosure()
    const wallet = useWallet()

    useEffect(() => {
    const fetchAccounts = async () => {
    if (program) {
    const accounts = (await program.account.movieAccountState.all()) ?? []

    const sort = [...accounts].sort((a, b) =>
    a.account.title > b.account.title ? 1 : -1
    )
    setMovies(sort)
    }
    }
    fetchAccounts()
    }, [])

    useEffect(() => {
    if (movies && search != "") {
    const filtered = movies.filter((movie: any) => {
    return movie.account.title
    .toLowerCase()
    .startsWith(search.toLowerCase())
    })
    setResult(filtered)
    }
    }, [search])

    useEffect(() => {
    if (movies && search == "") {
    const filtered = movies.slice((page - 1) * 3, page * 3)
    setResult(filtered)
    }
    }, [page, movies, search])

    const fetchMyReviews = async () => {
    if (wallet.connected && program) {
    const accounts =
    (await program.account.movieAccountState.all([
    {
    memcmp: {
    offset: 8,
    bytes: wallet.publicKey!.toBase58(),
    },
    },
    ])) ?? []

    const sort = [...accounts].sort((a, b) =>
    a.account.title > b.account.title ? 1 : -1
    )
    setResult(sort)
    } else {
    alert("Please Connect Wallet")
    }
    }

    const handleReviewSelected = (data: any) => {
    setSelectedMovie(data)
    onOpen()
    }

    return (
    <div>
    <Center>
    <Input
    id="search"
    color="gray.400"
    onChange={(event) => setSearch(event.currentTarget.value)}
    placeholder="Search"
    w="97%"
    mt={2}
    mb={2}
    margin={2}
    />
    <Button onClick={fetchMyReviews}>My Reviews</Button>
    </Center>
    <Heading as="h1" size="l" color="white" ml={4} mt={8}>
    Select Review To Comment
    </Heading>
    {selectedMovie && (
    <ReviewDetail isOpen={isOpen} onClose={onClose} movie={selectedMovie} />
    )}
    {result && (
    <div>
    {Object.keys(result).map((key) => {
    const data = result[key as unknown as number]
    return (
    <Card
    key={key}
    movie={data}
    onClick={() => {
    handleReviewSelected(data)
    }}
    />
    )
    })}
    </div>
    )}
    <Center>
    {movies && (
    <HStack w="full" mt={2} mb={8} ml={4} mr={4}>
    {page > 1 && (
    <Button onClick={() => setPage(page - 1)}>Previous</Button>
    )}
    <Spacer />
    {movies.length > page * 3 && (
    <Button onClick={() => setPage(page + 1)}>Next</Button>
    )}
    </HStack>
    )}
    </Center>
    </div>
    )
    }

    ReviewDetail.tsx

    • handleSubmit
      • 实施 addComment

    请注意,Anchor可以使用IDL来推断PDA账户和其他账户(系统程序/代币程序),因此不需要显式传递

    import {
    Button,
    Input,
    Modal,
    ModalOverlay,
    ModalContent,
    ModalHeader,
    ModalCloseButton,
    ModalBody,
    Stack,
    FormControl,
    } from "@chakra-ui/react"
    import { FC, useState } from "react"
    import * as anchor from "@project-serum/anchor"
    import { getAssociatedTokenAddress } from "@solana/spl-token"
    import { CommentList } from "./CommentList"
    import { useConnection, useWallet } from "@solana/wallet-adapter-react"
    import { useWorkspace } from "../context/Anchor"
    import BN from "bn.js"

    interface ReviewDetailProps {
    isOpen: boolean
    onClose: any
    movie: any
    }

    export const ReviewDetail: FC<ReviewDetailProps> = ({
    isOpen,
    onClose,
    movie,
    }: ReviewDetailProps) => {
    const [comment, setComment] = useState("")
    const { connection } = useConnection()
    const { publicKey, sendTransaction } = useWallet()
    const { program } = useWorkspace()

    const handleSubmit = async (event: any) => {
    event.preventDefault()

    if (!publicKey || !program) {
    alert("Please connect your wallet!")
    return
    }

    const movieReview = new anchor.web3.PublicKey(movie.publicKey)

    const [movieReviewCounterPda] =
    await anchor.web3.PublicKey.findProgramAddress(
    [Buffer.from("counter"), movieReview.toBuffer()],
    program.programId
    )

    const [mintPDA] = await anchor.web3.PublicKey.findProgramAddress(
    [Buffer.from("mint")],
    program.programId
    )

    const tokenAddress = await getAssociatedTokenAddress(mintPDA, publicKey)

    const transaction = new anchor.web3.Transaction()

    const instruction = await program.methods
    .addComment(comment)
    .accounts({
    movieReview: movieReview,
    movieCommentCounter: movieReviewCounterPda,
    tokenAccount: tokenAddress,
    })
    .instruction()

    transaction.add(instruction)

    try {
    let txid = await sendTransaction(transaction, connection)
    alert(
    `Transaction submitted: https://explorer.solana.com/tx/${txid}?cluster=devnet`
    )
    console.log(
    `Transaction submitted: https://explorer.solana.com/tx/${txid}?cluster=devnet`
    )
    } catch (e) {
    console.log(JSON.stringify(e))
    alert(JSON.stringify(e))
    }
    }

    return (
    <div>
    <Modal isOpen={isOpen} onClose={onClose}>
    <ModalOverlay />
    <ModalContent>
    <ModalHeader
    textTransform="uppercase"
    textAlign={{ base: "center", md: "center" }}
    >
    {movie.account.title}
    </ModalHeader>
    <ModalCloseButton />
    <ModalBody>
    <Stack textAlign={{ base: "center", md: "center" }}>
    <p>{movie.account.description}</p>
    <form onSubmit={handleSubmit}>
    <FormControl isRequired>
    <Input
    id="title"
    color="black"
    onChange={(event) => setComment(event.currentTarget.value)}
    placeholder="Submit a comment..."
    />
    </FormControl>
    <Button width="full" mt={4} type="submit">
    Send
    </Button>
    </form>
    <CommentList movie={movie} />
    </Stack>
    </ModalBody>
    </ModalContent>
    </Modal>
    </div>
    )
    }

    CommentList.tsx

    • fetch
      • 获取账户并筛选特定的电影评论账户
    • 实现评论的分页
    import {
    Button,
    Center,
    HStack,
    Spacer,
    Stack,
    Box,
    Heading,
    } from "@chakra-ui/react"
    import { FC, useState, useEffect } from "react"
    import { useWorkspace } from "../context/Anchor"

    interface CommentListProps {
    movie: any
    }

    export const CommentList: FC<CommentListProps> = ({
    movie,
    }: CommentListProps) => {
    const [page, setPage] = useState(1)
    const [comments, setComments] = useState<any[]>([])
    const [result, setResult] = useState<any[]>([])
    const { program } = useWorkspace()

    useEffect(() => {
    const fetch = async () => {
    if (program) {
    const comments = await program.account.movieComment.all([
    {
    memcmp: {
    offset: 8,
    bytes: movie.publicKey.toBase58(),
    },
    },
    ])

    const sort = [...comments].sort((a, b) =>
    a.account.count > b.account.count ? 1 : -1
    )
    setComments(comments)
    const filtered = sort.slice((page - 1) * 3, page * 3)
    setResult(filtered)
    }
    }
    fetch()
    }, [page])

    return (
    <div>
    <Heading as="h1" size="l" ml={4} mt={2}>
    Existing Comments
    </Heading>
    {result.map((comment, index) => (
    <Box
    p={4}
    textAlign={{ base: "left", md: "left" }}
    display={{ md: "flex" }}
    maxWidth="32rem"
    borderWidth={1}
    margin={2}
    key={index}
    >
    <div>{comment.account.comment}</div>
    </Box>
    ))}
    <Stack>
    <Center>
    <HStack w="full" mt={2} mb={8} ml={4} mr={4}>
    {page > 1 && (
    <Button onClick={() => setPage(page - 1)}>Previous</Button>
    )}
    <Spacer />
    {comments.length > page * 3 && (
    <Button onClick={() => setPage(page + 1)}>Next</Button>
    )}
    </HStack>
    </Center>
    </Stack>
    </div>
    )
    }

    使用以下命令运行:

    npm run dev

    恭喜!你做到了。我们的下一课是你建立和发货的大结局。

    - - +
    Skip to main content

    🎥 打造一款优秀的电影评论程序

    让我们开始行动,释放所有的魔力吧!

    我们将会将电影评论前端适配为使用Anchor IDL

    获取起始代码

    git clone https://github.com/buildspace/anchor-solana-movie-review-frontend
    cd anchor-solana-movie-review-frontend
    git checkout starter-add-tokens
    npm i
    • 请注意,起始代码在Anchor设置完成之前是无法运行的。
    • ./context/Anchor/MockWallet.ts 中,我们有一个临时的 AnchorWallet,在钱包连接之前可以使用。
    import { Keypair } from "@solana/web3.js"

    const MockWallet = {
    publicKey: Keypair.generate().publicKey,
    signTransaction: () => Promise.reject(),
    signAllTransactions: () => Promise.reject(),
    }

    export default MockWallet

    Anchor 的设置

    • 位于 ./context/Anchor/index.tsx 文件中。
    • 创建 WorkspaceProvider 的上下文,并提供一个名为 useWorkspace 的钩子。
      • 我们将使用 useWorkspace 钩子来访问我们组件中的 program 对象。
      • 这样做的好处是只需要进行一次设置。
    import { createContext, useContext } from "react"
    import {
    Program,
    AnchorProvider,
    Idl,
    setProvider,
    } from "@project-serum/anchor"
    import { MovieReview, IDL } from "./movie_review"
    import { Connection, PublicKey } from "@solana/web3.js"
    import { useAnchorWallet, useConnection } from "@solana/wallet-adapter-react"
    import MockWallet from "./MockWallet"
    const WorkspaceContext = createContext({})
    const programId = new PublicKey("BouTUP7a3MZLtXqMAm1NrkJSKwAjmid8abqiNjUyBJSr")

    interface WorkSpace {
    connection?: Connection
    provider?: AnchorProvider
    program?: Program<MovieReview>
    }

    const WorkspaceProvider = ({ children }: any) => {
    const wallet = useAnchorWallet() || MockWallet
    const { connection } = useConnection()

    const provider = new AnchorProvider(connection, wallet, {})

    setProvider(provider)
    const program = new Program(IDL as Idl, programId)
    const workspace = {
    connection,
    provider,
    program,
    }

    return (
    <WorkspaceContext.Provider value={workspace}>
    {children}
    </WorkspaceContext.Provider>
    )
    }

    const useWorkspace = (): WorkSpace => {
    return useContext(WorkspaceContext)
    }

    export { WorkspaceProvider, useWorkspace }
    • ..pages/_app.tsx 文件中。
    • 将整个应用程序包裹在 WorkspaceProvider 中。
    • 现在我们可以在不同的组件中使用 useWorkspace 钩子来访问 program 对象。
    import "../styles/globals.css"
    import type { AppProps } from "next/app"
    import { ChakraProvider } from "@chakra-ui/react"
    import WalletContextProvider from "../context/WalletContextProvider"
    import { WorkspaceProvider } from "../context/Anchor"

    function MyApp({ Component, pageProps }: AppProps) {
    return (
    <WalletContextProvider>
    <ChakraProvider>
    <WorkspaceProvider>
    <Component {...pageProps} />
    </WorkspaceProvider>
    </ChakraProvider>
    </WalletContextProvider>
    )
    }

    export default MyApp

    Form.tsx

    • handleSubmit 函数中:
      • 可以实现根据情况选择调用 addMovieReviewupdateMovieReview 指令。

    在使用Anchor时,利用IDL (Interface Description Language) 的特性可以推断PDA(程序派生地址)账户和其他账户(如系统程序或代币程序),因此无需显式传递这些信息。

    import { FC } from "react"
    import { useState } from "react"
    import {
    Box,
    Button,
    FormControl,
    FormLabel,
    Input,
    NumberDecrementStepper,
    NumberIncrementStepper,
    NumberInput,
    NumberInputField,
    NumberInputStepper,
    Textarea,
    Switch,
    } from "@chakra-ui/react"
    import * as anchor from "@project-serum/anchor"
    import { getAssociatedTokenAddress } from "@solana/spl-token"
    import { useConnection, useWallet } from "@solana/wallet-adapter-react"
    import { useWorkspace } from "../context/Anchor"

    export const Form: FC = () => {
    const [title, setTitle] = useState("")
    const [rating, setRating] = useState(0)
    const [description, setDescription] = useState("")
    const [toggle, setToggle] = useState(true)

    const { connection } = useConnection()
    const { publicKey, sendTransaction } = useWallet()

    const workspace = useWorkspace()
    const program = workspace.program

    const handleSubmit = async (event: any) => {
    event.preventDefault()

    if (!publicKey || !program) {
    alert("Please connect your wallet!")
    return
    }

    const [mintPDA] = await anchor.web3.PublicKey.findProgramAddress(
    [Buffer.from("mint")],
    program.programId
    )

    const tokenAddress = await getAssociatedTokenAddress(mintPDA, publicKey)

    const transaction = new anchor.web3.Transaction()

    if (toggle) {
    const instruction = await program.methods
    .addMovieReview(title, description, rating)
    .accounts({
    tokenAccount: tokenAddress,
    })
    .instruction()

    transaction.add(instruction)
    } else {
    const instruction = await program.methods
    .updateMovieReview(title, description, rating)
    .instruction()

    transaction.add(instruction)
    }

    try {
    let txid = await sendTransaction(transaction, connection)
    alert(
    `Transaction submitted: https://explorer.solana.com/tx/${txid}?cluster=devnet`
    )
    console.log(
    `Transaction submitted: https://explorer.solana.com/tx/${txid}?cluster=devnet`
    )
    } catch (e) {
    console.log(JSON.stringify(e))
    alert(JSON.stringify(e))
    }
    }

    return (
    <Box
    p={4}
    display={{ md: "flex" }}
    maxWidth="32rem"
    borderWidth={1}
    margin={2}
    justifyContent="center"
    >
    <form onSubmit={handleSubmit}>
    <FormControl isRequired>
    <FormLabel color="gray.200">Movie Title</FormLabel>
    <Input
    id="title"
    color="gray.400"
    onChange={(event) => setTitle(event.currentTarget.value)}
    />
    </FormControl>
    <FormControl isRequired>
    <FormLabel color="gray.200">Add your review</FormLabel>
    <Textarea
    id="review"
    color="gray.400"
    onChange={(event) => setDescription(event.currentTarget.value)}
    />
    </FormControl>
    <FormControl isRequired>
    <FormLabel color="gray.200">Rating</FormLabel>
    <NumberInput
    max={5}
    min={1}
    onChange={(valueString) => setRating(parseInt(valueString))}
    >
    <NumberInputField id="amount" color="gray.400" />
    <NumberInputStepper color="gray.400">
    <NumberIncrementStepper />
    <NumberDecrementStepper />
    </NumberInputStepper>
    </NumberInput>
    </FormControl>
    <FormControl display="center" alignItems="center">
    <FormLabel color="gray.100" mt={2}>
    Update
    </FormLabel>
    <Switch
    id="update"
    onChange={(event) => setToggle((prevCheck) => !prevCheck)}
    />
    </FormControl>
    <Button width="full" mt={4} type="submit">
    Submit Review
    </Button>
    </form>
    </Box>
    )
    }

    MovieList.tsx

    • fetchMyReviews
      • 为连接的钱包的评论实施 movieAccountState 账户过滤器
    • fetchAccounts
      • 执行获取所有账户
    • 实现评论的分页
    import { Card } from "./Card"
    import { FC, useEffect, useState } from "react"
    import {
    Button,
    Center,
    HStack,
    Input,
    Spacer,
    Heading,
    } from "@chakra-ui/react"
    import { useWorkspace } from "../context/Anchor"
    import { useWallet } from "@solana/wallet-adapter-react"
    import { useDisclosure } from "@chakra-ui/react"
    import { ReviewDetail } from "./ReviewDetail"

    export const MovieList: FC = () => {
    const { program } = useWorkspace()
    const [movies, setMovies] = useState<any | null>(null)
    const [page, setPage] = useState(1)
    const [search, setSearch] = useState("")
    const [result, setResult] = useState<any | null>(null)
    const [selectedMovie, setSelectedMovie] = useState<any | null>(null)
    const { isOpen, onOpen, onClose } = useDisclosure()
    const wallet = useWallet()

    useEffect(() => {
    const fetchAccounts = async () => {
    if (program) {
    const accounts = (await program.account.movieAccountState.all()) ?? []

    const sort = [...accounts].sort((a, b) =>
    a.account.title > b.account.title ? 1 : -1
    )
    setMovies(sort)
    }
    }
    fetchAccounts()
    }, [])

    useEffect(() => {
    if (movies && search != "") {
    const filtered = movies.filter((movie: any) => {
    return movie.account.title
    .toLowerCase()
    .startsWith(search.toLowerCase())
    })
    setResult(filtered)
    }
    }, [search])

    useEffect(() => {
    if (movies && search == "") {
    const filtered = movies.slice((page - 1) * 3, page * 3)
    setResult(filtered)
    }
    }, [page, movies, search])

    const fetchMyReviews = async () => {
    if (wallet.connected && program) {
    const accounts =
    (await program.account.movieAccountState.all([
    {
    memcmp: {
    offset: 8,
    bytes: wallet.publicKey!.toBase58(),
    },
    },
    ])) ?? []

    const sort = [...accounts].sort((a, b) =>
    a.account.title > b.account.title ? 1 : -1
    )
    setResult(sort)
    } else {
    alert("Please Connect Wallet")
    }
    }

    const handleReviewSelected = (data: any) => {
    setSelectedMovie(data)
    onOpen()
    }

    return (
    <div>
    <Center>
    <Input
    id="search"
    color="gray.400"
    onChange={(event) => setSearch(event.currentTarget.value)}
    placeholder="Search"
    w="97%"
    mt={2}
    mb={2}
    margin={2}
    />
    <Button onClick={fetchMyReviews}>My Reviews</Button>
    </Center>
    <Heading as="h1" size="l" color="white" ml={4} mt={8}>
    Select Review To Comment
    </Heading>
    {selectedMovie && (
    <ReviewDetail isOpen={isOpen} onClose={onClose} movie={selectedMovie} />
    )}
    {result && (
    <div>
    {Object.keys(result).map((key) => {
    const data = result[key as unknown as number]
    return (
    <Card
    key={key}
    movie={data}
    onClick={() => {
    handleReviewSelected(data)
    }}
    />
    )
    })}
    </div>
    )}
    <Center>
    {movies && (
    <HStack w="full" mt={2} mb={8} ml={4} mr={4}>
    {page > 1 && (
    <Button onClick={() => setPage(page - 1)}>Previous</Button>
    )}
    <Spacer />
    {movies.length > page * 3 && (
    <Button onClick={() => setPage(page + 1)}>Next</Button>
    )}
    </HStack>
    )}
    </Center>
    </div>
    )
    }

    ReviewDetail.tsx

    • handleSubmit
      • 实施 addComment

    请注意,Anchor可以使用IDL来推断PDA账户和其他账户(系统程序/代币程序),因此不需要显式传递

    import {
    Button,
    Input,
    Modal,
    ModalOverlay,
    ModalContent,
    ModalHeader,
    ModalCloseButton,
    ModalBody,
    Stack,
    FormControl,
    } from "@chakra-ui/react"
    import { FC, useState } from "react"
    import * as anchor from "@project-serum/anchor"
    import { getAssociatedTokenAddress } from "@solana/spl-token"
    import { CommentList } from "./CommentList"
    import { useConnection, useWallet } from "@solana/wallet-adapter-react"
    import { useWorkspace } from "../context/Anchor"
    import BN from "bn.js"

    interface ReviewDetailProps {
    isOpen: boolean
    onClose: any
    movie: any
    }

    export const ReviewDetail: FC<ReviewDetailProps> = ({
    isOpen,
    onClose,
    movie,
    }: ReviewDetailProps) => {
    const [comment, setComment] = useState("")
    const { connection } = useConnection()
    const { publicKey, sendTransaction } = useWallet()
    const { program } = useWorkspace()

    const handleSubmit = async (event: any) => {
    event.preventDefault()

    if (!publicKey || !program) {
    alert("Please connect your wallet!")
    return
    }

    const movieReview = new anchor.web3.PublicKey(movie.publicKey)

    const [movieReviewCounterPda] =
    await anchor.web3.PublicKey.findProgramAddress(
    [Buffer.from("counter"), movieReview.toBuffer()],
    program.programId
    )

    const [mintPDA] = await anchor.web3.PublicKey.findProgramAddress(
    [Buffer.from("mint")],
    program.programId
    )

    const tokenAddress = await getAssociatedTokenAddress(mintPDA, publicKey)

    const transaction = new anchor.web3.Transaction()

    const instruction = await program.methods
    .addComment(comment)
    .accounts({
    movieReview: movieReview,
    movieCommentCounter: movieReviewCounterPda,
    tokenAccount: tokenAddress,
    })
    .instruction()

    transaction.add(instruction)

    try {
    let txid = await sendTransaction(transaction, connection)
    alert(
    `Transaction submitted: https://explorer.solana.com/tx/${txid}?cluster=devnet`
    )
    console.log(
    `Transaction submitted: https://explorer.solana.com/tx/${txid}?cluster=devnet`
    )
    } catch (e) {
    console.log(JSON.stringify(e))
    alert(JSON.stringify(e))
    }
    }

    return (
    <div>
    <Modal isOpen={isOpen} onClose={onClose}>
    <ModalOverlay />
    <ModalContent>
    <ModalHeader
    textTransform="uppercase"
    textAlign={{ base: "center", md: "center" }}
    >
    {movie.account.title}
    </ModalHeader>
    <ModalCloseButton />
    <ModalBody>
    <Stack textAlign={{ base: "center", md: "center" }}>
    <p>{movie.account.description}</p>
    <form onSubmit={handleSubmit}>
    <FormControl isRequired>
    <Input
    id="title"
    color="black"
    onChange={(event) => setComment(event.currentTarget.value)}
    placeholder="Submit a comment..."
    />
    </FormControl>
    <Button width="full" mt={4} type="submit">
    Send
    </Button>
    </form>
    <CommentList movie={movie} />
    </Stack>
    </ModalBody>
    </ModalContent>
    </Modal>
    </div>
    )
    }

    CommentList.tsx

    • fetch
      • 获取账户并筛选特定的电影评论账户
    • 实现评论的分页
    import {
    Button,
    Center,
    HStack,
    Spacer,
    Stack,
    Box,
    Heading,
    } from "@chakra-ui/react"
    import { FC, useState, useEffect } from "react"
    import { useWorkspace } from "../context/Anchor"

    interface CommentListProps {
    movie: any
    }

    export const CommentList: FC<CommentListProps> = ({
    movie,
    }: CommentListProps) => {
    const [page, setPage] = useState(1)
    const [comments, setComments] = useState<any[]>([])
    const [result, setResult] = useState<any[]>([])
    const { program } = useWorkspace()

    useEffect(() => {
    const fetch = async () => {
    if (program) {
    const comments = await program.account.movieComment.all([
    {
    memcmp: {
    offset: 8,
    bytes: movie.publicKey.toBase58(),
    },
    },
    ])

    const sort = [...comments].sort((a, b) =>
    a.account.count > b.account.count ? 1 : -1
    )
    setComments(comments)
    const filtered = sort.slice((page - 1) * 3, page * 3)
    setResult(filtered)
    }
    }
    fetch()
    }, [page])

    return (
    <div>
    <Heading as="h1" size="l" ml={4} mt={2}>
    Existing Comments
    </Heading>
    {result.map((comment, index) => (
    <Box
    p={4}
    textAlign={{ base: "left", md: "left" }}
    display={{ md: "flex" }}
    maxWidth="32rem"
    borderWidth={1}
    margin={2}
    key={index}
    >
    <div>{comment.account.comment}</div>
    </Box>
    ))}
    <Stack>
    <Center>
    <HStack w="full" mt={2} mb={8} ml={4} mr={4}>
    {page > 1 && (
    <Button onClick={() => setPage(page - 1)}>Previous</Button>
    )}
    <Spacer />
    {comments.length > page * 3 && (
    <Button onClick={() => setPage(page + 1)}>Next</Button>
    )}
    </HStack>
    </Center>
    </Stack>
    </div>
    )
    }

    使用以下命令运行:

    npm run dev

    恭喜!你做到了。我们的下一课是你建立和发货的大结局。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module5/anchor-on-the-front-end/index.html b/Solana-Co-Learn/module5/anchor-on-the-front-end/index.html index e80a7d6cc..d26b2d801 100644 --- a/Solana-Co-Learn/module5/anchor-on-the-front-end/index.html +++ b/Solana-Co-Learn/module5/anchor-on-the-front-end/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module5/index.html b/Solana-Co-Learn/module5/index.html index da4a7fe93..10bef06df 100644 --- a/Solana-Co-Learn/module5/index.html +++ b/Solana-Co-Learn/module5/index.html @@ -9,14 +9,14 @@ - - + +
    Skip to main content
    - - +
  • 🏬 前端开发
  • + + \ No newline at end of file diff --git a/Solana-Co-Learn/module5/introduction-to-anchor/build-with-solana-frameworks/index.html b/Solana-Co-Learn/module5/introduction-to-anchor/build-with-solana-frameworks/index.html index 8e29f6f97..2cd2c908a 100644 --- a/Solana-Co-Learn/module5/introduction-to-anchor/build-with-solana-frameworks/index.html +++ b/Solana-Co-Learn/module5/introduction-to-anchor/build-with-solana-frameworks/index.html @@ -9,14 +9,14 @@ - - + +
    Skip to main content

    🧱 使用Anchor框架进行开发

    我们将从零开始。首先与Solana互动的是Ping程序。让我们使用Anchor从头开始构建它。你可以在playground上进行操作,但我建议在本地设置,因为这样测试会更方便。

    我们要构建的是一个相当简单的程序:

    • 设立一个账户
    • 记录某个指令被调用的次数。

    这样,我们需要两个指令:一个用于初始化该账户及其数据结构,另一个用于增加计数。

    Anchor利用Rust的一些独特魔法来处理所有这些问题✨,它被专门设计用于解决许多常见的安全问题,所以你可以构建出更安全的程序!现在,添加initialize指令:

    • #[program] 内实施 initialize 指令
    • initialize 需要一个类型为 InitializeContext,并且不需要额外的指令数据
    • 在指令逻辑中,将 counter 账户的 count 字段设置为 0
    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    let counter = &mut ctx.accounts.counter;
    counter.count = 0;
    msg!("Counter Account Created");
    msg!("Current Count: { }", counter.count);
    Ok(())
    }

    实施 Context 类型 Initialize

    • 使用 #[derive(Accounts)] 宏来实现 Initialize Context 类型
    • initialize指令需包括:
      • counter - 指令中将初始化的计数器账户
      • user - 初始化的付款人
      • system_program - 用于初始化任何新账户的系统程序
    • 指定账户类型以进行账户验证
    • 使用 #[account(..)] 属性来定义额外的约束条件
    #[derive(Accounts)]
    pub struct Initialize<'info> {
    #[account(init, payer = user, space = 8 + 8)]
    pub counter: Account<'info, Counter>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    实现计数器

    使用#[account]属性来定义一个新的计数器账户类型

    #[account]
    pub struct Counter {
    pub count: u64,
    }

    添加 increment 指令

    • #[program] 内,实施一个 increment 指令,用于增加现有 counter 账户上的 count
    • increment 需要一个类型为 IncrementContext,并且不需额外的指令数据
    • 在指令逻辑中,将现有计数器账户的计数字段增加1
    pub fn increment(ctx: Context<Increment>) -> Result<()> {
    let counter = &mut ctx.accounts.counter;
    msg!("Previous Count: { }", counter.count);
    counter.count = counter.count.checked_add(1).unwrap();
    msg!("Counter Incremented");
    msg!("Current Count: { }", counter.count);
    Ok(())
    }

    实施 Context 类型 Increment

    • 使用 #[derive(Accounts)] 宏来实现 Increment Context 类型
    • increment指令需要:
      • counter - 一个已存在的计数器账户来递增
      • user - 支付交易手续费的付款人
    • 指定账户类型以进行账户验证
    • 使用 #[account(..)] 属性来定义额外的约束条件
    #[derive(Accounts)]
    pub struct Increment<'info> {
    #[account(mut)]
    pub counter: Account<'info, Counter>,
    pub user: Signer<'info>,
    }

    构建,部署,测试

    解决方案:此处查看

    • 构建和部署
    • 使用SolPG进行测试(支持Anchor测试)

    🚢 挑战

    好了,是时候展现你的技能,独立地建造一些东西了。

    概述

    由于我们从一个非常简单的程序开始,所以你的程序看起来几乎和我们刚刚创建的一样。尽量独立从头开始编写代码,不参考之前的代码,这样会更有助于提高。所以请尽量不要在这里复制粘贴。

    行动步骤:

    1. 编写一个新程序,初始化一个 counter 账户,并使用传入指令数据参数设置 count 字段。
    2. 执行 initializeincrementdecrement 指令
    3. 按照我们在演示中的方式,为每个指令编写测试
    4. 使用 anchor deploy 来部署你的程序。如果你愿意,可以像之前一样编写一个脚本来发送交易到你新部署的程序,然后使用Solana Explorer查看程序日志。

    像往常一样,发挥你的创意,超越基本指示,如果你愿意的话,尽情享受吧!

    info

    提示 -如果可能的话,尽量独立完成这个任务!但如果遇到困难,可以参考此仓库的解决方案 - 递减分支

    - - +如果可能的话,尽量独立完成这个任务!但如果遇到困难,可以参考此仓库的解决方案 - 递减分支

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module5/introduction-to-anchor/index.html b/Solana-Co-Learn/module5/introduction-to-anchor/index.html index 6df74e3e9..b4ca61b8b 100644 --- a/Solana-Co-Learn/module5/introduction-to-anchor/index.html +++ b/Solana-Co-Learn/module5/introduction-to-anchor/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module5/introduction-to-anchor/setting-up-anchor/index.html b/Solana-Co-Learn/module5/introduction-to-anchor/setting-up-anchor/index.html index 0d9ee2244..cf18c3029 100644 --- a/Solana-Co-Learn/module5/introduction-to-anchor/setting-up-anchor/index.html +++ b/Solana-Co-Learn/module5/introduction-to-anchor/setting-up-anchor/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🛳 设置 Anchor

    你已经像一位先驱者一样原生地构建了 Solana 程序。当然,你可以继续原生地构建,但有了框架的帮助,事情将变得更加轻松迅捷。

    试想一下前端开发中的 React —— 你只需用很少的代码就能做很多事情。Anchor 也与之类似,它将程序划分为不同的部分,使指令逻辑与账户验证和安全检查分开。就像 React 处理状态更新一样,Anchor 能够处理许多基本任务,如账户解析、验证、序列化/反序列化等,让你能够更快速地构建程序。

    通过使用宏捆绑各种样板代码,Anchor 让你能够专注于程序的核心业务逻辑。此外,Anchor 还设计了许多常见的安全检查功能,并允许你轻松定义额外的检查,从而帮助你构建更加安全的程序。

    info

    简单来说,Anchor 可以通过减少你按键的次数,让你快速前进!

    🗂 Anchor 应用程序结构

    以下是具体的操作步骤。

    首先,确保你已经安装了 RustSolana CLI(除非你跳过了某些部分)。此外,你还需要安装 Yarn

    完成这些后,只需根据官方的 Anchor 文档 进行设置。一切顺利的话,运行 anchor --version 时,你会看到一个版本号被打印出来。

    下面是我执行后得到的 Anchor 的具体版本信息:

    anchor --version
    anchor-cli 0.28.0

    现在我们用 Anchor 来设置一个空白的 Solana 程序:

    anchor init <new-workspace-name>

    这将建立以下结构:

    • Anchor.tomlAnchor 配置文件。
    • Cargo.tomlRust 工作区配置文件。
    • package.jsonJavaScript 依赖文件。
    • programs/Solana 程序包的目录。
    • app/:你的应用前端所在地。
    • tests/TypeScript 集成测试的位置。
    • migrations/deploy.js:用于部署迁移到不同版本的程序的脚本。
    • .anchor 文件夹:包含最新程序日志和本地测试账本。

    你现在基本上可以忽略这些文件。打开 programs/<new-workspace-name>/src/lib.rs,你会发现它与我们的原生程序有所不同。Anchor 将定义入口点,我们将使用 Rust 属性告诉 Anchor 我们所有的需求,这样它就能自动化大部分工作。

    当我们使用 #[program] 时,我们实际上是在声明一个 Rust 宏。Anchor 将使用它为我们生成所有必要的本地 Solana 样板代码。

    Anchor CLI 的美妙之处还在于它集成了 TypeScript 测试。只需编写测试,然后使用 Anchor 命令就可以了!

    构建/部署的设置与本地程序相同,只不过使用的命令有所不同。以下是我们的构建方式:

    anchor build

    这将花费几秒钟时间,在工作区中构建适用于 SolanaBPF 运行时程序,并在 target/idl 目录中生成“IDLs”。运行 cargo build-sbf 时,你还应该在终端中看到类似的输出,其中包含一个部署命令。

    这里有一些关于目标文件夹你需要了解的信息:

    • target/deploy :存放部署程序的生成密钥对。
    • target/idl :程序的 IDL 文件,.json 格式。
    • target/typesTypeScriptIDL —— 所有我们需要的类型。
    info

    什么是 IDL

    IDL(接口描述语言)文件是一个 JSON 文件,用于描述程序的接口,它告诉你有哪些函数可用以及它们接受的参数。你可以将其看作程序的 API 文档。

    我们使用 IDL 程序来确定如何与客户端通信(可用的函数、参数等),并使用 TypeScriptIDL 来定义类型。这些是非常重要的,因为要让你的程序开源,你需要发布经过验证的构建版本和 IDLAnchor Programs Registry

    现在,我们想要部署程序。但我们还不能立即开始!我们需要做两件事情 - 获取程序地址并设置网络。

    声明程序ID

    首先,在先前的 lib.rs 文件中,有一个宏 declare_id! ,其中包含一个默认值。现在得版本anchor,在你使用anchor init生成一个新的项目的时候declare_id!中的值是每次都不一样的,为你生成一个新的值。你也可以通过运行 anchor keys list 来查看你的PROGRAM_ID

    使用下面的命令来获取程序的地址:

    anchor keys list

    选择网络

    我们需要解决的第二个问题是:程序默认会部署到本地主机网络。我们可以启动一个本地验证器,或者切换到开发网络。

    作为一个专业人士,我计划直接推送到开发网络,因此我将打开 Anchor.toml 文件,并将 cluster 改为 devnet。只要我拥有足够的开发网络的 SOL,我就可以直接部署。

    anchor deploy

    太好了!希望你能在终端上看到带有 Program Id 的“部署成功”的消息。

    现在,将你的集群更改为 localnet,这样我们就可以进行测试了。在测试过程中,Anchor 会自动设置一个本地验证器!机器人真是太棒了 🤖。

    测试过程相当简单:

    anchor test

    这将在配置的集群上运行一组集成测试套件,在运行之前部署工作区所有程序的新版本。

    就这样!你刚刚成功构建、部署,并测试了你的第一个 Anchor 程序 :D。

    下一步,我们将编写一个定制的 Anchor 程序,以此来体验它的真正威力!

    - - +
    Skip to main content

    🛳 设置 Anchor

    你已经像一位先驱者一样原生地构建了 Solana 程序。当然,你可以继续原生地构建,但有了框架的帮助,事情将变得更加轻松迅捷。

    试想一下前端开发中的 React —— 你只需用很少的代码就能做很多事情。Anchor 也与之类似,它将程序划分为不同的部分,使指令逻辑与账户验证和安全检查分开。就像 React 处理状态更新一样,Anchor 能够处理许多基本任务,如账户解析、验证、序列化/反序列化等,让你能够更快速地构建程序。

    通过使用宏捆绑各种样板代码,Anchor 让你能够专注于程序的核心业务逻辑。此外,Anchor 还设计了许多常见的安全检查功能,并允许你轻松定义额外的检查,从而帮助你构建更加安全的程序。

    info

    简单来说,Anchor 可以通过减少你按键的次数,让你快速前进!

    🗂 Anchor 应用程序结构

    以下是具体的操作步骤。

    首先,确保你已经安装了 RustSolana CLI(除非你跳过了某些部分)。此外,你还需要安装 Yarn

    完成这些后,只需根据官方的 Anchor 文档 进行设置。一切顺利的话,运行 anchor --version 时,你会看到一个版本号被打印出来。

    下面是我执行后得到的 Anchor 的具体版本信息:

    anchor --version
    anchor-cli 0.28.0

    现在我们用 Anchor 来设置一个空白的 Solana 程序:

    anchor init <new-workspace-name>

    这将建立以下结构:

    • Anchor.tomlAnchor 配置文件。
    • Cargo.tomlRust 工作区配置文件。
    • package.jsonJavaScript 依赖文件。
    • programs/Solana 程序包的目录。
    • app/:你的应用前端所在地。
    • tests/TypeScript 集成测试的位置。
    • migrations/deploy.js:用于部署迁移到不同版本的程序的脚本。
    • .anchor 文件夹:包含最新程序日志和本地测试账本。

    你现在基本上可以忽略这些文件。打开 programs/<new-workspace-name>/src/lib.rs,你会发现它与我们的原生程序有所不同。Anchor 将定义入口点,我们将使用 Rust 属性告诉 Anchor 我们所有的需求,这样它就能自动化大部分工作。

    当我们使用 #[program] 时,我们实际上是在声明一个 Rust 宏。Anchor 将使用它为我们生成所有必要的本地 Solana 样板代码。

    Anchor CLI 的美妙之处还在于它集成了 TypeScript 测试。只需编写测试,然后使用 Anchor 命令就可以了!

    构建/部署的设置与本地程序相同,只不过使用的命令有所不同。以下是我们的构建方式:

    anchor build

    这将花费几秒钟时间,在工作区中构建适用于 SolanaBPF 运行时程序,并在 target/idl 目录中生成“IDLs”。运行 cargo build-sbf 时,你还应该在终端中看到类似的输出,其中包含一个部署命令。

    这里有一些关于目标文件夹你需要了解的信息:

    • target/deploy :存放部署程序的生成密钥对。
    • target/idl :程序的 IDL 文件,.json 格式。
    • target/typesTypeScriptIDL —— 所有我们需要的类型。
    info

    什么是 IDL

    IDL(接口描述语言)文件是一个 JSON 文件,用于描述程序的接口,它告诉你有哪些函数可用以及它们接受的参数。你可以将其看作程序的 API 文档。

    我们使用 IDL 程序来确定如何与客户端通信(可用的函数、参数等),并使用 TypeScriptIDL 来定义类型。这些是非常重要的,因为要让你的程序开源,你需要发布经过验证的构建版本和 IDLAnchor Programs Registry

    现在,我们想要部署程序。但我们还不能立即开始!我们需要做两件事情 - 获取程序地址并设置网络。

    声明程序ID

    首先,在先前的 lib.rs 文件中,有一个宏 declare_id! ,其中包含一个默认值。现在得版本anchor,在你使用anchor init生成一个新的项目的时候declare_id!中的值是每次都不一样的,为你生成一个新的值。你也可以通过运行 anchor keys list 来查看你的PROGRAM_ID

    使用下面的命令来获取程序的地址:

    anchor keys list

    选择网络

    我们需要解决的第二个问题是:程序默认会部署到本地主机网络。我们可以启动一个本地验证器,或者切换到开发网络。

    作为一个专业人士,我计划直接推送到开发网络,因此我将打开 Anchor.toml 文件,并将 cluster 改为 devnet。只要我拥有足够的开发网络的 SOL,我就可以直接部署。

    anchor deploy

    太好了!希望你能在终端上看到带有 Program Id 的“部署成功”的消息。

    现在,将你的集群更改为 localnet,这样我们就可以进行测试了。在测试过程中,Anchor 会自动设置一个本地验证器!机器人真是太棒了 🤖。

    测试过程相当简单:

    anchor test

    这将在配置的集群上运行一组集成测试套件,在运行之前部署工作区所有程序的新版本。

    就这样!你刚刚成功构建、部署,并测试了你的第一个 Anchor 程序 :D。

    下一步,我们将编写一个定制的 Anchor 程序,以此来体验它的真正威力!

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module5/introduction-to-anchor/the-anchor-framework/index.html b/Solana-Co-Learn/module5/introduction-to-anchor/the-anchor-framework/index.html index 3e2b20b22..646f0e081 100644 --- a/Solana-Co-Learn/module5/introduction-to-anchor/the-anchor-framework/index.html +++ b/Solana-Co-Learn/module5/introduction-to-anchor/the-anchor-framework/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🐟 Anchor 框架

    当进行本地构建时,我们会将程序分解为多个文件,每个文件负责特定的任务。但由于 Anchor 大大精简了代码量,我们现在可以学习如何将程序组织到单一文件的不同部分中 😎。

    我们可以将所有内容整合到一个文件中,这得益于 Anchor 使用宏来抽象各种重复任务。只需在文件中放置一个宏,让 Anchor 替我们处理,无需编写大量的代码。这也意味着我们能将指令逻辑与账户验证和安全检查分开。

    在我们继续之前,先快速回顾一下过去必须编写许多样板代码的那些无趣部分:

    • 账户验证
    • 安全检查
    • 序列化/反序列化

    Anchor 使用一些 Rust 的巧妙技巧来解决所有这些问题✨。它被设计为处理许多常见的安全问题,使你能够构建更安全的程序!

    🍱 Anchor Program 的结构

    让我们一起深入了解 Anchor program 的结构。

    // use this import to gain access to common anchor features
    use anchor_lang::prelude::*;

    // Program on-chain address
    declare_id!("6bujjNgtKQtgWEu4XMAtoJgkCn5RoqxLobuA7ptZrL6y");

    #[program]
    pub mod program_module_name {
    use super::*;

    pub fn initialize_one(ctx: Context<InitializeAccounts>, instruction_data: u64) -> Result<()> {
    ctx.accounts.account_name.data = instruction_data;
    Ok(())
    }
    }

    // validate incoming account for instructions
    #[derive(Accounts)]
    pub struct InitializeAccounts<'info> {
    #[account(init, payer = user, space = 8 + 8]
    pub account_name: Account<'info, AccountStruct>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    // Define custom program account type
    #[account]
    pub struct AccountStruct {
    data: u64,
    }

    这是一个相当简洁的程序 — 它初始化一个新账户,并使用从指令中传入的数据更新账户的数据字段。

    你会注意到每个部分都是以宏或属性开始的,这些都有助于扩展你所编写的代码。

    我们有四个主要部分:

    • declare_id! - 程序的链上地址(这取代了 entrypoint!
    • #[program] - 程序的指令逻辑
    • #[derive(Accounts)] - list、验证和反序列化传入指令的账户
    • #[account] - 为程序定义自定义账户类型

    🗿 declare_id!

    让我们先来了解 declare_id! 宏,因为它非常简单:

    // Program on-chain address
    declare_id!("6bujjNgtKQtgWEu4XMAtoJgkCn5RoqxLobuA7ptZrL6y");

    这用于指定程序的链上地址(即 PROGRAM_ID)。当你第一次构建 Anchor 程序时,会生成一个新的密钥对(可以使用 anchor keys list 获取)。这个密钥对会作为部署程序的默认密钥对(除非你另外指定)。该密钥对的公钥被用作 PROGRAM_ID 并在 declare_id! 宏中定义。

    👑 #[program]

    #[program]
    pub mod program_module_name {
    use super::*;

    pub fn initialize_one(ctx: Context<InitializeAccounts>, instruction_data: u64) -> Result<()> {
    ctx.accounts.account_name.data = instruction_data;
    Ok(())
    }
    }

    #[program] 属性定义了包含所有程序指令的模块(因此是 mod)。这就是你将实现程序中每个指令逻辑的地方。你将为程序支持的每个指令创建一个公共函数。账户验证和安全检查与程序逻辑分离,因此不会出现在此处!

    每个指令都需要两个参数:一个 Context 和指令数据。Anchor 会自动反序列化指令数据,所以我们无需为此担心!

    在我们深入了解这些宏的其他部分之前,我们需要先了解指令逻辑中这个新的 Context 是什么。我们将深入探讨三个层次:Native 层、Rust 层和 Anchor 层,请紧随我来!

    📝 Context

    回想我们在本地处理指令时的工作流程。我们在 process_instruction 函数中传入了 program_idaccountsinstruction_data。除了指令数据外,其余部分可以统称为指令的“Context”。由于程序无状态,因此必须知道指令的上下文。在Anchor中,处理指令只需要Context和数据两部分。

    Context是一个结构体,承载着当前事务的所有信息。它传递到每个指令处理程序中,并包含以下字段:

    pub struct Context<'a, 'b, 'c, 'info, T> {
    /// 当前正在执行的程序ID
    pub program_id: &'a Pubkey,
    /// 反序列化的账户
    pub accounts: &'b mut T,
    /// 剩下的账户信息,但未被反序列化或验证
    /// 直接使用时需小心。
    pub remaining_accounts: &'c [AccountInfo<'info>],
    /// 在约束验证期间找到的Bump种子
    /// 提供此项便利,以便处理程序
    /// 不必重新计算Bump种子或
    /// 将它们作为参数传入。
    pub bumps: BTreeMap<String, u8>
    }

    第二层:Rust。

    我们之前没在Rust中谈论过“生命周期”,这在参数'a, 'b, 'c, 'info' 符号中体现。生命周期是Rust用来追踪引用有效期的机制。每个带生命周期标记的属性都与Context的生命周期关联。简而言之,它的意思是,在Context的其他属性消失前,不要释放或解引用它,以免出现悬挂引用。但现阶段我们无需过多深究,因为这对我们即将要做的事情影响不大。

    pub accounts: &'b mut T,

    重要的是 T,这是一个通用占位符,代表一种类型。这意味着Context将包含一个类型,并且该类型可以在运行时确定。

    简单来说,我们告诉Rust:“嘿,我现在还不知道accounts的确切类型,我会在实际使用时告诉你。”

    第三层:Anchor

    在运行时,accounts的类型变为我们在InstructionAccounts中定义的类型。这意味着我们的instruction_one函数现在能够访问在InstructionAccounts中声明的账户。

    • 执行程序的PROGRAM_IDctx.program_id
    • 传递到指令中的账户(ctx.accounts
    • 剩余的账户(ctx.remaining_accounts),包括所有传入指令但未在Accounts结构中声明的账户。这是不常用的。
    • 任何PDA账户的Bumpctx.bumps)。把它们放在这里,就不必在指令处理程序内重新计算。

    #[derive(Accounts)]

    让我们回归主题,探讨与#[derive(Accounts)]部分有关的Context类型。

    这是我们定义传入指令的账户的地方。#[derive(Accounts)]宏让Anchor创建了解析和验证这些账户所需的实现。

    例如,instruction_one需要一个类型为InstructionAccountsContext参数。#[derive(Accounts)]宏实现了InstructionAccounts结构,其中包括三个账户:

    • account_name
    • user
    • system_program

    instruction_one被调用时,程序会:

    • 核对传入指令的账户是否与InstructionAccounts结构中规定的账户类型匹配。
    • 检查账户是否满足指定的附加约束(这是#[account]行的作用)。

    最后,我想强调一下。在一行代码中,我们就执行了一个CPI到系统程序来创建一个账户!是不是有点疯狂?我们无需编写任何创建账户的代码,只需声明要创建的账户,Anchor就会完成剩下的工作!

    最后,对于用户账户,有一个“mut”属性,它标记了账户为可变。由于用户将为此付费(余额会有所变化),因此它必须是可变的。

    🔎 在Anchor中的账户类型

    或许你还记得上周我们使用的AccountInfo类型,当在编写native程序时。每当需要处理账户时,我们都会使用这个类型 - 处理指令、创建交易、进行CPI等。这个类型覆盖了我们可能会用到的各种账户类型,比如PDA、用户账户,甚至系统程序。想想看,使用同一个类型来描述如此多样的参数,的确有些奇特。

    Anchor将原生类型包裹起来,提供了一系列新类型,每个都带有不同的验证。我们不再需要在指令中检查是否拥有一个账户,因为我们可以声明它为特定类型,Anchor会为我们进行验证!

    下面让我们了解一下几种常见的类型,首先是Account

    你会注意到account_nameAccount类型的,它基本上是对AccountInfo的扩展,我们在原生开发中已经用过了。那么它在这里的作用是什么呢?

    对于account_name账户,Account包装器会:

    • AccountStruct的格式反序列化data
    • 检查账户的程序所有者是否与指定的账户类型匹配。
    • 当在Accounts包装器中指定的账户类型是使用#[account]宏在同一个crate中定义的时候,程序的所有权检查是针对declare_id!宏中定义的programId进行的。

    简直省了不少力气!

    🖖 Signer类型

    接下来是Signer类型。

    这个类型用来确认账户是否已经签署了交易。

    例如,我们可以要求user账户必须是指令的签署者。我们不检查其他任何内容 - 我们不关心账户的类型或签署者是否拥有该账户。

    如果他们没有签署交易,指令就会失败!

    💻 Program 输入类型

    #[program]
    pub mod program_module_name {
    use super::*;

    pub fn initialize_one(ctx: Context<InitializeAccounts>, instruction_data: u64) -> Result<()> {
    ctx.accounts.account_name.data = instruction_data;
    Ok(())
    }
    }

    最后,Program类型确保传入的账户符合我们的预期,并且确实是一个程序(可执行文件)。

    你可能已经开始注意到Anchor是如何让事情变得简单的。这段代码不仅更简洁,还更易于理解!因为每个元素都有自己的类型,所以你能够更快地理解程序的功能。只需掌握几个额外的“规则”就行了 :)

    🤔 额外的限制条件

    到现在为止,我们唯一还没有涉及的是#[account]位,无论是在InstructionAccounts结构体内还是外部。

    让我们先看看#[account]结构体内部:

    // validate incoming account for instructions
    #[derive(Accounts)]
    pub struct InitializeAccounts<'info> {
    #[account(init, payer = user, space = 8 + 8)]
    pub account_name: Account<'info, AccountStruct>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    这就是我们为账户指定额外限制条件的地方。Anchor在基本验证方面做得很好,但它还能帮我们检查一些其他特定的东西!

    对于account_name属性,它通过#[account(..)]指定了:

    • init - 通过CPI创建和初始化账户,将其设置为账户的discriminator
    • payer - 指定payer为结构中定义的user账户的初始化值。
    • space - 指定为账户分配的space的大小为8 + 8字节。
      • 8个字节是一个discriminatorAnchor会自动添加以识别账户类型。
      • 接下来的8个字节为账户中存储的数据分配空间,其定义在AccountStruct类型中。
      • 更多细节请参考:Space Reference

    再来复习一遍。我们在一行代码中执行一个CPI到系统程序来创建一个账户!想想看,这有多简便?我们不需要手动编写代码来创建账户,我们只需指定要创建的账户,Anchor就会完成剩下的工作!

    最后,对于用户账户,有一个mut属性,表示账户是可变的。因为用户会为此付费,余额可能会变化,所以它必须是可变的。

    #[account]

    再多陪我一会儿,我们已经到了最后的部分!

    #[account]属性用于表现Solana账户的数据结构,并且实现了以下几个Trait

    • AccountSerialize
    • AccountDeserialize
    • AnchorSerialize
    • AnchorDeserialize
    • Clone
    • Discriminator
    • Owner

    简单来说,#[account]属性实现了序列化和反序列化功能,并为账户实现了discriminatorOwner trait

    • discriminator是一个8字节的唯一标识符,代表账户类型,并由账户结构名称的SHA256的前8字节派生。
    • 任何对AccountDeserializetry_deserialize的调用都会检查这个discriminator
    • 如果不匹配,那么账户就会被视为无效,并且账户反序列化会以错误退出。

    #[account]属性还实现了Owner Trait

    • 使用programIddeclareId声明的crate#[account]的使用。
    • 使用程序中定义的#[account]属性初始化的帐户归程序所有

    就是这样,Anchor程序的构建结构就介绍完了。虽然有些复杂,但这些都是我们后续使用Anchor的必要知识。休息一下吧,很快就回来,是时候开始构建了!

    ‼ 赶快回来!

    这真的非常重要——你现在可能无法完全理解其中的全部内容。

    没关系,我也是。我花了整整两天的时间来写这一页。一旦你用Anchor构建了一个程序,再回来重新阅读一遍。你会发现更容易理解,一切都会变得更有意义。

    学习并不是一个线性的过程,它会有高潮和低谷。你不能仅仅通过一次阅读就掌握宇宙中最困难的主题。不断学习,不断建立,你会学得更精,建得更好。

    - - +
    Skip to main content

    🐟 Anchor 框架

    当进行本地构建时,我们会将程序分解为多个文件,每个文件负责特定的任务。但由于 Anchor 大大精简了代码量,我们现在可以学习如何将程序组织到单一文件的不同部分中 😎。

    我们可以将所有内容整合到一个文件中,这得益于 Anchor 使用宏来抽象各种重复任务。只需在文件中放置一个宏,让 Anchor 替我们处理,无需编写大量的代码。这也意味着我们能将指令逻辑与账户验证和安全检查分开。

    在我们继续之前,先快速回顾一下过去必须编写许多样板代码的那些无趣部分:

    • 账户验证
    • 安全检查
    • 序列化/反序列化

    Anchor 使用一些 Rust 的巧妙技巧来解决所有这些问题✨。它被设计为处理许多常见的安全问题,使你能够构建更安全的程序!

    🍱 Anchor Program 的结构

    让我们一起深入了解 Anchor program 的结构。

    // use this import to gain access to common anchor features
    use anchor_lang::prelude::*;

    // Program on-chain address
    declare_id!("6bujjNgtKQtgWEu4XMAtoJgkCn5RoqxLobuA7ptZrL6y");

    #[program]
    pub mod program_module_name {
    use super::*;

    pub fn initialize_one(ctx: Context<InitializeAccounts>, instruction_data: u64) -> Result<()> {
    ctx.accounts.account_name.data = instruction_data;
    Ok(())
    }
    }

    // validate incoming account for instructions
    #[derive(Accounts)]
    pub struct InitializeAccounts<'info> {
    #[account(init, payer = user, space = 8 + 8]
    pub account_name: Account<'info, AccountStruct>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    // Define custom program account type
    #[account]
    pub struct AccountStruct {
    data: u64,
    }

    这是一个相当简洁的程序 — 它初始化一个新账户,并使用从指令中传入的数据更新账户的数据字段。

    你会注意到每个部分都是以宏或属性开始的,这些都有助于扩展你所编写的代码。

    我们有四个主要部分:

    • declare_id! - 程序的链上地址(这取代了 entrypoint!
    • #[program] - 程序的指令逻辑
    • #[derive(Accounts)] - list、验证和反序列化传入指令的账户
    • #[account] - 为程序定义自定义账户类型

    🗿 declare_id!

    让我们先来了解 declare_id! 宏,因为它非常简单:

    // Program on-chain address
    declare_id!("6bujjNgtKQtgWEu4XMAtoJgkCn5RoqxLobuA7ptZrL6y");

    这用于指定程序的链上地址(即 PROGRAM_ID)。当你第一次构建 Anchor 程序时,会生成一个新的密钥对(可以使用 anchor keys list 获取)。这个密钥对会作为部署程序的默认密钥对(除非你另外指定)。该密钥对的公钥被用作 PROGRAM_ID 并在 declare_id! 宏中定义。

    👑 #[program]

    #[program]
    pub mod program_module_name {
    use super::*;

    pub fn initialize_one(ctx: Context<InitializeAccounts>, instruction_data: u64) -> Result<()> {
    ctx.accounts.account_name.data = instruction_data;
    Ok(())
    }
    }

    #[program] 属性定义了包含所有程序指令的模块(因此是 mod)。这就是你将实现程序中每个指令逻辑的地方。你将为程序支持的每个指令创建一个公共函数。账户验证和安全检查与程序逻辑分离,因此不会出现在此处!

    每个指令都需要两个参数:一个 Context 和指令数据。Anchor 会自动反序列化指令数据,所以我们无需为此担心!

    在我们深入了解这些宏的其他部分之前,我们需要先了解指令逻辑中这个新的 Context 是什么。我们将深入探讨三个层次:Native 层、Rust 层和 Anchor 层,请紧随我来!

    📝 Context

    回想我们在本地处理指令时的工作流程。我们在 process_instruction 函数中传入了 program_idaccountsinstruction_data。除了指令数据外,其余部分可以统称为指令的“Context”。由于程序无状态,因此必须知道指令的上下文。在Anchor中,处理指令只需要Context和数据两部分。

    Context是一个结构体,承载着当前事务的所有信息。它传递到每个指令处理程序中,并包含以下字段:

    pub struct Context<'a, 'b, 'c, 'info, T> {
    /// 当前正在执行的程序ID
    pub program_id: &'a Pubkey,
    /// 反序列化的账户
    pub accounts: &'b mut T,
    /// 剩下的账户信息,但未被反序列化或验证
    /// 直接使用时需小心。
    pub remaining_accounts: &'c [AccountInfo<'info>],
    /// 在约束验证期间找到的Bump种子
    /// 提供此项便利,以便处理程序
    /// 不必重新计算Bump种子或
    /// 将它们作为参数传入。
    pub bumps: BTreeMap<String, u8>
    }

    第二层:Rust。

    我们之前没在Rust中谈论过“生命周期”,这在参数'a, 'b, 'c, 'info' 符号中体现。生命周期是Rust用来追踪引用有效期的机制。每个带生命周期标记的属性都与Context的生命周期关联。简而言之,它的意思是,在Context的其他属性消失前,不要释放或解引用它,以免出现悬挂引用。但现阶段我们无需过多深究,因为这对我们即将要做的事情影响不大。

    pub accounts: &'b mut T,

    重要的是 T,这是一个通用占位符,代表一种类型。这意味着Context将包含一个类型,并且该类型可以在运行时确定。

    简单来说,我们告诉Rust:“嘿,我现在还不知道accounts的确切类型,我会在实际使用时告诉你。”

    第三层:Anchor

    在运行时,accounts的类型变为我们在InstructionAccounts中定义的类型。这意味着我们的instruction_one函数现在能够访问在InstructionAccounts中声明的账户。

    • 执行程序的PROGRAM_IDctx.program_id
    • 传递到指令中的账户(ctx.accounts
    • 剩余的账户(ctx.remaining_accounts),包括所有传入指令但未在Accounts结构中声明的账户。这是不常用的。
    • 任何PDA账户的Bumpctx.bumps)。把它们放在这里,就不必在指令处理程序内重新计算。

    #[derive(Accounts)]

    让我们回归主题,探讨与#[derive(Accounts)]部分有关的Context类型。

    这是我们定义传入指令的账户的地方。#[derive(Accounts)]宏让Anchor创建了解析和验证这些账户所需的实现。

    例如,instruction_one需要一个类型为InstructionAccountsContext参数。#[derive(Accounts)]宏实现了InstructionAccounts结构,其中包括三个账户:

    • account_name
    • user
    • system_program

    instruction_one被调用时,程序会:

    • 核对传入指令的账户是否与InstructionAccounts结构中规定的账户类型匹配。
    • 检查账户是否满足指定的附加约束(这是#[account]行的作用)。

    最后,我想强调一下。在一行代码中,我们就执行了一个CPI到系统程序来创建一个账户!是不是有点疯狂?我们无需编写任何创建账户的代码,只需声明要创建的账户,Anchor就会完成剩下的工作!

    最后,对于用户账户,有一个“mut”属性,它标记了账户为可变。由于用户将为此付费(余额会有所变化),因此它必须是可变的。

    🔎 在Anchor中的账户类型

    或许你还记得上周我们使用的AccountInfo类型,当在编写native程序时。每当需要处理账户时,我们都会使用这个类型 - 处理指令、创建交易、进行CPI等。这个类型覆盖了我们可能会用到的各种账户类型,比如PDA、用户账户,甚至系统程序。想想看,使用同一个类型来描述如此多样的参数,的确有些奇特。

    Anchor将原生类型包裹起来,提供了一系列新类型,每个都带有不同的验证。我们不再需要在指令中检查是否拥有一个账户,因为我们可以声明它为特定类型,Anchor会为我们进行验证!

    下面让我们了解一下几种常见的类型,首先是Account

    你会注意到account_nameAccount类型的,它基本上是对AccountInfo的扩展,我们在原生开发中已经用过了。那么它在这里的作用是什么呢?

    对于account_name账户,Account包装器会:

    • AccountStruct的格式反序列化data
    • 检查账户的程序所有者是否与指定的账户类型匹配。
    • 当在Accounts包装器中指定的账户类型是使用#[account]宏在同一个crate中定义的时候,程序的所有权检查是针对declare_id!宏中定义的programId进行的。

    简直省了不少力气!

    🖖 Signer类型

    接下来是Signer类型。

    这个类型用来确认账户是否已经签署了交易。

    例如,我们可以要求user账户必须是指令的签署者。我们不检查其他任何内容 - 我们不关心账户的类型或签署者是否拥有该账户。

    如果他们没有签署交易,指令就会失败!

    💻 Program 输入类型

    #[program]
    pub mod program_module_name {
    use super::*;

    pub fn initialize_one(ctx: Context<InitializeAccounts>, instruction_data: u64) -> Result<()> {
    ctx.accounts.account_name.data = instruction_data;
    Ok(())
    }
    }

    最后,Program类型确保传入的账户符合我们的预期,并且确实是一个程序(可执行文件)。

    你可能已经开始注意到Anchor是如何让事情变得简单的。这段代码不仅更简洁,还更易于理解!因为每个元素都有自己的类型,所以你能够更快地理解程序的功能。只需掌握几个额外的“规则”就行了 :)

    🤔 额外的限制条件

    到现在为止,我们唯一还没有涉及的是#[account]位,无论是在InstructionAccounts结构体内还是外部。

    让我们先看看#[account]结构体内部:

    // validate incoming account for instructions
    #[derive(Accounts)]
    pub struct InitializeAccounts<'info> {
    #[account(init, payer = user, space = 8 + 8)]
    pub account_name: Account<'info, AccountStruct>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    这就是我们为账户指定额外限制条件的地方。Anchor在基本验证方面做得很好,但它还能帮我们检查一些其他特定的东西!

    对于account_name属性,它通过#[account(..)]指定了:

    • init - 通过CPI创建和初始化账户,将其设置为账户的discriminator
    • payer - 指定payer为结构中定义的user账户的初始化值。
    • space - 指定为账户分配的space的大小为8 + 8字节。
      • 8个字节是一个discriminatorAnchor会自动添加以识别账户类型。
      • 接下来的8个字节为账户中存储的数据分配空间,其定义在AccountStruct类型中。
      • 更多细节请参考:Space Reference

    再来复习一遍。我们在一行代码中执行一个CPI到系统程序来创建一个账户!想想看,这有多简便?我们不需要手动编写代码来创建账户,我们只需指定要创建的账户,Anchor就会完成剩下的工作!

    最后,对于用户账户,有一个mut属性,表示账户是可变的。因为用户会为此付费,余额可能会变化,所以它必须是可变的。

    #[account]

    再多陪我一会儿,我们已经到了最后的部分!

    #[account]属性用于表现Solana账户的数据结构,并且实现了以下几个Trait

    • AccountSerialize
    • AccountDeserialize
    • AnchorSerialize
    • AnchorDeserialize
    • Clone
    • Discriminator
    • Owner

    简单来说,#[account]属性实现了序列化和反序列化功能,并为账户实现了discriminatorOwner trait

    • discriminator是一个8字节的唯一标识符,代表账户类型,并由账户结构名称的SHA256的前8字节派生。
    • 任何对AccountDeserializetry_deserialize的调用都会检查这个discriminator
    • 如果不匹配,那么账户就会被视为无效,并且账户反序列化会以错误退出。

    #[account]属性还实现了Owner Trait

    • 使用programIddeclareId声明的crate#[account]的使用。
    • 使用程序中定义的#[account]属性初始化的帐户归程序所有

    就是这样,Anchor程序的构建结构就介绍完了。虽然有些复杂,但这些都是我们后续使用Anchor的必要知识。休息一下吧,很快就回来,是时候开始构建了!

    ‼ 赶快回来!

    这真的非常重要——你现在可能无法完全理解其中的全部内容。

    没关系,我也是。我花了整整两天的时间来写这一页。一旦你用Anchor构建了一个程序,再回来重新阅读一遍。你会发现更容易理解,一切都会变得更有意义。

    学习并不是一个线性的过程,它会有高潮和低谷。你不能仅仅通过一次阅读就掌握宇宙中最困难的主题。不断学习,不断建立,你会学得更精,建得更好。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module5/program-in-anchor/build-with-anchor-cpis/index.html b/Solana-Co-Learn/module5/program-in-anchor/build-with-anchor-cpis/index.html index a685a052d..1ff8daf48 100644 --- a/Solana-Co-Learn/module5/program-in-anchor/build-with-anchor-cpis/index.html +++ b/Solana-Co-Learn/module5/program-in-anchor/build-with-anchor-cpis/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    使用Anchor CPIs构建

    回归未来。我们将通过以CPIs结尾的电影评论来为这个话题画上完美的句号。

    这一次,我们将:

    • 添加指令以创建带有元数据的代币铸造
    • 添加指令以添加评论
    • 在创建评论时铸造薄荷代币
    • 在添加评论时铸造薄荷代币

    初始代码

    • 起始代码链接:这里
    • 我们将在之前的PDA演示基础上进行扩展

    首先,我们来定义 create_reward_mint 指令:

    pub fn create_reward_mint(
    ctx: Context<CreateTokenReward>,
    uri: String,
    name: String,
    symbol: String,
    ) -> Result<()> {
    msg!("Create Reward Token");

    let seeds = &["mint".as_bytes(), &[*ctx.bumps.get("reward_mint").unwrap()]];

    let signer = [&seeds[..]];

    let account_info = vec![
    ctx.accounts.metadata.to_account_info(),
    ctx.accounts.reward_mint.to_account_info(),
    ctx.accounts.user.to_account_info(),
    ctx.accounts.token_metadata_program.to_account_info(),
    ctx.accounts.token_program.to_account_info(),
    ctx.accounts.system_program.to_account_info(),
    ctx.accounts.rent.to_account_info(),
    ];

    invoke_signed(
    &create_metadata_accounts_v2(
    ctx.accounts.token_metadata_program.key(),
    ctx.accounts.metadata.key(),
    ctx.accounts.reward_mint.key(),
    ctx.accounts.reward_mint.key(),
    ctx.accounts.user.key(),
    ctx.accounts.user.key(),
    name,
    symbol,
    uri,
    None,
    0,
    true,
    true,
    None,
    None,
    ),
    account_info.as_slice(),
    &signer,
    )?;

    Ok(())
    }

    尽管代码很长,但非常直观!我们正在为Token元数据程序创建一个CPI,用来指向 create_metadata_account_v2 指令。

    接下来,我们会看到 CreateTokenReward 上下文类型。

    有关 /// CHECK 的详细信息在这里:安全检查。

    #[derive(Accounts)]
    pub struct CreateTokenReward<'info> {
    #[account(
    init,
    seeds = ["mint".as_bytes()],
    bump,
    payer = user,
    mint::decimals = 6,
    mint::authority = reward_mint,

    )]
    pub reward_mint: Account<'info, Mint>,

    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
    pub rent: Sysvar<'info, Rent>,
    pub token_program: Program<'info, Token>,

    /// CHECK:
    #[account(mut)]
    pub metadata: AccountInfo<'info>,
    /// CHECK:
    pub token_metadata_program: AccountInfo<'info>,
    }

    创建错误代码(ErrorCode)

    • 用于检查评级的错误代码
    • Anchor已处理我们在原生版本中的其他检查)
    #[error_code]
    pub enum ErrorCode {
    #[msg("评分大于5或小于1")]
    InvalidRating,
    }

    更新 add_movie_review

    • 添加对ErrorCode的检查
    • 设置评论计数器账户
    • 通过CPImintTo 指令,将代币铸造给评论人
    pub fn add_movie_review(
    ctx: Context<AddMovieReview>,
    title: String,
    description: String,
    rating: u8,
    ) -> Result<()> {
    msg!("创建了影评账户");
    msg!("标题:{}", title);
    msg!("描述:{}", description);
    msg!("评分:{}", rating);

    if rating > 5 || rating < 1 {
    msg!("评分不能高于5");
    return err!(ErrorCode::InvalidRating);
    }

    let movie_review = &mut ctx.accounts.movie_review;
    movie_review.reviewer = ctx.accounts.initializer.key();
    movie_review.title = title;
    movie_review.rating = rating;
    movie_review.description = description;

    msg!("创建了影评计数器账户");
    let movie_comment_counter = &mut ctx.accounts.movie_comment_counter;
    movie_comment_counter.counter = 0;
    msg!("计数器:{}", movie_comment_counter.counter);

    let seeds = &["mint".as_bytes(), &[*ctx.bumps.get("reward_mint").unwrap()]];

    let signer = [&seeds[..]];

    let cpi_ctx = CpiContext::new_with_signer(
    ctx.accounts.token_program.to_account_info(),
    token::MintTo {
    mint: ctx.accounts.reward_mint.to_account_info(),
    to: ctx.accounts.token_account.to_account_info(),
    authority: ctx.accounts.reward_mint.to_account_info(),
    },
    &signer,
    );

    token::mint_to(cpi_ctx, 10000000)?;
    msg!("已铸币");
    Ok(())
    }

    更新 AddMovieReview 上下文

    • 初始化 movie_review
    • 初始化 movie_comment_counter
    • 使用 init_if_needed 来初始化令牌账户
    #[derive(Accounts)]
    #[instruction(title: String, description: String)]
    pub struct AddMovieReview<'info> {
    #[account(
    init,
    seeds = [title.as_bytes(), initializer.key().as_ref()],
    bump,
    payer = initializer,
    space = 8 + 32 + 1 + 4 + title.len() + 4 + description.len()
    )]
    pub movie_review: Account<'info, MovieAccountState>,
    #[account(
    init,
    seeds = ["counter".as_bytes(), movie_review.key().as_ref()],
    bump,
    payer = initializer,
    space = 8 + 8
    )]
    pub movie_comment_counter: Account<'info, MovieCommentCounter>,
    #[account(mut,
    seeds = ["mint".as_bytes()],
    bump
    )]
    pub reward_mint: Account<'info, Mint>,
    #[account(
    init_if_needed,
    payer = initializer,
    associated_token::mint = reward_mint,
    associated_token::authority = initializer
    )]
    pub token_account: Account<'info, TokenAccount>,
    #[account(mut)]
    pub initializer: Signer<'info>,
    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub rent: Sysvar<'info, Rent>,
    pub system_program: Program<'info, System>,
    }

    ErrorCode 添加到 update_movie_review

    • update_movie_review 指令中添加 ErrorCode 检查
    pub fn update_movie_review(
    ctx: Context<UpdateMovieReview>,
    title: String,
    description: String,
    rating: u8,
    ) -> Result<()> {
    msg!("更新了影评账户");
    msg!("标题:{}", title);
    msg!("描述:{}", description);
    msg!("评分:{}", rating);

    if rating > 5 || rating < 1 {
    msg!("评分不能高于5");
    return err!(ErrorCode::InvalidRating);
    }

    let movie_review = &mut ctx.accounts.movie_review;
    movie_review.rating = rating;
    movie_review.description = description;

    Ok(())
    }

    添加 add_comment

    • 创建 add_comment 指令
    • 设置 movie_comment 数据
    • 通过 CPImintTo 指令,将代币铸造给审核者
    pub fn add_comment(ctx: Context<AddComment>, comment: String) -> Result<()> {
    msg!("已创建评论账户");
    msg!("评论:{}", comment);

    let movie_comment = &mut ctx.accounts.movie_comment;
    let movie_comment_counter = &mut ctx.accounts.movie_comment_counter;

    movie_comment.review = ctx.accounts.movie_review.key();
    movie_comment.commenter = ctx.accounts.initializer.key();
    movie_comment.comment = comment;
    movie_comment.count = movie_comment_counter.counter;

    movie_comment_counter.counter += 1;

    let seeds = &["mint".as_bytes(), &[*ctx.bumps.get("reward_mint").unwrap()]];

    let signer = [&seeds[..]];

    let cpi_ctx = CpiContext::new_with_signer(
    ctx.accounts.token_program.to_account_info(),
    token::MintTo {
    mint: ctx.accounts.reward_mint.to_account_info(),
    to: ctx.accounts.token_account.to_account_info(),
    authority: ctx.accounts.reward_mint.to_account_info(),
    },
    &signer,
    );

    token::mint_to(cpi_ctx, 5000000)?;
    msg!("已铸造代币");

    Ok(())
    }

    添加 AddComment 上下文

    • 初始化 movie_comment
    • 使用 init_if_needed 来初始化令牌账户
    #[derive(Accounts)]
    #[instruction(comment: String)]
    pub struct AddComment<'info> {
    #[account(
    init,
    seeds = [movie_review.key().as_ref(), &movie_comment_counter.counter.to_le_bytes()],
    bump,
    payer = initializer,
    space = 8 + 32 + 32 + 4 + comment.len() + 8
    )]
    pub movie_comment: Account<'info, MovieComment>,
    pub movie_review: Account<'info, MovieAccountState>,
    #[account(
    mut,
    seeds = ["counter".as_bytes(), movie_review.key().as_ref()],
    bump,
    )]
    pub movie_comment_counter: Account<'info, MovieCommentCounter>,
    #[account(mut,
    seeds = ["mint".as_bytes()],
    bump
    )]
    pub reward_mint: Account<'info, Mint>,
    #[account(
    init_if_needed,
    payer = initializer,
    associated_token::mint = reward_mint,
    associated_token::authority = initializer
    )]
    pub token_account: Account<'info, TokenAccount>,
    #[account(mut)]
    pub initializer: Signer<'info>,
    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub rent: Sysvar<'info, Rent>,
    pub system_program: Program<'info, System>,
    }

    构建,部署,测试

    解决方案:https://beta.solpg.io/6319c7bf77ea7f12846aee87

    如果你使用自己的编辑器,你必须在 mpl-token-metadataCargo.toml 中添加 features = ["no-entrypoint"]

    否则,将会出现以下错误:the #[global_allocator] in this crate conflicts with global allocator in: mpl_token_metadata

    • 构建和部署
    • 使用 SolPG 进行测试
    - - +
    Skip to main content

    使用Anchor CPIs构建

    回归未来。我们将通过以CPIs结尾的电影评论来为这个话题画上完美的句号。

    这一次,我们将:

    • 添加指令以创建带有元数据的代币铸造
    • 添加指令以添加评论
    • 在创建评论时铸造薄荷代币
    • 在添加评论时铸造薄荷代币

    初始代码

    • 起始代码链接:这里
    • 我们将在之前的PDA演示基础上进行扩展

    首先,我们来定义 create_reward_mint 指令:

    pub fn create_reward_mint(
    ctx: Context<CreateTokenReward>,
    uri: String,
    name: String,
    symbol: String,
    ) -> Result<()> {
    msg!("Create Reward Token");

    let seeds = &["mint".as_bytes(), &[*ctx.bumps.get("reward_mint").unwrap()]];

    let signer = [&seeds[..]];

    let account_info = vec![
    ctx.accounts.metadata.to_account_info(),
    ctx.accounts.reward_mint.to_account_info(),
    ctx.accounts.user.to_account_info(),
    ctx.accounts.token_metadata_program.to_account_info(),
    ctx.accounts.token_program.to_account_info(),
    ctx.accounts.system_program.to_account_info(),
    ctx.accounts.rent.to_account_info(),
    ];

    invoke_signed(
    &create_metadata_accounts_v2(
    ctx.accounts.token_metadata_program.key(),
    ctx.accounts.metadata.key(),
    ctx.accounts.reward_mint.key(),
    ctx.accounts.reward_mint.key(),
    ctx.accounts.user.key(),
    ctx.accounts.user.key(),
    name,
    symbol,
    uri,
    None,
    0,
    true,
    true,
    None,
    None,
    ),
    account_info.as_slice(),
    &signer,
    )?;

    Ok(())
    }

    尽管代码很长,但非常直观!我们正在为Token元数据程序创建一个CPI,用来指向 create_metadata_account_v2 指令。

    接下来,我们会看到 CreateTokenReward 上下文类型。

    有关 /// CHECK 的详细信息在这里:安全检查。

    #[derive(Accounts)]
    pub struct CreateTokenReward<'info> {
    #[account(
    init,
    seeds = ["mint".as_bytes()],
    bump,
    payer = user,
    mint::decimals = 6,
    mint::authority = reward_mint,

    )]
    pub reward_mint: Account<'info, Mint>,

    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
    pub rent: Sysvar<'info, Rent>,
    pub token_program: Program<'info, Token>,

    /// CHECK:
    #[account(mut)]
    pub metadata: AccountInfo<'info>,
    /// CHECK:
    pub token_metadata_program: AccountInfo<'info>,
    }

    创建错误代码(ErrorCode)

    • 用于检查评级的错误代码
    • Anchor已处理我们在原生版本中的其他检查)
    #[error_code]
    pub enum ErrorCode {
    #[msg("评分大于5或小于1")]
    InvalidRating,
    }

    更新 add_movie_review

    • 添加对ErrorCode的检查
    • 设置评论计数器账户
    • 通过CPImintTo 指令,将代币铸造给评论人
    pub fn add_movie_review(
    ctx: Context<AddMovieReview>,
    title: String,
    description: String,
    rating: u8,
    ) -> Result<()> {
    msg!("创建了影评账户");
    msg!("标题:{}", title);
    msg!("描述:{}", description);
    msg!("评分:{}", rating);

    if rating > 5 || rating < 1 {
    msg!("评分不能高于5");
    return err!(ErrorCode::InvalidRating);
    }

    let movie_review = &mut ctx.accounts.movie_review;
    movie_review.reviewer = ctx.accounts.initializer.key();
    movie_review.title = title;
    movie_review.rating = rating;
    movie_review.description = description;

    msg!("创建了影评计数器账户");
    let movie_comment_counter = &mut ctx.accounts.movie_comment_counter;
    movie_comment_counter.counter = 0;
    msg!("计数器:{}", movie_comment_counter.counter);

    let seeds = &["mint".as_bytes(), &[*ctx.bumps.get("reward_mint").unwrap()]];

    let signer = [&seeds[..]];

    let cpi_ctx = CpiContext::new_with_signer(
    ctx.accounts.token_program.to_account_info(),
    token::MintTo {
    mint: ctx.accounts.reward_mint.to_account_info(),
    to: ctx.accounts.token_account.to_account_info(),
    authority: ctx.accounts.reward_mint.to_account_info(),
    },
    &signer,
    );

    token::mint_to(cpi_ctx, 10000000)?;
    msg!("已铸币");
    Ok(())
    }

    更新 AddMovieReview 上下文

    • 初始化 movie_review
    • 初始化 movie_comment_counter
    • 使用 init_if_needed 来初始化令牌账户
    #[derive(Accounts)]
    #[instruction(title: String, description: String)]
    pub struct AddMovieReview<'info> {
    #[account(
    init,
    seeds = [title.as_bytes(), initializer.key().as_ref()],
    bump,
    payer = initializer,
    space = 8 + 32 + 1 + 4 + title.len() + 4 + description.len()
    )]
    pub movie_review: Account<'info, MovieAccountState>,
    #[account(
    init,
    seeds = ["counter".as_bytes(), movie_review.key().as_ref()],
    bump,
    payer = initializer,
    space = 8 + 8
    )]
    pub movie_comment_counter: Account<'info, MovieCommentCounter>,
    #[account(mut,
    seeds = ["mint".as_bytes()],
    bump
    )]
    pub reward_mint: Account<'info, Mint>,
    #[account(
    init_if_needed,
    payer = initializer,
    associated_token::mint = reward_mint,
    associated_token::authority = initializer
    )]
    pub token_account: Account<'info, TokenAccount>,
    #[account(mut)]
    pub initializer: Signer<'info>,
    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub rent: Sysvar<'info, Rent>,
    pub system_program: Program<'info, System>,
    }

    ErrorCode 添加到 update_movie_review

    • update_movie_review 指令中添加 ErrorCode 检查
    pub fn update_movie_review(
    ctx: Context<UpdateMovieReview>,
    title: String,
    description: String,
    rating: u8,
    ) -> Result<()> {
    msg!("更新了影评账户");
    msg!("标题:{}", title);
    msg!("描述:{}", description);
    msg!("评分:{}", rating);

    if rating > 5 || rating < 1 {
    msg!("评分不能高于5");
    return err!(ErrorCode::InvalidRating);
    }

    let movie_review = &mut ctx.accounts.movie_review;
    movie_review.rating = rating;
    movie_review.description = description;

    Ok(())
    }

    添加 add_comment

    • 创建 add_comment 指令
    • 设置 movie_comment 数据
    • 通过 CPImintTo 指令,将代币铸造给审核者
    pub fn add_comment(ctx: Context<AddComment>, comment: String) -> Result<()> {
    msg!("已创建评论账户");
    msg!("评论:{}", comment);

    let movie_comment = &mut ctx.accounts.movie_comment;
    let movie_comment_counter = &mut ctx.accounts.movie_comment_counter;

    movie_comment.review = ctx.accounts.movie_review.key();
    movie_comment.commenter = ctx.accounts.initializer.key();
    movie_comment.comment = comment;
    movie_comment.count = movie_comment_counter.counter;

    movie_comment_counter.counter += 1;

    let seeds = &["mint".as_bytes(), &[*ctx.bumps.get("reward_mint").unwrap()]];

    let signer = [&seeds[..]];

    let cpi_ctx = CpiContext::new_with_signer(
    ctx.accounts.token_program.to_account_info(),
    token::MintTo {
    mint: ctx.accounts.reward_mint.to_account_info(),
    to: ctx.accounts.token_account.to_account_info(),
    authority: ctx.accounts.reward_mint.to_account_info(),
    },
    &signer,
    );

    token::mint_to(cpi_ctx, 5000000)?;
    msg!("已铸造代币");

    Ok(())
    }

    添加 AddComment 上下文

    • 初始化 movie_comment
    • 使用 init_if_needed 来初始化令牌账户
    #[derive(Accounts)]
    #[instruction(comment: String)]
    pub struct AddComment<'info> {
    #[account(
    init,
    seeds = [movie_review.key().as_ref(), &movie_comment_counter.counter.to_le_bytes()],
    bump,
    payer = initializer,
    space = 8 + 32 + 32 + 4 + comment.len() + 8
    )]
    pub movie_comment: Account<'info, MovieComment>,
    pub movie_review: Account<'info, MovieAccountState>,
    #[account(
    mut,
    seeds = ["counter".as_bytes(), movie_review.key().as_ref()],
    bump,
    )]
    pub movie_comment_counter: Account<'info, MovieCommentCounter>,
    #[account(mut,
    seeds = ["mint".as_bytes()],
    bump
    )]
    pub reward_mint: Account<'info, Mint>,
    #[account(
    init_if_needed,
    payer = initializer,
    associated_token::mint = reward_mint,
    associated_token::authority = initializer
    )]
    pub token_account: Account<'info, TokenAccount>,
    #[account(mut)]
    pub initializer: Signer<'info>,
    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub rent: Sysvar<'info, Rent>,
    pub system_program: Program<'info, System>,
    }

    构建,部署,测试

    解决方案:https://beta.solpg.io/6319c7bf77ea7f12846aee87

    如果你使用自己的编辑器,你必须在 mpl-token-metadataCargo.toml 中添加 features = ["no-entrypoint"]

    否则,将会出现以下错误:the #[global_allocator] in this crate conflicts with global allocator in: mpl_token_metadata

    • 构建和部署
    • 使用 SolPG 进行测试
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module5/program-in-anchor/build-with-anchor-pdas/index.html b/Solana-Co-Learn/module5/program-in-anchor/build-with-anchor-pdas/index.html index 9546c93ab..57cbd5406 100644 --- a/Solana-Co-Learn/module5/program-in-anchor/build-with-anchor-pdas/index.html +++ b/Solana-Co-Learn/module5/program-in-anchor/build-with-anchor-pdas/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    使用Anchor PDA进行构建

    在深入讨论CPI之前,让我们展示一下这些PDA的魅力吧!🎸

    我们将利用Anchor框架创建一个电影评论程序。

    该程序将让用户能够:

    • 使用PDA初始化一个新的电影评论账户,用于存放评论
    • 更新现有电影评论账户中的内容
    • 关闭现有的电影评论账户

    设置流程

    请访问https://beta.solpg.io/,如果你还没有SolPG钱包,请按照提示创建一个。然后,将lib.rs中的默认代码替换为以下内容:

    use anchor_lang::prelude::*;

    declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

    #[program]
    pub mod movie_review {
    use super::*;

    }

    🎥 电影账户状态(MovieAccountState)

    我们首先要做的是定义State账户。

    use anchor_lang::prelude::*;

    declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

    #[program]
    pub mod movie_review {
    use super::*;

    }

    #[account]
    pub struct MovieAccountState {
    pub reviewer: Pubkey, // 评论者
    pub rating: u8, // 评分
    pub title: String, // 标题
    pub description: String, // 描述
    }

    每个电影评论账户将包含以下信息:

    • reviewer - 进行评论的用户
    • rating - 对电影的评分
    • title - 电影的标题
    • description - 评论的具体内容

    到现在为止,一切都相当简洁明了!

    🎬 添加电影评论

    感谢 Anchor 的便利性,我们可以轻松跳过所有的验证和安全检查,直接添加add_movie_review功能:

    #[program]
    pub mod movie_review{
    use super::*;

    pub fn add_movie_review(
    ctx: Context<AddMovieReview>,
    title: String,
    description: String,
    rating: u8,
    ) -> Result<()> {
    msg!("创建了电影评论账户");
    msg!("标题:{}", title);
    msg!("描述:{}", description);
    msg!("评分:{}", rating);

    let movie_review = &mut ctx.accounts.movie_review;
    movie_review.reviewer = ctx.accounts.initializer.key();
    movie_review.title = title;
    movie_review.rating = rating;
    movie_review.description = description;
    Ok(())
    }
    }

    ...

    这些操作对你应该不陌生——这只是我们构建的本地电影评论程序的精简版。

    现在,让我们为此添加Context

    #[program]
    pub mod movie_review {
    use super::*;

    ...
    }

    #[derive(Accounts)]
    #[instruction(title: String, description: String)]
    pub struct AddMovieReview<'info> {
    #[account(
    init,
    seeds = [title.as_bytes(), initializer.key().as_ref()],
    bump,
    payer = initializer,
    space = 8 + 32 + 1 + 4 + title.len() + 4 + description.len()
    )]
    pub movie_review: Account<'info, MovieAccountState>,
    #[account(mut)]
    pub initializer: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    ...

    再次强调,我们正以与本地操作完全相同的方式进行操作,但这次我们可以借助Anchor的力量。

    我们正在使用两个seeds来初始化一个新的movie_review账户:

    • title - 指令数据中的电影标题
    • initializer.key() - 创建电影评论的initializer的公钥

    此外,我们还根据space账户类型的结构将资金分配到新账户中。

    🎞 更新电影评论

    没有必要对这个小程序进行测试,我们可以直接完成它!下面是更新函数的代码示例:

    #[program]
    pub mod movie_review {
    use super::*;

    ...

    pub fn update_movie_review(
    ctx: Context<UpdateMovieReview>,
    title: String,
    description: String,
    rating: u8,
    ) -> Result<()> {
    msg!("正在更新电影评论账户");
    msg!("标题:{}", title);
    msg!("描述:{}", description);
    msg!("评分:{}", rating);

    let movie_review = &mut ctx.accounts.movie_review;
    movie_review.rating = rating;
    movie_review.description = description;

    return Ok(());
    }

    }

    ...

    数据参数与add_movie_review相同,主要区别在于我们传入的Context。现在我们来定义它:

    #[program]
    pub mod movie_review {
    use super::*;

    ...
    }

    #[derive(Accounts)]
    #[instruction(title: String, description: String)]
    pub struct UpdateMovieReview<'info> {
    #[account(
    mut,
    seeds = [title.as_bytes(), initializer.key().as_ref()],
    bump,
    realloc = 8 + 32 + 1 + 4 + title.len() + 4 + description.len(),
    realloc::payer = initializer,
    realloc::zero = true,
    )]
    pub movie_review: Account<'info, MovieAccountState>,
    #[account(mut)]
    pub initializer: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    ...

    我们使用seedsbump约束来验证movie_review账户。由于可能会有空间的变化,所以我们使用了realloc约束,让Anchor根据更新后的描述长度来自动处理账户空间和租金的重新分配。

    realloc::payer约束规定了所需的额外lamports将来自或发送到初始化账户。

    realloc::zero约束被设置为true,这是因为movie_review账户可能会多次更新,无论是缩小还是扩大分配给该账户的空间都可以灵活应对。

    ❌ 关闭电影评论

    最后一部分是实现close指令,用以关闭已存在的movie_review账户。我们只需要Context类型的Close,不需要其他任何数据!

    #[program]
    pub mod movie_review {
    use super::*;

    ...

    pub fn close(_ctx: Context<Close>) -> Result<()> {
    Ok(())
    }

    }

    ...

    关于这个的Context定义:

    #[program]
    pub mod movie_review {
    use super::*;

    ...
    }

    #[derive(Accounts)]
    pub struct Close<'info> {
    #[account(mut, close = reviewer, has_one = reviewer)]
    movie_review: Account<'info, MovieAccountState>,
    #[account(mut)]
    reviewer: Signer<'info>,
    }

    ...

    我们使用close约束来指明我们要关闭的是movie_review账户,并且租金应退还到reviewer账户。

    has_one约束用于限制关闭账户操作 - reviewer账户必须与电影评论账户上的reviewer相匹配。

    我们完成了!试一下,它应该会像之前的本地电影评论程序一样运行。如果有任何问题,你可以与此处的解决方案代码进行对比 :)

    🚢 挑战(这部分内容和build with solana Framework的内容重复了)

    现在轮到你亲自构建一些内容了。由于我们从一个非常简单的程序开始,你所创建的程序将与我们刚刚创建的程序几乎完全相同。请尽量不要在这里复制粘贴,努力达到能够独立编写代码的程度。

    • 编写一个新程序,初始化一个counter账户,并使用传入指令数据参数来设置count字段。
    • 执行initializeincrementdecrement指令。
    • 按照我们在演示中的做法,为每个指令编写测试。
    • 使用anchor deploy来部署你的程序。如果你愿意,你可以像之前那样编写一个脚本来发送交易到你新部署的程序,然后使用Solana Explorer来查看程序日志。

    像往常一样,对这些挑战充满创意,超越基本指示,如果你愿意,可以发挥你的想象力!

    如果可能的话,请尽量独立完成这个任务!但如果遇到困难,你可以参考这个存储库solution-decrement分支。

    - - +
    Skip to main content

    使用Anchor PDA进行构建

    在深入讨论CPI之前,让我们展示一下这些PDA的魅力吧!🎸

    我们将利用Anchor框架创建一个电影评论程序。

    该程序将让用户能够:

    • 使用PDA初始化一个新的电影评论账户,用于存放评论
    • 更新现有电影评论账户中的内容
    • 关闭现有的电影评论账户

    设置流程

    请访问https://beta.solpg.io/,如果你还没有SolPG钱包,请按照提示创建一个。然后,将lib.rs中的默认代码替换为以下内容:

    use anchor_lang::prelude::*;

    declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

    #[program]
    pub mod movie_review {
    use super::*;

    }

    🎥 电影账户状态(MovieAccountState)

    我们首先要做的是定义State账户。

    use anchor_lang::prelude::*;

    declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

    #[program]
    pub mod movie_review {
    use super::*;

    }

    #[account]
    pub struct MovieAccountState {
    pub reviewer: Pubkey, // 评论者
    pub rating: u8, // 评分
    pub title: String, // 标题
    pub description: String, // 描述
    }

    每个电影评论账户将包含以下信息:

    • reviewer - 进行评论的用户
    • rating - 对电影的评分
    • title - 电影的标题
    • description - 评论的具体内容

    到现在为止,一切都相当简洁明了!

    🎬 添加电影评论

    感谢 Anchor 的便利性,我们可以轻松跳过所有的验证和安全检查,直接添加add_movie_review功能:

    #[program]
    pub mod movie_review{
    use super::*;

    pub fn add_movie_review(
    ctx: Context<AddMovieReview>,
    title: String,
    description: String,
    rating: u8,
    ) -> Result<()> {
    msg!("创建了电影评论账户");
    msg!("标题:{}", title);
    msg!("描述:{}", description);
    msg!("评分:{}", rating);

    let movie_review = &mut ctx.accounts.movie_review;
    movie_review.reviewer = ctx.accounts.initializer.key();
    movie_review.title = title;
    movie_review.rating = rating;
    movie_review.description = description;
    Ok(())
    }
    }

    ...

    这些操作对你应该不陌生——这只是我们构建的本地电影评论程序的精简版。

    现在,让我们为此添加Context

    #[program]
    pub mod movie_review {
    use super::*;

    ...
    }

    #[derive(Accounts)]
    #[instruction(title: String, description: String)]
    pub struct AddMovieReview<'info> {
    #[account(
    init,
    seeds = [title.as_bytes(), initializer.key().as_ref()],
    bump,
    payer = initializer,
    space = 8 + 32 + 1 + 4 + title.len() + 4 + description.len()
    )]
    pub movie_review: Account<'info, MovieAccountState>,
    #[account(mut)]
    pub initializer: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    ...

    再次强调,我们正以与本地操作完全相同的方式进行操作,但这次我们可以借助Anchor的力量。

    我们正在使用两个seeds来初始化一个新的movie_review账户:

    • title - 指令数据中的电影标题
    • initializer.key() - 创建电影评论的initializer的公钥

    此外,我们还根据space账户类型的结构将资金分配到新账户中。

    🎞 更新电影评论

    没有必要对这个小程序进行测试,我们可以直接完成它!下面是更新函数的代码示例:

    #[program]
    pub mod movie_review {
    use super::*;

    ...

    pub fn update_movie_review(
    ctx: Context<UpdateMovieReview>,
    title: String,
    description: String,
    rating: u8,
    ) -> Result<()> {
    msg!("正在更新电影评论账户");
    msg!("标题:{}", title);
    msg!("描述:{}", description);
    msg!("评分:{}", rating);

    let movie_review = &mut ctx.accounts.movie_review;
    movie_review.rating = rating;
    movie_review.description = description;

    return Ok(());
    }

    }

    ...

    数据参数与add_movie_review相同,主要区别在于我们传入的Context。现在我们来定义它:

    #[program]
    pub mod movie_review {
    use super::*;

    ...
    }

    #[derive(Accounts)]
    #[instruction(title: String, description: String)]
    pub struct UpdateMovieReview<'info> {
    #[account(
    mut,
    seeds = [title.as_bytes(), initializer.key().as_ref()],
    bump,
    realloc = 8 + 32 + 1 + 4 + title.len() + 4 + description.len(),
    realloc::payer = initializer,
    realloc::zero = true,
    )]
    pub movie_review: Account<'info, MovieAccountState>,
    #[account(mut)]
    pub initializer: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    ...

    我们使用seedsbump约束来验证movie_review账户。由于可能会有空间的变化,所以我们使用了realloc约束,让Anchor根据更新后的描述长度来自动处理账户空间和租金的重新分配。

    realloc::payer约束规定了所需的额外lamports将来自或发送到初始化账户。

    realloc::zero约束被设置为true,这是因为movie_review账户可能会多次更新,无论是缩小还是扩大分配给该账户的空间都可以灵活应对。

    ❌ 关闭电影评论

    最后一部分是实现close指令,用以关闭已存在的movie_review账户。我们只需要Context类型的Close,不需要其他任何数据!

    #[program]
    pub mod movie_review {
    use super::*;

    ...

    pub fn close(_ctx: Context<Close>) -> Result<()> {
    Ok(())
    }

    }

    ...

    关于这个的Context定义:

    #[program]
    pub mod movie_review {
    use super::*;

    ...
    }

    #[derive(Accounts)]
    pub struct Close<'info> {
    #[account(mut, close = reviewer, has_one = reviewer)]
    movie_review: Account<'info, MovieAccountState>,
    #[account(mut)]
    reviewer: Signer<'info>,
    }

    ...

    我们使用close约束来指明我们要关闭的是movie_review账户,并且租金应退还到reviewer账户。

    has_one约束用于限制关闭账户操作 - reviewer账户必须与电影评论账户上的reviewer相匹配。

    我们完成了!试一下,它应该会像之前的本地电影评论程序一样运行。如果有任何问题,你可以与此处的解决方案代码进行对比 :)

    🚢 挑战(这部分内容和build with solana Framework的内容重复了)

    现在轮到你亲自构建一些内容了。由于我们从一个非常简单的程序开始,你所创建的程序将与我们刚刚创建的程序几乎完全相同。请尽量不要在这里复制粘贴,努力达到能够独立编写代码的程度。

    • 编写一个新程序,初始化一个counter账户,并使用传入指令数据参数来设置count字段。
    • 执行initializeincrementdecrement指令。
    • 按照我们在演示中的做法,为每个指令编写测试。
    • 使用anchor deploy来部署你的程序。如果你愿意,你可以像之前那样编写一个脚本来发送交易到你新部署的程序,然后使用Solana Explorer来查看程序日志。

    像往常一样,对这些挑战充满创意,超越基本指示,如果你愿意,可以发挥你的想象力!

    如果可能的话,请尽量独立完成这个任务!但如果遇到困难,你可以参考这个存储库solution-decrement分支。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module5/program-in-anchor/cpis-in-anchor/index.html b/Solana-Co-Learn/module5/program-in-anchor/cpis-in-anchor/index.html index fe9816d23..67eb37afb 100644 --- a/Solana-Co-Learn/module5/program-in-anchor/cpis-in-anchor/index.html +++ b/Solana-Co-Learn/module5/program-in-anchor/cpis-in-anchor/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🔀 Anchor的CPIs

    现在我们可以通过添加CPI(跨程序调用)来提升我们的代码水平。

    首先回顾一下,CPI是通过使用invokeinvoke_signed方法来制作的。

    Anchor框架还提供了一种特殊的CPI制作格式。要使用这种格式,你需要访问所调用程序的CPI模块。一些常见的程序可能会有现成的包供你使用,例如anchor_spl,这可以用于令牌程序。否则,你将需要使用所调用程序的源代码或已发布的IDL(接口定义语言)来生成CPI模块。

    如果没有现成的CPI模块,你仍然可以直接在指令中使用invokeinvoke_signed方法。正如Anchor指令需要Context类型一样,Anchor CPI则使用CpiContext类型。

    CpiContext提供了执行指令所需的所有账户和种子信息。当不需要PDA(程序衍生账户)签名者时,使用CpiContext::new

    CpiContext::new(cpi_program, cpi_accounts)

    当需要一个PDA作为签名者时,使用CpiContext::new_with_signer

    CpiContext::new_with_signer(cpi_program, cpi_accounts, seeds)
    • accounts - 账户列表
    • remaining_accounts - 如果有的话
    • program - 正在调用CPI的程序
    • signer_seeds - 如果需要使用PDA签署CPI
    pub struct CpiContext<'a, 'b, 'c, 'info, T>
    where
    T: ToAccountMetas + ToAccountInfos<'info>,
    {
    pub accounts: T,
    pub remaining_accounts: Vec<AccountInfo<'info>>,
    pub program: AccountInfo<'info>,
    pub signer_seeds: &'a [&'b [&'c [u8]]],
    }

    当不需要signer_seeds时使用CpiContext::new(不使用PDA签名)。

    pub fn new(
    program: AccountInfo<'info>,
    accounts: T
    ) -> Self {
    Self {
    accounts,
    program,
    remaining_accounts: Vec::new(),
    signer_seeds: &[],
    }
    }

    CpiContext::new_with_signer用于在PDA上用种子签名。

    pub fn new_with_signer(
    program: AccountInfo<'info>,
    accounts: T,
    signer_seeds: &'a [&'b [&'c [u8]]],
    ) -> Self {
    Self {
    accounts,
    program,
    signer_seeds,
    remaining_accounts: Vec::new(),
    }
    }

    anchor_spl包还包括了一个token模块,用于简化创建到令牌程序的CPI的过程。

    在这里,“Structs”指的是每个相应的令牌程序指令所需的账户列表。“Functions”指的是每个相应指令的CPI

    例如,下面的MintTo就是所需的账户:

    #[derive(Accounts)]
    pub struct MintTo<'info> {
    pub mint: AccountInfo<'info>,
    pub to: AccountInfo<'info>,
    pub authority: AccountInfo<'info>,
    }

    我们也可以深入了解一下mint_to方法的内部工作原理。

    它使用CpiContext来构建一个到mint_to指令的CPI,并使用invoke_signed来执行CPI

    pub fn mint_to<'a, 'b, 'c, 'info>(
    ctx: CpiContext<'a, 'b, 'c, 'info, MintTo<'info>>,
    amount: u64,
    ) -> Result<()> {
    let ix = spl_token::instruction::mint_to(
    &spl_token::ID,
    ctx.accounts.mint.key,
    ctx.accounts.to.key,
    ctx.accounts.authority.key,
    &[],
    amount,
    )?;
    solana_program::program::invoke_signed(
    &ix,
    &[
    ctx.accounts.to.clone(),
    ctx.accounts.mint.clone(),
    ctx.accounts.authority.clone(),
    ],
    ctx.signer_seeds,
    )
    .map_err(Into::into)
    }

    例如:

    • 使用 mint_to CPI 来铸造代币
    let auth_bump = *ctx.bumps.get("mint_authority").unwrap();
    let seeds = &[
    b"mint".as_ref(),
    &[auth_bump],
    ];
    let signer = &[&seeds[..]];

    let cpi_program = ctx.accounts.token_program.to_account_info();

    let cpi_accounts = MintTo {
    mint: ctx.accounts.token_mint.to_account_info(),
    to: ctx.accounts.token_account.to_account_info(),
    authority: ctx.accounts.mint_authority.to_account_info()
    };

    let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);

    token::mint_to(cpi_ctx, amount)?;

    我们可以重构这个代码段,得到:

    token::mint_to(
    CpiContext::new_with_signer(
    ctx.accounts.token_program.to_account_info(),
    token::MintTo {
    mint: ctx.accounts.mint_account.to_account_info(),
    to: ctx.accounts.token_account.to_account_info(),
    authority: ctx.accounts.mint_authority.to_account_info(),
    },
    &[&[
    b"mint",
    &[*ctx.bumps.get("mint_authority").unwrap()],
    ]]
    ),
    amount,
    )?;

    ❌ Anchor 错误处理

    错误可以分为以下几种类型:

    • 来自 Anchor 框架自身代码的内部错误
    • 用户(也就是你!)定义的自定义错误

    AnchorErrors 能提供许多有关错误的信息,例如:

    • 错误的名称和编号
    • 错误在代码中的位置
    • 违反的约束条件和相关账户

    最后,所有程序会返回一个通用的错误:ProgramError

    Anchor 有许多不同的内部错误代码。虽然这些代码不是为用户所设计,但通过研究可以了解代码和其背后原因的关联,这对理解很有帮助。

    自定义错误代码的编号将从自定义错误偏移量开始。

    你可以使用 error_code 属性为你的程序定义独特的错误。只需将其添加到所选枚举中即可。然后,你可以在程序中将枚举的变体用作错误。

    此外,你还可以使用 msg 为各个变体定义消息。如果发生错误,客户端将显示此错误消息。要实际触发错误,请使用 err!error! 宏。这些宏会将文件和行信息添加到错误中,然后由 anchor 记录。

    #[program]
    mod hello_anchor {
    use super::*;
    pub fn set_data(ctx: Context<SetData>, data: MyAccount) -> Result<()> {
    if data.data >= 100 {
    return err!(MyError::DataTooLarge);
    }
    ctx.accounts.my_account.set_inner(data);
    Ok(())
    }
    }

    #[error_code]
    pub enum MyError {
    #[msg("MyAccount 的数据只能小于 100")]
    DataTooLarge
    }

    你还可以使用 require 宏来简化错误的编写。上面的代码可以简化为下面的样子(注意 >= 翻转为 < )。

    #[program]
    mod hello_anchor {
    use super::*;
    pub fn set_data(ctx: Context<SetData>, data: MyAccount) -> Result<()> {
    require!(data.data < 100, MyError::DataTooLarge);
    ctx.accounts.my_account.set_inner(data);
    Ok(())
    }
    }

    #[error_code]
    pub enum MyError {
    #[msg("MyAccount 的数据只能小于 100")]
    DataTooLarge
    }

    constraint 约束条件

    如果账户不存在,系统将初始化一个账户。如果账户已存在,仍需检查其他的限制条件。

    如果你在使用自定义的编辑器,请确保在 anchor-langCargo.toml 文件中添加了 features = ["init-if-needed"] 特性。

    例如:anchor-lang = {version = "0.26.0", features = ["init-if-needed"]}

    下面是一个关联令牌账户的示例代码:

    #[program]
    mod example {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    Ok(())
    }
    }

    #[derive(Accounts)]
    pub struct Initialize<'info> {
    #[account(
    init_if_needed,
    payer = payer,
    associated_token::mint = mint,
    associated_token::authority = payer
    )]
    pub token_account: Account<'info, TokenAccount>,
    pub mint: Account<'info, Mint>,
    #[account(mut)]
    pub payer: Signer<'info>,
    pub system_program: Program<'info, System>,
    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub rent: Sysvar<'info, Rent>,
    }

    以下是 init_if_needed 生成的代码(这段代码片段来自 anchor expand 命令):

    let token_account: anchor_lang::accounts::account::Account<TokenAccount> = {
    if !true
    || AsRef::<AccountInfo>::as_ref(&token_account).owner
    == &anchor_lang::solana_program::system_program::ID
    {
    let payer = payer.to_account_info();
    let cpi_program = associated_token_program.to_account_info();
    let cpi_accounts = anchor_spl::associated_token::Create {
    payer: payer.to_account_info(),
    associated_token: token_account.to_account_info(),
    authority: payer.to_account_info(),
    mint: mint.to_account_info(),
    system_program: system_program.to_account_info(),
    token_program: token_program.to_account_info(),
    rent: rent.to_account_info(),
    };
    let cpi_ctx = anchor_lang::context::CpiContext::new(
    cpi_program,
    cpi_accounts,
    );
    anchor_spl::associated_token::create(cpi_ctx)?;
    }
    ...
    }

    通过这个约束条件,可以确保在初始化时根据需要创建关联的令牌账户,使得整个流程更加自动化和智能。

    - - +
    Skip to main content

    🔀 Anchor的CPIs

    现在我们可以通过添加CPI(跨程序调用)来提升我们的代码水平。

    首先回顾一下,CPI是通过使用invokeinvoke_signed方法来制作的。

    Anchor框架还提供了一种特殊的CPI制作格式。要使用这种格式,你需要访问所调用程序的CPI模块。一些常见的程序可能会有现成的包供你使用,例如anchor_spl,这可以用于令牌程序。否则,你将需要使用所调用程序的源代码或已发布的IDL(接口定义语言)来生成CPI模块。

    如果没有现成的CPI模块,你仍然可以直接在指令中使用invokeinvoke_signed方法。正如Anchor指令需要Context类型一样,Anchor CPI则使用CpiContext类型。

    CpiContext提供了执行指令所需的所有账户和种子信息。当不需要PDA(程序衍生账户)签名者时,使用CpiContext::new

    CpiContext::new(cpi_program, cpi_accounts)

    当需要一个PDA作为签名者时,使用CpiContext::new_with_signer

    CpiContext::new_with_signer(cpi_program, cpi_accounts, seeds)
    • accounts - 账户列表
    • remaining_accounts - 如果有的话
    • program - 正在调用CPI的程序
    • signer_seeds - 如果需要使用PDA签署CPI
    pub struct CpiContext<'a, 'b, 'c, 'info, T>
    where
    T: ToAccountMetas + ToAccountInfos<'info>,
    {
    pub accounts: T,
    pub remaining_accounts: Vec<AccountInfo<'info>>,
    pub program: AccountInfo<'info>,
    pub signer_seeds: &'a [&'b [&'c [u8]]],
    }

    当不需要signer_seeds时使用CpiContext::new(不使用PDA签名)。

    pub fn new(
    program: AccountInfo<'info>,
    accounts: T
    ) -> Self {
    Self {
    accounts,
    program,
    remaining_accounts: Vec::new(),
    signer_seeds: &[],
    }
    }

    CpiContext::new_with_signer用于在PDA上用种子签名。

    pub fn new_with_signer(
    program: AccountInfo<'info>,
    accounts: T,
    signer_seeds: &'a [&'b [&'c [u8]]],
    ) -> Self {
    Self {
    accounts,
    program,
    signer_seeds,
    remaining_accounts: Vec::new(),
    }
    }

    anchor_spl包还包括了一个token模块,用于简化创建到令牌程序的CPI的过程。

    在这里,“Structs”指的是每个相应的令牌程序指令所需的账户列表。“Functions”指的是每个相应指令的CPI

    例如,下面的MintTo就是所需的账户:

    #[derive(Accounts)]
    pub struct MintTo<'info> {
    pub mint: AccountInfo<'info>,
    pub to: AccountInfo<'info>,
    pub authority: AccountInfo<'info>,
    }

    我们也可以深入了解一下mint_to方法的内部工作原理。

    它使用CpiContext来构建一个到mint_to指令的CPI,并使用invoke_signed来执行CPI

    pub fn mint_to<'a, 'b, 'c, 'info>(
    ctx: CpiContext<'a, 'b, 'c, 'info, MintTo<'info>>,
    amount: u64,
    ) -> Result<()> {
    let ix = spl_token::instruction::mint_to(
    &spl_token::ID,
    ctx.accounts.mint.key,
    ctx.accounts.to.key,
    ctx.accounts.authority.key,
    &[],
    amount,
    )?;
    solana_program::program::invoke_signed(
    &ix,
    &[
    ctx.accounts.to.clone(),
    ctx.accounts.mint.clone(),
    ctx.accounts.authority.clone(),
    ],
    ctx.signer_seeds,
    )
    .map_err(Into::into)
    }

    例如:

    • 使用 mint_to CPI 来铸造代币
    let auth_bump = *ctx.bumps.get("mint_authority").unwrap();
    let seeds = &[
    b"mint".as_ref(),
    &[auth_bump],
    ];
    let signer = &[&seeds[..]];

    let cpi_program = ctx.accounts.token_program.to_account_info();

    let cpi_accounts = MintTo {
    mint: ctx.accounts.token_mint.to_account_info(),
    to: ctx.accounts.token_account.to_account_info(),
    authority: ctx.accounts.mint_authority.to_account_info()
    };

    let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);

    token::mint_to(cpi_ctx, amount)?;

    我们可以重构这个代码段,得到:

    token::mint_to(
    CpiContext::new_with_signer(
    ctx.accounts.token_program.to_account_info(),
    token::MintTo {
    mint: ctx.accounts.mint_account.to_account_info(),
    to: ctx.accounts.token_account.to_account_info(),
    authority: ctx.accounts.mint_authority.to_account_info(),
    },
    &[&[
    b"mint",
    &[*ctx.bumps.get("mint_authority").unwrap()],
    ]]
    ),
    amount,
    )?;

    ❌ Anchor 错误处理

    错误可以分为以下几种类型:

    • 来自 Anchor 框架自身代码的内部错误
    • 用户(也就是你!)定义的自定义错误

    AnchorErrors 能提供许多有关错误的信息,例如:

    • 错误的名称和编号
    • 错误在代码中的位置
    • 违反的约束条件和相关账户

    最后,所有程序会返回一个通用的错误:ProgramError

    Anchor 有许多不同的内部错误代码。虽然这些代码不是为用户所设计,但通过研究可以了解代码和其背后原因的关联,这对理解很有帮助。

    自定义错误代码的编号将从自定义错误偏移量开始。

    你可以使用 error_code 属性为你的程序定义独特的错误。只需将其添加到所选枚举中即可。然后,你可以在程序中将枚举的变体用作错误。

    此外,你还可以使用 msg 为各个变体定义消息。如果发生错误,客户端将显示此错误消息。要实际触发错误,请使用 err!error! 宏。这些宏会将文件和行信息添加到错误中,然后由 anchor 记录。

    #[program]
    mod hello_anchor {
    use super::*;
    pub fn set_data(ctx: Context<SetData>, data: MyAccount) -> Result<()> {
    if data.data >= 100 {
    return err!(MyError::DataTooLarge);
    }
    ctx.accounts.my_account.set_inner(data);
    Ok(())
    }
    }

    #[error_code]
    pub enum MyError {
    #[msg("MyAccount 的数据只能小于 100")]
    DataTooLarge
    }

    你还可以使用 require 宏来简化错误的编写。上面的代码可以简化为下面的样子(注意 >= 翻转为 < )。

    #[program]
    mod hello_anchor {
    use super::*;
    pub fn set_data(ctx: Context<SetData>, data: MyAccount) -> Result<()> {
    require!(data.data < 100, MyError::DataTooLarge);
    ctx.accounts.my_account.set_inner(data);
    Ok(())
    }
    }

    #[error_code]
    pub enum MyError {
    #[msg("MyAccount 的数据只能小于 100")]
    DataTooLarge
    }

    constraint 约束条件

    如果账户不存在,系统将初始化一个账户。如果账户已存在,仍需检查其他的限制条件。

    如果你在使用自定义的编辑器,请确保在 anchor-langCargo.toml 文件中添加了 features = ["init-if-needed"] 特性。

    例如:anchor-lang = {version = "0.26.0", features = ["init-if-needed"]}

    下面是一个关联令牌账户的示例代码:

    #[program]
    mod example {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    Ok(())
    }
    }

    #[derive(Accounts)]
    pub struct Initialize<'info> {
    #[account(
    init_if_needed,
    payer = payer,
    associated_token::mint = mint,
    associated_token::authority = payer
    )]
    pub token_account: Account<'info, TokenAccount>,
    pub mint: Account<'info, Mint>,
    #[account(mut)]
    pub payer: Signer<'info>,
    pub system_program: Program<'info, System>,
    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub rent: Sysvar<'info, Rent>,
    }

    以下是 init_if_needed 生成的代码(这段代码片段来自 anchor expand 命令):

    let token_account: anchor_lang::accounts::account::Account<TokenAccount> = {
    if !true
    || AsRef::<AccountInfo>::as_ref(&token_account).owner
    == &anchor_lang::solana_program::system_program::ID
    {
    let payer = payer.to_account_info();
    let cpi_program = associated_token_program.to_account_info();
    let cpi_accounts = anchor_spl::associated_token::Create {
    payer: payer.to_account_info(),
    associated_token: token_account.to_account_info(),
    authority: payer.to_account_info(),
    mint: mint.to_account_info(),
    system_program: system_program.to_account_info(),
    token_program: token_program.to_account_info(),
    rent: rent.to_account_info(),
    };
    let cpi_ctx = anchor_lang::context::CpiContext::new(
    cpi_program,
    cpi_accounts,
    );
    anchor_spl::associated_token::create(cpi_ctx)?;
    }
    ...
    }

    通过这个约束条件,可以确保在初始化时根据需要创建关联的令牌账户,使得整个流程更加自动化和智能。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module5/program-in-anchor/index.html b/Solana-Co-Learn/module5/program-in-anchor/index.html index 56e622cf4..719e4c0fd 100644 --- a/Solana-Co-Learn/module5/program-in-anchor/index.html +++ b/Solana-Co-Learn/module5/program-in-anchor/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module5/program-in-anchor/pdas-in-anchor/index.html b/Solana-Co-Learn/module5/program-in-anchor/pdas-in-anchor/index.html index 5ba10a8a0..baf3fe131 100644 --- a/Solana-Co-Learn/module5/program-in-anchor/pdas-in-anchor/index.html +++ b/Solana-Co-Learn/module5/program-in-anchor/pdas-in-anchor/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🛣 Anchor中的PDA(程序派生地址)

    你做得很好!让我们继续深入探讨。

    本课程中,我们将深入探讨如何使用#[account(...)]属性,并深入了解以下限制条件:

    • seedsbump - 初始化和验证PDA
    • realloc - 重新分配账户空间
    • close - 关闭账户

    🛣 Anchor里的PDAs

    我们再次回顾一下,PDA是通过一系列可选的种子、一个bump seed和一个 programId来衍生的。Anchor提供了一种方便的方式来验证带有seedsbump限制的PDA

    #[account(seeds = [], bump)]
    pub pda_account: Account<'info, AccountType>,

    在账户验证过程中,Anchor会使用seeds约束中指定的种子生成一个PDA,并确认传入指令的账户是否与找到的PDA匹配。

    当包含bump约束,但未指定具体的bump时,Anchor将默认使用规范bump(即找到有效PDA的第一个bump)。

    #[derive(Accounts)]
    #[instruction(instruction_data: String)]
    pub struct Example<'info> {
    #[account(seeds = [b"example-seed", user.key().as_ref(), instruction_data.as_ref()]
    pub pad_account: Account<'info, AccountType>,
    #[account(mut)]
    pub user: Signer<'info>,
    }

    在此示例中,通过seedbump约束验证pda_account的地址是否是预期的PDA

    推导PDAseeds包括:

    • example_seed - 一个硬编码的字符串值
    • user.key() - 传入账户的公钥 user
    • instruction_data - 传入指令的数据
      • 你可以通过#[instruction(...)]属性来访问这些数据
    pub fn example_instruction(
    ctx: Context<Example>,
    input_one: String,
    input_two: String,
    input_three: String,
    ) -> Result<()> {
    // ....
    Ok(()
    }

    #[derive(Accounts)]
    #[instruction(input_one: String, input_two: String)]
    pub struct Example<'info> {
    // ...
    }
    • 使用#[instruction(...)]属性时,指令数据必须按照传入指令的顺序排列
    • 你可以忽略不需要的最后一个参数及其之后的所有参数
    #[derive(Accounts)]
    #[instruction(input_one: String, input_two: String)]
    pub struct Example<'info> {
    // ...
    }

    如果输入顺序错误,将会导致错误

    #[derive(Accounts)]
    pub struct InitializedPda<'info> {
    #[account(
    init,
    seeds = [b"example_seed", user.key().as_ref()]
    bump,
    payer = user,
    space = 8 + 8
    )]
    pub pda_account: Account<'info, AccountType>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    #[account]
    pub struct AccountType {
    pub data: u64
    }

    你可以将init约束与seedsbump约束组合,以使用PDA初始化账户。

    init约束必须与以下内容结合使用:

    • payer - 指定用于支付初始化费用的账户
    • space - 新账户所分配的空间大小
    • system_program - 在账户验证结构中必须存在的system_program

    默认情况下,init会将创建账户的所有者设置为当前正在执行的程序。

    • 当使用initseedsbump初始化PDA账户时,所有者必须是正在执行的程序
    • 这是因为创建账户需要签名,只有执行程序的PDA才能提供
    • 如果用于派生PDAprogramId与正在执行的程序的programId不匹配,则PDA账户初始化的签名验证将失败
    • 因为init使用find_program_address来推导PDA,所以不需要指定bump
    • 这意味着PDA将使用规范的bump进行推导
    • 在为执行Anchor程序所初始化和拥有的账户分配space时,请记住前8个字节是保留给唯一账户discriminator的,Anchor程序使用该discriminator来识别程序账户类型

    🧮 重新分配

    在许多情况下,你可能需要更新现有账户而不是创建新账户。Anchor提供了出色的realloc约束,为现有账户重新分配空间提供了一种简便的方法。

    #[derive(Accounts)]
    #[instruction(instruction_data: String)]
    pub struct ReallocExampl<'info> {
    #[account(
    mut,
    seeds = [b"example_seed", user.key().as_ref()]
    bump,
    realloc = 8 + 4 + instruction_data.len(),
    realloc::payer = user,
    realloc::zero = false,
    )]
    pub pda_account: Account<'info, AccountType>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    #[account]
    pub struct AccountType {
    pub data: u64
    }

    realloc约束必须与以下内容结合使用:

    • mut - 账户必须设置为可变
    • realloc::payer - 账户空间的增加或减少将相应增加或减少账户的lamports
    • realloc::zero - 一个布尔值,用于指定是否应将新内存初始化为零
    • system_program - realloc约束要求在账户验证结构中存在system_program

    例如,重新分配用于存储String类型字段的账户的空间。

    • 使用String类型时,除了String本身所需的空间外,还需要额外的4个字节来存储String的长度
    • 如果账户数据长度是增加的,为了保持租金豁免,Lamport将从realloc::payer转移到程序账户
    • 如果长度减少,Lamport将从程序账户转回realloc::payer
    • 需要realloc::zero约束来确定重新分配后是否应对新内存进行零初始化
    • 在之前减小过空间的账户上增加空间时,应将此约束设置为true

    close 关闭操作

    当你用完一个账户并不再需要它时会发生什么呢?你可以将它关闭!

    通过这样做,你可以腾出空间,并收回用于支付租金的SOL

    执行关闭操作是通过使用 close 约束来完成的:

    pub fn close(ctx: Context<Close>) -> Result<()> {
    Ok(())
    }

    #[derive(Accounts)]
    pub struct Close<'info> {
    #[account(mut, close = receiver)]
    pub data_account: Account<'info, AccountType>,
    #[account(mut)]
    pub receiver: Signer<'info>,
    }
    • close 约束会在指令执行结束时将账户标记为已关闭,并通过将其discriminator设置为 CLOSED_ACCOUNT_DISCRIMINATOR,同时将其 lamports 发送到特定的账户。
    • discriminator设置为特定的变量,以阻止账户复活攻击(例如,后续指令重新添加租金豁免的lamports)。
    • 我们将关闭名为 data_account 的账户,并将用于租金的lamports发送到名为 receiver 的账户。
    • 然而,目前任何人都可以调用关闭指令并关闭 data_account
    pub fn close(ctx: Context<Close>) -> Result<()> {
    Ok(())
    }

    #[derive(Accounts)]
    pub struct Close<'info> {
    #[account(mut, close = receiver, has_one = receiver)]
    pub data_account: Account<'info, AccountType>,
    #[account(mut)]
    pub receiver: Signer<'info>,
    }

    #[account]
    pub struct AccountType {
    pub data: String,
    pub receiver: PubKey,
    }
    • has_one 约束可以用来核实传入指令的账户是否与存储在 data 账户字段中的账户匹配。
    • 你必须在所使用的账户的 data 字段上应用特定的命名规则,以便进行 has_one 约束检查。
    • 使用 has_one = receiver时:
      • 账户的 data 需要有一个名为 receiver 的字段与之匹配。
      • #[derive(Accounts)] 结构中,账户名称也必须称为 receiver
    • 请注意,虽然使用 close 约束只是一个例子,但 has_one 约束可以有更广泛的用途。
    info

    这里需要知道的是 has_one 这个限制是很有用的。

    - - +
    Skip to main content

    🛣 Anchor中的PDA(程序派生地址)

    你做得很好!让我们继续深入探讨。

    本课程中,我们将深入探讨如何使用#[account(...)]属性,并深入了解以下限制条件:

    • seedsbump - 初始化和验证PDA
    • realloc - 重新分配账户空间
    • close - 关闭账户

    🛣 Anchor里的PDAs

    我们再次回顾一下,PDA是通过一系列可选的种子、一个bump seed和一个 programId来衍生的。Anchor提供了一种方便的方式来验证带有seedsbump限制的PDA

    #[account(seeds = [], bump)]
    pub pda_account: Account<'info, AccountType>,

    在账户验证过程中,Anchor会使用seeds约束中指定的种子生成一个PDA,并确认传入指令的账户是否与找到的PDA匹配。

    当包含bump约束,但未指定具体的bump时,Anchor将默认使用规范bump(即找到有效PDA的第一个bump)。

    #[derive(Accounts)]
    #[instruction(instruction_data: String)]
    pub struct Example<'info> {
    #[account(seeds = [b"example-seed", user.key().as_ref(), instruction_data.as_ref()]
    pub pad_account: Account<'info, AccountType>,
    #[account(mut)]
    pub user: Signer<'info>,
    }

    在此示例中,通过seedbump约束验证pda_account的地址是否是预期的PDA

    推导PDAseeds包括:

    • example_seed - 一个硬编码的字符串值
    • user.key() - 传入账户的公钥 user
    • instruction_data - 传入指令的数据
      • 你可以通过#[instruction(...)]属性来访问这些数据
    pub fn example_instruction(
    ctx: Context<Example>,
    input_one: String,
    input_two: String,
    input_three: String,
    ) -> Result<()> {
    // ....
    Ok(()
    }

    #[derive(Accounts)]
    #[instruction(input_one: String, input_two: String)]
    pub struct Example<'info> {
    // ...
    }
    • 使用#[instruction(...)]属性时,指令数据必须按照传入指令的顺序排列
    • 你可以忽略不需要的最后一个参数及其之后的所有参数
    #[derive(Accounts)]
    #[instruction(input_one: String, input_two: String)]
    pub struct Example<'info> {
    // ...
    }

    如果输入顺序错误,将会导致错误

    #[derive(Accounts)]
    pub struct InitializedPda<'info> {
    #[account(
    init,
    seeds = [b"example_seed", user.key().as_ref()]
    bump,
    payer = user,
    space = 8 + 8
    )]
    pub pda_account: Account<'info, AccountType>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    #[account]
    pub struct AccountType {
    pub data: u64
    }

    你可以将init约束与seedsbump约束组合,以使用PDA初始化账户。

    init约束必须与以下内容结合使用:

    • payer - 指定用于支付初始化费用的账户
    • space - 新账户所分配的空间大小
    • system_program - 在账户验证结构中必须存在的system_program

    默认情况下,init会将创建账户的所有者设置为当前正在执行的程序。

    • 当使用initseedsbump初始化PDA账户时,所有者必须是正在执行的程序
    • 这是因为创建账户需要签名,只有执行程序的PDA才能提供
    • 如果用于派生PDAprogramId与正在执行的程序的programId不匹配,则PDA账户初始化的签名验证将失败
    • 因为init使用find_program_address来推导PDA,所以不需要指定bump
    • 这意味着PDA将使用规范的bump进行推导
    • 在为执行Anchor程序所初始化和拥有的账户分配space时,请记住前8个字节是保留给唯一账户discriminator的,Anchor程序使用该discriminator来识别程序账户类型

    🧮 重新分配

    在许多情况下,你可能需要更新现有账户而不是创建新账户。Anchor提供了出色的realloc约束,为现有账户重新分配空间提供了一种简便的方法。

    #[derive(Accounts)]
    #[instruction(instruction_data: String)]
    pub struct ReallocExampl<'info> {
    #[account(
    mut,
    seeds = [b"example_seed", user.key().as_ref()]
    bump,
    realloc = 8 + 4 + instruction_data.len(),
    realloc::payer = user,
    realloc::zero = false,
    )]
    pub pda_account: Account<'info, AccountType>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    #[account]
    pub struct AccountType {
    pub data: u64
    }

    realloc约束必须与以下内容结合使用:

    • mut - 账户必须设置为可变
    • realloc::payer - 账户空间的增加或减少将相应增加或减少账户的lamports
    • realloc::zero - 一个布尔值,用于指定是否应将新内存初始化为零
    • system_program - realloc约束要求在账户验证结构中存在system_program

    例如,重新分配用于存储String类型字段的账户的空间。

    • 使用String类型时,除了String本身所需的空间外,还需要额外的4个字节来存储String的长度
    • 如果账户数据长度是增加的,为了保持租金豁免,Lamport将从realloc::payer转移到程序账户
    • 如果长度减少,Lamport将从程序账户转回realloc::payer
    • 需要realloc::zero约束来确定重新分配后是否应对新内存进行零初始化
    • 在之前减小过空间的账户上增加空间时,应将此约束设置为true

    close 关闭操作

    当你用完一个账户并不再需要它时会发生什么呢?你可以将它关闭!

    通过这样做,你可以腾出空间,并收回用于支付租金的SOL

    执行关闭操作是通过使用 close 约束来完成的:

    pub fn close(ctx: Context<Close>) -> Result<()> {
    Ok(())
    }

    #[derive(Accounts)]
    pub struct Close<'info> {
    #[account(mut, close = receiver)]
    pub data_account: Account<'info, AccountType>,
    #[account(mut)]
    pub receiver: Signer<'info>,
    }
    • close 约束会在指令执行结束时将账户标记为已关闭,并通过将其discriminator设置为 CLOSED_ACCOUNT_DISCRIMINATOR,同时将其 lamports 发送到特定的账户。
    • discriminator设置为特定的变量,以阻止账户复活攻击(例如,后续指令重新添加租金豁免的lamports)。
    • 我们将关闭名为 data_account 的账户,并将用于租金的lamports发送到名为 receiver 的账户。
    • 然而,目前任何人都可以调用关闭指令并关闭 data_account
    pub fn close(ctx: Context<Close>) -> Result<()> {
    Ok(())
    }

    #[derive(Accounts)]
    pub struct Close<'info> {
    #[account(mut, close = receiver, has_one = receiver)]
    pub data_account: Account<'info, AccountType>,
    #[account(mut)]
    pub receiver: Signer<'info>,
    }

    #[account]
    pub struct AccountType {
    pub data: String,
    pub receiver: PubKey,
    }
    • has_one 约束可以用来核实传入指令的账户是否与存储在 data 账户字段中的账户匹配。
    • 你必须在所使用的账户的 data 字段上应用特定的命名规则,以便进行 has_one 约束检查。
    • 使用 has_one = receiver时:
      • 账户的 data 需要有一个名为 receiver 的字段与之匹配。
      • #[derive(Accounts)] 结构中,账户名称也必须称为 receiver
    • 请注意,虽然使用 close 约束只是一个例子,但 has_one 约束可以有更广泛的用途。
    info

    这里需要知道的是 has_one 这个限制是很有用的。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module6/finishing-touches/index.html b/Solana-Co-Learn/module6/finishing-touches/index.html index 172863a26..e50e72823 100644 --- a/Solana-Co-Learn/module6/finishing-touches/index.html +++ b/Solana-Co-Learn/module6/finishing-touches/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module6/finishing-touches/onwards/index.html b/Solana-Co-Learn/module6/finishing-touches/onwards/index.html index 8a697b1c4..1bf0ca2d3 100644 --- a/Solana-Co-Learn/module6/finishing-touches/onwards/index.html +++ b/Solana-Co-Learn/module6/finishing-touches/onwards/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content

    🌈 前进

    下一站去哪里?

    世界就是你的游乐场。天空是极限。

    自己动手建造一些东西,参加黑客马拉松,抓住机会。你可以做任何事情。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module6/finishing-touches/preparing-for-takeoff/index.html b/Solana-Co-Learn/module6/finishing-touches/preparing-for-takeoff/index.html index ec8693160..8aa9db947 100644 --- a/Solana-Co-Learn/module6/finishing-touches/preparing-for-takeoff/index.html +++ b/Solana-Co-Learn/module6/finishing-touches/preparing-for-takeoff/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🚀 准备起飞

    好的,让我们一起启动项目吧。在深入探讨/components/Lootbox.tsx文件的逻辑之前,我们先来快速预览一下布局的构造。

    我们将所有相关的组件集中在一起,只需进行三个主要检查:是否有可用的战利品箱、是否存在押注账户,以及总收益是否超过战利品箱的值。如果检查结果为真,则会渲染一个带有各种选项的按钮;否则,用户将会收到一个提示,建议他们继续押注。接下来,我们将深入了解如何处理handleRedeemLoothandleOpenLootbox 函数的逻辑。

    return (
    <Center
    height="120px"
    width="120px"
    bgColor={"containerBg"}
    borderRadius="10px"
    >
    {availableLootbox &&
    stakeAccount &&
    stakeAccount.totalEarned.toNumber() >= availableLootbox ? (
    <Button
    borderRadius="25"
    onClick={mint ? handleRedeemLoot : handleOpenLootbox}
    isLoading={isConfirmingTransaction}
    >
    {mint
    ? "Redeem"
    : userAccountExists
    ? `${availableLootbox} $BLD`
    : "Enable"}
    </Button>
    ) : (
    <Text color="bodyText">Keep Staking</Text>
    )}
    </Center>
    )

    在这个函数体内,首先我们进行了大量的设置和状态定义。其中有一个useEffect钩子用来确保我们拥有公钥、战利品箱程序和质押程序。一旦这些都到位,它就会调用handleStateRefresh方法来刷新状态。

    通过这样的组织,我们可以确保逻辑清晰,并且易于理解和维护。

    export const Lootbox = ({
    stakeAccount,
    nftTokenAccount,
    fetchUpstreamState,
    }: {
    stakeAccount?: StakeAccount
    nftTokenAccount: PublicKey
    fetchUpstreamState: () => void
    }) => {
    const [isConfirmingTransaction, setIsConfirmingTransaction] = useState(false)
    const [availableLootbox, setAvailableLootbox] = useState(0)
    const walletAdapter = useWallet()
    const { stakingProgram, lootboxProgram, switchboardProgram } = useWorkspace()
    const { connection } = useConnection()

    const [userAccountExists, setUserAccountExist] = useState(false)
    const [mint, setMint] = useState<PublicKey>()

    useEffect(() => {
    if (!walletAdapter.publicKey || !lootboxProgram || !stakingProgram) return

    handleStateRefresh(lootboxProgram, walletAdapter.publicKey)
    }, [walletAdapter, lootboxProgram])

    状态的刷新是通过一个独立的函数来完成的,因为在每次交易后都需要调用它。这部分只是通过调用两个函数来实现。

    const handleStateRefresh = async (
    lootboxProgram: Program<LootboxProgram>,
    publicKey: PublicKey
    ) => {
    checkUserAccount(lootboxProgram, publicKey);
    fetchLootboxPointer(lootboxProgram, publicKey);
    }

    checkUserAccount将检查用户状态的PDA,如果存在,则通过调用setUserAccountExist将其设置为true

    // 检查UserState账户是否存在
    // 如果UserState账户存在,还要检查是否有可从战利品箱兑换的物品
    const checkUserAccount = async (
    lootboxProgram: Program<LootboxProgram>,
    publicKey: PublicKey
    ) => {
    try {
    const [userStatePda] = PublicKey.findProgramAddressSync(
    [publicKey.toBytes()],
    lootboxProgram.programId
    );
    const account = await lootboxProgram.account.userState.fetch(userStatePda);
    if (account) {
    setUserAccountExist(true);
    } else {
    setMint(undefined);
    setUserAccountExist(false);
    }
    } catch {}
    }

    fetchLootboxPointer 主要用于获取战利品盒的指针,并设置可用的战利品盒和可兑换的物品。

    const fetchLootboxPointer = async (
    lootboxProgram: Program<LootboxProgram>,
    publicKey: PublicKey
    ) => {
    try {
    const [lootboxPointerPda] = PublicKey.findProgramAddressSync(
    [Buffer.from("lootbox"), publicKey.toBytes()],
    LOOTBOX_PROGRAM_ID
    )

    const lootboxPointer = await lootboxProgram.account.lootboxPointer.fetch(
    lootboxPointerPda
    )

    setAvailableLootbox(lootboxPointer.availableLootbox.toNumber())
    setMint(lootboxPointer.redeemable ? lootboxPointer.mint : undefined)
    } catch (error) {
    console.log(error)
    setAvailableLootbox(10)
    setMint(undefined)
    }
    }

    回到两个主要的逻辑部分,一个是 handleOpenLootbox 。它首先检查我们是否拥有传递给函数所需的所有必要项目,然后调用 openLootbox

    const handleOpenLootbox: MouseEventHandler<HTMLButtonElement> = useCallback(
    async (event) => {
    if (
    event.defaultPrevented ||
    !walletAdapter.publicKey ||
    !lootboxProgram ||
    !switchboardProgram ||
    !stakingProgram
    )
    return

    openLootbox(
    connection,
    userAccountExists,
    walletAdapter.publicKey,
    lootboxProgram,
    switchboardProgram,
    stakingProgram
    )
    },
    [
    lootboxProgram,
    connection,
    walletAdapter,
    userAccountExists,
    walletAdapter,
    switchboardProgram,
    stakingProgram,
    ]
    )

    openLootbox 从检查用户账户是否存在开始,如果不存在,则调用指令文件中的 createInitSwitchboardInstructions ,该文件会返回给我们指令vrfKeypair。如果该账户不存在,我们尚未初始化交换机

    const openLootbox = async (
    connection: Connection,
    userAccountExists: boolean,
    publicKey: PublicKey,
    lootboxProgram: Program<LootboxProgram>,
    switchboardProgram: SwitchboardProgram,
    stakingProgram: Program<AnchorNftStaking>
    ) => {
    if (!userAccountExists) {
    const { instructions, vrfKeypair } =
    await createInitSwitchboardInstructions(
    switchboardProgram,
    lootboxProgram,
    publicKey
    )

    然后我们创建一个新的交易,添加指令并调用我们创建的 sendAndConfirmTransaction 。它以一个对象作为vrfKeypair的签名者。

    const transaction = new Transaction()
    transaction.add(...instructions)
    sendAndConfirmTransaction(connection, walletAdapter, transaction, {
    signers: [vrfKeypair],
    })
    }

    让我们跳出逻辑,看看 sendAndConfirmTransaction 。首先,我们设定我们正在加载 setIsConfirmingTransaction(true)

    然后我们调用发送交易,但我们传递了选项,这是可选的,因为我们并不总是需要它。这是我们如何发送vrfKeypair的签名者,但我们并不总是这样做。

    一旦确认,我们使用 await Promise.all 在我们调用 handleStateRefreshfetchUpstreamState 的地方。后者作为一个属性传入,基本上是在stake组件上的fetch状态函数。

    const sendAndConfirmTransaction = async (
    connection: Connection,
    walletAdapter: WalletContextState,
    transaction: Transaction,
    options?: SendTransactionOptions
    ) => {
    setIsConfirmingTransaction(true)

    try {
    const signature = await walletAdapter.sendTransaction(
    transaction,
    connection,
    options
    )
    const latestBlockhash = await connection.getLatestBlockhash()
    await connection.confirmTransaction(
    {
    blockhash: latestBlockhash.blockhash,
    lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
    signature: signature,
    },
    "finalized"
    )

    console.log("Transaction complete")
    await Promise.all([
    handleStateRefresh(lootboxProgram!, walletAdapter.publicKey!),
    fetchUpstreamState(),
    ])
    } catch (error) {
    console.log(error)
    throw error
    } finally {
    setIsConfirmingTransaction(false)
    }
    }

    现在回到 handleOpenLootboxelse语句,这是处理账户存在的逻辑。所以我们设置了打开战利品箱指令并发送它们。然后调用 sendAndConfirmTransaction 。一旦确认,该函数将把is confirming设置为false,然后我们再次将其设置为true

    ...
    else {
    const instructions = await createOpenLootboxInstructions(
    connection,
    stakingProgram,
    switchboardProgram,
    lootboxProgram,
    publicKey,
    nftTokenAccount,
    availableLootbox
    )

    const transaction = new Transaction()
    transaction.add(...instructions)
    try {
    await sendAndConfirmTransaction(connection, walletAdapter, transaction)
    setIsConfirmingTransaction(true)

    最后,这是等待看到mint被存入战利品箱指针的逻辑,这样我们就可以兑换它。(这段代码只能偶尔工作,不要依赖它,如果可以的话请修复它)。

        const [lootboxPointerPda] = PublicKey.findProgramAddressSync(
    [Buffer.from("lootbox"), publicKey.toBytes()],
    lootboxProgram.programId
    )

    const id = await connection.onAccountChange(
    lootboxPointerPda,
    async (_) => {
    try {
    const account = await lootboxProgram.account.lootboxPointer.fetch(
    lootboxPointerPda
    )
    if (account.redeemable) {
    setMint(account.mint)
    connection.removeAccountChangeListener(id)
    setIsConfirmingTransaction(false)
    }
    } catch (error) {
    console.log("Error in waiter:", error)
    }
    }
    )
    } catch (error) {
    console.log(error)
    }
    }
    }

    快速跳转到 /pages/stake.tsx 。我们做一个小修改,如果有 nftDatanftTokenAccount ,则显示战利品箱,并传入赌注账户、NFT代币账户,并调用fetchstate,将mint address作为上游属性传递。

    <HStack>
    {nftData && nftTokenAccount && (
    <Lootbox
    stakeAccount={stakeAccount}
    nftTokenAccount={nftTokenAccount}
    fetchUpstreamState={() => {
    fetchstate(nftData.mint.address)
    }}
    />
    )}
    </HStack>

    现在希望回顾一下 handleRedeemLoot ,这个过程更加简单明了。我们首先获取相关的令牌。然后使用我们的 retrieveItemFromLootbox 函数创建一个新的交易,然后发送并确认该交易。

    onst handleRedeemLoot: MouseEventHandler<HTMLButtonElement> = useCallback(
    async (event) => {
    if (
    event.defaultPrevented ||
    !walletAdapter.publicKey ||
    !lootboxProgram ||
    !mint
    )
    return

    const userGearAta = await getAssociatedTokenAddress(
    mint,
    walletAdapter.publicKey
    )

    const transaction = new Transaction()
    transaction.add(
    await lootboxProgram.methods
    .retrieveItemFromLootbox()
    .accounts({
    mint: mint,
    userGearAta: userGearAta,
    })
    .instruction()
    )

    sendAndConfirmTransaction(connection, walletAdapter, transaction)
    },
    [walletAdapter, lootboxProgram, mint]
    )

    那是很多的内容,我们跳来跳去的,所以如果你需要参考整个文件的代码,请看这里

    唉,让我们来看看 GearItem 组件。这个组件相对简单一些,也要短得多。

    import { Center, Image, VStack, Text } from "@chakra-ui/react"
    import { Metaplex, walletAdapterIdentity } from "@metaplex-foundation/js"
    import { useConnection, useWallet } from "@solana/wallet-adapter-react"
    import { PublicKey } from "@solana/web3.js"
    import { useEffect, useState } from "react"

    export const GearItem = ({
    item,
    balance,
    }: {
    item: string
    balance: number
    }) => {
    const [metadata, setMetadata] = useState<any>()
    const { connection } = useConnection()
    const walletAdapter = useWallet()

    useEffect(() => {
    const metaplex = Metaplex.make(connection).use(
    walletAdapterIdentity(walletAdapter)
    )

    const mint = new PublicKey(item)

    try {
    metaplex
    .nfts()
    .findByMint({ mintAddress: mint })
    .run()
    .then((nft) => fetch(nft.uri))
    .then((response) => response.json())
    .then((nftData) => setMetadata(nftData))
    } catch (error) {
    console.log("error getting gear token:", error)
    }
    }, [item, connection, walletAdapter])

    return (
    <VStack>
    <Center
    height="120px"
    width="120px"
    bgColor={"containerBg"}
    borderRadius="10px"
    >
    <Image src={metadata?.image ?? ""} alt="gear token" padding={4} />
    </Center>
    <Text color="white" as="b" fontSize="md" width="100%" textAlign="center">
    {`x${balance}`}
    </Text>
    </VStack>
    )
    }

    布局与之前相似,不同的是,现在我们以一张图片来展示齿轮代币,使用代币上的元数据作为来源。在图片下方,我们会显示你拥有的每个齿轮代币的数量。

    关于逻辑部分,我们会传入代表代币铸造的base58编码字符串和你拥有的数量。

    useEffect中,我们创建了一个metaplex对象,并将item字符串转换为公钥。然后我们通过mint调用metaplex来查找物品。一旦得到nft,我们便在nfturi上调用fetch方法,从而可以访问到链下的元数据。我们将响应转换为json格式,并设置为元数据,这样就可以在返回调用中显示一个图像属性。

    切换回stake.tsx文件。首先,我们为齿轮平衡添加了一个状态行。

    const [gearBalances, setGearBalances] = useState<any>({})

    我们在fetchState函数内调用它。

    在获取状态的过程中,我们首先将余额设置为空对象。然后,我们循环遍历不同的齿轮选项,并获取与该铸币相关联的当前用户的ATA。这为我们提供了一个地址,我们用它来获取账户,并将特定齿轮铸币的余额设置为我们所拥有的数字。在循环结束后,我们调用setGearBalances(balances)

    所以,在用户界面中,我们会检查齿轮平衡的长度是否大于零。如果是,就显示所有与齿轮相关的内容;否则,就不显示任何内容。

    <HStack spacing={10} align="start">
    {Object.keys(gearBalances).length > 0 && (
    <VStack alignItems="flex-start">
    <Text color="white" as="b" fontSize="2xl">
    Gear
    </Text>
    <SimpleGrid
    columns={Math.min(2, Object.keys(gearBalances).length)}
    spacing={3}
    >
    {Object.keys(gearBalances).map((key, _) => {
    return (
    <GearItem
    item={key}
    balance={gearBalances[key]}
    key={key}
    />
    )
    })}
    </SimpleGrid>
    </VStack>
    )}
    <VStack alignItems="flex-start">
    <Text color="white" as="b" fontSize="2xl">
    Loot Box
    </Text>
    <HStack>
    {nftData && nftTokenAccount && (
    <Lootbox
    stakeAccount={stakeAccount}
    nftTokenAccount={nftTokenAccount}
    fetchUpstreamState={() => {
    fetchstate(nftData.mint.address)
    }}
    />
    )}
    </HStack>
    </VStack>
    </HStack>

    这部分描述了如何完成检查和显示装备的操作,并提供了存储库中的代码作为参考。

    接下来的步骤由你来决定。你可以权衡要修复哪些错误,以及哪些错误可以接受。然后将所有内容从本地主机迁移出去并发布,这样你就可以分享一个公共链接。

    如果你有兴趣,甚至可以准备并部署到主网。当然,在上线主网之前,还有许多地方可以改进和优化,例如修复错误、添加更多检查、拥有更多的NFT等等。如果这些让你感兴趣,那么就放手一搏吧!

    - - +
    Skip to main content

    🚀 准备起飞

    好的,让我们一起启动项目吧。在深入探讨/components/Lootbox.tsx文件的逻辑之前,我们先来快速预览一下布局的构造。

    我们将所有相关的组件集中在一起,只需进行三个主要检查:是否有可用的战利品箱、是否存在押注账户,以及总收益是否超过战利品箱的值。如果检查结果为真,则会渲染一个带有各种选项的按钮;否则,用户将会收到一个提示,建议他们继续押注。接下来,我们将深入了解如何处理handleRedeemLoothandleOpenLootbox 函数的逻辑。

    return (
    <Center
    height="120px"
    width="120px"
    bgColor={"containerBg"}
    borderRadius="10px"
    >
    {availableLootbox &&
    stakeAccount &&
    stakeAccount.totalEarned.toNumber() >= availableLootbox ? (
    <Button
    borderRadius="25"
    onClick={mint ? handleRedeemLoot : handleOpenLootbox}
    isLoading={isConfirmingTransaction}
    >
    {mint
    ? "Redeem"
    : userAccountExists
    ? `${availableLootbox} $BLD`
    : "Enable"}
    </Button>
    ) : (
    <Text color="bodyText">Keep Staking</Text>
    )}
    </Center>
    )

    在这个函数体内,首先我们进行了大量的设置和状态定义。其中有一个useEffect钩子用来确保我们拥有公钥、战利品箱程序和质押程序。一旦这些都到位,它就会调用handleStateRefresh方法来刷新状态。

    通过这样的组织,我们可以确保逻辑清晰,并且易于理解和维护。

    export const Lootbox = ({
    stakeAccount,
    nftTokenAccount,
    fetchUpstreamState,
    }: {
    stakeAccount?: StakeAccount
    nftTokenAccount: PublicKey
    fetchUpstreamState: () => void
    }) => {
    const [isConfirmingTransaction, setIsConfirmingTransaction] = useState(false)
    const [availableLootbox, setAvailableLootbox] = useState(0)
    const walletAdapter = useWallet()
    const { stakingProgram, lootboxProgram, switchboardProgram } = useWorkspace()
    const { connection } = useConnection()

    const [userAccountExists, setUserAccountExist] = useState(false)
    const [mint, setMint] = useState<PublicKey>()

    useEffect(() => {
    if (!walletAdapter.publicKey || !lootboxProgram || !stakingProgram) return

    handleStateRefresh(lootboxProgram, walletAdapter.publicKey)
    }, [walletAdapter, lootboxProgram])

    状态的刷新是通过一个独立的函数来完成的,因为在每次交易后都需要调用它。这部分只是通过调用两个函数来实现。

    const handleStateRefresh = async (
    lootboxProgram: Program<LootboxProgram>,
    publicKey: PublicKey
    ) => {
    checkUserAccount(lootboxProgram, publicKey);
    fetchLootboxPointer(lootboxProgram, publicKey);
    }

    checkUserAccount将检查用户状态的PDA,如果存在,则通过调用setUserAccountExist将其设置为true

    // 检查UserState账户是否存在
    // 如果UserState账户存在,还要检查是否有可从战利品箱兑换的物品
    const checkUserAccount = async (
    lootboxProgram: Program<LootboxProgram>,
    publicKey: PublicKey
    ) => {
    try {
    const [userStatePda] = PublicKey.findProgramAddressSync(
    [publicKey.toBytes()],
    lootboxProgram.programId
    );
    const account = await lootboxProgram.account.userState.fetch(userStatePda);
    if (account) {
    setUserAccountExist(true);
    } else {
    setMint(undefined);
    setUserAccountExist(false);
    }
    } catch {}
    }

    fetchLootboxPointer 主要用于获取战利品盒的指针,并设置可用的战利品盒和可兑换的物品。

    const fetchLootboxPointer = async (
    lootboxProgram: Program<LootboxProgram>,
    publicKey: PublicKey
    ) => {
    try {
    const [lootboxPointerPda] = PublicKey.findProgramAddressSync(
    [Buffer.from("lootbox"), publicKey.toBytes()],
    LOOTBOX_PROGRAM_ID
    )

    const lootboxPointer = await lootboxProgram.account.lootboxPointer.fetch(
    lootboxPointerPda
    )

    setAvailableLootbox(lootboxPointer.availableLootbox.toNumber())
    setMint(lootboxPointer.redeemable ? lootboxPointer.mint : undefined)
    } catch (error) {
    console.log(error)
    setAvailableLootbox(10)
    setMint(undefined)
    }
    }

    回到两个主要的逻辑部分,一个是 handleOpenLootbox 。它首先检查我们是否拥有传递给函数所需的所有必要项目,然后调用 openLootbox

    const handleOpenLootbox: MouseEventHandler<HTMLButtonElement> = useCallback(
    async (event) => {
    if (
    event.defaultPrevented ||
    !walletAdapter.publicKey ||
    !lootboxProgram ||
    !switchboardProgram ||
    !stakingProgram
    )
    return

    openLootbox(
    connection,
    userAccountExists,
    walletAdapter.publicKey,
    lootboxProgram,
    switchboardProgram,
    stakingProgram
    )
    },
    [
    lootboxProgram,
    connection,
    walletAdapter,
    userAccountExists,
    walletAdapter,
    switchboardProgram,
    stakingProgram,
    ]
    )

    openLootbox 从检查用户账户是否存在开始,如果不存在,则调用指令文件中的 createInitSwitchboardInstructions ,该文件会返回给我们指令vrfKeypair。如果该账户不存在,我们尚未初始化交换机

    const openLootbox = async (
    connection: Connection,
    userAccountExists: boolean,
    publicKey: PublicKey,
    lootboxProgram: Program<LootboxProgram>,
    switchboardProgram: SwitchboardProgram,
    stakingProgram: Program<AnchorNftStaking>
    ) => {
    if (!userAccountExists) {
    const { instructions, vrfKeypair } =
    await createInitSwitchboardInstructions(
    switchboardProgram,
    lootboxProgram,
    publicKey
    )

    然后我们创建一个新的交易,添加指令并调用我们创建的 sendAndConfirmTransaction 。它以一个对象作为vrfKeypair的签名者。

    const transaction = new Transaction()
    transaction.add(...instructions)
    sendAndConfirmTransaction(connection, walletAdapter, transaction, {
    signers: [vrfKeypair],
    })
    }

    让我们跳出逻辑,看看 sendAndConfirmTransaction 。首先,我们设定我们正在加载 setIsConfirmingTransaction(true)

    然后我们调用发送交易,但我们传递了选项,这是可选的,因为我们并不总是需要它。这是我们如何发送vrfKeypair的签名者,但我们并不总是这样做。

    一旦确认,我们使用 await Promise.all 在我们调用 handleStateRefreshfetchUpstreamState 的地方。后者作为一个属性传入,基本上是在stake组件上的fetch状态函数。

    const sendAndConfirmTransaction = async (
    connection: Connection,
    walletAdapter: WalletContextState,
    transaction: Transaction,
    options?: SendTransactionOptions
    ) => {
    setIsConfirmingTransaction(true)

    try {
    const signature = await walletAdapter.sendTransaction(
    transaction,
    connection,
    options
    )
    const latestBlockhash = await connection.getLatestBlockhash()
    await connection.confirmTransaction(
    {
    blockhash: latestBlockhash.blockhash,
    lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
    signature: signature,
    },
    "finalized"
    )

    console.log("Transaction complete")
    await Promise.all([
    handleStateRefresh(lootboxProgram!, walletAdapter.publicKey!),
    fetchUpstreamState(),
    ])
    } catch (error) {
    console.log(error)
    throw error
    } finally {
    setIsConfirmingTransaction(false)
    }
    }

    现在回到 handleOpenLootboxelse语句,这是处理账户存在的逻辑。所以我们设置了打开战利品箱指令并发送它们。然后调用 sendAndConfirmTransaction 。一旦确认,该函数将把is confirming设置为false,然后我们再次将其设置为true

    ...
    else {
    const instructions = await createOpenLootboxInstructions(
    connection,
    stakingProgram,
    switchboardProgram,
    lootboxProgram,
    publicKey,
    nftTokenAccount,
    availableLootbox
    )

    const transaction = new Transaction()
    transaction.add(...instructions)
    try {
    await sendAndConfirmTransaction(connection, walletAdapter, transaction)
    setIsConfirmingTransaction(true)

    最后,这是等待看到mint被存入战利品箱指针的逻辑,这样我们就可以兑换它。(这段代码只能偶尔工作,不要依赖它,如果可以的话请修复它)。

        const [lootboxPointerPda] = PublicKey.findProgramAddressSync(
    [Buffer.from("lootbox"), publicKey.toBytes()],
    lootboxProgram.programId
    )

    const id = await connection.onAccountChange(
    lootboxPointerPda,
    async (_) => {
    try {
    const account = await lootboxProgram.account.lootboxPointer.fetch(
    lootboxPointerPda
    )
    if (account.redeemable) {
    setMint(account.mint)
    connection.removeAccountChangeListener(id)
    setIsConfirmingTransaction(false)
    }
    } catch (error) {
    console.log("Error in waiter:", error)
    }
    }
    )
    } catch (error) {
    console.log(error)
    }
    }
    }

    快速跳转到 /pages/stake.tsx 。我们做一个小修改,如果有 nftDatanftTokenAccount ,则显示战利品箱,并传入赌注账户、NFT代币账户,并调用fetchstate,将mint address作为上游属性传递。

    <HStack>
    {nftData && nftTokenAccount && (
    <Lootbox
    stakeAccount={stakeAccount}
    nftTokenAccount={nftTokenAccount}
    fetchUpstreamState={() => {
    fetchstate(nftData.mint.address)
    }}
    />
    )}
    </HStack>

    现在希望回顾一下 handleRedeemLoot ,这个过程更加简单明了。我们首先获取相关的令牌。然后使用我们的 retrieveItemFromLootbox 函数创建一个新的交易,然后发送并确认该交易。

    onst handleRedeemLoot: MouseEventHandler<HTMLButtonElement> = useCallback(
    async (event) => {
    if (
    event.defaultPrevented ||
    !walletAdapter.publicKey ||
    !lootboxProgram ||
    !mint
    )
    return

    const userGearAta = await getAssociatedTokenAddress(
    mint,
    walletAdapter.publicKey
    )

    const transaction = new Transaction()
    transaction.add(
    await lootboxProgram.methods
    .retrieveItemFromLootbox()
    .accounts({
    mint: mint,
    userGearAta: userGearAta,
    })
    .instruction()
    )

    sendAndConfirmTransaction(connection, walletAdapter, transaction)
    },
    [walletAdapter, lootboxProgram, mint]
    )

    那是很多的内容,我们跳来跳去的,所以如果你需要参考整个文件的代码,请看这里

    唉,让我们来看看 GearItem 组件。这个组件相对简单一些,也要短得多。

    import { Center, Image, VStack, Text } from "@chakra-ui/react"
    import { Metaplex, walletAdapterIdentity } from "@metaplex-foundation/js"
    import { useConnection, useWallet } from "@solana/wallet-adapter-react"
    import { PublicKey } from "@solana/web3.js"
    import { useEffect, useState } from "react"

    export const GearItem = ({
    item,
    balance,
    }: {
    item: string
    balance: number
    }) => {
    const [metadata, setMetadata] = useState<any>()
    const { connection } = useConnection()
    const walletAdapter = useWallet()

    useEffect(() => {
    const metaplex = Metaplex.make(connection).use(
    walletAdapterIdentity(walletAdapter)
    )

    const mint = new PublicKey(item)

    try {
    metaplex
    .nfts()
    .findByMint({ mintAddress: mint })
    .run()
    .then((nft) => fetch(nft.uri))
    .then((response) => response.json())
    .then((nftData) => setMetadata(nftData))
    } catch (error) {
    console.log("error getting gear token:", error)
    }
    }, [item, connection, walletAdapter])

    return (
    <VStack>
    <Center
    height="120px"
    width="120px"
    bgColor={"containerBg"}
    borderRadius="10px"
    >
    <Image src={metadata?.image ?? ""} alt="gear token" padding={4} />
    </Center>
    <Text color="white" as="b" fontSize="md" width="100%" textAlign="center">
    {`x${balance}`}
    </Text>
    </VStack>
    )
    }

    布局与之前相似,不同的是,现在我们以一张图片来展示齿轮代币,使用代币上的元数据作为来源。在图片下方,我们会显示你拥有的每个齿轮代币的数量。

    关于逻辑部分,我们会传入代表代币铸造的base58编码字符串和你拥有的数量。

    useEffect中,我们创建了一个metaplex对象,并将item字符串转换为公钥。然后我们通过mint调用metaplex来查找物品。一旦得到nft,我们便在nfturi上调用fetch方法,从而可以访问到链下的元数据。我们将响应转换为json格式,并设置为元数据,这样就可以在返回调用中显示一个图像属性。

    切换回stake.tsx文件。首先,我们为齿轮平衡添加了一个状态行。

    const [gearBalances, setGearBalances] = useState<any>({})

    我们在fetchState函数内调用它。

    在获取状态的过程中,我们首先将余额设置为空对象。然后,我们循环遍历不同的齿轮选项,并获取与该铸币相关联的当前用户的ATA。这为我们提供了一个地址,我们用它来获取账户,并将特定齿轮铸币的余额设置为我们所拥有的数字。在循环结束后,我们调用setGearBalances(balances)

    所以,在用户界面中,我们会检查齿轮平衡的长度是否大于零。如果是,就显示所有与齿轮相关的内容;否则,就不显示任何内容。

    <HStack spacing={10} align="start">
    {Object.keys(gearBalances).length > 0 && (
    <VStack alignItems="flex-start">
    <Text color="white" as="b" fontSize="2xl">
    Gear
    </Text>
    <SimpleGrid
    columns={Math.min(2, Object.keys(gearBalances).length)}
    spacing={3}
    >
    {Object.keys(gearBalances).map((key, _) => {
    return (
    <GearItem
    item={key}
    balance={gearBalances[key]}
    key={key}
    />
    )
    })}
    </SimpleGrid>
    </VStack>
    )}
    <VStack alignItems="flex-start">
    <Text color="white" as="b" fontSize="2xl">
    Loot Box
    </Text>
    <HStack>
    {nftData && nftTokenAccount && (
    <Lootbox
    stakeAccount={stakeAccount}
    nftTokenAccount={nftTokenAccount}
    fetchUpstreamState={() => {
    fetchstate(nftData.mint.address)
    }}
    />
    )}
    </HStack>
    </VStack>
    </HStack>

    这部分描述了如何完成检查和显示装备的操作,并提供了存储库中的代码作为参考。

    接下来的步骤由你来决定。你可以权衡要修复哪些错误,以及哪些错误可以接受。然后将所有内容从本地主机迁移出去并发布,这样你就可以分享一个公共链接。

    如果你有兴趣,甚至可以准备并部署到主网。当然,在上线主网之前,还有许多地方可以改进和优化,例如修复错误、添加更多检查、拥有更多的NFT等等。如果这些让你感兴趣,那么就放手一搏吧!

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module6/finishing-touches/the-final-pieces/index.html b/Solana-Co-Learn/module6/finishing-touches/the-final-pieces/index.html index b04ace739..142ddd2e8 100644 --- a/Solana-Co-Learn/module6/finishing-touches/the-final-pieces/index.html +++ b/Solana-Co-Learn/module6/finishing-touches/the-final-pieces/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🚶‍ 最终作品

    概述

    终于到了最后的冲刺阶段!恭喜你抵达此地!这对每个人来说都是一次令人激动的旅程。不论你的NFT项目处于何种阶段,都要深呼吸,给自己一个赞扬,你做得非常棒!

    现在,审视一下你手头的成果,然后思考一下,为了让项目做好交付准备,你至少还需要做些什么。如果需要暂时跳过Switchboard的部分,那就这样做。

    现在是时候把你的用户界面与战利品箱和装备指示器连接起来,完成最后的修整工作,然后交付这个作品!

    具体来说,我们需要:

    • 使用GearItemLootbox组件替换UI中使用的模拟ItemBox
    • 添加一个instructions.ts文件,在其中创建函数来:
      • 创建初始化战利品箱和交换机所需的所有指令。
      • 创建打开战利品箱所需的所有指令。
      • 注意:这部分可能有些复杂 - 你可以参考我们的解决方案代码,但也不妨尝试自己的方法。
    • 进行大量的调试和优化。

    坦白说,这个列表可能还远远不够。我们添加了许多组件来确保交易和链上变化后状态得到更新,但它仍有不完美的地方。总有更多的空间可以改进,但不要让完美主义成为你前进的障碍。尽你所能,然后交付吧!

    解决方案代码

    我们的解决方案位于Buildoors代码库solution-lootboxes分支上。与你上次查看的代码可能有些许差异,因此如果你想查看所有更改,请确保从上周的分支查看差异

    有一些引导,但你可以自由开始。祝你好运!

    下一步

    现在,最后一个项目所需的一切都在上一课中。从这一刻起,这就是你和文字之间的事情了,宝贝。我们开始吧!

    我们接下来要深入研究一些代码的更改。从/components/WorkspaceProvider.tsx开始。

    这里只有一些小更改,主要是为了引入switchboard program

    const [switchboardProgram, setProgramSwitchboard] = useState<any>()

    然后,我们加载switchboard program,并使用useEffect设置the program switchboard,确保我们的工作区始终能够及时更新所有所需程序。这可能会是一个挑战,除非你是React的专家,否则请随意深入研究这段代码。

    async function program() {
    let response = await loadSwitchboardProgram(
    "devnet",
    connection,
    ((provider as AnchorProvider).wallet as AnchorWallet).payer
    )
    return response
    }

    useEffect(() => {
    program().then((result) => {
    setProgramSwitchboard(result)
    console.log("result", result)
    })
    }, [connection])

    好的,接下来我们进入 instructions.ts 文件夹中的 utils 文件,这是一个新文件。这里有两个公共函数,分别是 createOpenLootboxInstructions 指令和 createInitSwitchboardInstructions 指令。后者用于打包交换机程序的初始化内容,并初始化抽奖箱程序中的用户。

    export async function createOpenLootboxInstructions(
    connection: Connection,
    stakingProgram: Program<AnchorNftStaking>,
    switchboardProgram: SwitchboardProgram,
    lootboxProgram: Program<LootboxProgram>,
    userPubkey: PublicKey,
    nftTokenAccount: PublicKey,
    box: number
    ): Promise<TransactionInstruction[]> {
    const [userStatePda] = PublicKey.findProgramAddressSync(
    [userPubkey.toBytes()],
    lootboxProgram.programId
    )

    const state = await lootboxProgram.account.userState.fetch(userStatePda)

    const accounts = await getAccountsAndData(
    lootboxProgram,
    switchboardProgram,
    userPubkey,
    state.vrf
    )

    return await createAllOpenLootboxInstructions(
    connection,
    stakingProgram,
    lootboxProgram,
    switchboardProgram,
    accounts,
    nftTokenAccount,
    box
    )
    }

    进一步往下,有一个 getAccountsAndData 函数,它接受四个字段,正如你所见,对于最后一个字段,你需要事先生成或获取vrf账户。这个函数的作用是获取一些账户、增加和其他数据,将它们打包起来,并作为一个对象返回。

    async function getAccountsAndData(
    lootboxProgram: Program<LootboxProgram>,
    switchboardProgram: SwitchboardProgram,
    userPubkey: PublicKey,
    vrfAccount: PublicKey
    ): Promise<AccountsAndDataSuperset> {
    const [userStatePda] = PublicKey.findProgramAddressSync(
    [userPubkey.toBytes()],
    lootboxProgram.programId
    )

    // required switchboard accoount
    const [programStateAccount, stateBump] =
    ProgramStateAccount.fromSeed(switchboardProgram)

    // required switchboard accoount
    const queueAccount = new OracleQueueAccount({
    program: switchboardProgram,
    // devnet permissionless queue
    publicKey: new PublicKey("F8ce7MsckeZAbAGmxjJNetxYXQa9mKr9nnrC3qKubyYy"),
    })

    // required switchboard accoount
    const queueState = await queueAccount.loadData()
    // wrapped SOL is used to pay for switchboard VRF requests
    const wrappedSOLMint = await queueAccount.loadMint()

    // required switchboard accoount
    const [permissionAccount, permissionBump] = PermissionAccount.fromSeed(
    switchboardProgram,
    queueState.authority,
    queueAccount.publicKey,
    vrfAccount
    )

    // required switchboard accoount
    // escrow wrapped SOL token account owned by the VRF account we will initialize
    const escrow = await spl.getAssociatedTokenAddress(
    wrappedSOLMint.address,
    vrfAccount,
    true
    )

    const size = switchboardProgram.account.vrfAccountData.size

    return {
    userPubkey: userPubkey,
    userStatePda: userStatePda,
    vrfAccount: vrfAccount,
    escrow: escrow,
    wrappedSOLMint: wrappedSOLMint,
    programStateAccount: programStateAccount,
    stateBump: stateBump,
    permissionBump: permissionBump,
    queueAccount: queueAccount,
    queueState: queueState,
    permissionAccount: permissionAccount,
    size: size,
    }
    }

    该段描述了在文件底部定义的一个接口对象,这主要是为了确保你拥有所需的所有内容,并能够适当地调用它们。这个接口包括了许多公钥和与程序状态、权限等有关的字段。

    以下是接口的代码定义:

    interface AccountsAndDataSuperset {
    userPubkey: PublicKey
    userStatePda: PublicKey
    vrfAccount: PublicKey
    escrow: PublicKey
    wrappedSOLMint: spl.Mint
    programStateAccount: ProgramStateAccount
    stateBump: number
    permissionBump: number
    queueAccount: OracleQueueAccount
    queueState: any
    permissionAccount: PermissionAccount
    size: number
    }

    该段还深入介绍了createInitSwitchboardInstructions函数。这个函数首先生成一个vrf密钥对,然后调用getAccountsAndData以获取所有必要的账户。接着,通过initSwitchboardLootboxUser,它组装了指令,并返回这些指令和用于签名的vrf密钥对。

    export async function createInitSwitchboardInstructions(
    switchboardProgram: SwitchboardProgram,
    lootboxProgram: Program<LootboxProgram>,
    userPubkey: PublicKey
    ): Promise<{
    instructions: Array<TransactionInstruction>
    vrfKeypair: Keypair
    }> {
    const vrfKeypair = Keypair.generate()

    const accounts = await getAccountsAndData(
    lootboxProgram,
    switchboardProgram,
    userPubkey,
    vrfKeypair.publicKey
    )

    const initInstructions = await initSwitchboardLootboxUser(
    switchboardProgram,
    lootboxProgram,
    accounts,
    vrfKeypair
    )

    return { instructions: initInstructions, vrfKeypair: vrfKeypair }
    }

    关于 initSwitchboardLootboxUser ,我们首先获得一个PDAstate bump

    async function initSwitchboardLootboxUser(
    switchboardProgram: SwitchboardProgram,
    lootboxProgram: Program<LootboxProgram>,
    accountsAndData: AccountsAndDataSuperset,
    vrfKeypair: Keypair
    ): Promise<Array<TransactionInstruction>> {
    // lootbox account PDA
    const [lootboxPointerPda] = await PublicKey.findProgramAddress(
    [Buffer.from("lootbox"), accountsAndData.userPubkey.toBytes()],
    lootboxProgram.programId
    )

    const stateBump = accountsAndData.stateBump

    然后我们开始组装一系列的指令。首先,我们需要做的是创建一个与托管相关的令牌账户,由vrf密钥对拥有。

    const txnIxns: TransactionInstruction[] = [
    // create escrow ATA owned by VRF account
    spl.createAssociatedTokenAccountInstruction(
    accountsAndData.userPubkey,
    accountsAndData.escrow,
    vrfKeypair.publicKey,
    accountsAndData.wrappedSOLMint.address
    ),

    接下来是设置权限指令。

    // transfer escrow ATA owner to switchboard programStateAccount
    spl.createSetAuthorityInstruction(
    accountsAndData.escrow,
    vrfKeypair.publicKey,
    spl.AuthorityType.AccountOwner,
    accountsAndData.programStateAccount.publicKey,
    [vrfKeypair]
    ),

    然后我们调用create account来创建vrf账户。

    // request system program to create new account using newly generated keypair for VRF account
    SystemProgram.createAccount({
    fromPubkey: accountsAndData.userPubkey,
    newAccountPubkey: vrfKeypair.publicKey,
    space: accountsAndData.size,
    lamports:
    await switchboardProgram.provider.connection.getMinimumBalanceForRentExemption(
    accountsAndData.size
    ),
    programId: switchboardProgram.programId,
    }),

    然后我们使用switchboard program 方法进行vrf初始化,其中我们提供了消耗随机性回调函数。

    // initialize new VRF account, included the callback CPI into lootbox program as instruction data
    await switchboardProgram.methods
    .vrfInit({
    stateBump,
    callback: {
    programId: lootboxProgram.programId,
    accounts: [
    {
    pubkey: accountsAndData.userStatePda,
    isSigner: false,
    isWritable: true,
    },
    {
    pubkey: vrfKeypair.publicKey,
    isSigner: false,
    isWritable: false,
    },
    { pubkey: lootboxPointerPda, isSigner: false, isWritable: true },
    {
    pubkey: accountsAndData.userPubkey,
    isSigner: false,
    isWritable: false,
    },
    ],
    ixData: new BorshInstructionCoder(lootboxProgram.idl).encode(
    "consumeRandomness",
    ""
    ),
    },
    })
    .accounts({
    vrf: vrfKeypair.publicKey,
    escrow: accountsAndData.escrow,
    authority: accountsAndData.userStatePda,
    oracleQueue: accountsAndData.queueAccount.publicKey,
    programState: accountsAndData.programStateAccount.publicKey,
    tokenProgram: spl.TOKEN_PROGRAM_ID,
    })
    .instruction(),
    // initialize switchboard permission account, required account

    接下来我们使用switchboard来调用权限初始化。

    await switchboardProgram.methods
    .permissionInit({})
    .accounts({
    permission: accountsAndData.permissionAccount.publicKey,
    authority: accountsAndData.queueState.authority,
    granter: accountsAndData.queueAccount.publicKey,
    grantee: vrfKeypair.publicKey,
    payer: accountsAndData.userPubkey,
    systemProgram: SystemProgram.programId,
    })
    .instruction(),

    最后,我们将我们的战利品箱计划称为init user,并返回指示,这将由调用者打包成交易。

    await lootboxProgram.methods
    .initUser({
    switchboardStateBump: accountsAndData.stateBump,
    vrfPermissionBump: accountsAndData.permissionBump,
    })
    .accounts({
    // state: userStatePDA,
    vrf: vrfKeypair.publicKey,
    // payer: publicKey,
    // systemProgram: anchor.web3.SystemProgram.programId,
    })
    .instruction(),
    ]

    return txnIxns
    }

    最后,让我们回顾一下 createOpenLootboxInstructions 。首先,我们获取用户状态PDA,我们必须实际获取该账户,以便我们可以从中提取vrf密钥对。

    export async function createOpenLootboxInstructions(
    connection: Connection,
    stakingProgram: Program<AnchorNftStaking>,
    switchboardProgram: SwitchboardProgram,
    lootboxProgram: Program<LootboxProgram>,
    userPubkey: PublicKey,
    nftTokenAccount: PublicKey,
    box: number
    ): Promise<TransactionInstruction[]> {
    const [userStatePda] = PublicKey.findProgramAddressSync(
    [userPubkey.toBytes()],
    lootboxProgram.programId
    )

    const state = await lootboxProgram.account.userState.fetch(userStatePda)

    在这里,我们称之为 getAccountsAndData 来获取我们所需的所有账户。接下来是 createAllOpenLootboxInstructions ,我们将深入探讨。

    const accounts = await getAccountsAndData(
    lootboxProgram,
    switchboardProgram,
    userPubkey,
    state.vrf
    )

    return await createAllOpenLootboxInstructions(
    connection,
    stakingProgram,
    lootboxProgram,
    switchboardProgram,
    accounts,
    nftTokenAccount,
    box
    )
    }

    我们获得了包装的代币账户,其中包含了包装的SOL,因为这是我们用来支付请求随机数的必需品。

    async function createAllOpenLootboxInstructions(
    connection: Connection,
    stakingProgram: Program<AnchorNftStaking>,
    lootboxProgram: Program<LootboxProgram>,
    switchboardProgram: SwitchboardProgram,
    accountsAndData: AccountsAndDataSuperset,
    nftTokenAccount: PublicKey,
    box: number
    ): Promise<TransactionInstruction[]> {
    // user Wrapped SOL token account
    // wSOL amount is then transferred to escrow account to pay switchboard oracle for VRF request
    const wrappedTokenAccount = await spl.getAssociatedTokenAddress(
    accountsAndData.wrappedSOLMint.address,
    accountsAndData.userPubkey
    )

    接下来我们获得与BLD相关的 stakeTokenAccount ,因此你可以使用BLD代币来换取开启战利品箱。然后是质押账户,以确保你通过质押获得足够的BLD来开启战利品箱。

    // user BLD token account, used to pay BLD tokens to call the request randomness instruction on Lootbox program
    const stakeTokenAccount = await spl.getAssociatedTokenAddress(
    STAKE_MINT,
    accountsAndData.userPubkey
    )

    const [stakeAccount] = PublicKey.findProgramAddressSync(
    [accountsAndData.userPubkey.toBytes(), nftTokenAccount.toBuffer()],
    stakingProgram.programId
    )

    这里开始组装说明。如果没有封装的令牌账户,我们会添加一个创建它的指令。

    let instructions: TransactionInstruction[] = []
    // check if a wrapped SOL token account exists, if not add instruction to create one
    const account = await connection.getAccountInfo(wrappedTokenAccount)
    if (!account) {
    instructions.push(
    spl.createAssociatedTokenAccountInstruction(
    accountsAndData.userPubkey,
    wrappedTokenAccount,
    accountsAndData.userPubkey,
    accountsAndData.wrappedSOLMint.address
    )
    )
    }

    然后我们推送一个转账指令,将SOL转移到wrapped SOL。然后是一个同步wrapped SOL余额的指令。

    // transfer SOL to user's own wSOL token account
    instructions.push(
    SystemProgram.transfer({
    fromPubkey: accountsAndData.userPubkey,
    toPubkey: wrappedTokenAccount,
    lamports: 0.002 * LAMPORTS_PER_SOL,
    })
    )
    // sync wrapped SOL balance
    instructions.push(spl.createSyncNativeInstruction(wrappedTokenAccount))

    最后,我们制作并返回了打开战利品箱的说明书,这样呼叫者就可以将它们打包并发送出去。

    // Lootbox program request randomness instruction
    instructions.push(
    await lootboxProgram.methods
    .openLootbox(new BN(box))
    .accounts({
    user: accountsAndData.userPubkey,
    stakeMint: STAKE_MINT,
    stakeMintAta: stakeTokenAccount,
    stakeState: stakeAccount,
    state: accountsAndData.userStatePda,
    vrf: accountsAndData.vrfAccount,
    oracleQueue: accountsAndData.queueAccount.publicKey,
    queueAuthority: accountsAndData.queueState.authority,
    dataBuffer: accountsAndData.queueState.dataBuffer,
    permission: accountsAndData.permissionAccount.publicKey,
    escrow: accountsAndData.escrow,
    programState: accountsAndData.programStateAccount.publicKey,
    switchboardProgram: switchboardProgram.programId,
    payerWallet: wrappedTokenAccount,
    recentBlockhashes: SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
    })
    .instruction()
    )

    return instructions
    }

    这就是说明的全部内容,让我们去看看新的战利品箱组件,这些说明将会被用到那里。

    - - +
    Skip to main content

    🚶‍ 最终作品

    概述

    终于到了最后的冲刺阶段!恭喜你抵达此地!这对每个人来说都是一次令人激动的旅程。不论你的NFT项目处于何种阶段,都要深呼吸,给自己一个赞扬,你做得非常棒!

    现在,审视一下你手头的成果,然后思考一下,为了让项目做好交付准备,你至少还需要做些什么。如果需要暂时跳过Switchboard的部分,那就这样做。

    现在是时候把你的用户界面与战利品箱和装备指示器连接起来,完成最后的修整工作,然后交付这个作品!

    具体来说,我们需要:

    • 使用GearItemLootbox组件替换UI中使用的模拟ItemBox
    • 添加一个instructions.ts文件,在其中创建函数来:
      • 创建初始化战利品箱和交换机所需的所有指令。
      • 创建打开战利品箱所需的所有指令。
      • 注意:这部分可能有些复杂 - 你可以参考我们的解决方案代码,但也不妨尝试自己的方法。
    • 进行大量的调试和优化。

    坦白说,这个列表可能还远远不够。我们添加了许多组件来确保交易和链上变化后状态得到更新,但它仍有不完美的地方。总有更多的空间可以改进,但不要让完美主义成为你前进的障碍。尽你所能,然后交付吧!

    解决方案代码

    我们的解决方案位于Buildoors代码库solution-lootboxes分支上。与你上次查看的代码可能有些许差异,因此如果你想查看所有更改,请确保从上周的分支查看差异

    有一些引导,但你可以自由开始。祝你好运!

    下一步

    现在,最后一个项目所需的一切都在上一课中。从这一刻起,这就是你和文字之间的事情了,宝贝。我们开始吧!

    我们接下来要深入研究一些代码的更改。从/components/WorkspaceProvider.tsx开始。

    这里只有一些小更改,主要是为了引入switchboard program

    const [switchboardProgram, setProgramSwitchboard] = useState<any>()

    然后,我们加载switchboard program,并使用useEffect设置the program switchboard,确保我们的工作区始终能够及时更新所有所需程序。这可能会是一个挑战,除非你是React的专家,否则请随意深入研究这段代码。

    async function program() {
    let response = await loadSwitchboardProgram(
    "devnet",
    connection,
    ((provider as AnchorProvider).wallet as AnchorWallet).payer
    )
    return response
    }

    useEffect(() => {
    program().then((result) => {
    setProgramSwitchboard(result)
    console.log("result", result)
    })
    }, [connection])

    好的,接下来我们进入 instructions.ts 文件夹中的 utils 文件,这是一个新文件。这里有两个公共函数,分别是 createOpenLootboxInstructions 指令和 createInitSwitchboardInstructions 指令。后者用于打包交换机程序的初始化内容,并初始化抽奖箱程序中的用户。

    export async function createOpenLootboxInstructions(
    connection: Connection,
    stakingProgram: Program<AnchorNftStaking>,
    switchboardProgram: SwitchboardProgram,
    lootboxProgram: Program<LootboxProgram>,
    userPubkey: PublicKey,
    nftTokenAccount: PublicKey,
    box: number
    ): Promise<TransactionInstruction[]> {
    const [userStatePda] = PublicKey.findProgramAddressSync(
    [userPubkey.toBytes()],
    lootboxProgram.programId
    )

    const state = await lootboxProgram.account.userState.fetch(userStatePda)

    const accounts = await getAccountsAndData(
    lootboxProgram,
    switchboardProgram,
    userPubkey,
    state.vrf
    )

    return await createAllOpenLootboxInstructions(
    connection,
    stakingProgram,
    lootboxProgram,
    switchboardProgram,
    accounts,
    nftTokenAccount,
    box
    )
    }

    进一步往下,有一个 getAccountsAndData 函数,它接受四个字段,正如你所见,对于最后一个字段,你需要事先生成或获取vrf账户。这个函数的作用是获取一些账户、增加和其他数据,将它们打包起来,并作为一个对象返回。

    async function getAccountsAndData(
    lootboxProgram: Program<LootboxProgram>,
    switchboardProgram: SwitchboardProgram,
    userPubkey: PublicKey,
    vrfAccount: PublicKey
    ): Promise<AccountsAndDataSuperset> {
    const [userStatePda] = PublicKey.findProgramAddressSync(
    [userPubkey.toBytes()],
    lootboxProgram.programId
    )

    // required switchboard accoount
    const [programStateAccount, stateBump] =
    ProgramStateAccount.fromSeed(switchboardProgram)

    // required switchboard accoount
    const queueAccount = new OracleQueueAccount({
    program: switchboardProgram,
    // devnet permissionless queue
    publicKey: new PublicKey("F8ce7MsckeZAbAGmxjJNetxYXQa9mKr9nnrC3qKubyYy"),
    })

    // required switchboard accoount
    const queueState = await queueAccount.loadData()
    // wrapped SOL is used to pay for switchboard VRF requests
    const wrappedSOLMint = await queueAccount.loadMint()

    // required switchboard accoount
    const [permissionAccount, permissionBump] = PermissionAccount.fromSeed(
    switchboardProgram,
    queueState.authority,
    queueAccount.publicKey,
    vrfAccount
    )

    // required switchboard accoount
    // escrow wrapped SOL token account owned by the VRF account we will initialize
    const escrow = await spl.getAssociatedTokenAddress(
    wrappedSOLMint.address,
    vrfAccount,
    true
    )

    const size = switchboardProgram.account.vrfAccountData.size

    return {
    userPubkey: userPubkey,
    userStatePda: userStatePda,
    vrfAccount: vrfAccount,
    escrow: escrow,
    wrappedSOLMint: wrappedSOLMint,
    programStateAccount: programStateAccount,
    stateBump: stateBump,
    permissionBump: permissionBump,
    queueAccount: queueAccount,
    queueState: queueState,
    permissionAccount: permissionAccount,
    size: size,
    }
    }

    该段描述了在文件底部定义的一个接口对象,这主要是为了确保你拥有所需的所有内容,并能够适当地调用它们。这个接口包括了许多公钥和与程序状态、权限等有关的字段。

    以下是接口的代码定义:

    interface AccountsAndDataSuperset {
    userPubkey: PublicKey
    userStatePda: PublicKey
    vrfAccount: PublicKey
    escrow: PublicKey
    wrappedSOLMint: spl.Mint
    programStateAccount: ProgramStateAccount
    stateBump: number
    permissionBump: number
    queueAccount: OracleQueueAccount
    queueState: any
    permissionAccount: PermissionAccount
    size: number
    }

    该段还深入介绍了createInitSwitchboardInstructions函数。这个函数首先生成一个vrf密钥对,然后调用getAccountsAndData以获取所有必要的账户。接着,通过initSwitchboardLootboxUser,它组装了指令,并返回这些指令和用于签名的vrf密钥对。

    export async function createInitSwitchboardInstructions(
    switchboardProgram: SwitchboardProgram,
    lootboxProgram: Program<LootboxProgram>,
    userPubkey: PublicKey
    ): Promise<{
    instructions: Array<TransactionInstruction>
    vrfKeypair: Keypair
    }> {
    const vrfKeypair = Keypair.generate()

    const accounts = await getAccountsAndData(
    lootboxProgram,
    switchboardProgram,
    userPubkey,
    vrfKeypair.publicKey
    )

    const initInstructions = await initSwitchboardLootboxUser(
    switchboardProgram,
    lootboxProgram,
    accounts,
    vrfKeypair
    )

    return { instructions: initInstructions, vrfKeypair: vrfKeypair }
    }

    关于 initSwitchboardLootboxUser ,我们首先获得一个PDAstate bump

    async function initSwitchboardLootboxUser(
    switchboardProgram: SwitchboardProgram,
    lootboxProgram: Program<LootboxProgram>,
    accountsAndData: AccountsAndDataSuperset,
    vrfKeypair: Keypair
    ): Promise<Array<TransactionInstruction>> {
    // lootbox account PDA
    const [lootboxPointerPda] = await PublicKey.findProgramAddress(
    [Buffer.from("lootbox"), accountsAndData.userPubkey.toBytes()],
    lootboxProgram.programId
    )

    const stateBump = accountsAndData.stateBump

    然后我们开始组装一系列的指令。首先,我们需要做的是创建一个与托管相关的令牌账户,由vrf密钥对拥有。

    const txnIxns: TransactionInstruction[] = [
    // create escrow ATA owned by VRF account
    spl.createAssociatedTokenAccountInstruction(
    accountsAndData.userPubkey,
    accountsAndData.escrow,
    vrfKeypair.publicKey,
    accountsAndData.wrappedSOLMint.address
    ),

    接下来是设置权限指令。

    // transfer escrow ATA owner to switchboard programStateAccount
    spl.createSetAuthorityInstruction(
    accountsAndData.escrow,
    vrfKeypair.publicKey,
    spl.AuthorityType.AccountOwner,
    accountsAndData.programStateAccount.publicKey,
    [vrfKeypair]
    ),

    然后我们调用create account来创建vrf账户。

    // request system program to create new account using newly generated keypair for VRF account
    SystemProgram.createAccount({
    fromPubkey: accountsAndData.userPubkey,
    newAccountPubkey: vrfKeypair.publicKey,
    space: accountsAndData.size,
    lamports:
    await switchboardProgram.provider.connection.getMinimumBalanceForRentExemption(
    accountsAndData.size
    ),
    programId: switchboardProgram.programId,
    }),

    然后我们使用switchboard program 方法进行vrf初始化,其中我们提供了消耗随机性回调函数。

    // initialize new VRF account, included the callback CPI into lootbox program as instruction data
    await switchboardProgram.methods
    .vrfInit({
    stateBump,
    callback: {
    programId: lootboxProgram.programId,
    accounts: [
    {
    pubkey: accountsAndData.userStatePda,
    isSigner: false,
    isWritable: true,
    },
    {
    pubkey: vrfKeypair.publicKey,
    isSigner: false,
    isWritable: false,
    },
    { pubkey: lootboxPointerPda, isSigner: false, isWritable: true },
    {
    pubkey: accountsAndData.userPubkey,
    isSigner: false,
    isWritable: false,
    },
    ],
    ixData: new BorshInstructionCoder(lootboxProgram.idl).encode(
    "consumeRandomness",
    ""
    ),
    },
    })
    .accounts({
    vrf: vrfKeypair.publicKey,
    escrow: accountsAndData.escrow,
    authority: accountsAndData.userStatePda,
    oracleQueue: accountsAndData.queueAccount.publicKey,
    programState: accountsAndData.programStateAccount.publicKey,
    tokenProgram: spl.TOKEN_PROGRAM_ID,
    })
    .instruction(),
    // initialize switchboard permission account, required account

    接下来我们使用switchboard来调用权限初始化。

    await switchboardProgram.methods
    .permissionInit({})
    .accounts({
    permission: accountsAndData.permissionAccount.publicKey,
    authority: accountsAndData.queueState.authority,
    granter: accountsAndData.queueAccount.publicKey,
    grantee: vrfKeypair.publicKey,
    payer: accountsAndData.userPubkey,
    systemProgram: SystemProgram.programId,
    })
    .instruction(),

    最后,我们将我们的战利品箱计划称为init user,并返回指示,这将由调用者打包成交易。

    await lootboxProgram.methods
    .initUser({
    switchboardStateBump: accountsAndData.stateBump,
    vrfPermissionBump: accountsAndData.permissionBump,
    })
    .accounts({
    // state: userStatePDA,
    vrf: vrfKeypair.publicKey,
    // payer: publicKey,
    // systemProgram: anchor.web3.SystemProgram.programId,
    })
    .instruction(),
    ]

    return txnIxns
    }

    最后,让我们回顾一下 createOpenLootboxInstructions 。首先,我们获取用户状态PDA,我们必须实际获取该账户,以便我们可以从中提取vrf密钥对。

    export async function createOpenLootboxInstructions(
    connection: Connection,
    stakingProgram: Program<AnchorNftStaking>,
    switchboardProgram: SwitchboardProgram,
    lootboxProgram: Program<LootboxProgram>,
    userPubkey: PublicKey,
    nftTokenAccount: PublicKey,
    box: number
    ): Promise<TransactionInstruction[]> {
    const [userStatePda] = PublicKey.findProgramAddressSync(
    [userPubkey.toBytes()],
    lootboxProgram.programId
    )

    const state = await lootboxProgram.account.userState.fetch(userStatePda)

    在这里,我们称之为 getAccountsAndData 来获取我们所需的所有账户。接下来是 createAllOpenLootboxInstructions ,我们将深入探讨。

    const accounts = await getAccountsAndData(
    lootboxProgram,
    switchboardProgram,
    userPubkey,
    state.vrf
    )

    return await createAllOpenLootboxInstructions(
    connection,
    stakingProgram,
    lootboxProgram,
    switchboardProgram,
    accounts,
    nftTokenAccount,
    box
    )
    }

    我们获得了包装的代币账户,其中包含了包装的SOL,因为这是我们用来支付请求随机数的必需品。

    async function createAllOpenLootboxInstructions(
    connection: Connection,
    stakingProgram: Program<AnchorNftStaking>,
    lootboxProgram: Program<LootboxProgram>,
    switchboardProgram: SwitchboardProgram,
    accountsAndData: AccountsAndDataSuperset,
    nftTokenAccount: PublicKey,
    box: number
    ): Promise<TransactionInstruction[]> {
    // user Wrapped SOL token account
    // wSOL amount is then transferred to escrow account to pay switchboard oracle for VRF request
    const wrappedTokenAccount = await spl.getAssociatedTokenAddress(
    accountsAndData.wrappedSOLMint.address,
    accountsAndData.userPubkey
    )

    接下来我们获得与BLD相关的 stakeTokenAccount ,因此你可以使用BLD代币来换取开启战利品箱。然后是质押账户,以确保你通过质押获得足够的BLD来开启战利品箱。

    // user BLD token account, used to pay BLD tokens to call the request randomness instruction on Lootbox program
    const stakeTokenAccount = await spl.getAssociatedTokenAddress(
    STAKE_MINT,
    accountsAndData.userPubkey
    )

    const [stakeAccount] = PublicKey.findProgramAddressSync(
    [accountsAndData.userPubkey.toBytes(), nftTokenAccount.toBuffer()],
    stakingProgram.programId
    )

    这里开始组装说明。如果没有封装的令牌账户,我们会添加一个创建它的指令。

    let instructions: TransactionInstruction[] = []
    // check if a wrapped SOL token account exists, if not add instruction to create one
    const account = await connection.getAccountInfo(wrappedTokenAccount)
    if (!account) {
    instructions.push(
    spl.createAssociatedTokenAccountInstruction(
    accountsAndData.userPubkey,
    wrappedTokenAccount,
    accountsAndData.userPubkey,
    accountsAndData.wrappedSOLMint.address
    )
    )
    }

    然后我们推送一个转账指令,将SOL转移到wrapped SOL。然后是一个同步wrapped SOL余额的指令。

    // transfer SOL to user's own wSOL token account
    instructions.push(
    SystemProgram.transfer({
    fromPubkey: accountsAndData.userPubkey,
    toPubkey: wrappedTokenAccount,
    lamports: 0.002 * LAMPORTS_PER_SOL,
    })
    )
    // sync wrapped SOL balance
    instructions.push(spl.createSyncNativeInstruction(wrappedTokenAccount))

    最后,我们制作并返回了打开战利品箱的说明书,这样呼叫者就可以将它们打包并发送出去。

    // Lootbox program request randomness instruction
    instructions.push(
    await lootboxProgram.methods
    .openLootbox(new BN(box))
    .accounts({
    user: accountsAndData.userPubkey,
    stakeMint: STAKE_MINT,
    stakeMintAta: stakeTokenAccount,
    stakeState: stakeAccount,
    state: accountsAndData.userStatePda,
    vrf: accountsAndData.vrfAccount,
    oracleQueue: accountsAndData.queueAccount.publicKey,
    queueAuthority: accountsAndData.queueState.authority,
    dataBuffer: accountsAndData.queueState.dataBuffer,
    permission: accountsAndData.permissionAccount.publicKey,
    escrow: accountsAndData.escrow,
    programState: accountsAndData.programStateAccount.publicKey,
    switchboardProgram: switchboardProgram.programId,
    payerWallet: wrappedTokenAccount,
    recentBlockhashes: SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
    })
    .instruction()
    )

    return instructions
    }

    这就是说明的全部内容,让我们去看看新的战利品箱组件,这些说明将会被用到那里。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module6/finishing-touches/the-last-ship/index.html b/Solana-Co-Learn/module6/finishing-touches/the-last-ship/index.html index 5291b934f..2f6fe6ae5 100644 --- a/Solana-Co-Learn/module6/finishing-touches/the-last-ship/index.html +++ b/Solana-Co-Learn/module6/finishing-touches/the-last-ship/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🏁 最后一艘船

    天哪,你终于做到了 🫡

    非常感谢你参与我们的内测计划!没有你的支持,这一切都不可能实现。

    来自 706 & RustyCab 团队的最后几件事情

    1. 请将你的提交放入展示频道!我们想看看你建造了什么!!

    2. 快发个推文吧!你做了大量的工作,全世界都应该知道!

    你现在正式在玻璃上冲浪——我们在那里见。

    - - +
    Skip to main content

    🏁 最后一艘船

    天哪,你终于做到了 🫡

    非常感谢你参与我们的内测计划!没有你的支持,这一切都不可能实现。

    来自 706 & RustyCab 团队的最后几件事情

    1. 请将你的提交放入展示频道!我们想看看你建造了什么!!

    2. 快发个推文吧!你做了大量的工作,全世界都应该知道!

    你现在正式在玻璃上冲浪——我们在那里见。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module6/index.html b/Solana-Co-Learn/module6/index.html index 112d9229f..b5aab1a7b 100644 --- a/Solana-Co-Learn/module6/index.html +++ b/Solana-Co-Learn/module6/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module6/randomness/build-a-randomiser/index.html b/Solana-Co-Learn/module6/randomness/build-a-randomiser/index.html index 6518a7780..99d84c62d 100644 --- a/Solana-Co-Learn/module6/randomness/build-a-randomiser/index.html +++ b/Solana-Co-Learn/module6/randomness/build-a-randomiser/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    👁‍🗨 构造随机器

    Switchboard设置的详细步骤 🚶🏽🔀

    概览

    我们将通过Switchboard来构建一个基础程序,以实现随机数的请求。在此视频中,我们将重点关注如何在测试环境中配置Switchboard客户端。

    首先,我们要进行交换机的初始化设置,你可以在/tests/utils/setupSwitchboard.ts文件中找到相关代码。

    这个设置是用于运行测试的。虽然他们的文档非常精简,但对于随机化部分,我们应该已经了解得足够清楚了。

    让我们一起回顾一下代码。首先,我们需要导入以下三个库:

    import { SwitchboardTestContext } from "@switchboard-xyz/sbv2-utils"
    import * as anchor from "@project-serum/anchor"
    import * as sbv2 from "@switchboard-xyz/switchboard-v2"

    在实际功能方面,你会注意到我们传入的三个项目分别是提供者、战利品箱计划和付款人。

    我们要做的第一件事是加载devnet队列,这样我们就可以在devnet上进行测试了。ID是Switchboard的程序ID100,000,000则是switchboard代币数量,我们需要访问它们。

    export const setupSwitchboard = async (provider, lootboxProgram, payer) => {

    const switchboard = await SwitchboardTestContext.loadDevnetQueue(
    provider,
    "F8ce7MsckeZAbAGmxjJNetxYXQa9mKr9nnrC3qKubyYy",
    100_000_000
    )

    接下来,我们会看到一些日志,确保一切都准备就绪。

    console.log(switchboard.mint.address.toString())

    await switchboard.oracleHeartbeat();

    const { queue, unpermissionedVrfEnabled, authority } =
    await switchboard.queue.loadData();

    console.log(`oracleQueue: ${switchboard.queue.publicKey}`);
    console.log(`unpermissionedVrfEnabled: ${unpermissionedVrfEnabled}`);
    console.log(`# of oracles heartbeating: ${queue.length}`);
    console.log(
    "\x1b[32m%s\x1b[0m",
    `\u2714 Switchboard devnet环境成功加载\n`
    );

    以上的const语句加载了我们所需的交换机队列数据,在函数的后续部分我们将用到这些数据。

    接下来,我们创建验证随机函数(VRF)账户,这一部分对于我们使用的交换板非常特殊。你会看到,它会生成一个新的密钥对。

    // 创建VRF账户
    // VRF账户的密钥对
    const vrfKeypair = anchor.web3.Keypair.generate()

    在创建VRF账户的过程中,我们需要访问一些PDA设备。

    // 寻找用于客户端状态公钥的PDA
    const [userState] = anchor.utils.publicKey.findProgramAddressSync(
    [vrfKeypair.publicKey.toBytes(), payer.publicKey.toBytes()],
    lootboxProgram.programId
    )

    // 用于回调的lootboxPointerPda
    const [lootboxPointerPda] = anchor.web3.PublicKey.findProgramAddressSync(
    [Buffer.from("lootbox"), payer.publicKey.toBuffer()],
    lootboxProgram.programId
    )

    你会注意到我们使用了vrfpayer的公钥作为种子。在生产环境中,它们需要是静态的,只有payer的公钥会变化。这段代码确保我们在每次测试运行时都有不同的vrf密钥对和用户状态,这样我们在测试过程中不会遇到试图重新创建已经存在的账户的问题。

    现在,我们可以使用sbv2库创建VRF账户,并传入交换程序、我们为VRF账户提供的密钥对、作为授权的用户状态PDA、交换机队列和回调函数。

    因此,当我们需要一个新的随机数时,我们将通过与交换机程序进行CPI交互来获取随机数。它必须知道我们程序中的一条特定指令来执行CPI回调,以便为我们提供随机数。像所有的指令一样,它具有一个程序ID、一个账户列表和指令数据。关于账户,第一个是用于写入数据的位置,然后是vrf账户,我们将在其中写入已选的mintlootbox指针PDA,最后是付款人。

    // 创建新的vrf账户
    const vrfAccount = await sbv2.VrfAccount.create(switchboard.program, {
    keypair: vrfKeypair,
    authority: userState, // 将PDA设为vrf账户的授权
    queue: switchboard.queue,
    callback: {
    programId: lootboxProgram.programId,
    accounts: [
    { pubkey: userState, isSigner: false, isWritable: true },
    { pubkey: vrfKeypair.publicKey, isSigner: false, isWritable: false },
    { pubkey: lootboxPointerPda, isSigner: false, isWritable: true },
    { pubkey: payer.publicKey, isSigner: false, isWritable: false },
    ],
    ixData: new anchor.BorshInstructionCoder(lootboxProgram.idl).encode(
    "consumeRandomness",
    ""
    ),
    },
    })

    接下来我们要创建一个所谓的权限账户。

    // CREATE PERMISSION ACCOUNT
    const permissionAccount = await sbv2.PermissionAccount.create(
    switchboard.program,
    {
    authority,
    granter: switchboard.queue.publicKey,
    grantee: vrfAccount.publicKey,
    }
    )

    权限字段是从上文的队列中获取的加载数据。这将在交换机中给我们的 vrf 账户授权。

    下一步,我们会将权限更改为我们自己,并将其设置为付款方。

    // 如果队列需要权限来使用 VRF,请检查是否提供了正确的授权
    if (!unpermissionedVrfEnabled) {
    if (!payer.publicKey.equals(authority)) {
    throw new Error(
    `队列需要 PERMIT_VRF_REQUESTS 权限,而提供的队列授权错误`
    )
    }

    await permissionAccount.set({
    authority: payer,
    permission: sbv2.SwitchboardPermission.PERMIT_VRF_REQUESTS,
    enable: true,
    })
    }

    由于稍后我们需要切换板账户的提升,因此我们将其提取出来,还有 switchboardStateBump,这是切换板的程序账户。

    // 获取权限提升和切换板状态提升
    const [_permissionAccount, permissionBump] = sbv2.PermissionAccount.fromSeed(
    switchboard.program,
    authority,
    switchboard.queue.publicKey,
    vrfAccount.publicKey
    )

    const [switchboardStateAccount, switchboardStateBump] =
    sbv2.ProgramStateAccount.fromSeed(switchboard.program)

    这就是我们进行测试与程序和交换机互动所需的所有数据,我们将在最后返回这些数据。

    return {
    switchboard: switchboard,
    lootboxPointerPda: lootboxPointerPda,
    permissionBump: permissionBump,
    permissionAccount: permissionAccount,
    switchboardStateBump: switchboardStateBump,
    switchboardStateAccount: switchboardStateAccount,
    vrfAccount: vrfAccount,
    }

    我们最终会在测试环境设置中调用整个函数,所以现在的 before 代码块是这样的。

    before(async () => {
    ;({ nft, stakeStatePda, mint, tokenAddress } = await setupNft(
    program,
    wallet.payer
    ))
    ;({
    switchboard,
    lootboxPointerPda,
    permissionBump,
    switchboardStateBump,
    vrfAccount,
    switchboardStateAccount,
    permissionAccount,
    } = await setupSwitchboard(provider, lootboxProgram, wallet.payer))
    })

    下面是关于客户端交换机所需的基本知识。

    init_user 指令的详细步骤 👶

    首先,对于我们的战利品箱计划,我们以前把所有东西都放在 lib.rs 里,但随着项目变得越来越庞大,也变得难以管理,所以现在我们对其进行了拆分,你可以在此链接查看文件结构。

    现在的 lib 文件主要只是一堆 use 语句、declare_id! 宏和我们的四个指令,它们只是调用其他文件。

    Init_user 将创建用户状态账户,我们将在程序和交换机之间共享该账户,它就像一个联络账户。

    打开战利品箱的过程与之前相同,它将开始生成随机货币的过程,但不会完成该过程,而是生成一个 CPI 来呼叫交换机以请求一个随机数。

    交换机将调用消耗随机性,以返回指令中的号码,以便我们可以使用它,并在设置薄荷时完成该过程。

    从战利品箱中获取物品基本上没有改变。

    让我们开始吧,首先是 init_user

    在文件的顶部,你会找到初始用户上下文,在底部有一个实现,其中有一个名为 process instruction 的函数,在该函数中执行了之前在 libs 文件中的逻辑。

    InitUser 上下文中有四个账户。状态是我们的用户状态对象,其中包含 vrfpayer 密钥种子,这是用于测试的版本。对于生产代码,你只需要 payer 种子。我们这样做是为了节省时间,而不是使用环境变量。然后有 vrf 账户,switchboard 不会自动加载它,因此需要使用 .load() 调用来加载。可能有其他使用 switchboard 的方法,但我们目前采用的是最简单/最快的路径来启动和运行,随时可以对其进行探索和改进。最后,我们有 payersystem 程序来创建一个新账户。

    use crate::*;

    #[derive(Accounts)]
    #[instruction(params: InitUserParams)]
    pub struct InitUser<'info> {
    #[account(
    init,
    // 测试 - 注释掉这些种子用于测试
    // seeds = [
    // payer.key().as_ref(),
    // ],
    // 测试 - 取消注释这些种子用于测试
    seeds = [
    vrf.key().as_ref(),
    payer.key().as_ref()
    ],
    payer = payer,
    space = 8 + std::mem::size_of::<UserState>(),
    bump,
    )]
    pub state: AccountLoader<'info, UserState>,
    #[account(
    constraint = vrf.load()?.authority == state.key() @ LootboxError::InvalidVrfAuthorityError
    )]
    pub vrf: AccountLoader<'info, VrfAccountData>,
    #[account(mut)]
    pub payer: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    在逻辑部分,我们正在操作名为state的账户,该账户设置了bumpswitchboard state bumpvrf permission bumpvrf账户以及与之关联的用户。你会注意到存在一个结构体,其中只包括了我们前面提到的两个bump

    #[derive(Clone, AnchorSerialize, AnchorDeserialize)]
    pub struct InitUserParams {
    pub switchboard_state_bump: u8,
    pub vrf_permission_bump: u8,
    }

    impl InitUser<'_> {
    pub fn process_instruction(ctx: &Context<Self>, params: &InitUserParams) -> Result<()> {
    let mut state = ctx.accounts.state.load_init()?;
    *state = UserState::default();
    state.bump = ctx.bumps.get("state").unwrap().clone();
    state.switchboard_state_bump = params.switchboard_state_bump;
    state.vrf_permission_bump = params.vrf_permission_bump;
    state.vrf = ctx.accounts.vrf.key();
    state.user = ctx.accounts.payer.key();

    Ok(())
    }
    }

    让我们快速了解一下用户状态文件,从而更好地理解其中的内容。

    其中新引入的部分是结果缓冲区。这是我们获取随机性的地方。系统会将随机数据作为一个32字节的数组发送给我们,我们可以将其转换为任何所需的随机性。

    请注意,这里还添加了两个属性。#[account(zero_copy)] 是一个需要加载的部分,我只是按照交换机示例中的建议进行操作的。

    #[repr(packed)]
    #[account(zero_copy)]
    #[derive(Default)]
    pub struct UserState {
    pub bump: u8,
    pub switchboard_state_bump: u8,
    pub vrf_permission_bump: u8,
    pub result_buffer: [u8; 32],
    pub vrf: Pubkey,
    pub user: Pubkey,
    }

    以上就是初始用户介绍的全部内容,我们可以继续深入了解了。

    - - +
    Skip to main content

    👁‍🗨 构造随机器

    Switchboard设置的详细步骤 🚶🏽🔀

    概览

    我们将通过Switchboard来构建一个基础程序,以实现随机数的请求。在此视频中,我们将重点关注如何在测试环境中配置Switchboard客户端。

    首先,我们要进行交换机的初始化设置,你可以在/tests/utils/setupSwitchboard.ts文件中找到相关代码。

    这个设置是用于运行测试的。虽然他们的文档非常精简,但对于随机化部分,我们应该已经了解得足够清楚了。

    让我们一起回顾一下代码。首先,我们需要导入以下三个库:

    import { SwitchboardTestContext } from "@switchboard-xyz/sbv2-utils"
    import * as anchor from "@project-serum/anchor"
    import * as sbv2 from "@switchboard-xyz/switchboard-v2"

    在实际功能方面,你会注意到我们传入的三个项目分别是提供者、战利品箱计划和付款人。

    我们要做的第一件事是加载devnet队列,这样我们就可以在devnet上进行测试了。ID是Switchboard的程序ID100,000,000则是switchboard代币数量,我们需要访问它们。

    export const setupSwitchboard = async (provider, lootboxProgram, payer) => {

    const switchboard = await SwitchboardTestContext.loadDevnetQueue(
    provider,
    "F8ce7MsckeZAbAGmxjJNetxYXQa9mKr9nnrC3qKubyYy",
    100_000_000
    )

    接下来,我们会看到一些日志,确保一切都准备就绪。

    console.log(switchboard.mint.address.toString())

    await switchboard.oracleHeartbeat();

    const { queue, unpermissionedVrfEnabled, authority } =
    await switchboard.queue.loadData();

    console.log(`oracleQueue: ${switchboard.queue.publicKey}`);
    console.log(`unpermissionedVrfEnabled: ${unpermissionedVrfEnabled}`);
    console.log(`# of oracles heartbeating: ${queue.length}`);
    console.log(
    "\x1b[32m%s\x1b[0m",
    `\u2714 Switchboard devnet环境成功加载\n`
    );

    以上的const语句加载了我们所需的交换机队列数据,在函数的后续部分我们将用到这些数据。

    接下来,我们创建验证随机函数(VRF)账户,这一部分对于我们使用的交换板非常特殊。你会看到,它会生成一个新的密钥对。

    // 创建VRF账户
    // VRF账户的密钥对
    const vrfKeypair = anchor.web3.Keypair.generate()

    在创建VRF账户的过程中,我们需要访问一些PDA设备。

    // 寻找用于客户端状态公钥的PDA
    const [userState] = anchor.utils.publicKey.findProgramAddressSync(
    [vrfKeypair.publicKey.toBytes(), payer.publicKey.toBytes()],
    lootboxProgram.programId
    )

    // 用于回调的lootboxPointerPda
    const [lootboxPointerPda] = anchor.web3.PublicKey.findProgramAddressSync(
    [Buffer.from("lootbox"), payer.publicKey.toBuffer()],
    lootboxProgram.programId
    )

    你会注意到我们使用了vrfpayer的公钥作为种子。在生产环境中,它们需要是静态的,只有payer的公钥会变化。这段代码确保我们在每次测试运行时都有不同的vrf密钥对和用户状态,这样我们在测试过程中不会遇到试图重新创建已经存在的账户的问题。

    现在,我们可以使用sbv2库创建VRF账户,并传入交换程序、我们为VRF账户提供的密钥对、作为授权的用户状态PDA、交换机队列和回调函数。

    因此,当我们需要一个新的随机数时,我们将通过与交换机程序进行CPI交互来获取随机数。它必须知道我们程序中的一条特定指令来执行CPI回调,以便为我们提供随机数。像所有的指令一样,它具有一个程序ID、一个账户列表和指令数据。关于账户,第一个是用于写入数据的位置,然后是vrf账户,我们将在其中写入已选的mintlootbox指针PDA,最后是付款人。

    // 创建新的vrf账户
    const vrfAccount = await sbv2.VrfAccount.create(switchboard.program, {
    keypair: vrfKeypair,
    authority: userState, // 将PDA设为vrf账户的授权
    queue: switchboard.queue,
    callback: {
    programId: lootboxProgram.programId,
    accounts: [
    { pubkey: userState, isSigner: false, isWritable: true },
    { pubkey: vrfKeypair.publicKey, isSigner: false, isWritable: false },
    { pubkey: lootboxPointerPda, isSigner: false, isWritable: true },
    { pubkey: payer.publicKey, isSigner: false, isWritable: false },
    ],
    ixData: new anchor.BorshInstructionCoder(lootboxProgram.idl).encode(
    "consumeRandomness",
    ""
    ),
    },
    })

    接下来我们要创建一个所谓的权限账户。

    // CREATE PERMISSION ACCOUNT
    const permissionAccount = await sbv2.PermissionAccount.create(
    switchboard.program,
    {
    authority,
    granter: switchboard.queue.publicKey,
    grantee: vrfAccount.publicKey,
    }
    )

    权限字段是从上文的队列中获取的加载数据。这将在交换机中给我们的 vrf 账户授权。

    下一步,我们会将权限更改为我们自己,并将其设置为付款方。

    // 如果队列需要权限来使用 VRF,请检查是否提供了正确的授权
    if (!unpermissionedVrfEnabled) {
    if (!payer.publicKey.equals(authority)) {
    throw new Error(
    `队列需要 PERMIT_VRF_REQUESTS 权限,而提供的队列授权错误`
    )
    }

    await permissionAccount.set({
    authority: payer,
    permission: sbv2.SwitchboardPermission.PERMIT_VRF_REQUESTS,
    enable: true,
    })
    }

    由于稍后我们需要切换板账户的提升,因此我们将其提取出来,还有 switchboardStateBump,这是切换板的程序账户。

    // 获取权限提升和切换板状态提升
    const [_permissionAccount, permissionBump] = sbv2.PermissionAccount.fromSeed(
    switchboard.program,
    authority,
    switchboard.queue.publicKey,
    vrfAccount.publicKey
    )

    const [switchboardStateAccount, switchboardStateBump] =
    sbv2.ProgramStateAccount.fromSeed(switchboard.program)

    这就是我们进行测试与程序和交换机互动所需的所有数据,我们将在最后返回这些数据。

    return {
    switchboard: switchboard,
    lootboxPointerPda: lootboxPointerPda,
    permissionBump: permissionBump,
    permissionAccount: permissionAccount,
    switchboardStateBump: switchboardStateBump,
    switchboardStateAccount: switchboardStateAccount,
    vrfAccount: vrfAccount,
    }

    我们最终会在测试环境设置中调用整个函数,所以现在的 before 代码块是这样的。

    before(async () => {
    ;({ nft, stakeStatePda, mint, tokenAddress } = await setupNft(
    program,
    wallet.payer
    ))
    ;({
    switchboard,
    lootboxPointerPda,
    permissionBump,
    switchboardStateBump,
    vrfAccount,
    switchboardStateAccount,
    permissionAccount,
    } = await setupSwitchboard(provider, lootboxProgram, wallet.payer))
    })

    下面是关于客户端交换机所需的基本知识。

    init_user 指令的详细步骤 👶

    首先,对于我们的战利品箱计划,我们以前把所有东西都放在 lib.rs 里,但随着项目变得越来越庞大,也变得难以管理,所以现在我们对其进行了拆分,你可以在此链接查看文件结构。

    现在的 lib 文件主要只是一堆 use 语句、declare_id! 宏和我们的四个指令,它们只是调用其他文件。

    Init_user 将创建用户状态账户,我们将在程序和交换机之间共享该账户,它就像一个联络账户。

    打开战利品箱的过程与之前相同,它将开始生成随机货币的过程,但不会完成该过程,而是生成一个 CPI 来呼叫交换机以请求一个随机数。

    交换机将调用消耗随机性,以返回指令中的号码,以便我们可以使用它,并在设置薄荷时完成该过程。

    从战利品箱中获取物品基本上没有改变。

    让我们开始吧,首先是 init_user

    在文件的顶部,你会找到初始用户上下文,在底部有一个实现,其中有一个名为 process instruction 的函数,在该函数中执行了之前在 libs 文件中的逻辑。

    InitUser 上下文中有四个账户。状态是我们的用户状态对象,其中包含 vrfpayer 密钥种子,这是用于测试的版本。对于生产代码,你只需要 payer 种子。我们这样做是为了节省时间,而不是使用环境变量。然后有 vrf 账户,switchboard 不会自动加载它,因此需要使用 .load() 调用来加载。可能有其他使用 switchboard 的方法,但我们目前采用的是最简单/最快的路径来启动和运行,随时可以对其进行探索和改进。最后,我们有 payersystem 程序来创建一个新账户。

    use crate::*;

    #[derive(Accounts)]
    #[instruction(params: InitUserParams)]
    pub struct InitUser<'info> {
    #[account(
    init,
    // 测试 - 注释掉这些种子用于测试
    // seeds = [
    // payer.key().as_ref(),
    // ],
    // 测试 - 取消注释这些种子用于测试
    seeds = [
    vrf.key().as_ref(),
    payer.key().as_ref()
    ],
    payer = payer,
    space = 8 + std::mem::size_of::<UserState>(),
    bump,
    )]
    pub state: AccountLoader<'info, UserState>,
    #[account(
    constraint = vrf.load()?.authority == state.key() @ LootboxError::InvalidVrfAuthorityError
    )]
    pub vrf: AccountLoader<'info, VrfAccountData>,
    #[account(mut)]
    pub payer: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    在逻辑部分,我们正在操作名为state的账户,该账户设置了bumpswitchboard state bumpvrf permission bumpvrf账户以及与之关联的用户。你会注意到存在一个结构体,其中只包括了我们前面提到的两个bump

    #[derive(Clone, AnchorSerialize, AnchorDeserialize)]
    pub struct InitUserParams {
    pub switchboard_state_bump: u8,
    pub vrf_permission_bump: u8,
    }

    impl InitUser<'_> {
    pub fn process_instruction(ctx: &Context<Self>, params: &InitUserParams) -> Result<()> {
    let mut state = ctx.accounts.state.load_init()?;
    *state = UserState::default();
    state.bump = ctx.bumps.get("state").unwrap().clone();
    state.switchboard_state_bump = params.switchboard_state_bump;
    state.vrf_permission_bump = params.vrf_permission_bump;
    state.vrf = ctx.accounts.vrf.key();
    state.user = ctx.accounts.payer.key();

    Ok(())
    }
    }

    让我们快速了解一下用户状态文件,从而更好地理解其中的内容。

    其中新引入的部分是结果缓冲区。这是我们获取随机性的地方。系统会将随机数据作为一个32字节的数组发送给我们,我们可以将其转换为任何所需的随机性。

    请注意,这里还添加了两个属性。#[account(zero_copy)] 是一个需要加载的部分,我只是按照交换机示例中的建议进行操作的。

    #[repr(packed)]
    #[account(zero_copy)]
    #[derive(Default)]
    pub struct UserState {
    pub bump: u8,
    pub switchboard_state_bump: u8,
    pub vrf_permission_bump: u8,
    pub result_buffer: [u8; 32],
    pub vrf: Pubkey,
    pub user: Pubkey,
    }

    以上就是初始用户介绍的全部内容,我们可以继续深入了解了。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module6/randomness/index.html b/Solana-Co-Learn/module6/randomness/index.html index c58ab22c8..41e520a40 100644 --- a/Solana-Co-Learn/module6/randomness/index.html +++ b/Solana-Co-Learn/module6/randomness/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module6/randomness/opening-loot-boxes/index.html b/Solana-Co-Learn/module6/randomness/opening-loot-boxes/index.html index a42f671e6..0584cd5db 100644 --- a/Solana-Co-Learn/module6/randomness/opening-loot-boxes/index.html +++ b/Solana-Co-Learn/module6/randomness/opening-loot-boxes/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🎁 开启战利品箱

    现在我们来深入探讨开启战利品箱的指南。首先你会注意到的是,这个过程涉及许多账号,总共有19个!

    直到stake_state为止,这些信息都是我们之前已经了解的。

    我们正在添加与总线相关的内容,包括我们在初始化用户中刚刚设置的用户状态。然后还有一系列总线账户,包括vrf账户、oracle队列账户、队列权限账户(这只是权限的PDA)、数据缓冲区账户、权限账户、托管账户、程序状态账户和总线程序账户本身。

    你会发现还有一些我们尚未讨论过的类型,它们来自switchboard-v2 crate。以下是你需要添加到Cargo.toml中的两个依赖项,以确保所有这些类型都能正常工作。

    switchboard-v2 = { version = "^0.1.14", features = ["devnet"] }
    bytemuck = "1.7.2"

    最后两个账户是付款人钱包,它与你的switchboard代币关联,用于支付随机性和最近的区块哈希。

    use crate::*;
    use anchor_lang::solana_program;

    #[derive(Accounts)]
    pub struct OpenLootbox<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
    init_if_needed,
    payer = user,
    space = std::mem::size_of::<LootboxPointer>() + 8,
    seeds=["lootbox".as_bytes(), user.key().as_ref()],
    bump
    )]
    pub lootbox_pointer: Box<Account<'info, LootboxPointer>>,
    pub system_program: Program<'info, System>,
    pub token_program: Program<'info, Token>,
    // TESTING - Uncomment the next line during testing
    // #[account(mut)]
    // TESTING - Comment out the next three lines during testing
    #[account(
    mut,
    address="D7F9JnGcjxQwz9zEQmasksX1VrwFcfRKu8Vdqrk2enHR".parse::<Pubkey>().unwrap()
    )]
    pub stake_mint: Account<'info, Mint>,
    #[account(
    mut,
    associated_token::mint=stake_mint,
    associated_token::authority=user
    )]
    pub stake_mint_ata: Box<Account<'info, TokenAccount>>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    #[account(
    constraint=stake_state.user_pubkey==user.key(),
    )]
    pub stake_state: Box<Account<'info, UserStakeInfo>>,
    #[account(
    mut,
    // TESTING - Comment out these seeds for testing
    seeds = [
    user.key().as_ref(),
    ],
    // TESTING - Uncomment these seeds for testing
    // seeds = [
    // vrf.key().as_ref(),
    // user.key().as_ref()
    // ],
    bump = state.load()?.bump,
    has_one = vrf @ LootboxError::InvalidVrfAccount
    )]
    pub state: AccountLoader<'info, UserState>,

    // SWITCHBOARD ACCOUNTS
    #[account(mut,
    has_one = escrow
    )]
    pub vrf: AccountLoader<'info, VrfAccountData>,
    #[account(mut,
    has_one = data_buffer
    )]
    pub oracle_queue: AccountLoader<'info, OracleQueueAccountData>,
    /// CHECK:
    #[account(mut,
    constraint =
    oracle_queue.load()?.authority == queue_authority.key()
    )]
    pub queue_authority: UncheckedAccount<'info>,
    /// CHECK
    #[account(mut)]
    pub data_buffer: AccountInfo<'info>,
    #[account(mut)]
    pub permission: AccountLoader<'info, PermissionAccountData>,
    #[account(mut,
    constraint =
    escrow.owner == program_state.key()
    && escrow.mint == program_state.load()?.token_mint
    )]
    pub escrow: Account<'info, TokenAccount>,
    #[account(mut)]
    pub program_state: AccountLoader<'info, SbState>,
    /// CHECK:
    #[account(
    address = *vrf.to_account_info().owner,
    constraint = switchboard_program.executable == true
    )]
    pub switchboard_program: AccountInfo<'info>,

    // PAYER ACCOUNTS
    #[account(mut,
    constraint =
    payer_wallet.owner == user.key()
    && escrow.mint == program_state.load()?.token_mint
    )]
    pub payer_wallet: Account<'info, TokenAccount>,
    // SYSTEM ACCOUNTS
    /// CHECK:
    #[account(address = solana_program::sysvar::recent_blockhashes::ID)]
    pub recent_blockhashes: AccountInfo<'info>,
    }

    在我们的账户配置之后,下面的代码片段是我们在开放式战利品箱实现中真正进行的操作,需要注意的是,这正是我们逻辑所在的地方。

    起初,我们加载状态的部分与以前完全相同。一旦我们加载了状态,我们就从状态中获取了我们的 bump(译者注:bump通常用于校验或确保唯一性),还有我们在初始化用户时添加的另外两个 bump。我们还从内存中删除了状态。

    let state = ctx.accounts.state.load()?;
    let bump = state.bump.clone();
    let switchboard_state_bump = state.switchboard_state_bump;
    let vrf_permission_bump = state.vrf_permission_bump;
    drop(state);

    接下来,我们从账户列表中获取了交换机程序本身。然后,我们构建了VRF请求的随机性,这实际上是我们用于CPI(跨程序调用)的上下文,在我们几行后调用vrf_request_randomness时会用到。

    再次,你会看到一些被注释掉的代码,用来区分生产环境和测试环境。我们仅在测试目的下使用vrf账户。

    let switchboard_program = ctx.accounts.switchboard_program.to_account_info();

    let vrf_request_randomness = VrfRequestRandomness {
    authority: ctx.accounts.state.to_account_info(),
    vrf: ctx.accounts.vrf.to_account_info(),
    oracle_queue: ctx.accounts.oracle_queue.to_account_info(),
    queue_authority: ctx.accounts.queue_authority.to_account_info(),
    data_buffer: ctx.accounts.data_buffer.to_account_info(),
    permission: ctx.accounts.permission.to_account_info(),
    escrow: ctx.accounts.escrow.clone(),
    payer_wallet: ctx.accounts.payer_wallet.clone(),
    payer_authority: ctx.accounts.user.to_account_info(),
    recent_blockhashes: ctx.accounts.recent_blockhashes.to_account_info(),
    program_state: ctx.accounts.program_state.to_account_info(),
    token_program: ctx.accounts.token_program.to_account_info(),
    };

    let payer = ctx.accounts.user.key();
    // TESTING - uncomment the following during tests
    let vrf = ctx.accounts.vrf.key();
    let state_seeds: &[&[&[u8]]] = &[&[vrf.as_ref(), payer.as_ref(), &[bump]]];
    // TESTING - comment out the next line during tests
    // let state_seeds: &[&[&[u8]]] = &[&[payer.as_ref(), &[bump]]];

    这是对switchboard的呼叫。

    msg!("requesting randomness");
    vrf_request_randomness.invoke_signed(
    switchboard_program,
    switchboard_state_bump,
    vrf_permission_bump,
    state_seeds,
    )?;

    msg!("randomness requested successfully");

    最后,我们将随机请求更改为已初始化为true

    ctx.accounts.lootbox_pointer.randomness_requested = true;
    ctx.accounts.lootbox_pointer.is_initialized = true;
    ctx.accounts.lootbox_pointer.available_lootbox = box_number * 2;

    Ok(())

    我们再来探讨战利品盒指针结构体,注意到其中有一个名为 redeemable 的属性。这个属性让客户端可以观察战利品盒指针账户,一旦它从false变为true,我们便能知道随机性已经恢复,可以开始进行铸造。此变化是在消耗随机性函数中发生的。

    #[account]
    pub struct LootboxPointer {
    pub mint: Pubkey,
    pub redeemable: bool,
    pub randomness_requested: bool,
    pub available_lootbox: u64,
    pub is_initialized: bool,
    }

    下面我们来看一下这个函数,并对它进行解读。该函数由交换机调用,并且内容在 callback 文件中提供。回调中的四个账户与ConsumeRandomness中的账户匹配,loobox指针和状态是可变的。

    use crate::state::*;
    use crate::*;

    #[derive(Accounts)]
    pub struct ConsumeRandomness<'info> {
    #[account(
    mut,
    // TESTING - Comment out these seeds for testing
    seeds = [
    payer.key().as_ref(),
    ],
    // TESTING - Uncomment these seeds for testing
    // seeds = [
    // vrf.key().as_ref(),
    // payer.key().as_ref()
    // ],
    bump = state.load()?.bump,
    has_one = vrf @ LootboxError::InvalidVrfAccount
    )]
    pub state: AccountLoader<'info, UserState>,
    pub vrf: AccountLoader<'info, VrfAccountData>,
    #[account(
    mut,
    seeds=["lootbox".as_bytes(), payer.key().as_ref()],
    bump
    )]
    pub lootbox_pointer: Account<'info, LootboxPointer>,
    /// CHECK: ...
    pub payer: AccountInfo<'info>,
    }

    在实际执行上,我们在流程指令功能中首先加载vrf和状态账户。随后,我们从vrf账户获取结果缓冲区,并检查确保其不为空。

    impl ConsumeRandomness<'_> {
    pub fn process_instruction(ctx: &mut Context<Self>) -> Result<()> {
    let vrf = ctx.accounts.vrf.load()?;
    let state = &mut ctx.accounts.state.load_mut()?;

    let result_buffer = vrf.get_result()?;
    if result_buffer == [0u8; 32] {
    msg!("vrf buffer empty");
    return Ok(());
    }

    if result_buffer == state.result_buffer {
    msg!("result_buffer unchanged");
    return Ok(());
    }
    }

    接下来,我们将对可用的装备进行映射。此时,我们仅使用下方定义的常量,方便在构建程序时进行必要的修改。这将给我们一个公钥向量。

    let available_gear: Vec<Pubkey> = Self::AVAILABLE_GEAR
    .into_iter()
    .map(|key| key.parse::<Pubkey>().unwrap())
    .collect();

    value 变量中,我们将结果缓冲区转换为无符号8位整数,这是switchboard推荐的实现方式,采用了 bytemuck crate。最后,我们通过取模运算和可用的最大薄荷数量来随机选择一个。

    // maximum value to convert randomness buffer
    let max_result = available_gear.len();
    let value: &[u8] = bytemuck::cast_slice(&result_buffer[..]);
    let i = (value[0] as usize) % max_result;
    msg!("The chosen mint index is {} out of {}", i, max_result);

    最后,我们会选中第i个索引处的值,并分配给lootbox指针的mint,然后将redeemable的值更改为true。这样一来,客户端便可观察到这一变化,一旦redeemabletrue,用户就能开始铸造他们的装备。

    let mint = available_gear[i];
    msg!("Next mint is {:?}", mint);
    ctx.accounts.lootbox_pointer.mint = mint;
    ctx.accounts.lootbox_pointer.redeemable = true;

    Ok(())
    }

    const AVAILABLE_GEAR: [&'static str; 5] = [
    "87QkviUPcxNqjdo1N6C4FrQe3ZiYdAyxGoT44ioDUG8m",
    "EypLPq3xBRREfpsdbyXfFjobVAnHsNerP892NMHWzrKj",
    "Ds1txTXZadjsjKtt2ybH56GQ2do4nbGc8nrSH3Ln8G9p",
    "EHPo4mSNCfYzX3Dtr832boZAiR8vy39eTsUfKprXbFus",
    "HzUvbXymUCBtubKQD9yiwWdivAbTiyKhpzVBcgD9DhrV",
    ];
    }

    正如之前所提及的,从战利品箱中获取物品的指令基本保持不变。如果您更细致地观察,就会发现它并没有与交换机进行任何交互,因此无需进行任何更新。

    客户端交互与测试

    最后,我们要来探讨与交换机相关的测试环节。我们已经审视了setupSwitchboard函数,以便准备测试。前三个测试主要用于质押、赎回和解质押。紧随其后的是init_user测试,非常直接明了。我们只需传入交换机状态的增量和权限增量,再加上四个账户即可。

    it("init user", async () => {
    const tx = await lootboxProgram.methods
    .initUser({
    switchboardStateBump: switchboardStateBump,
    vrfPermissionBump: permissionBump,
    })
    .accounts({
    state: userState,
    vrf: vrfAccount.publicKey,
    payer: wallet.pubkey,
    systemProgram: anchor.web3.SystemProgram.programId,
    })
    .rpc();
    })

    随后的选择性随机测试则相对复杂一些。前半部分与其他测试相似。我们首先创建一个虚拟的铸币机,用以铸造这些物品。然后获取或创建一个所谓的ATA,并将物品铸造到其中。除此之外,还有我们的质押账户,负责实际质押我们的NFT。

    it("Chooses a mint pseudorandomly", async () => {
    const mint = await createMint(
    provider.connection,
    wallet.payer,
    wallet.publicKey,
    wallet.publicKey,
    2
    )
    const ata = await getOrCreateAssociatedTokenAccount(
    provider.connection,
    wallet.payer,
    mint,
    wallet.publicKey
    )

    await mintToChecked(
    provider.connection,
    wallet.payer,
    mint,
    ata.address,
    wallet.payer,
    1000,
    2
    )

    const [stakeAccount] = anchor.web3.PublicKey.findProgramAddressSync(
    [wallet.publicKey.toBuffer(), nft.tokenAddress.toBuffer()],
    program.programId
    )

    我们首先从vrf账户中加载数据,并从交换机队列中获取我们的权限和数据缓冲区。随后,我们调用了openLootbox函数,这个函数需要许多合适的账户,数量相当多。其中大部分来自setupSwitchboard函数,还有一些则来自我们刚刚从交换机队列中获取的内容。

    const vrfState = await vrfAccount.loadData();
    const { authority, dataBuffer } = await switchboard.queue.loadData();

    await lootboxProgram.methods
    .openLootbox(new BN(10))
    .accounts({
    user: wallet.publicKey,
    stakeMint: mint,
    stakeMintAta: ata.address,
    stakeState: stakeAccount,
    state: userState,
    vrf: vrfAccount.publicKey,
    oracleQueue: switchboard.queue.publicKey,
    queueAuthority: authority,
    dataBuffer: dataBuffer,
    permission: permissionAccount.publicKey,
    escrow: vrfState.escrow,
    programState: switchboardStateAccount.publicKey,
    switchboardProgram: switchboard.program.programId,
    payerWallet: switchboard.payerTokenWallet,
    recentBlockhashes: anchor.web3.SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
    })
    .rpc();

    接下来,我们使用了awaitCallback函数,在其中我们传递了lootbox程序、指针PDA,并设置了20秒的等待时间。在这段时间内,我们将观察lootbox指针是否更新为新的mint

    await awaitCallback(
    lootboxProgram,
    lootboxPointerPda,
    20_000,
    "Didn't get random mint"
    );

    下面是等待回调函数的部分,您可以随意引用。在这里,您会看到它实际上只是静静地等待。它会观察战利品盒指针上的账户变化,一旦有变化,它就会检查战利品盒指针,看看是否已设置为“可兑换”为真。如果是这样,它就会解决并完成回调,一切都将顺利进行。如果在20秒内没有发生任何变化,它将报告"未获得随机铸币"的错误。

    async function awaitCallback(
    program: Program<LootboxProgram>,
    lootboxPointerAddress: anchor.web3.PublicKey,
    timeoutInterval: number,
    errorMsg = "Timed out waiting for VRF Client callback"
    ) {
    let ws: number | undefined = undefined
    const result: boolean = await promiseWithTimeout(
    timeoutInterval,
    new Promise((resolve: (result: boolean) => void) => {
    ws = program.provider.connection.onAccountChange(
    lootboxPointerAddress,
    async (
    accountInfo: anchor.web3.AccountInfo<Buffer>,
    context: anchor.web3.Context
    ) => {
    const lootboxPointer = await program.account.lootboxPointer.fetch(
    lootboxPointerAddress
    )

    if (lootboxPointer.redeemable) {
    resolve(true)
    }
    }
    )
    }).finally(async () => {
    if (ws) {
    await program.provider.connection.removeAccountChangeListener(ws)
    }
    ws = undefined
    }),
    new Error(errorMsg)
    ).finally(async () => {
    if (ws) {
    await program.provider.connection.removeAccountChangeListener(ws)
    }
    ws = undefined
    })

    return result
    }

    最后,我们来测试选定齿轮的铸造过程。首先,我们获取战利品箱指针,从中找到铸币,并获取我们需要的ATA以使其工作。然后,我们将检查是否之前已经有了相同的齿轮,以防止我们重复运行。随后,我们调用从战利品箱中检索物品的函数,并再次确认新的齿轮数量是之前的数量加一。

    it("Mints the selected gear", async () => {
    const [pointerAddress] = anchor.web3.PublicKey.findProgramAddressSync(
    [Buffer.from("lootbox"), wallet.publicKey.toBuffer()],
    lootboxProgram.programId
    );

    const pointer = await lootboxProgram.account.lootboxPointer.fetch(
    pointerAddress
    );

    let previousGearCount = 0;
    const gearAta = await getAssociatedTokenAddress(
    pointer.mint,
    wallet.publicKey
    );
    try {
    let gearAccount = await getAccount(provider.connection, gearAta);
    previousGearCount = Number(gearAccount.amount);
    } catch (error) {}

    await lootboxProgram.methods
    .retrieveItemFromLootbox()
    .accounts({
    mint: pointer.mint,
    userGearAta: gearAta,
    })
    .rpc();

    const gearAccount = await getAccount(provider.connection, gearAta);
    expect(Number(gearAccount.amount)).to.equal(previousGearCount + 1);
    })

    现在您可以运行上述代码,希望一切能正常工作。如果刚开始不成功,请不要气馁。我们自己也花了好几天的时间进行调试。

    - - +
    Skip to main content

    🎁 开启战利品箱

    现在我们来深入探讨开启战利品箱的指南。首先你会注意到的是,这个过程涉及许多账号,总共有19个!

    直到stake_state为止,这些信息都是我们之前已经了解的。

    我们正在添加与总线相关的内容,包括我们在初始化用户中刚刚设置的用户状态。然后还有一系列总线账户,包括vrf账户、oracle队列账户、队列权限账户(这只是权限的PDA)、数据缓冲区账户、权限账户、托管账户、程序状态账户和总线程序账户本身。

    你会发现还有一些我们尚未讨论过的类型,它们来自switchboard-v2 crate。以下是你需要添加到Cargo.toml中的两个依赖项,以确保所有这些类型都能正常工作。

    switchboard-v2 = { version = "^0.1.14", features = ["devnet"] }
    bytemuck = "1.7.2"

    最后两个账户是付款人钱包,它与你的switchboard代币关联,用于支付随机性和最近的区块哈希。

    use crate::*;
    use anchor_lang::solana_program;

    #[derive(Accounts)]
    pub struct OpenLootbox<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
    init_if_needed,
    payer = user,
    space = std::mem::size_of::<LootboxPointer>() + 8,
    seeds=["lootbox".as_bytes(), user.key().as_ref()],
    bump
    )]
    pub lootbox_pointer: Box<Account<'info, LootboxPointer>>,
    pub system_program: Program<'info, System>,
    pub token_program: Program<'info, Token>,
    // TESTING - Uncomment the next line during testing
    // #[account(mut)]
    // TESTING - Comment out the next three lines during testing
    #[account(
    mut,
    address="D7F9JnGcjxQwz9zEQmasksX1VrwFcfRKu8Vdqrk2enHR".parse::<Pubkey>().unwrap()
    )]
    pub stake_mint: Account<'info, Mint>,
    #[account(
    mut,
    associated_token::mint=stake_mint,
    associated_token::authority=user
    )]
    pub stake_mint_ata: Box<Account<'info, TokenAccount>>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    #[account(
    constraint=stake_state.user_pubkey==user.key(),
    )]
    pub stake_state: Box<Account<'info, UserStakeInfo>>,
    #[account(
    mut,
    // TESTING - Comment out these seeds for testing
    seeds = [
    user.key().as_ref(),
    ],
    // TESTING - Uncomment these seeds for testing
    // seeds = [
    // vrf.key().as_ref(),
    // user.key().as_ref()
    // ],
    bump = state.load()?.bump,
    has_one = vrf @ LootboxError::InvalidVrfAccount
    )]
    pub state: AccountLoader<'info, UserState>,

    // SWITCHBOARD ACCOUNTS
    #[account(mut,
    has_one = escrow
    )]
    pub vrf: AccountLoader<'info, VrfAccountData>,
    #[account(mut,
    has_one = data_buffer
    )]
    pub oracle_queue: AccountLoader<'info, OracleQueueAccountData>,
    /// CHECK:
    #[account(mut,
    constraint =
    oracle_queue.load()?.authority == queue_authority.key()
    )]
    pub queue_authority: UncheckedAccount<'info>,
    /// CHECK
    #[account(mut)]
    pub data_buffer: AccountInfo<'info>,
    #[account(mut)]
    pub permission: AccountLoader<'info, PermissionAccountData>,
    #[account(mut,
    constraint =
    escrow.owner == program_state.key()
    && escrow.mint == program_state.load()?.token_mint
    )]
    pub escrow: Account<'info, TokenAccount>,
    #[account(mut)]
    pub program_state: AccountLoader<'info, SbState>,
    /// CHECK:
    #[account(
    address = *vrf.to_account_info().owner,
    constraint = switchboard_program.executable == true
    )]
    pub switchboard_program: AccountInfo<'info>,

    // PAYER ACCOUNTS
    #[account(mut,
    constraint =
    payer_wallet.owner == user.key()
    && escrow.mint == program_state.load()?.token_mint
    )]
    pub payer_wallet: Account<'info, TokenAccount>,
    // SYSTEM ACCOUNTS
    /// CHECK:
    #[account(address = solana_program::sysvar::recent_blockhashes::ID)]
    pub recent_blockhashes: AccountInfo<'info>,
    }

    在我们的账户配置之后,下面的代码片段是我们在开放式战利品箱实现中真正进行的操作,需要注意的是,这正是我们逻辑所在的地方。

    起初,我们加载状态的部分与以前完全相同。一旦我们加载了状态,我们就从状态中获取了我们的 bump(译者注:bump通常用于校验或确保唯一性),还有我们在初始化用户时添加的另外两个 bump。我们还从内存中删除了状态。

    let state = ctx.accounts.state.load()?;
    let bump = state.bump.clone();
    let switchboard_state_bump = state.switchboard_state_bump;
    let vrf_permission_bump = state.vrf_permission_bump;
    drop(state);

    接下来,我们从账户列表中获取了交换机程序本身。然后,我们构建了VRF请求的随机性,这实际上是我们用于CPI(跨程序调用)的上下文,在我们几行后调用vrf_request_randomness时会用到。

    再次,你会看到一些被注释掉的代码,用来区分生产环境和测试环境。我们仅在测试目的下使用vrf账户。

    let switchboard_program = ctx.accounts.switchboard_program.to_account_info();

    let vrf_request_randomness = VrfRequestRandomness {
    authority: ctx.accounts.state.to_account_info(),
    vrf: ctx.accounts.vrf.to_account_info(),
    oracle_queue: ctx.accounts.oracle_queue.to_account_info(),
    queue_authority: ctx.accounts.queue_authority.to_account_info(),
    data_buffer: ctx.accounts.data_buffer.to_account_info(),
    permission: ctx.accounts.permission.to_account_info(),
    escrow: ctx.accounts.escrow.clone(),
    payer_wallet: ctx.accounts.payer_wallet.clone(),
    payer_authority: ctx.accounts.user.to_account_info(),
    recent_blockhashes: ctx.accounts.recent_blockhashes.to_account_info(),
    program_state: ctx.accounts.program_state.to_account_info(),
    token_program: ctx.accounts.token_program.to_account_info(),
    };

    let payer = ctx.accounts.user.key();
    // TESTING - uncomment the following during tests
    let vrf = ctx.accounts.vrf.key();
    let state_seeds: &[&[&[u8]]] = &[&[vrf.as_ref(), payer.as_ref(), &[bump]]];
    // TESTING - comment out the next line during tests
    // let state_seeds: &[&[&[u8]]] = &[&[payer.as_ref(), &[bump]]];

    这是对switchboard的呼叫。

    msg!("requesting randomness");
    vrf_request_randomness.invoke_signed(
    switchboard_program,
    switchboard_state_bump,
    vrf_permission_bump,
    state_seeds,
    )?;

    msg!("randomness requested successfully");

    最后,我们将随机请求更改为已初始化为true

    ctx.accounts.lootbox_pointer.randomness_requested = true;
    ctx.accounts.lootbox_pointer.is_initialized = true;
    ctx.accounts.lootbox_pointer.available_lootbox = box_number * 2;

    Ok(())

    我们再来探讨战利品盒指针结构体,注意到其中有一个名为 redeemable 的属性。这个属性让客户端可以观察战利品盒指针账户,一旦它从false变为true,我们便能知道随机性已经恢复,可以开始进行铸造。此变化是在消耗随机性函数中发生的。

    #[account]
    pub struct LootboxPointer {
    pub mint: Pubkey,
    pub redeemable: bool,
    pub randomness_requested: bool,
    pub available_lootbox: u64,
    pub is_initialized: bool,
    }

    下面我们来看一下这个函数,并对它进行解读。该函数由交换机调用,并且内容在 callback 文件中提供。回调中的四个账户与ConsumeRandomness中的账户匹配,loobox指针和状态是可变的。

    use crate::state::*;
    use crate::*;

    #[derive(Accounts)]
    pub struct ConsumeRandomness<'info> {
    #[account(
    mut,
    // TESTING - Comment out these seeds for testing
    seeds = [
    payer.key().as_ref(),
    ],
    // TESTING - Uncomment these seeds for testing
    // seeds = [
    // vrf.key().as_ref(),
    // payer.key().as_ref()
    // ],
    bump = state.load()?.bump,
    has_one = vrf @ LootboxError::InvalidVrfAccount
    )]
    pub state: AccountLoader<'info, UserState>,
    pub vrf: AccountLoader<'info, VrfAccountData>,
    #[account(
    mut,
    seeds=["lootbox".as_bytes(), payer.key().as_ref()],
    bump
    )]
    pub lootbox_pointer: Account<'info, LootboxPointer>,
    /// CHECK: ...
    pub payer: AccountInfo<'info>,
    }

    在实际执行上,我们在流程指令功能中首先加载vrf和状态账户。随后,我们从vrf账户获取结果缓冲区,并检查确保其不为空。

    impl ConsumeRandomness<'_> {
    pub fn process_instruction(ctx: &mut Context<Self>) -> Result<()> {
    let vrf = ctx.accounts.vrf.load()?;
    let state = &mut ctx.accounts.state.load_mut()?;

    let result_buffer = vrf.get_result()?;
    if result_buffer == [0u8; 32] {
    msg!("vrf buffer empty");
    return Ok(());
    }

    if result_buffer == state.result_buffer {
    msg!("result_buffer unchanged");
    return Ok(());
    }
    }

    接下来,我们将对可用的装备进行映射。此时,我们仅使用下方定义的常量,方便在构建程序时进行必要的修改。这将给我们一个公钥向量。

    let available_gear: Vec<Pubkey> = Self::AVAILABLE_GEAR
    .into_iter()
    .map(|key| key.parse::<Pubkey>().unwrap())
    .collect();

    value 变量中,我们将结果缓冲区转换为无符号8位整数,这是switchboard推荐的实现方式,采用了 bytemuck crate。最后,我们通过取模运算和可用的最大薄荷数量来随机选择一个。

    // maximum value to convert randomness buffer
    let max_result = available_gear.len();
    let value: &[u8] = bytemuck::cast_slice(&result_buffer[..]);
    let i = (value[0] as usize) % max_result;
    msg!("The chosen mint index is {} out of {}", i, max_result);

    最后,我们会选中第i个索引处的值,并分配给lootbox指针的mint,然后将redeemable的值更改为true。这样一来,客户端便可观察到这一变化,一旦redeemabletrue,用户就能开始铸造他们的装备。

    let mint = available_gear[i];
    msg!("Next mint is {:?}", mint);
    ctx.accounts.lootbox_pointer.mint = mint;
    ctx.accounts.lootbox_pointer.redeemable = true;

    Ok(())
    }

    const AVAILABLE_GEAR: [&'static str; 5] = [
    "87QkviUPcxNqjdo1N6C4FrQe3ZiYdAyxGoT44ioDUG8m",
    "EypLPq3xBRREfpsdbyXfFjobVAnHsNerP892NMHWzrKj",
    "Ds1txTXZadjsjKtt2ybH56GQ2do4nbGc8nrSH3Ln8G9p",
    "EHPo4mSNCfYzX3Dtr832boZAiR8vy39eTsUfKprXbFus",
    "HzUvbXymUCBtubKQD9yiwWdivAbTiyKhpzVBcgD9DhrV",
    ];
    }

    正如之前所提及的,从战利品箱中获取物品的指令基本保持不变。如果您更细致地观察,就会发现它并没有与交换机进行任何交互,因此无需进行任何更新。

    客户端交互与测试

    最后,我们要来探讨与交换机相关的测试环节。我们已经审视了setupSwitchboard函数,以便准备测试。前三个测试主要用于质押、赎回和解质押。紧随其后的是init_user测试,非常直接明了。我们只需传入交换机状态的增量和权限增量,再加上四个账户即可。

    it("init user", async () => {
    const tx = await lootboxProgram.methods
    .initUser({
    switchboardStateBump: switchboardStateBump,
    vrfPermissionBump: permissionBump,
    })
    .accounts({
    state: userState,
    vrf: vrfAccount.publicKey,
    payer: wallet.pubkey,
    systemProgram: anchor.web3.SystemProgram.programId,
    })
    .rpc();
    })

    随后的选择性随机测试则相对复杂一些。前半部分与其他测试相似。我们首先创建一个虚拟的铸币机,用以铸造这些物品。然后获取或创建一个所谓的ATA,并将物品铸造到其中。除此之外,还有我们的质押账户,负责实际质押我们的NFT。

    it("Chooses a mint pseudorandomly", async () => {
    const mint = await createMint(
    provider.connection,
    wallet.payer,
    wallet.publicKey,
    wallet.publicKey,
    2
    )
    const ata = await getOrCreateAssociatedTokenAccount(
    provider.connection,
    wallet.payer,
    mint,
    wallet.publicKey
    )

    await mintToChecked(
    provider.connection,
    wallet.payer,
    mint,
    ata.address,
    wallet.payer,
    1000,
    2
    )

    const [stakeAccount] = anchor.web3.PublicKey.findProgramAddressSync(
    [wallet.publicKey.toBuffer(), nft.tokenAddress.toBuffer()],
    program.programId
    )

    我们首先从vrf账户中加载数据,并从交换机队列中获取我们的权限和数据缓冲区。随后,我们调用了openLootbox函数,这个函数需要许多合适的账户,数量相当多。其中大部分来自setupSwitchboard函数,还有一些则来自我们刚刚从交换机队列中获取的内容。

    const vrfState = await vrfAccount.loadData();
    const { authority, dataBuffer } = await switchboard.queue.loadData();

    await lootboxProgram.methods
    .openLootbox(new BN(10))
    .accounts({
    user: wallet.publicKey,
    stakeMint: mint,
    stakeMintAta: ata.address,
    stakeState: stakeAccount,
    state: userState,
    vrf: vrfAccount.publicKey,
    oracleQueue: switchboard.queue.publicKey,
    queueAuthority: authority,
    dataBuffer: dataBuffer,
    permission: permissionAccount.publicKey,
    escrow: vrfState.escrow,
    programState: switchboardStateAccount.publicKey,
    switchboardProgram: switchboard.program.programId,
    payerWallet: switchboard.payerTokenWallet,
    recentBlockhashes: anchor.web3.SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
    })
    .rpc();

    接下来,我们使用了awaitCallback函数,在其中我们传递了lootbox程序、指针PDA,并设置了20秒的等待时间。在这段时间内,我们将观察lootbox指针是否更新为新的mint

    await awaitCallback(
    lootboxProgram,
    lootboxPointerPda,
    20_000,
    "Didn't get random mint"
    );

    下面是等待回调函数的部分,您可以随意引用。在这里,您会看到它实际上只是静静地等待。它会观察战利品盒指针上的账户变化,一旦有变化,它就会检查战利品盒指针,看看是否已设置为“可兑换”为真。如果是这样,它就会解决并完成回调,一切都将顺利进行。如果在20秒内没有发生任何变化,它将报告"未获得随机铸币"的错误。

    async function awaitCallback(
    program: Program<LootboxProgram>,
    lootboxPointerAddress: anchor.web3.PublicKey,
    timeoutInterval: number,
    errorMsg = "Timed out waiting for VRF Client callback"
    ) {
    let ws: number | undefined = undefined
    const result: boolean = await promiseWithTimeout(
    timeoutInterval,
    new Promise((resolve: (result: boolean) => void) => {
    ws = program.provider.connection.onAccountChange(
    lootboxPointerAddress,
    async (
    accountInfo: anchor.web3.AccountInfo<Buffer>,
    context: anchor.web3.Context
    ) => {
    const lootboxPointer = await program.account.lootboxPointer.fetch(
    lootboxPointerAddress
    )

    if (lootboxPointer.redeemable) {
    resolve(true)
    }
    }
    )
    }).finally(async () => {
    if (ws) {
    await program.provider.connection.removeAccountChangeListener(ws)
    }
    ws = undefined
    }),
    new Error(errorMsg)
    ).finally(async () => {
    if (ws) {
    await program.provider.connection.removeAccountChangeListener(ws)
    }
    ws = undefined
    })

    return result
    }

    最后,我们来测试选定齿轮的铸造过程。首先,我们获取战利品箱指针,从中找到铸币,并获取我们需要的ATA以使其工作。然后,我们将检查是否之前已经有了相同的齿轮,以防止我们重复运行。随后,我们调用从战利品箱中检索物品的函数,并再次确认新的齿轮数量是之前的数量加一。

    it("Mints the selected gear", async () => {
    const [pointerAddress] = anchor.web3.PublicKey.findProgramAddressSync(
    [Buffer.from("lootbox"), wallet.publicKey.toBuffer()],
    lootboxProgram.programId
    );

    const pointer = await lootboxProgram.account.lootboxPointer.fetch(
    pointerAddress
    );

    let previousGearCount = 0;
    const gearAta = await getAssociatedTokenAddress(
    pointer.mint,
    wallet.publicKey
    );
    try {
    let gearAccount = await getAccount(provider.connection, gearAta);
    previousGearCount = Number(gearAccount.amount);
    } catch (error) {}

    await lootboxProgram.methods
    .retrieveItemFromLootbox()
    .accounts({
    mint: pointer.mint,
    userGearAta: gearAta,
    })
    .rpc();

    const gearAccount = await getAccount(provider.connection, gearAta);
    expect(Number(gearAccount.amount)).to.equal(previousGearCount + 1);
    })

    现在您可以运行上述代码,希望一切能正常工作。如果刚开始不成功,请不要气馁。我们自己也花了好几天的时间进行调试。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module6/randomness/randomising-loot-with-switchborar/index.html b/Solana-Co-Learn/module6/randomness/randomising-loot-with-switchborar/index.html index 1415e3d2d..4b701d324 100644 --- a/Solana-Co-Learn/module6/randomness/randomising-loot-with-switchborar/index.html +++ b/Solana-Co-Learn/module6/randomness/randomising-loot-with-switchborar/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    使用Switchboard进行随机化战利品

    现在我们将深入探讨简单战利品箱的实现解决方案。我们会创建一个新程序,用于创建战利品箱并从中获取物品。

    我们将审查的解决方案代码位于Anchor NFT Staking存储库的solution-naive-loot-boxes分支之一。

    我再次强调,建议你自行尝试操作,而不是直接复制粘贴解决方案代码。

    在programs目录中,你可以使用命令anchor new <program-name>来创建一个新程序,我们将其称为lootbox-program

    如果你仔细观察,Anchor.toml文件中nft-staking程序的ID已经变化了,我们还新增了一个loot box程序的ID。你需要在自己的端更新这两个ID。

    首先,让我们回顾一下对原始质押计划所作的修改。

    如果你向下滚动到UserStakeInfo对象,你会发现我们添加了total_earned字段。它会跟踪用户的质押旅程,随着时间的推移,他们将赚取更多的奖励,并且在达到新的里程碑时,将获得更多的战利品箱物品。

    同样相关的是redeem_amount

    首先,你会注意到有些注释被注释掉了,这仅是为了确保我们有足够的令牌进行测试。在测试时,请确保正确地注释/取消注释代码。

    往下滚动一点,你会看到这一行新添加的内容。

    ctx.accounts.stake_state.total_earned += redeem_amount as u64;

    这是一种跟踪总收益的方法,从0开始,然后你添加已兑换的金额,这将成为新的总收益。

    在下面的解除质押功能中,你还会发现测试说明和赎回金额都发生了变化。

    最后,在这个文件中还有一个最后的更改。如果你的程序与我的完全相同,当我们运行它时,由于添加了这个新字段,我们可能会在堆栈中耗尽空间。我选择了一个随机账户,并在其周围放置了一个盒子,确保它被分配到堆中而不是栈中,以解决这个空间问题。你可以在用户的stake ATA上进行操作,或者选择任何其他账户。

    pub user_stake_ata: Box<Account<'info, TokenAccount>>,

    好的,让我们进入新的战利品箱计划的文件。

    Cargo.toml中,你会注意到我们为我们原始的锚定NFT质押程序添加了一个新的依赖项。

    [dependencies]
    anchor-lang = { version="0.25.0", features=["init-if-needed"] }
    anchor-spl = "0.25.0"
    anchor-nft-staking = { path = "../anchor-nft-staking", features = ["cpi"] }

    现在让我们进入主要的战利品箱程序文件

    在使用语句中,你会注意到我们现在导入了锚定NFT质押,这样我们就可以检查总收益字段了。

    use anchor_lang::prelude::*;
    use anchor_nft_staking::UserStakeInfo;
    use anchor_spl::token;
    use anchor_spl::{
    associated_token::AssociatedToken,
    token::{Burn, Mint, MintTo, Token, TokenAccount},
    };

    这里,我们只有两个指令,open_lootboxretrieve_item_from_lootbox。有两个指令的原因是,当你请求“给我一个随机的战利品”时,程序必须决定要铸造和赠送的所有可能物品,客户端必须传入所有可能的铸造账户。这使程序变得不那么灵活,并增加了检查一堆不同账户以确保有选项的开销,对客户端来说也非常麻烦。因此,我们创建了一个用于打开战利品箱的指令,基本上是在所有可能的铸造选项中给我一个。我们还选择了这个地方作为支付的地方,这是我们将烧毁BLD代币的地方。至于第二个指令,在这一点上,客户端知道他们将获得哪个铸造物品,并可以传入该信息,然后我们可以从中铸造。

    首先,让我们打开战利品箱,看看我们需要的账号。

    #[derive(Accounts)]
    pub struct OpenLootbox<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
    init_if_needed,
    payer = user,
    space = std::mem::size_of::<LootboxPointer>() + 8,
    seeds=["lootbox".as_bytes(), user.key().as_ref()],
    bump
    )]
    pub lootbox_pointer: Account<'info, LootboxPointer>,
    pub system_program: Program<'info, System>,
    pub token_program: Program<'info, Token>,
    // Swap the next two lines out between prod/testing
    // #[account(mut)]
    #[account(
    mut,
    address="6YR1nuLqkk8VC1v42xJaPKvE9X9pnuqVAvthFUSDsMUL".parse::<Pubkey>().unwrap()
    )]
    pub stake_mint: Account<'info, Mint>,
    #[account(
    mut,
    associated_token::mint=stake_mint,
    associated_token::authority=user
    )]
    pub stake_mint_ata: Account<'info, TokenAccount>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    #[account(
    constraint=stake_state.user_pubkey==user.key(),
    )]
    pub stake_state: Account<'info, UserStakeInfo>,
    }

    你会发现一个名为lootbox_pointer的新元素,这是一种全新的类型。它包括一个薄荷属性、一个布尔值用来表示是否已被领取,以及一个is_initialized属性。

    这是一个与用户关联的PDA(Program-derived address),因此其种子是“战利品箱”和用户。通过这样做,当我们选中一个薄荷时,我们不将数据返回给客户端,而是存储在某个账户中。因此,这是一个用户可以查询并检索物品的PDA

    此外,需要注意的是,在某行代码的开头有一个“Swap”注释行。为了使测试正常运行,需要取消这些行的注释,并注释掉其他包含心智地址的stake_mint属性行。

    下面的Rust代码展示了LootboxPointer结构:

    #[account]
    pub struct LootboxPointer {
    mint: Pubkey,
    claimed: bool,
    is_initialized: bool,
    }

    接下来,我们来详细了解这个功能。首先,我们要验证它是否是一个有效的战利品箱。

    用户输入一个盒子号码,然后程序会运行一个无限循环。在每次迭代中,如果BLD令牌的数量过低,我们会返回错误。其他两种可能的路径是:要么将loot_box号码加倍,要么如果在loot_box号码和box_number之间找到匹配,我们要求stake_state PDAs的总收益不少于传入的box_number。简而言之,你必须赚得比盒子号码更多。

    以下是打开战利品箱的函数:

    pub fn open_lootbox(ctx: Context<OpenLootbox>, box_number: u64) -> Result<()> {
    let mut loot_box = 10;
    loop {
    if loot_box > box_number {
    return err!(LootboxError::InvalidLootbox);
    }

    if loot_box == box_number {
    require!(
    ctx.accounts.stake_state.total_earned >= box_number,
    LootboxError::InvalidLootbox
    );
    break;
    } else {
    loot_box = loot_box * 2;
    }
    }

    require!(
    !ctx.accounts.lootbox_pointer.is_initialized || ctx.accounts.lootbox_pointer.claimed,
    LootboxError::InvalidLootbox
    );
    }

    然后我们继续进行代币销毁,销毁与盒子编号所需数量相对应的代币。

    token::burn(
    CpiContext::new(
    ctx.accounts.token_program.to_account_info(),
    Burn {
    mint: ctx.accounts.stake_mint.to_account_info(),
    from: ctx.accounts.stake_mint_ata.to_account_info(),
    authority: ctx.accounts.user.to_account_info(),
    },
    ),
    box_number * u64::pow(10, 2),
    )?;

    该函数还涉及代币销毁操作,即销毁与盒子编号所需数量相匹配的代币。之后,我们将描述可用装备。当前是硬编码的,这是客户端代码中cache.json文件的数据,但有更灵活的方式来实现。

    let available_gear: Vec<Pubkey> = vec![
    "DQmrQJkErmfe6a1fD2hPwdLSnawzkdyrKfSUmd6vkC89"
    .parse::<Pubkey>()
    .unwrap(),
    "A26dg2NBfGgU6gpFPfsiLpxwsV13ZKiD58zgjeQvuad"
    .parse::<Pubkey>()
    .unwrap(),
    "GxR5UVvQDRwB19bCsB1wJh6RtLRZUbEAigtgeAsm6J7N"
    .parse::<Pubkey>()
    .unwrap(),
    "3rL2p6LsGyHVn3iwQQYV9bBmchxMHYPice6ntp7Qw8Pa"
    .parse::<Pubkey>()
    .unwrap(),
    "73JnegAtAWHmBYL7pipcSTpQkkAx77pqCQaEys2Qmrb2"
    .parse::<Pubkey>()
    .unwrap(),
    ];

    随后的代码片段展示了一种非安全的伪随机方法,获取当前时间(以秒为单位),然后对5取模,以确定我们应该选择这5个物品中的哪一个。一旦选择,我们将其分配给战利品盒指针。

    let clock = Clock::get()?;
    let i: usize = (clock.unix_timestamp % 5).try_into().unwrap();
    // Add in randomness later for selecting mint
    let mint = available_gear[i];
    ctx.accounts.lootbox_pointer.mint = mint;
    ctx.accounts.lootbox_pointer.claimed = false;
    ctx.accounts.lootbox_pointer.is_initialized = true;

    Ok(())
    }

    我们将在后续版本中处理真正的随机性,但目前这个版本已经足够。我们还将添加一个检查,以确保用户不能反复打开战利品箱,以获取他们想要的物品。现在,只要用户打开战利品箱,他们就可以看到其中的物品。我们可以检查战利品箱指针是否已初始化,如果没有,则无问题,可以继续进行。虽然每次尝试都需要付费,但是否将其作为功能由你决定。

    好了,现在让我们转到检索指令并查看所需的账户。

    #[derive(Accounts)]
    pub struct RetrieveItem<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
    seeds=["lootbox".as_bytes(), user.key().as_ref()],
    bump,
    constraint=lootbox_pointer.is_initialized
    )]
    pub lootbox_pointer: Account<'info, LootboxPointer>,
    #[account(
    mut,
    constraint=lootbox_pointer.mint==mint.key()
    )]
    pub mint: Account<'info, Mint>,
    #[account(
    init_if_needed,
    payer=user,
    associated_token::mint=mint,
    associated_token::authority=user
    )]
    pub user_gear_ata: Account<'info, TokenAccount>,
    /// CHECK: Mint authority - not used as account
    #[account(
    seeds=["mint".as_bytes()],
    bump
    )]
    pub mint_authority: UncheckedAccount<'info>,
    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
    pub rent: Sysvar<'info, Rent>,
    }

    有几件事情我们需要明确。"mint account" 可以理解为他们所要求的装备的存储账户。"mint authority" 是我们在客户端脚本中分配的用于控制铸币的程序派生账户(PDA)。

    关于这部分的逻辑,首先,我们需要确保战利品箱指针还未被认领。

    pub fn retrieve_item_from_lootbox(ctx: Context<RetrieveItem>) -> Result<()> {
    require!(
    !ctx.accounts.lootbox_pointer.claimed,
    LootboxError::AlreadyClaimed
    );

    接下来,我们将战利品铸造给你。

    token::mint_to(
    CpiContext::new_with_signer(
    ctx.accounts.token_program.to_account_info(),
    MintTo {
    mint: ctx.accounts.mint.to_account_info(),
    to: ctx.accounts.user_gear_ata.to_account_info(),
    authority: ctx.accounts.mint_authority.to_account_info(),
    },
    &[&[
    b"mint".as_ref(),
    &[*ctx.bumps.get("mint_authority").unwrap()],
    ]],
    ),
    1,
    )?;

    最后,我们将认领标记设为真实。

        ctx.accounts.lootbox_pointer.claimed = true;

    Ok(())
    }

    别忘了查看我们在文件底部创建的自定义错误代码。

    #[error_code]
    enum LootboxError {
    #[msg("Mint already claimed")]
    AlreadyClaimed,

    #[msg("Haven't staked long enough for this loot box or invalid loot box number")]
    InvalidLootbox,
    }

    这便是流程。如果你还没尝试实施这个,那么尝试一下,并进行一些测试。尽量自己独立完成。

    在这个文件中,你可以找到相关测试。你会注意到我们添加了两个测试,分别是“随机选择一种铸币口味”和“制造所选装备”。请注意在我们标注“Swap”的地方,更改代码行以使测试正常工作。然后运行测试,它们应该都会按预期运行。

    利用Switchboard的验证功能来随机分配战利品🔀

    任务

    既然你已经成功实现了简单的战利品箱,现在我们可以考虑通过`Switchboard`````````````````````来增强随机性的真实性(虽然从技术上说仍是伪随机,但比之前的随机性要好几个数量级)。

    Switchboard是建立在Solana上的分散式预言机网络。预言机是区块链与现实世界之间的连接桥梁,提供了在多个来源中数据达成共识的机制。在随机性方面,这意味着提供了一个可验证的伪随机结果,没有预言机则无法获得。这对于实现不能“作弊”的战利品箱至关重要。

    Oracle交互是一项涵盖我们在整个课程中所学的所有内容的综合练习。通常包括以下几个步骤:

    • Oracle程序进行客户端设置
    • 使用你自己的程序初始化与Oracle特定的账户(通常是PDAs
    • 你的程序向Oracle程序发出CPI调用,请求特定数据,例如,可验证的随机缓冲区
    • Oracle可以调用你的程序以提供所请求信息的指令
    • 执行你的程序对所请求数据进行操作的指令

    文档

    首先,Switchboard的文档在Web3上仍然相对稀缺,但你可以在此处阅读关于Switchboard可验证随机性的简要概述。然后你应该深入他们的集成文档。

    你可能还会有很多疑问。这没关系,不要感到气馁。这是一个培养自主解决问题能力的好机会。

    接下来你可以查看他们的逐步指南,了解获取随机性的过程。这会引导你了解如何设置Switchboard环境、初始化请求客户端、发出CPI指令、在你的程序中添加Switchboard可以调用的指令来提供随机性等步骤。

    最后的备注

    这个任务可能具有挑战性。这是故意设计的,是对过去六周努力理解Solana的工作的总结。我们还提供了一些关于如何在战利品箱计划中使用Switchboard的视频概览。

    你可以随时观看这些视频。通常,我会建议你先完成一些独立工作,但由于Switchboard的文档相对稀缺,所以尽早查看步骤说明可能会有所帮助。然而,我想提醒你,不要仅仅复制粘贴我的解决方案。相反,观看步骤说明后,尽量自己重新创建类似的内容。如果你准备在我们发布步骤说明之前参考解决方案代码,你可以随时查看这里solution-randomize-loot branch

    你可能需要超过本周结束前的时间来完成这项任务。这是正常的,也可能需要更多的时间来解决问题。没有关系

    - - +
    Skip to main content

    使用Switchboard进行随机化战利品

    现在我们将深入探讨简单战利品箱的实现解决方案。我们会创建一个新程序,用于创建战利品箱并从中获取物品。

    我们将审查的解决方案代码位于Anchor NFT Staking存储库的solution-naive-loot-boxes分支之一。

    我再次强调,建议你自行尝试操作,而不是直接复制粘贴解决方案代码。

    在programs目录中,你可以使用命令anchor new <program-name>来创建一个新程序,我们将其称为lootbox-program

    如果你仔细观察,Anchor.toml文件中nft-staking程序的ID已经变化了,我们还新增了一个loot box程序的ID。你需要在自己的端更新这两个ID。

    首先,让我们回顾一下对原始质押计划所作的修改。

    如果你向下滚动到UserStakeInfo对象,你会发现我们添加了total_earned字段。它会跟踪用户的质押旅程,随着时间的推移,他们将赚取更多的奖励,并且在达到新的里程碑时,将获得更多的战利品箱物品。

    同样相关的是redeem_amount

    首先,你会注意到有些注释被注释掉了,这仅是为了确保我们有足够的令牌进行测试。在测试时,请确保正确地注释/取消注释代码。

    往下滚动一点,你会看到这一行新添加的内容。

    ctx.accounts.stake_state.total_earned += redeem_amount as u64;

    这是一种跟踪总收益的方法,从0开始,然后你添加已兑换的金额,这将成为新的总收益。

    在下面的解除质押功能中,你还会发现测试说明和赎回金额都发生了变化。

    最后,在这个文件中还有一个最后的更改。如果你的程序与我的完全相同,当我们运行它时,由于添加了这个新字段,我们可能会在堆栈中耗尽空间。我选择了一个随机账户,并在其周围放置了一个盒子,确保它被分配到堆中而不是栈中,以解决这个空间问题。你可以在用户的stake ATA上进行操作,或者选择任何其他账户。

    pub user_stake_ata: Box<Account<'info, TokenAccount>>,

    好的,让我们进入新的战利品箱计划的文件。

    Cargo.toml中,你会注意到我们为我们原始的锚定NFT质押程序添加了一个新的依赖项。

    [dependencies]
    anchor-lang = { version="0.25.0", features=["init-if-needed"] }
    anchor-spl = "0.25.0"
    anchor-nft-staking = { path = "../anchor-nft-staking", features = ["cpi"] }

    现在让我们进入主要的战利品箱程序文件

    在使用语句中,你会注意到我们现在导入了锚定NFT质押,这样我们就可以检查总收益字段了。

    use anchor_lang::prelude::*;
    use anchor_nft_staking::UserStakeInfo;
    use anchor_spl::token;
    use anchor_spl::{
    associated_token::AssociatedToken,
    token::{Burn, Mint, MintTo, Token, TokenAccount},
    };

    这里,我们只有两个指令,open_lootboxretrieve_item_from_lootbox。有两个指令的原因是,当你请求“给我一个随机的战利品”时,程序必须决定要铸造和赠送的所有可能物品,客户端必须传入所有可能的铸造账户。这使程序变得不那么灵活,并增加了检查一堆不同账户以确保有选项的开销,对客户端来说也非常麻烦。因此,我们创建了一个用于打开战利品箱的指令,基本上是在所有可能的铸造选项中给我一个。我们还选择了这个地方作为支付的地方,这是我们将烧毁BLD代币的地方。至于第二个指令,在这一点上,客户端知道他们将获得哪个铸造物品,并可以传入该信息,然后我们可以从中铸造。

    首先,让我们打开战利品箱,看看我们需要的账号。

    #[derive(Accounts)]
    pub struct OpenLootbox<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
    init_if_needed,
    payer = user,
    space = std::mem::size_of::<LootboxPointer>() + 8,
    seeds=["lootbox".as_bytes(), user.key().as_ref()],
    bump
    )]
    pub lootbox_pointer: Account<'info, LootboxPointer>,
    pub system_program: Program<'info, System>,
    pub token_program: Program<'info, Token>,
    // Swap the next two lines out between prod/testing
    // #[account(mut)]
    #[account(
    mut,
    address="6YR1nuLqkk8VC1v42xJaPKvE9X9pnuqVAvthFUSDsMUL".parse::<Pubkey>().unwrap()
    )]
    pub stake_mint: Account<'info, Mint>,
    #[account(
    mut,
    associated_token::mint=stake_mint,
    associated_token::authority=user
    )]
    pub stake_mint_ata: Account<'info, TokenAccount>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    #[account(
    constraint=stake_state.user_pubkey==user.key(),
    )]
    pub stake_state: Account<'info, UserStakeInfo>,
    }

    你会发现一个名为lootbox_pointer的新元素,这是一种全新的类型。它包括一个薄荷属性、一个布尔值用来表示是否已被领取,以及一个is_initialized属性。

    这是一个与用户关联的PDA(Program-derived address),因此其种子是“战利品箱”和用户。通过这样做,当我们选中一个薄荷时,我们不将数据返回给客户端,而是存储在某个账户中。因此,这是一个用户可以查询并检索物品的PDA

    此外,需要注意的是,在某行代码的开头有一个“Swap”注释行。为了使测试正常运行,需要取消这些行的注释,并注释掉其他包含心智地址的stake_mint属性行。

    下面的Rust代码展示了LootboxPointer结构:

    #[account]
    pub struct LootboxPointer {
    mint: Pubkey,
    claimed: bool,
    is_initialized: bool,
    }

    接下来,我们来详细了解这个功能。首先,我们要验证它是否是一个有效的战利品箱。

    用户输入一个盒子号码,然后程序会运行一个无限循环。在每次迭代中,如果BLD令牌的数量过低,我们会返回错误。其他两种可能的路径是:要么将loot_box号码加倍,要么如果在loot_box号码和box_number之间找到匹配,我们要求stake_state PDAs的总收益不少于传入的box_number。简而言之,你必须赚得比盒子号码更多。

    以下是打开战利品箱的函数:

    pub fn open_lootbox(ctx: Context<OpenLootbox>, box_number: u64) -> Result<()> {
    let mut loot_box = 10;
    loop {
    if loot_box > box_number {
    return err!(LootboxError::InvalidLootbox);
    }

    if loot_box == box_number {
    require!(
    ctx.accounts.stake_state.total_earned >= box_number,
    LootboxError::InvalidLootbox
    );
    break;
    } else {
    loot_box = loot_box * 2;
    }
    }

    require!(
    !ctx.accounts.lootbox_pointer.is_initialized || ctx.accounts.lootbox_pointer.claimed,
    LootboxError::InvalidLootbox
    );
    }

    然后我们继续进行代币销毁,销毁与盒子编号所需数量相对应的代币。

    token::burn(
    CpiContext::new(
    ctx.accounts.token_program.to_account_info(),
    Burn {
    mint: ctx.accounts.stake_mint.to_account_info(),
    from: ctx.accounts.stake_mint_ata.to_account_info(),
    authority: ctx.accounts.user.to_account_info(),
    },
    ),
    box_number * u64::pow(10, 2),
    )?;

    该函数还涉及代币销毁操作,即销毁与盒子编号所需数量相匹配的代币。之后,我们将描述可用装备。当前是硬编码的,这是客户端代码中cache.json文件的数据,但有更灵活的方式来实现。

    let available_gear: Vec<Pubkey> = vec![
    "DQmrQJkErmfe6a1fD2hPwdLSnawzkdyrKfSUmd6vkC89"
    .parse::<Pubkey>()
    .unwrap(),
    "A26dg2NBfGgU6gpFPfsiLpxwsV13ZKiD58zgjeQvuad"
    .parse::<Pubkey>()
    .unwrap(),
    "GxR5UVvQDRwB19bCsB1wJh6RtLRZUbEAigtgeAsm6J7N"
    .parse::<Pubkey>()
    .unwrap(),
    "3rL2p6LsGyHVn3iwQQYV9bBmchxMHYPice6ntp7Qw8Pa"
    .parse::<Pubkey>()
    .unwrap(),
    "73JnegAtAWHmBYL7pipcSTpQkkAx77pqCQaEys2Qmrb2"
    .parse::<Pubkey>()
    .unwrap(),
    ];

    随后的代码片段展示了一种非安全的伪随机方法,获取当前时间(以秒为单位),然后对5取模,以确定我们应该选择这5个物品中的哪一个。一旦选择,我们将其分配给战利品盒指针。

    let clock = Clock::get()?;
    let i: usize = (clock.unix_timestamp % 5).try_into().unwrap();
    // Add in randomness later for selecting mint
    let mint = available_gear[i];
    ctx.accounts.lootbox_pointer.mint = mint;
    ctx.accounts.lootbox_pointer.claimed = false;
    ctx.accounts.lootbox_pointer.is_initialized = true;

    Ok(())
    }

    我们将在后续版本中处理真正的随机性,但目前这个版本已经足够。我们还将添加一个检查,以确保用户不能反复打开战利品箱,以获取他们想要的物品。现在,只要用户打开战利品箱,他们就可以看到其中的物品。我们可以检查战利品箱指针是否已初始化,如果没有,则无问题,可以继续进行。虽然每次尝试都需要付费,但是否将其作为功能由你决定。

    好了,现在让我们转到检索指令并查看所需的账户。

    #[derive(Accounts)]
    pub struct RetrieveItem<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
    seeds=["lootbox".as_bytes(), user.key().as_ref()],
    bump,
    constraint=lootbox_pointer.is_initialized
    )]
    pub lootbox_pointer: Account<'info, LootboxPointer>,
    #[account(
    mut,
    constraint=lootbox_pointer.mint==mint.key()
    )]
    pub mint: Account<'info, Mint>,
    #[account(
    init_if_needed,
    payer=user,
    associated_token::mint=mint,
    associated_token::authority=user
    )]
    pub user_gear_ata: Account<'info, TokenAccount>,
    /// CHECK: Mint authority - not used as account
    #[account(
    seeds=["mint".as_bytes()],
    bump
    )]
    pub mint_authority: UncheckedAccount<'info>,
    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
    pub rent: Sysvar<'info, Rent>,
    }

    有几件事情我们需要明确。"mint account" 可以理解为他们所要求的装备的存储账户。"mint authority" 是我们在客户端脚本中分配的用于控制铸币的程序派生账户(PDA)。

    关于这部分的逻辑,首先,我们需要确保战利品箱指针还未被认领。

    pub fn retrieve_item_from_lootbox(ctx: Context<RetrieveItem>) -> Result<()> {
    require!(
    !ctx.accounts.lootbox_pointer.claimed,
    LootboxError::AlreadyClaimed
    );

    接下来,我们将战利品铸造给你。

    token::mint_to(
    CpiContext::new_with_signer(
    ctx.accounts.token_program.to_account_info(),
    MintTo {
    mint: ctx.accounts.mint.to_account_info(),
    to: ctx.accounts.user_gear_ata.to_account_info(),
    authority: ctx.accounts.mint_authority.to_account_info(),
    },
    &[&[
    b"mint".as_ref(),
    &[*ctx.bumps.get("mint_authority").unwrap()],
    ]],
    ),
    1,
    )?;

    最后,我们将认领标记设为真实。

        ctx.accounts.lootbox_pointer.claimed = true;

    Ok(())
    }

    别忘了查看我们在文件底部创建的自定义错误代码。

    #[error_code]
    enum LootboxError {
    #[msg("Mint already claimed")]
    AlreadyClaimed,

    #[msg("Haven't staked long enough for this loot box or invalid loot box number")]
    InvalidLootbox,
    }

    这便是流程。如果你还没尝试实施这个,那么尝试一下,并进行一些测试。尽量自己独立完成。

    在这个文件中,你可以找到相关测试。你会注意到我们添加了两个测试,分别是“随机选择一种铸币口味”和“制造所选装备”。请注意在我们标注“Swap”的地方,更改代码行以使测试正常工作。然后运行测试,它们应该都会按预期运行。

    利用Switchboard的验证功能来随机分配战利品🔀

    任务

    既然你已经成功实现了简单的战利品箱,现在我们可以考虑通过`Switchboard`````````````````````来增强随机性的真实性(虽然从技术上说仍是伪随机,但比之前的随机性要好几个数量级)。

    Switchboard是建立在Solana上的分散式预言机网络。预言机是区块链与现实世界之间的连接桥梁,提供了在多个来源中数据达成共识的机制。在随机性方面,这意味着提供了一个可验证的伪随机结果,没有预言机则无法获得。这对于实现不能“作弊”的战利品箱至关重要。

    Oracle交互是一项涵盖我们在整个课程中所学的所有内容的综合练习。通常包括以下几个步骤:

    • Oracle程序进行客户端设置
    • 使用你自己的程序初始化与Oracle特定的账户(通常是PDAs
    • 你的程序向Oracle程序发出CPI调用,请求特定数据,例如,可验证的随机缓冲区
    • Oracle可以调用你的程序以提供所请求信息的指令
    • 执行你的程序对所请求数据进行操作的指令

    文档

    首先,Switchboard的文档在Web3上仍然相对稀缺,但你可以在此处阅读关于Switchboard可验证随机性的简要概述。然后你应该深入他们的集成文档。

    你可能还会有很多疑问。这没关系,不要感到气馁。这是一个培养自主解决问题能力的好机会。

    接下来你可以查看他们的逐步指南,了解获取随机性的过程。这会引导你了解如何设置Switchboard环境、初始化请求客户端、发出CPI指令、在你的程序中添加Switchboard可以调用的指令来提供随机性等步骤。

    最后的备注

    这个任务可能具有挑战性。这是故意设计的,是对过去六周努力理解Solana的工作的总结。我们还提供了一些关于如何在战利品箱计划中使用Switchboard的视频概览。

    你可以随时观看这些视频。通常,我会建议你先完成一些独立工作,但由于Switchboard的文档相对稀缺,所以尽早查看步骤说明可能会有所帮助。然而,我想提醒你,不要仅仅复制粘贴我的解决方案。相反,观看步骤说明后,尽量自己重新创建类似的内容。如果你准备在我们发布步骤说明之前参考解决方案代码,你可以随时查看这里solution-randomize-loot branch

    你可能需要超过本周结束前的时间来完成这项任务。这是正常的,也可能需要更多的时间来解决问题。没有关系

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module6/ship-week/build-a-loot-box-program/index.html b/Solana-Co-Learn/module6/ship-week/build-a-loot-box-program/index.html index c23d8b045..51db02735 100644 --- a/Solana-Co-Learn/module6/ship-week/build-a-loot-box-program/index.html +++ b/Solana-Co-Learn/module6/ship-week/build-a-loot-box-program/index.html @@ -9,14 +9,14 @@ - - + +
    Skip to main content

    💰 构建一个战利品箱程序

    既然你已经让程序正常运转了,那么我们就来调整前端代码,以适应Anchor。这个配置只需一分钟,稍作等待,我们将会调整一些内容。欢迎来到“发货周”!整个周都将致力于让你的项目做好发货准备。对我们来说,这意味着完善buildoors项目。对你来说,同样的情况,但我们希望能看到你加入一些个人风格!

    剩下要做的主要事情就是实现战利品箱。目前,可以创建一个简单的战利品箱实现。

    这意味着:

    • 没有经过验证的随机性 - 你可以选择一个简单的伪随机实现,或者根本不使用随机性。
    • 现在可以使用静态解决方案 - 例如,不必设计成可以随后更新装备类型。暂时保持简单即可。
    • 创造力的展现 - 我说的不仅仅是“使用不同的图片”。请随意构建你自己的架构,并进行真正的创新实验。用密斯·弗里兹尔的名言来说:“冒险一试,犯错误,搞得一团糟!”
    info

    提示 -虽然如此,根据我们对战利品箱计划的处理方式,以下是一些建议(没错,我们将其作为一个独立的程序来处理):

    • 我们使用可替代资产而非非同质化代币(NFT)来分发战利品箱中的装备。
    • 我们刚刚修改了用于创建BLD代币的脚本,使每件装备都能生成一个“可替代资产”代币。
    • 战利品箱其实只是一个概念 - 没有任何代表它的代币或物品。

    为了从多个可能的铸币中分配(随机或其他方式),我们需要设置程序以进行两个单独的交易:

    1. open_lootbox: 它处理燃烧所需的$BLD代币以打开战利品箱,然后伪随机选择要铸造给调用者的装备,但是不立即铸造,而是将选择存储在与用户关联的PDA中。必须这样做,否则你将不得不将所有可能的装备铸造传递给指令,这将非常麻烦。
    2. retrieve_item_from_lootbox: 这个函数会检查之前提到的PDA,并将指定的装备发放给用户。

    再次强调,不要局限于我们的实现方式。在查看我们的解决方案之前,请自己尝试一下。我并不是说只试试20分钟就好了。我花了整整一天的时间才弄明白,所以在寻求我们的解决方案之前,不要害怕独立工作相当长的一段时间。

    祝你好运!

    - - +虽然如此,根据我们对战利品箱计划的处理方式,以下是一些建议(没错,我们将其作为一个独立的程序来处理):

    为了从多个可能的铸币中分配(随机或其他方式),我们需要设置程序以进行两个单独的交易:

    1. open_lootbox: 它处理燃烧所需的$BLD代币以打开战利品箱,然后伪随机选择要铸造给调用者的装备,但是不立即铸造,而是将选择存储在与用户关联的PDA中。必须这样做,否则你将不得不将所有可能的装备铸造传递给指令,这将非常麻烦。
    2. retrieve_item_from_lootbox: 这个函数会检查之前提到的PDA,并将指定的装备发放给用户。

    再次强调,不要局限于我们的实现方式。在查看我们的解决方案之前,请自己尝试一下。我并不是说只试试20分钟就好了。我花了整整一天的时间才弄明白,所以在寻求我们的解决方案之前,不要害怕独立工作相当长的一段时间。

    祝你好运!

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module6/ship-week/create-gear-tokens/index.html b/Solana-Co-Learn/module6/ship-week/create-gear-tokens/index.html index db00bf084..d0824f41a 100644 --- a/Solana-Co-Learn/module6/ship-week/create-gear-tokens/index.html +++ b/Solana-Co-Learn/module6/ship-week/create-gear-tokens/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    ⚙ 创建齿轮代币

    让我们一起探讨一种可能的齿轮代币解决方案。

    我们即将深入探讨的解决方案代码位于Buildoors前端代码库solution-simple-gear分支上。如果你还没有尝试自己构建,请尽量避免直接从解决方案代码中复制粘贴。

    我们将会浏览两个不同的代码库。如果你还记得,我们在客户端项目中创建了BLD代币和NFT。幸运的是,我们在那里完成了这项工作,如果我们愿意,我们还可以将其转移到程序项目中。

    你可以在/tokens/gear/assets文件夹中找到齿轮的图像。我们选择将其制作为可替代资产,或带有关联元数据和0位小数的SPL代币,而不是NFT,这样它们就不仅限于一个单位。

    /tokens/gear/index.ts中的脚本负责生成与这些资产相关的货币,并将其存储在同一文件夹中的cache.json文件中。

    在脚本的内部部分,向下滚动你会看到我们的主要函数。

    async function main() {
    const connection = new web3.Connection(web3.clusterApiUrl("devnet"))
    const payer = await initializeKeypair(connection)

    await createGear(
    connection,
    payer,
    new web3.PublicKey("6GE3ki2igpw2ZTAt6BV4pTjF5qvtCbFVQP7SGPJaEuoa"),
    ["Bow", "Glasses", "Hat", "Keyboard", "Mustache"]
    )
    }

    我们传入的公钥是为了我们的程序,以及铸币厂的名称列表,这些名称需要与资产文件夹中的内容相匹配。

    如果你在函数中向上滚动,你会看到它首先用一个空对象开始,其中将放置薄荷糖。

    let collection: any = {}

    然后我们创建了我们的metaplex对象,接着是一个循环,该循环为每个铸币执行脚本的功能。

    它从一个空的薄荷数组开始,这样我们就可以为每个资产添加多个薄荷。

    let mints: Array<string> = []

    接下来我们获取图像缓冲区并将其上传到`Arweave,进行持久化存储。

    const imageBuffer = fs.readFileSync(`tokens/gear/assets/${assets[i]}.png`)
    const file = toMetaplexFile(imageBuffer, `${assets[i]}.png`)
    const imageUri = await metaplex.storage().upload(file)

    在那之后,如果你想要不同的经验等级,我们就循环执行相应的次数,针对这个装备。在我们的示例中,只执行一次,因为经验等级从10开始并结束于10。如果你想要每个等级的五个装备,只需将上限增加到50,即xp <= 50

    for (let xp = 10; xp <= 10; xp += 10)...

    一旦进入循环,我们获取了将在后续分配的Mint Auth,即我们想要进行铸币的程序中的PDA - 用于战利品箱程序的PDA

    const [mintAuth] = await web3.PublicKey.findProgramAddress(
    [Buffer.from("mint")],
    programId
    )

    随后,我们创建了一个全新的代币,并将其小数位设置为0,因为它是一种不可分割的资产。

    const tokenMint = await token.createMint(
    connection,
    payer,
    payer.publicKey,
    payer.publicKey,
    0
    )

    一旦创建了该薄荷,我们将其推入薄荷数组中。

    mints.push(tokenMint.toBase58())

    接下来,我们会上传我们的链下元数据,其中包括名称、描述、图像链接和两个属性。

    const { uri } = await metaplex
    .nfts()
    .uploadMetadata({
    name: assets[i],
    description: "这是用来提升你的buildoor的装备",
    image: imageUri,
    attributes: [
    {
    trait_type: "xp",
    value: `${xp}`,
    },
    ],
    })
    .run()

    然后我们获取该薄荷的元数据PDA

    const metadataPda = await findMetadataPda(tokenMint)

    接下来,我们创建元数据的链上版本。

    const tokenMetadata = {
    name: assets[i],
    symbol: "BLDRGEAR",
    uri: uri,
    sellerFeeBasisPoints: 0,
    creators: null,
    collection: null,
    uses: null,
    } as DataV2

    按照之前的步骤,继续创建我们的V2指令。

    const instruction = createCreateMetadataAccountV2Instruction(
    {
    metadata: metadataPda,
    mint: tokenMint,
    mintAuthority: payer.publicKey,
    payer: payer.publicKey,
    updateAuthority: payer.publicKey,
    },
    {
    createMetadataAccountArgsV2: {
    data: tokenMetadata,
    isMutable: true,
    },
    }
    )

    你会注意到我们的付款人是我们的薄荷权威,我们将会很快对其进行更改。

    接下来,我们创建一个交易,添加指令并发送。

    const transaction = new web3.Transaction()
    transaction.add(instruction)

    const transactionSignature = await web3.sendAndConfirmTransaction(
    connection,
    transaction,
    [payer]
    )

    现在我们将权限更改为mintAuth,即战利品箱程序上的PDA

    await token.setAuthority(
    connection,
    payer,
    tokenMint,
    payer.publicKey,
    token.AuthorityType.MintTokens,
    mintAuth
    )
    }

    最后,在内循环之外,我们将薄荷放入集合中,所以第一个是“Bow”(作为我们的例子)。

    collection[assets[i]] = mints

    最后,在所有的循环之外,我们将整个集合写入文件。对于我们的实现来说,这个集合只有五个项目。

    fs.writeFileSync("tokens/gear/cache.json", JSON.stringify(collection))

    这只是一种方法,它是一个相当简单的解决方案。如果你还没有编写代码,并且你看了这个视频,请尝试自己完成,然后再回来使用解决方案代码(如果需要的话)。这个过程不仅是对代码逻辑的理解和实践,也是一次关于如何将链下数据与链上数据结合的经验学习。

    - - +
    Skip to main content

    ⚙ 创建齿轮代币

    让我们一起探讨一种可能的齿轮代币解决方案。

    我们即将深入探讨的解决方案代码位于Buildoors前端代码库solution-simple-gear分支上。如果你还没有尝试自己构建,请尽量避免直接从解决方案代码中复制粘贴。

    我们将会浏览两个不同的代码库。如果你还记得,我们在客户端项目中创建了BLD代币和NFT。幸运的是,我们在那里完成了这项工作,如果我们愿意,我们还可以将其转移到程序项目中。

    你可以在/tokens/gear/assets文件夹中找到齿轮的图像。我们选择将其制作为可替代资产,或带有关联元数据和0位小数的SPL代币,而不是NFT,这样它们就不仅限于一个单位。

    /tokens/gear/index.ts中的脚本负责生成与这些资产相关的货币,并将其存储在同一文件夹中的cache.json文件中。

    在脚本的内部部分,向下滚动你会看到我们的主要函数。

    async function main() {
    const connection = new web3.Connection(web3.clusterApiUrl("devnet"))
    const payer = await initializeKeypair(connection)

    await createGear(
    connection,
    payer,
    new web3.PublicKey("6GE3ki2igpw2ZTAt6BV4pTjF5qvtCbFVQP7SGPJaEuoa"),
    ["Bow", "Glasses", "Hat", "Keyboard", "Mustache"]
    )
    }

    我们传入的公钥是为了我们的程序,以及铸币厂的名称列表,这些名称需要与资产文件夹中的内容相匹配。

    如果你在函数中向上滚动,你会看到它首先用一个空对象开始,其中将放置薄荷糖。

    let collection: any = {}

    然后我们创建了我们的metaplex对象,接着是一个循环,该循环为每个铸币执行脚本的功能。

    它从一个空的薄荷数组开始,这样我们就可以为每个资产添加多个薄荷。

    let mints: Array<string> = []

    接下来我们获取图像缓冲区并将其上传到`Arweave,进行持久化存储。

    const imageBuffer = fs.readFileSync(`tokens/gear/assets/${assets[i]}.png`)
    const file = toMetaplexFile(imageBuffer, `${assets[i]}.png`)
    const imageUri = await metaplex.storage().upload(file)

    在那之后,如果你想要不同的经验等级,我们就循环执行相应的次数,针对这个装备。在我们的示例中,只执行一次,因为经验等级从10开始并结束于10。如果你想要每个等级的五个装备,只需将上限增加到50,即xp <= 50

    for (let xp = 10; xp <= 10; xp += 10)...

    一旦进入循环,我们获取了将在后续分配的Mint Auth,即我们想要进行铸币的程序中的PDA - 用于战利品箱程序的PDA

    const [mintAuth] = await web3.PublicKey.findProgramAddress(
    [Buffer.from("mint")],
    programId
    )

    随后,我们创建了一个全新的代币,并将其小数位设置为0,因为它是一种不可分割的资产。

    const tokenMint = await token.createMint(
    connection,
    payer,
    payer.publicKey,
    payer.publicKey,
    0
    )

    一旦创建了该薄荷,我们将其推入薄荷数组中。

    mints.push(tokenMint.toBase58())

    接下来,我们会上传我们的链下元数据,其中包括名称、描述、图像链接和两个属性。

    const { uri } = await metaplex
    .nfts()
    .uploadMetadata({
    name: assets[i],
    description: "这是用来提升你的buildoor的装备",
    image: imageUri,
    attributes: [
    {
    trait_type: "xp",
    value: `${xp}`,
    },
    ],
    })
    .run()

    然后我们获取该薄荷的元数据PDA

    const metadataPda = await findMetadataPda(tokenMint)

    接下来,我们创建元数据的链上版本。

    const tokenMetadata = {
    name: assets[i],
    symbol: "BLDRGEAR",
    uri: uri,
    sellerFeeBasisPoints: 0,
    creators: null,
    collection: null,
    uses: null,
    } as DataV2

    按照之前的步骤,继续创建我们的V2指令。

    const instruction = createCreateMetadataAccountV2Instruction(
    {
    metadata: metadataPda,
    mint: tokenMint,
    mintAuthority: payer.publicKey,
    payer: payer.publicKey,
    updateAuthority: payer.publicKey,
    },
    {
    createMetadataAccountArgsV2: {
    data: tokenMetadata,
    isMutable: true,
    },
    }
    )

    你会注意到我们的付款人是我们的薄荷权威,我们将会很快对其进行更改。

    接下来,我们创建一个交易,添加指令并发送。

    const transaction = new web3.Transaction()
    transaction.add(instruction)

    const transactionSignature = await web3.sendAndConfirmTransaction(
    connection,
    transaction,
    [payer]
    )

    现在我们将权限更改为mintAuth,即战利品箱程序上的PDA

    await token.setAuthority(
    connection,
    payer,
    tokenMint,
    payer.publicKey,
    token.AuthorityType.MintTokens,
    mintAuth
    )
    }

    最后,在内循环之外,我们将薄荷放入集合中,所以第一个是“Bow”(作为我们的例子)。

    collection[assets[i]] = mints

    最后,在所有的循环之外,我们将整个集合写入文件。对于我们的实现来说,这个集合只有五个项目。

    fs.writeFileSync("tokens/gear/cache.json", JSON.stringify(collection))

    这只是一种方法,它是一个相当简单的解决方案。如果你还没有编写代码,并且你看了这个视频,请尝试自己完成,然后再回来使用解决方案代码(如果需要的话)。这个过程不仅是对代码逻辑的理解和实践,也是一次关于如何将链下数据与链上数据结合的经验学习。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module6/ship-week/index.html b/Solana-Co-Learn/module6/ship-week/index.html index 85160ebb0..a1ea6ce76 100644 --- a/Solana-Co-Learn/module6/ship-week/index.html +++ b/Solana-Co-Learn/module6/ship-week/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content
    - - +
    Skip to main content
    + + \ No newline at end of file diff --git a/Solana-Co-Learn/module6/ship-week/intro-to-ship-week/index.html b/Solana-Co-Learn/module6/ship-week/intro-to-ship-week/index.html index 0f637461a..5123c3e84 100644 --- a/Solana-Co-Learn/module6/ship-week/intro-to-ship-week/index.html +++ b/Solana-Co-Learn/module6/ship-week/intro-to-ship-week/index.html @@ -9,13 +9,13 @@ - - + +
    -
    Skip to main content

    🚢挑战周介绍

    既然你已经让程序正常运转了,那么我们就来调整前端代码,以适应Anchor。这个配置只需一分钟,稍作等待,我们会调整一些内容。欢迎来到你的Solana Core的最后一周!这一周与之前的五周有所不同。我们唯一的焦点是确保我们的项目在核心课程结束时做好发布准备。

    结构方面,不再有正规的课程安排,而是给你一系列任务让你自行完成。今天的任务是构建一个战利品箱解决方案。我们会从简单的部分开始。下一节将有详细的书面说明,还会有一些提示告诉你应该怎么做。发挥你的创造力,根据项目的需要选择最合适的方案。

    我们还将发布解决方案的演示视频,但与以往的视频有所不同。过去,我们会与你一起编码,但本周我们只会解释代码解决方案,这样你就可以更多地自己动手。请记住,我们的解决方案只是众多可能选择中的一个,你完全可以自由发挥,构建你心仪的内容。

    我们已经讲解了很多内容,你现在已经做好了准备,可以独立完成这项工作。如果需要,你可以按照步骤一步一步来操作,或者如果你愿意,你也可以尝试其它方法来使代码正常运作。

    - - +
    Skip to main content

    🚢挑战周介绍

    既然你已经让程序正常运转了,那么我们就来调整前端代码,以适应Anchor。这个配置只需一分钟,稍作等待,我们会调整一些内容。欢迎来到你的Solana Core的最后一周!这一周与之前的五周有所不同。我们唯一的焦点是确保我们的项目在核心课程结束时做好发布准备。

    结构方面,不再有正规的课程安排,而是给你一系列任务让你自行完成。今天的任务是构建一个战利品箱解决方案。我们会从简单的部分开始。下一节将有详细的书面说明,还会有一些提示告诉你应该怎么做。发挥你的创造力,根据项目的需要选择最合适的方案。

    我们还将发布解决方案的演示视频,但与以往的视频有所不同。过去,我们会与你一起编码,但本周我们只会解释代码解决方案,这样你就可以更多地自己动手。请记住,我们的解决方案只是众多可能选择中的一个,你完全可以自由发挥,构建你心仪的内容。

    我们已经讲解了很多内容,你现在已经做好了准备,可以独立完成这项工作。如果需要,你可以按照步骤一步一步来操作,或者如果你愿意,你也可以尝试其它方法来使代码正常运作。

    + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/anchor-program-hello/index.html b/Solana-Co-Learn/tags/anchor-program-hello/index.html index 7f38779b2..dbbfb5944 100644 --- a/Solana-Co-Learn/tags/anchor-program-hello/index.html +++ b/Solana-Co-Learn/tags/anchor-program-hello/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    One doc tagged with "anchor-program-hello"

    View All Tags
    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/backpack/index.html b/Solana-Co-Learn/tags/backpack/index.html index 3150cdf6b..e2e1913b3 100644 --- a/Solana-Co-Learn/tags/backpack/index.html +++ b/Solana-Co-Learn/tags/backpack/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    One doc tagged with "backpack"

    View All Tags

    Solana钱包使用 - Backpack 🎒

    Solana的钱包种类繁多,如众所周知的Phantom钱包。然而,在此我并不推荐使用Phantom,因为对于开发者来说,它并不够友好。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/basic/index.html b/Solana-Co-Learn/tags/basic/index.html index 8ef5d0d11..076faca40 100644 --- a/Solana-Co-Learn/tags/basic/index.html +++ b/Solana-Co-Learn/tags/basic/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    One doc tagged with "basic"

    View All Tags
    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/blockchain/index.html b/Solana-Co-Learn/tags/blockchain/index.html index 419de3312..b479a3286 100644 --- a/Solana-Co-Learn/tags/blockchain/index.html +++ b/Solana-Co-Learn/tags/blockchain/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    One doc tagged with "blockchain"

    View All Tags
    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/candy-machine/index.html b/Solana-Co-Learn/tags/candy-machine/index.html index 054b2e372..df7ca6ad3 100644 --- a/Solana-Co-Learn/tags/candy-machine/index.html +++ b/Solana-Co-Learn/tags/candy-machine/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    2 docs tagged with "candy-machine"

    View All Tags

    🍬 创建糖果机

    现在我们已经铸造了一个NFT,接下来我们要学习如何铸造一系列NFT。我们将使用Candy Machine来完成这个任务。Candy Machine是一个Solana程序,它可以让创作者将他们的艺术品和资产上链。虽然还有其他方式可以创建NFT系列,但Candy Machine在Solana上已成为一项标准,因为它具备许多实用功能,如防机器人保护和安全随机化。准备好添加一些内容到我们上一课创建但未使用的文件夹中了吗?

    🍭 糖果机和Sugar CLI

    将自己的脸做成NFT有何不好呢?你可以永久地将自己视为一个早期的建设者,并告诉你的妈妈你已经进入了区块链世界。既然我们已经铸造了一个单独的NFT,现在我们将学习如何铸造一系列的NFT。为了实现这一目标,我们将使用Candy Machine——这是一个Solana程序,让创作者能够将他们的资产上链。尽管这不是创建系列的唯一方法,但在Solana上它已经成为一种标准,因为它拥有一些有用的功能,如防机器人保护和安全随机化。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/candy/index.html b/Solana-Co-Learn/tags/candy/index.html index aa7a8525e..e43cd69d4 100644 --- a/Solana-Co-Learn/tags/candy/index.html +++ b/Solana-Co-Learn/tags/candy/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    One doc tagged with "candy"

    View All Tags

    🖼 从糖果机展示NFTs

    现在我们已经铸造了一个NFT,接下来我们将学习如何铸造一系列的NFT。为此,我们将利用Candy Machine来实现——这是Solana的一个程序,使创作者能够将他们的资产上链。虽然这并非创建系列的唯一方式,但在Solana上它却成了标准,因为它具备了许多有用的功能,例如机器人保护和安全随机化。你是否感受到过看到闪亮的新iPhone时的那股兴奋感?稀有的NFT有点儿类似于此。对于优秀的艺术家而言,仅仅是观看这些NFT也极富乐趣。毕竟,艺术的本质就是用来欣赏的!接下来,我们将探讨如果我们只有Candy Machine的地址,应该如何展示NFTs。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/client-side-development/index.html b/Solana-Co-Learn/tags/client-side-development/index.html index 9b1386308..6baaea7f4 100644 --- a/Solana-Co-Learn/tags/client-side-development/index.html +++ b/Solana-Co-Learn/tags/client-side-development/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    3 docs tagged with "client-side-development"

    View All Tags

    ✍将数据写入区块链

    我们已经熟练掌握了区块链的阅读操作,现在开始学习如何将数据写入Solana区块链。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/custom-instruction/index.html b/Solana-Co-Learn/tags/custom-instruction/index.html index adacfa3ce..86a8395bf 100644 --- a/Solana-Co-Learn/tags/custom-instruction/index.html +++ b/Solana-Co-Learn/tags/custom-instruction/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    3 docs tagged with "custom-instruction"

    View All Tags

    🎥 构建一个电影评论应用

    现在我们已经完成了钱包连接的设置,是时候让我们的ping按钮发挥作用了!我们将整合所有元素,构建一个基于区块链的电影评论应用——它将允许任何人提交对他们最喜欢的电影的评论,有点像烂番茄网站那样。

    📡 Run is back - 反序列化

    现在我们已经完成了钱包连接的设置,是时候让我们的 ping 按钮真正起作用了!向网络账户写入数据只是任务的一半,另一半则是读取这些数据。在第一部分,我们借助Web3.js库中的内置函数来读取内容,这只适用于基础数据,如余额和交易详情。但正如我们在上一部分所见,所有精彩的东西都藏在 PDAs 里。

    🤔 自定义指令

    既然我们已经完成了钱包连接的设置,那么让我们使我们的ping按钮真正有所作为吧!你现在知道如何读取数据并通过简单的交易将其写入网络。几乎立刻,你可能会发现自己想要通过交易发送数据。那么让我们了解一下如何向Solana区块链讲述你的故事。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/deploy/index.html b/Solana-Co-Learn/tags/deploy/index.html index 91680c718..638be4773 100644 --- a/Solana-Co-Learn/tags/deploy/index.html +++ b/Solana-Co-Learn/tags/deploy/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    One doc tagged with "deploy"

    View All Tags

    🌐 部署到 Vercel

    这一步是你本周工作中至关重要的一环,即将你的项目从本地环境部署到线上环境。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/displayings-nfts/index.html b/Solana-Co-Learn/tags/displayings-nfts/index.html index 9f51d1d46..3f5d13848 100644 --- a/Solana-Co-Learn/tags/displayings-nfts/index.html +++ b/Solana-Co-Learn/tags/displayings-nfts/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    6 docs tagged with "displayings-nfts"

    View All Tags

    🍬 创建糖果机

    现在我们已经铸造了一个NFT,接下来我们要学习如何铸造一系列NFT。我们将使用Candy Machine来完成这个任务。Candy Machine是一个Solana程序,它可以让创作者将他们的艺术品和资产上链。虽然还有其他方式可以创建NFT系列,但Candy Machine在Solana上已成为一项标准,因为它具备许多实用功能,如防机器人保护和安全随机化。准备好添加一些内容到我们上一课创建但未使用的文件夹中了吗?

    🎨 创建奖励代币

    既然我们已经铸造了一个NFT,那么接下来我们要探讨如何铸造一系列的NFT。为了达成这个目标,我们将采用Candy Machine——一款在Solana上广泛使用的程序,允许创作者将其资产上链。Candy Machine在Solana上受到欢迎的原因在于,它具备了如机器人防护和安全随机化等实用功能。现在我们可以回归到我们自定义的NFT质押应用上来,借助我们在代币程序和Candy Machine上的经验来构建这个应用。

    🎨 创建铸币用户界面

    现在我们成功创建了代币和非同质化代币(NFT),让我们继续着手构建我们的铸币用户界面。这样一来,我们就能直观地与智能合约互动,并允许他人在我们的浏览器上铸造我们的NFT。是不是非常酷?你可能已经注意到,你的网站上现有一个名为 minting 的按钮,但它目前尚未实现任何功能。让我们从定义一个函数开始,然后添加逻辑来允许我们铸造NFT。如果你没有起始代码,可以在这里克隆。

    💃 展示NFTs

    既然我们已经铸造了一个NFT,现在我们将进一步探讨如何铸造一整套NFT。我们将借助Candy Machine来实现这个任务——这是一个Solana程序,可以让创作者轻松地将他们的资产上链。当然,这并不是在Solana上创建系列的唯一方法,但它确实成为了标准,因为它具备了许多实用功能,例如防机器人保护和安全随机化。毕竟,如果你不能向人们展示你的NFT,那它还有什么价值呢!在这一节,我们将引导你展示你的作品——首先在钱包中展示,然后在Candy Machine中展示。

    📱 在钱包中展示NFTs

    现在我们已经铸造了一个NFT,接下来我们要探索如何铸造一系列的NFT。我们将使用Candy Machine来完成这项任务,这是一款Solana程序,能让创作者方便地将他们的资产上链。虽然这不是创建系列的唯一方式,但在Solana上它已经成为标准,因为它具有诸如防机器人保护和安全随机化等有用的功能。你懂的,模板时间到了。然而,随着我们构建的项目越来越复杂,我们的模板也会变得更先进。这次我们将基于Solana dApp脚手架构建一个模板。与之前的模板一样,它是一个由create-next-app创建的Next.js应用程序。不过这次,它具有更多功能。不用担心!我们依然会使用相同的工具。

    🖼 从糖果机展示NFTs

    现在我们已经铸造了一个NFT,接下来我们将学习如何铸造一系列的NFT。为此,我们将利用Candy Machine来实现——这是Solana的一个程序,使创作者能够将他们的资产上链。虽然这并非创建系列的唯一方式,但在Solana上它却成了标准,因为它具备了许多有用的功能,例如机器人保护和安全随机化。你是否感受到过看到闪亮的新iPhone时的那股兴奋感?稀有的NFT有点儿类似于此。对于优秀的艺术家而言,仅仅是观看这些NFT也极富乐趣。毕竟,艺术的本质就是用来欣赏的!接下来,我们将探讨如果我们只有Candy Machine的地址,应该如何展示NFTs。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/error-handle-and-data-validation/index.html b/Solana-Co-Learn/tags/error-handle-and-data-validation/index.html index 21560e9e5..954a2c4dc 100644 --- a/Solana-Co-Learn/tags/error-handle-and-data-validation/index.html +++ b/Solana-Co-Learn/tags/error-handle-and-data-validation/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    One doc tagged with "error-handle-and-data-validation"

    View All Tags

    ❗ 错误处理和数据验证

    本节课将为你介绍一些程序安全方面的基本注意事项。虽然这并非全面的概述,但它能让你像攻击者那样思考,思索重要的问题:我如何破解这个程序?

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/frontend/index.html b/Solana-Co-Learn/tags/frontend/index.html index 11c3ec7e7..6bf8cc514 100644 --- a/Solana-Co-Learn/tags/frontend/index.html +++ b/Solana-Co-Learn/tags/frontend/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    2 docs tagged with "frontend"

    View All Tags

    🎨 创建铸币用户界面

    现在我们成功创建了代币和非同质化代币(NFT),让我们继续着手构建我们的铸币用户界面。这样一来,我们就能直观地与智能合约互动,并允许他人在我们的浏览器上铸造我们的NFT。是不是非常酷?你可能已经注意到,你的网站上现有一个名为 minting 的按钮,但它目前尚未实现任何功能。让我们从定义一个函数开始,然后添加逻辑来允许我们铸造NFT。如果你没有起始代码,可以在这里克隆。

    💻 构建 NFT 铸造者前端

    欢迎来到第一周的挑战环节。每周,你都会有一个特定的部分,用来将你所学的内容应用到自定义的NFT质押应用程序上,并且还有战利品箱子等你拿!

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/how-staking-works/index.html b/Solana-Co-Learn/tags/how-staking-works/index.html index 69d6ff8ec..e2dbb0385 100644 --- a/Solana-Co-Learn/tags/how-staking-works/index.html +++ b/Solana-Co-Learn/tags/how-staking-works/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    One doc tagged with "how-staking-works"

    View All Tags

    🕒 质押工作机制详解

    恭喜你已经接近第三周的完成了!现在让我们将你学到的所有知识运用到你正在进行的NFT项目(buildoors项目)的相关质押计划中。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/index.html b/Solana-Co-Learn/tags/index.html index b751b6ba9..46573c033 100644 --- a/Solana-Co-Learn/tags/index.html +++ b/Solana-Co-Learn/tags/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content
    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/intro-rust/index.html b/Solana-Co-Learn/tags/intro-rust/index.html index ed712bf85..21360b604 100644 --- a/Solana-Co-Learn/tags/intro-rust/index.html +++ b/Solana-Co-Learn/tags/intro-rust/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    3 docs tagged with "intro-rust"

    View All Tags

    ✨ 魔法互联网计算机

    我们将在游乐场中编写一个简单的Hello World程序。它仅会在交易日志中记录一条消息。至今为止,我们已经完成了许多酷炫的项目,包括建立各种类型的客户端,创建NFT收藏品,铸造SPL代币,甚至构建用户界面让其他人与之互动。然而,我们迄今为止所做的一切都是基于现有的程序。

    👋 与你部署的程序互动

    我们将在Solana的游乐场上创建一个简单的Hello World程序。它只会在交易日志中记录一条消息。现在我们的程序已经部署完成了,是时候与之互动了。别忘了,在之前的阶段,你已经多次实现过这个过程!你可以像之前一样通过create-solana-client设置本地客户端,或者直接使用Solana的游乐场。

    📝 你好,世界

    我们将在游乐场上制作一个简单的Hello World程序,这个程序只会在交易日志中记录一条消息,相当有趣。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/local-development/index.html b/Solana-Co-Learn/tags/local-development/index.html index 33ceee029..5aa81e654 100644 --- a/Solana-Co-Learn/tags/local-development/index.html +++ b/Solana-Co-Learn/tags/local-development/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    One doc tagged with "local-development"

    View All Tags
    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/metaplex/index.html b/Solana-Co-Learn/tags/metaplex/index.html index ef2a5e11e..882f1822d 100644 --- a/Solana-Co-Learn/tags/metaplex/index.html +++ b/Solana-Co-Learn/tags/metaplex/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    7 docs tagged with "metaplex"

    View All Tags

    🍬 创建糖果机

    现在我们已经铸造了一个NFT,接下来我们要学习如何铸造一系列NFT。我们将使用Candy Machine来完成这个任务。Candy Machine是一个Solana程序,它可以让创作者将他们的艺术品和资产上链。虽然还有其他方式可以创建NFT系列,但Candy Machine在Solana上已成为一项标准,因为它具备许多实用功能,如防机器人保护和安全随机化。准备好添加一些内容到我们上一课创建但未使用的文件夹中了吗?

    🍭 糖果机和Sugar CLI

    将自己的脸做成NFT有何不好呢?你可以永久地将自己视为一个早期的建设者,并告诉你的妈妈你已经进入了区块链世界。既然我们已经铸造了一个单独的NFT,现在我们将学习如何铸造一系列的NFT。为了实现这一目标,我们将使用Candy Machine——这是一个Solana程序,让创作者能够将他们的资产上链。尽管这不是创建系列的唯一方法,但在Solana上它已经成为一种标准,因为它拥有一些有用的功能,如防机器人保护和安全随机化。

    🎨 创建奖励代币

    既然我们已经铸造了一个NFT,那么接下来我们要探讨如何铸造一系列的NFT。为了达成这个目标,我们将采用Candy Machine——一款在Solana上广泛使用的程序,允许创作者将其资产上链。Candy Machine在Solana上受到欢迎的原因在于,它具备了如机器人防护和安全随机化等实用功能。现在我们可以回归到我们自定义的NFT质押应用上来,借助我们在代币程序和Candy Machine上的经验来构建这个应用。

    🎨 创建铸币用户界面

    现在我们成功创建了代币和非同质化代币(NFT),让我们继续着手构建我们的铸币用户界面。这样一来,我们就能直观地与智能合约互动,并允许他人在我们的浏览器上铸造我们的NFT。是不是非常酷?你可能已经注意到,你的网站上现有一个名为 minting 的按钮,但它目前尚未实现任何功能。让我们从定义一个函数开始,然后添加逻辑来允许我们铸造NFT。如果你没有起始代码,可以在这里克隆。

    💃 展示NFTs

    既然我们已经铸造了一个NFT,现在我们将进一步探讨如何铸造一整套NFT。我们将借助Candy Machine来实现这个任务——这是一个Solana程序,可以让创作者轻松地将他们的资产上链。当然,这并不是在Solana上创建系列的唯一方法,但它确实成为了标准,因为它具备了许多实用功能,例如防机器人保护和安全随机化。毕竟,如果你不能向人们展示你的NFT,那它还有什么价值呢!在这一节,我们将引导你展示你的作品——首先在钱包中展示,然后在Candy Machine中展示。

    📱 在钱包中展示NFTs

    现在我们已经铸造了一个NFT,接下来我们要探索如何铸造一系列的NFT。我们将使用Candy Machine来完成这项任务,这是一款Solana程序,能让创作者方便地将他们的资产上链。虽然这不是创建系列的唯一方式,但在Solana上它已经成为标准,因为它具有诸如防机器人保护和安全随机化等有用的功能。你懂的,模板时间到了。然而,随着我们构建的项目越来越复杂,我们的模板也会变得更先进。这次我们将基于Solana dApp脚手架构建一个模板。与之前的模板一样,它是一个由create-next-app创建的Next.js应用程序。不过这次,它具有更多功能。不用担心!我们依然会使用相同的工具。

    🖼 从糖果机展示NFTs

    现在我们已经铸造了一个NFT,接下来我们将学习如何铸造一系列的NFT。为此,我们将利用Candy Machine来实现——这是Solana的一个程序,使创作者能够将他们的资产上链。虽然这并非创建系列的唯一方式,但在Solana上它却成了标准,因为它具备了许多有用的功能,例如机器人保护和安全随机化。你是否感受到过看到闪亮的新iPhone时的那股兴奋感?稀有的NFT有点儿类似于此。对于优秀的艺术家而言,仅仅是观看这些NFT也极富乐趣。毕竟,艺术的本质就是用来欣赏的!接下来,我们将探讨如果我们只有Candy Machine的地址,应该如何展示NFTs。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/mint-spl-token/index.html b/Solana-Co-Learn/tags/mint-spl-token/index.html index e877f2c01..8859038e0 100644 --- a/Solana-Co-Learn/tags/mint-spl-token/index.html +++ b/Solana-Co-Learn/tags/mint-spl-token/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    One doc tagged with "mint-spl-token"

    View All Tags

    🏧 在Solana上铸造代币

    话不多说,让我们来创造一些神奇的互联网货币吧。在我们的最终项目中,我们将创建一个代币,你将随着抵押你的社区NFT而逐渐获得它。在那之前,让我们先玩一下这个铸币过程的实际构建部分。现在是激发你的想象力,尽情享受的好时机。也许你一直想创建自己的模因币——现在是你的机会了 🚀。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/native-program-hello/index.html b/Solana-Co-Learn/tags/native-program-hello/index.html index bd31bd96f..52307a2e4 100644 --- a/Solana-Co-Learn/tags/native-program-hello/index.html +++ b/Solana-Co-Learn/tags/native-program-hello/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    One doc tagged with "native-program-hello"

    View All Tags
    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/native-solana-development/index.html b/Solana-Co-Learn/tags/native-solana-development/index.html index 4e82b19f4..bdb7927b8 100644 --- a/Solana-Co-Learn/tags/native-solana-development/index.html +++ b/Solana-Co-Learn/tags/native-solana-development/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    3 docs tagged with "native-solana-development"

    View All Tags

    🎂 Rust的分层蛋糕

    我们将在游乐场上制作一个简单的Hello World程序,仅仅会在交易日志中记录一条消息。招呼已经打过了。现在是时候学习如何处理指令数据,就像在客户端开发中一样。

    🎥 构建一个电影评论程序

    还记得我们在第一节中互动开发的电影评论节目吗?现在我们要继续深入开发它。当然,你可以随意评论任何内容,不仅限于电影,毕竟我并不是你的长辈,你自由发挥就好。

    🤠 状态管理

    你还记得我们在第一节中互动的电影评论程序吗?现在我们要在这里构建它。你想评论的不一定只是电影,我可不会限制你。状态是指存储在链上的程序数据。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/native-solana-program/index.html b/Solana-Co-Learn/tags/native-solana-program/index.html index 4ec22558a..180d147a5 100644 --- a/Solana-Co-Learn/tags/native-solana-program/index.html +++ b/Solana-Co-Learn/tags/native-solana-program/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    7 docs tagged with "native-solana-program"

    View All Tags

    ❗ 错误处理和数据验证

    本节课将为你介绍一些程序安全方面的基本注意事项。虽然这并非全面的概述,但它能让你像攻击者那样思考,思索重要的问题:我如何破解这个程序?

    🎂 Rust的分层蛋糕

    我们将在游乐场上制作一个简单的Hello World程序,仅仅会在交易日志中记录一条消息。招呼已经打过了。现在是时候学习如何处理指令数据,就像在客户端开发中一样。

    🎥 构建一个电影评论程序

    还记得我们在第一节中互动开发的电影评论节目吗?现在我们要继续深入开发它。当然,你可以随意评论任何内容,不仅限于电影,毕竟我并不是你的长辈,你自由发挥就好。

    🔑 保障我们程序的安全

    是时候保障我们的Solana电影数据库程序不受到干扰了。我们将加入一些基础的安全防护,进行输入验证,并增添一个 updatemoviereview 指令。

    🕒 质押工作机制详解

    恭喜你已经接近第三周的完成了!现在让我们将你学到的所有知识运用到你正在进行的NFT项目(buildoors项目)的相关质押计划中。

    🤠 状态管理

    你还记得我们在第一节中互动的电影评论程序吗?现在我们要在这里构建它。你想评论的不一定只是电影,我可不会限制你。状态是指存储在链上的程序数据。

    🛠️ 构建NFT质押程序

    今天,我们将编写质押程序,并实现所有必要的质押功能,暂时不涉及任何代币转账。我将陪伴你,一步一步讲解整个过程,解释每个环节,以便你了解正在进行的操作。首先,让我们进入Solana Playground,点击create a new project,并创建一个名为src的新文件夹,其中包括一个名为lib.rs的文件。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/nft-staking/index.html b/Solana-Co-Learn/tags/nft-staking/index.html index 8f2b6aa52..9c37fb741 100644 --- a/Solana-Co-Learn/tags/nft-staking/index.html +++ b/Solana-Co-Learn/tags/nft-staking/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    2 docs tagged with "nft-staking"

    View All Tags

    🕒 质押工作机制详解

    恭喜你已经接近第三周的完成了!现在让我们将你学到的所有知识运用到你正在进行的NFT项目(buildoors项目)的相关质押计划中。

    🛠️ 构建NFT质押程序

    今天,我们将编写质押程序,并实现所有必要的质押功能,暂时不涉及任何代币转账。我将陪伴你,一步一步讲解整个过程,解释每个环节,以便你了解正在进行的操作。首先,让我们进入Solana Playground,点击create a new project,并创建一个名为src的新文件夹,其中包括一个名为lib.rs的文件。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/nft/index.html b/Solana-Co-Learn/tags/nft/index.html index 12b36e238..c28bab092 100644 --- a/Solana-Co-Learn/tags/nft/index.html +++ b/Solana-Co-Learn/tags/nft/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    9 docs tagged with "nft"

    View All Tags

    🍬 创建糖果机

    现在我们已经铸造了一个NFT,接下来我们要学习如何铸造一系列NFT。我们将使用Candy Machine来完成这个任务。Candy Machine是一个Solana程序,它可以让创作者将他们的艺术品和资产上链。虽然还有其他方式可以创建NFT系列,但Candy Machine在Solana上已成为一项标准,因为它具备许多实用功能,如防机器人保护和安全随机化。准备好添加一些内容到我们上一课创建但未使用的文件夹中了吗?

    🍭 糖果机和Sugar CLI

    将自己的脸做成NFT有何不好呢?你可以永久地将自己视为一个早期的建设者,并告诉你的妈妈你已经进入了区块链世界。既然我们已经铸造了一个单独的NFT,现在我们将学习如何铸造一系列的NFT。为了实现这一目标,我们将使用Candy Machine——这是一个Solana程序,让创作者能够将他们的资产上链。尽管这不是创建系列的唯一方法,但在Solana上它已经成为一种标准,因为它拥有一些有用的功能,如防机器人保护和安全随机化。

    🎨 Solana上的NFT

    我们来了,不过花了不了多长时间。猴子画像、猩猩、岩石,以及其他一些看起来丑陋却能卖到10万美元的动物主题头像。这就是NFT。

    🎨 创建奖励代币

    既然我们已经铸造了一个NFT,那么接下来我们要探讨如何铸造一系列的NFT。为了达成这个目标,我们将采用Candy Machine——一款在Solana上广泛使用的程序,允许创作者将其资产上链。Candy Machine在Solana上受到欢迎的原因在于,它具备了如机器人防护和安全随机化等实用功能。现在我们可以回归到我们自定义的NFT质押应用上来,借助我们在代币程序和Candy Machine上的经验来构建这个应用。

    🎨 创建铸币用户界面

    现在我们成功创建了代币和非同质化代币(NFT),让我们继续着手构建我们的铸币用户界面。这样一来,我们就能直观地与智能合约互动,并允许他人在我们的浏览器上铸造我们的NFT。是不是非常酷?你可能已经注意到,你的网站上现有一个名为 minting 的按钮,但它目前尚未实现任何功能。让我们从定义一个函数开始,然后添加逻辑来允许我们铸造NFT。如果你没有起始代码,可以在这里克隆。

    💃 展示NFTs

    既然我们已经铸造了一个NFT,现在我们将进一步探讨如何铸造一整套NFT。我们将借助Candy Machine来实现这个任务——这是一个Solana程序,可以让创作者轻松地将他们的资产上链。当然,这并不是在Solana上创建系列的唯一方法,但它确实成为了标准,因为它具备了许多实用功能,例如防机器人保护和安全随机化。毕竟,如果你不能向人们展示你的NFT,那它还有什么价值呢!在这一节,我们将引导你展示你的作品——首先在钱包中展示,然后在Candy Machine中展示。

    📱 在钱包中展示NFTs

    现在我们已经铸造了一个NFT,接下来我们要探索如何铸造一系列的NFT。我们将使用Candy Machine来完成这项任务,这是一款Solana程序,能让创作者方便地将他们的资产上链。虽然这不是创建系列的唯一方式,但在Solana上它已经成为标准,因为它具有诸如防机器人保护和安全随机化等有用的功能。你懂的,模板时间到了。然而,随着我们构建的项目越来越复杂,我们的模板也会变得更先进。这次我们将基于Solana dApp脚手架构建一个模板。与之前的模板一样,它是一个由create-next-app创建的Next.js应用程序。不过这次,它具有更多功能。不用担心!我们依然会使用相同的工具。

    🖼 从糖果机展示NFTs

    现在我们已经铸造了一个NFT,接下来我们将学习如何铸造一系列的NFT。为此,我们将利用Candy Machine来实现——这是Solana的一个程序,使创作者能够将他们的资产上链。虽然这并非创建系列的唯一方式,但在Solana上它却成了标准,因为它具备了许多有用的功能,例如机器人保护和安全随机化。你是否感受到过看到闪亮的新iPhone时的那股兴奋感?稀有的NFT有点儿类似于此。对于优秀的艺术家而言,仅仅是观看这些NFT也极富乐趣。毕竟,艺术的本质就是用来欣赏的!接下来,我们将探讨如果我们只有Candy Machine的地址,应该如何展示NFTs。

    🤨 NFT你的脸

    有什么比将你的脸做成NFT更有趣的选择呢?你可以将自己永远铭记为早期的开拓者,并骄傲地告诉你的妈妈你已经成为了区块链的一部分。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/nfts-and-minting-with-metaplex/index.html b/Solana-Co-Learn/tags/nfts-and-minting-with-metaplex/index.html index 30a5ea015..2c8271e89 100644 --- a/Solana-Co-Learn/tags/nfts-and-minting-with-metaplex/index.html +++ b/Solana-Co-Learn/tags/nfts-and-minting-with-metaplex/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    3 docs tagged with "nfts-and-minting-with-metaplex"

    View All Tags

    🍭 糖果机和Sugar CLI

    将自己的脸做成NFT有何不好呢?你可以永久地将自己视为一个早期的建设者,并告诉你的妈妈你已经进入了区块链世界。既然我们已经铸造了一个单独的NFT,现在我们将学习如何铸造一系列的NFT。为了实现这一目标,我们将使用Candy Machine——这是一个Solana程序,让创作者能够将他们的资产上链。尽管这不是创建系列的唯一方法,但在Solana上它已经成为一种标准,因为它拥有一些有用的功能,如防机器人保护和安全随机化。

    🎨 Solana上的NFT

    我们来了,不过花了不了多长时间。猴子画像、猩猩、岩石,以及其他一些看起来丑陋却能卖到10万美元的动物主题头像。这就是NFT。

    🤨 NFT你的脸

    有什么比将你的脸做成NFT更有趣的选择呢?你可以将自己永远铭记为早期的开拓者,并骄傲地告诉你的妈妈你已经成为了区块链的一部分。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/program/index.html b/Solana-Co-Learn/tags/program/index.html index 98eda51fd..3f902e92f 100644 --- a/Solana-Co-Learn/tags/program/index.html +++ b/Solana-Co-Learn/tags/program/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    9 docs tagged with "program"

    View All Tags

    🎂 Rust的分层蛋糕

    我们将在游乐场上制作一个简单的Hello World程序,仅仅会在交易日志中记录一条消息。招呼已经打过了。现在是时候学习如何处理指令数据,就像在客户端开发中一样。

    🎥 构建一个电影评论程序

    还记得我们在第一节中互动开发的电影评论节目吗?现在我们要继续深入开发它。当然,你可以随意评论任何内容,不仅限于电影,毕竟我并不是你的长辈,你自由发挥就好。

    👋 与你部署的程序互动

    我们将在Solana的游乐场上创建一个简单的Hello World程序。它只会在交易日志中记录一条消息。现在我们的程序已经部署完成了,是时候与之互动了。别忘了,在之前的阶段,你已经多次实现过这个过程!你可以像之前一样通过create-solana-client设置本地客户端,或者直接使用Solana的游乐场。

    🤠 状态管理

    你还记得我们在第一节中互动的电影评论程序吗?现在我们要在这里构建它。你想评论的不一定只是电影,我可不会限制你。状态是指存储在链上的程序数据。

    🛠️ 构建NFT质押程序

    今天,我们将编写质押程序,并实现所有必要的质押功能,暂时不涉及任何代币转账。我将陪伴你,一步一步讲解整个过程,解释每个环节,以便你了解正在进行的操作。首先,让我们进入Solana Playground,点击create a new project,并创建一个名为src的新文件夹,其中包括一个名为lib.rs的文件。

    Solang solidity合约实现 - hello, World

    欢迎来到Solana入门指南!Solang是一个Solidity编译器,它允许你使用Solidity编程语言编写Solana程序,其他区块链中称为“智能合约”。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/rpc/index.html b/Solana-Co-Learn/tags/rpc/index.html index 6ab27e91e..5a8fbbb49 100644 --- a/Solana-Co-Learn/tags/rpc/index.html +++ b/Solana-Co-Learn/tags/rpc/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    6 docs tagged with "rpc"

    View All Tags

    ✍将数据写入区块链

    我们已经熟练掌握了区块链的阅读操作,现在开始学习如何将数据写入Solana区块链。

    🎥 构建一个电影评论应用

    现在我们已经完成了钱包连接的设置,是时候让我们的ping按钮发挥作用了!我们将整合所有元素,构建一个基于区块链的电影评论应用——它将允许任何人提交对他们最喜欢的电影的评论,有点像烂番茄网站那样。

    📡 Run is back - 反序列化

    现在我们已经完成了钱包连接的设置,是时候让我们的 ping 按钮真正起作用了!向网络账户写入数据只是任务的一半,另一半则是读取这些数据。在第一部分,我们借助Web3.js库中的内置函数来读取内容,这只适用于基础数据,如余额和交易详情。但正如我们在上一部分所见,所有精彩的东西都藏在 PDAs 里。

    🤔 自定义指令

    既然我们已经完成了钱包连接的设置,那么让我们使我们的ping按钮真正有所作为吧!你现在知道如何读取数据并通过简单的交易将其写入网络。几乎立刻,你可能会发现自己想要通过交易发送数据。那么让我们了解一下如何向Solana区块链讲述你的故事。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/rust-layer-cake/index.html b/Solana-Co-Learn/tags/rust-layer-cake/index.html index 1649b4a2a..6818d79c0 100644 --- a/Solana-Co-Learn/tags/rust-layer-cake/index.html +++ b/Solana-Co-Learn/tags/rust-layer-cake/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    One doc tagged with "rust-layer-cake"

    View All Tags

    🎂 Rust的分层蛋糕

    我们将在游乐场上制作一个简单的Hello World程序,仅仅会在交易日志中记录一条消息。招呼已经打过了。现在是时候学习如何处理指令数据,就像在客户端开发中一样。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/security-and-validation/index.html b/Solana-Co-Learn/tags/security-and-validation/index.html index 9220d7532..52b24f4ae 100644 --- a/Solana-Co-Learn/tags/security-and-validation/index.html +++ b/Solana-Co-Learn/tags/security-and-validation/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    2 docs tagged with "security-and-validation"

    View All Tags

    ❗ 错误处理和数据验证

    本节课将为你介绍一些程序安全方面的基本注意事项。虽然这并非全面的概述,但它能让你像攻击者那样思考,思索重要的问题:我如何破解这个程序?

    🔑 保障我们程序的安全

    是时候保障我们的Solana电影数据库程序不受到干扰了。我们将加入一些基础的安全防护,进行输入验证,并增添一个 updatemoviereview 指令。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/solana/index.html b/Solana-Co-Learn/tags/solana/index.html index 5c746d4d9..34e4132fd 100644 --- a/Solana-Co-Learn/tags/solana/index.html +++ b/Solana-Co-Learn/tags/solana/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    38 docs tagged with "solana"

    View All Tags

    ✍将数据写入区块链

    我们已经熟练掌握了区块链的阅读操作,现在开始学习如何将数据写入Solana区块链。

    ✨ 魔法互联网计算机

    我们将在游乐场中编写一个简单的Hello World程序。它仅会在交易日志中记录一条消息。至今为止,我们已经完成了许多酷炫的项目,包括建立各种类型的客户端,创建NFT收藏品,铸造SPL代币,甚至构建用户界面让其他人与之互动。然而,我们迄今为止所做的一切都是基于现有的程序。

    ❗ 错误处理和数据验证

    本节课将为你介绍一些程序安全方面的基本注意事项。虽然这并非全面的概述,但它能让你像攻击者那样思考,思索重要的问题:我如何破解这个程序?

    🌐 部署到 Vercel

    这一步是你本周工作中至关重要的一环,即将你的项目从本地环境部署到线上环境。

    🍬 创建糖果机

    现在我们已经铸造了一个NFT,接下来我们要学习如何铸造一系列NFT。我们将使用Candy Machine来完成这个任务。Candy Machine是一个Solana程序,它可以让创作者将他们的艺术品和资产上链。虽然还有其他方式可以创建NFT系列,但Candy Machine在Solana上已成为一项标准,因为它具备许多实用功能,如防机器人保护和安全随机化。准备好添加一些内容到我们上一课创建但未使用的文件夹中了吗?

    🍭 糖果机和Sugar CLI

    将自己的脸做成NFT有何不好呢?你可以永久地将自己视为一个早期的建设者,并告诉你的妈妈你已经进入了区块链世界。既然我们已经铸造了一个单独的NFT,现在我们将学习如何铸造一系列的NFT。为了实现这一目标,我们将使用Candy Machine——这是一个Solana程序,让创作者能够将他们的资产上链。尽管这不是创建系列的唯一方法,但在Solana上它已经成为一种标准,因为它拥有一些有用的功能,如防机器人保护和安全随机化。

    🎂 Rust的分层蛋糕

    我们将在游乐场上制作一个简单的Hello World程序,仅仅会在交易日志中记录一条消息。招呼已经打过了。现在是时候学习如何处理指令数据,就像在客户端开发中一样。

    🎥 构建一个电影评论应用

    现在我们已经完成了钱包连接的设置,是时候让我们的ping按钮发挥作用了!我们将整合所有元素,构建一个基于区块链的电影评论应用——它将允许任何人提交对他们最喜欢的电影的评论,有点像烂番茄网站那样。

    🎥 构建一个电影评论程序

    还记得我们在第一节中互动开发的电影评论节目吗?现在我们要继续深入开发它。当然,你可以随意评论任何内容,不仅限于电影,毕竟我并不是你的长辈,你自由发挥就好。

    🎨 Solana上的NFT

    我们来了,不过花了不了多长时间。猴子画像、猩猩、岩石,以及其他一些看起来丑陋却能卖到10万美元的动物主题头像。这就是NFT。

    🎨 创建奖励代币

    既然我们已经铸造了一个NFT,那么接下来我们要探讨如何铸造一系列的NFT。为了达成这个目标,我们将采用Candy Machine——一款在Solana上广泛使用的程序,允许创作者将其资产上链。Candy Machine在Solana上受到欢迎的原因在于,它具备了如机器人防护和安全随机化等实用功能。现在我们可以回归到我们自定义的NFT质押应用上来,借助我们在代币程序和Candy Machine上的经验来构建这个应用。

    🎨 创建铸币用户界面

    现在我们成功创建了代币和非同质化代币(NFT),让我们继续着手构建我们的铸币用户界面。这样一来,我们就能直观地与智能合约互动,并允许他人在我们的浏览器上铸造我们的NFT。是不是非常酷?你可能已经注意到,你的网站上现有一个名为 minting 的按钮,但它目前尚未实现任何功能。让我们从定义一个函数开始,然后添加逻辑来允许我们铸造NFT。如果你没有起始代码,可以在这里克隆。

    🏧 在Solana上铸造代币

    话不多说,让我们来创造一些神奇的互联网货币吧。在我们的最终项目中,我们将创建一个代币,你将随着抵押你的社区NFT而逐渐获得它。在那之前,让我们先玩一下这个铸币过程的实际构建部分。现在是激发你的想象力,尽情享受的好时机。也许你一直想创建自己的模因币——现在是你的机会了 🚀。

    👋 与你部署的程序互动

    我们将在Solana的游乐场上创建一个简单的Hello World程序。它只会在交易日志中记录一条消息。现在我们的程序已经部署完成了,是时候与之互动了。别忘了,在之前的阶段,你已经多次实现过这个过程!你可以像之前一样通过create-solana-client设置本地客户端,或者直接使用Solana的游乐场。

    💃 展示NFTs

    既然我们已经铸造了一个NFT,现在我们将进一步探讨如何铸造一整套NFT。我们将借助Candy Machine来实现这个任务——这是一个Solana程序,可以让创作者轻松地将他们的资产上链。当然,这并不是在Solana上创建系列的唯一方法,但它确实成为了标准,因为它具备了许多实用功能,例如防机器人保护和安全随机化。毕竟,如果你不能向人们展示你的NFT,那它还有什么价值呢!在这一节,我们将引导你展示你的作品——首先在钱包中展示,然后在Candy Machine中展示。

    💵 Token Program

    作为区块链最基本的承诺,这些代币也许是你安装钱包的主要原因,它们是区块链上资产最纯粹的表现形式,从合成股票到数百种狗币。

    💻 构建 NFT 铸造者前端

    欢迎来到第一周的挑战环节。每周,你都会有一个特定的部分,用来将你所学的内容应用到自定义的NFT质押应用程序上,并且还有战利品箱子等你拿!

    📝 你好,世界

    我们将在游乐场上制作一个简单的Hello World程序,这个程序只会在交易日志中记录一条消息,相当有趣。

    📡 Run is back - 反序列化

    现在我们已经完成了钱包连接的设置,是时候让我们的 ping 按钮真正起作用了!向网络账户写入数据只是任务的一半,另一半则是读取这些数据。在第一部分,我们借助Web3.js库中的内置函数来读取内容,这只适用于基础数据,如余额和交易详情。但正如我们在上一部分所见,所有精彩的东西都藏在 PDAs 里。

    📱 在钱包中展示NFTs

    现在我们已经铸造了一个NFT,接下来我们要探索如何铸造一系列的NFT。我们将使用Candy Machine来完成这项任务,这是一款Solana程序,能让创作者方便地将他们的资产上链。虽然这不是创建系列的唯一方式,但在Solana上它已经成为标准,因为它具有诸如防机器人保护和安全随机化等有用的功能。你懂的,模板时间到了。然而,随着我们构建的项目越来越复杂,我们的模板也会变得更先进。这次我们将基于Solana dApp脚手架构建一个模板。与之前的模板一样,它是一个由create-next-app创建的Next.js应用程序。不过这次,它具有更多功能。不用担心!我们依然会使用相同的工具。

    🔌 连接到钱包

    现在我们已经知道如何使用代码与网络交互,通过直接使用私钥来初始化账户。显然,在正常的去中心化应用(dapp)中,这样做是不可行的(永远不要将你的私钥暴露给任何人或任何dapp)。

    🔑 保障我们程序的安全

    是时候保障我们的Solana电影数据库程序不受到干扰了。我们将加入一些基础的安全防护,进行输入验证,并增添一个 updatemoviereview 指令。

    🕒 质押工作机制详解

    恭喜你已经接近第三周的完成了!现在让我们将你学到的所有知识运用到你正在进行的NFT项目(buildoors项目)的相关质押计划中。

    🖼 从糖果机展示NFTs

    现在我们已经铸造了一个NFT,接下来我们将学习如何铸造一系列的NFT。为此,我们将利用Candy Machine来实现——这是Solana的一个程序,使创作者能够将他们的资产上链。虽然这并非创建系列的唯一方式,但在Solana上它却成了标准,因为它具备了许多有用的功能,例如机器人保护和安全随机化。你是否感受到过看到闪亮的新iPhone时的那股兴奋感?稀有的NFT有点儿类似于此。对于优秀的艺术家而言,仅仅是观看这些NFT也极富乐趣。毕竟,艺术的本质就是用来欣赏的!接下来,我们将探讨如果我们只有Candy Machine的地址,应该如何展示NFTs。

    🤔 自定义指令

    既然我们已经完成了钱包连接的设置,那么让我们使我们的ping按钮真正有所作为吧!你现在知道如何读取数据并通过简单的交易将其写入网络。几乎立刻,你可能会发现自己想要通过交易发送数据。那么让我们了解一下如何向Solana区块链讲述你的故事。

    🤠 状态管理

    你还记得我们在第一节中互动的电影评论程序吗?现在我们要在这里构建它。你想评论的不一定只是电影,我可不会限制你。状态是指存储在链上的程序数据。

    🤨 NFT你的脸

    有什么比将你的脸做成NFT更有趣的选择呢?你可以将自己永远铭记为早期的开拓者,并骄傲地告诉你的妈妈你已经成为了区块链的一部分。

    🦺 与程序进行交互

    在成功设置了钱包连接后,我们可以让ping按钮真正执行操作了。以下是如何实现的详细说明。

    🧬 为你的代币赋予身份

    现在是时候让代币与它们的创造者(也就是你)相遇了。我们将在之前构建的基础上继续前进。如果需要,你可以从这个链接获取起始代码(确保你处于 solution-without-burn 分支)。

    🧮 令牌元数据

    Token元数据指的是代币的基本信息,例如名称、符号和标志。注意你钱包中的各种代币都拥有这些特性,除了你自己创建的代币。

    🛠️ 构建NFT质押程序

    今天,我们将编写质押程序,并实现所有必要的质押功能,暂时不涉及任何代币转账。我将陪伴你,一步一步讲解整个过程,解释每个环节,以便你了解正在进行的操作。首先,让我们进入Solana Playground,点击create a new project,并创建一个名为src的新文件夹,其中包括一个名为lib.rs的文件。

    Solana钱包使用 - Backpack 🎒

    Solana的钱包种类繁多,如众所周知的Phantom钱包。然而,在此我并不推荐使用Phantom,因为对于开发者来说,它并不够友好。

    Solang solidity合约实现 - hello, World

    欢迎来到Solana入门指南!Solang是一个Solidity编译器,它允许你使用Solidity编程语言编写Solana程序,其他区块链中称为“智能合约”。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/solang-program-hello/index.html b/Solana-Co-Learn/tags/solang-program-hello/index.html index 8f9db63c2..89a120c5a 100644 --- a/Solana-Co-Learn/tags/solang-program-hello/index.html +++ b/Solana-Co-Learn/tags/solang-program-hello/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    One doc tagged with "solang-program-hello"

    View All Tags

    Solang solidity合约实现 - hello, World

    欢迎来到Solana入门指南!Solang是一个Solidity编译器,它允许你使用Solidity编程语言编写Solana程序,其他区块链中称为“智能合约”。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/spl-token/index.html b/Solana-Co-Learn/tags/spl-token/index.html index 7cecade38..ef9d62c36 100644 --- a/Solana-Co-Learn/tags/spl-token/index.html +++ b/Solana-Co-Learn/tags/spl-token/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    5 docs tagged with "spl-token"

    View All Tags

    🎨 创建奖励代币

    既然我们已经铸造了一个NFT,那么接下来我们要探讨如何铸造一系列的NFT。为了达成这个目标,我们将采用Candy Machine——一款在Solana上广泛使用的程序,允许创作者将其资产上链。Candy Machine在Solana上受到欢迎的原因在于,它具备了如机器人防护和安全随机化等实用功能。现在我们可以回归到我们自定义的NFT质押应用上来,借助我们在代币程序和Candy Machine上的经验来构建这个应用。

    🏧 在Solana上铸造代币

    话不多说,让我们来创造一些神奇的互联网货币吧。在我们的最终项目中,我们将创建一个代币,你将随着抵押你的社区NFT而逐渐获得它。在那之前,让我们先玩一下这个铸币过程的实际构建部分。现在是激发你的想象力,尽情享受的好时机。也许你一直想创建自己的模因币——现在是你的机会了 🚀。

    💵 Token Program

    作为区块链最基本的承诺,这些代币也许是你安装钱包的主要原因,它们是区块链上资产最纯粹的表现形式,从合成股票到数百种狗币。

    🧬 为你的代币赋予身份

    现在是时候让代币与它们的创造者(也就是你)相遇了。我们将在之前构建的基础上继续前进。如果需要,你可以从这个链接获取起始代码(确保你处于 solution-without-burn 分支)。

    🧮 令牌元数据

    Token元数据指的是代币的基本信息,例如名称、符号和标志。注意你钱包中的各种代币都拥有这些特性,除了你自己创建的代币。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/srcure-our-program/index.html b/Solana-Co-Learn/tags/srcure-our-program/index.html index 0ac61c55c..351128908 100644 --- a/Solana-Co-Learn/tags/srcure-our-program/index.html +++ b/Solana-Co-Learn/tags/srcure-our-program/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    One doc tagged with "srcure-our-program"

    View All Tags

    🔑 保障我们程序的安全

    是时候保障我们的Solana电影数据库程序不受到干扰了。我们将加入一些基础的安全防护,进行输入验证,并增添一个 updatemoviereview 指令。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/start-your-own-custom-project/index.html b/Solana-Co-Learn/tags/start-your-own-custom-project/index.html index 11dac2604..3183639b8 100644 --- a/Solana-Co-Learn/tags/start-your-own-custom-project/index.html +++ b/Solana-Co-Learn/tags/start-your-own-custom-project/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    2 docs tagged with "start-your-own-custom-project"

    View All Tags

    🌐 部署到 Vercel

    这一步是你本周工作中至关重要的一环,即将你的项目从本地环境部署到线上环境。

    💻 构建 NFT 铸造者前端

    欢迎来到第一周的挑战环节。每周,你都会有一个特定的部分,用来将你所学的内容应用到自定义的NFT质押应用程序上,并且还有战利品箱子等你拿!

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/state-management/index.html b/Solana-Co-Learn/tags/state-management/index.html index ba23cd6c7..ffc7eeb76 100644 --- a/Solana-Co-Learn/tags/state-management/index.html +++ b/Solana-Co-Learn/tags/state-management/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    One doc tagged with "state-management"

    View All Tags

    🤠 状态管理

    你还记得我们在第一节中互动的电影评论程序吗?现在我们要在这里构建它。你想评论的不一定只是电影,我可不会限制你。状态是指存储在链上的程序数据。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/sugar-cli/index.html b/Solana-Co-Learn/tags/sugar-cli/index.html index 5202aa8d8..44c198d87 100644 --- a/Solana-Co-Learn/tags/sugar-cli/index.html +++ b/Solana-Co-Learn/tags/sugar-cli/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    One doc tagged with "sugar-cli"

    View All Tags

    🍭 糖果机和Sugar CLI

    将自己的脸做成NFT有何不好呢?你可以永久地将自己视为一个早期的建设者,并告诉你的妈妈你已经进入了区块链世界。既然我们已经铸造了一个单独的NFT,现在我们将学习如何铸造一系列的NFT。为了实现这一目标,我们将使用Candy Machine——这是一个Solana程序,让创作者能够将他们的资产上链。尽管这不是创建系列的唯一方法,但在Solana上它已经成为一种标准,因为它拥有一些有用的功能,如防机器人保护和安全随机化。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/token-metadata/index.html b/Solana-Co-Learn/tags/token-metadata/index.html index 374d33198..94b775316 100644 --- a/Solana-Co-Learn/tags/token-metadata/index.html +++ b/Solana-Co-Learn/tags/token-metadata/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    One doc tagged with "token-metadata"

    View All Tags

    🧮 令牌元数据

    Token元数据指的是代币的基本信息,例如名称、符号和标志。注意你钱包中的各种代币都拥有这些特性,除了你自己创建的代币。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/token-program/index.html b/Solana-Co-Learn/tags/token-program/index.html index 8829cdeea..29601caa9 100644 --- a/Solana-Co-Learn/tags/token-program/index.html +++ b/Solana-Co-Learn/tags/token-program/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    One doc tagged with "token-program"

    View All Tags

    💵 Token Program

    作为区块链最基本的承诺,这些代币也许是你安装钱包的主要原因,它们是区块链上资产最纯粹的表现形式,从合成股票到数百种狗币。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/wallet-and-frontend/index.html b/Solana-Co-Learn/tags/wallet-and-frontend/index.html index cc905713c..0f6fdbeaa 100644 --- a/Solana-Co-Learn/tags/wallet-and-frontend/index.html +++ b/Solana-Co-Learn/tags/wallet-and-frontend/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    2 docs tagged with "wallet-and-frontend"

    View All Tags

    🔌 连接到钱包

    现在我们已经知道如何使用代码与网络交互,通过直接使用私钥来初始化账户。显然,在正常的去中心化应用(dapp)中,这样做是不可行的(永远不要将你的私钥暴露给任何人或任何dapp)。

    🦺 与程序进行交互

    在成功设置了钱包连接后,我们可以让ping按钮真正执行操作了。以下是如何实现的详细说明。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/wallet-usage/index.html b/Solana-Co-Learn/tags/wallet-usage/index.html index 56e13fce5..b1fe9d9b3 100644 --- a/Solana-Co-Learn/tags/wallet-usage/index.html +++ b/Solana-Co-Learn/tags/wallet-usage/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    One doc tagged with "wallet-usage"

    View All Tags

    Solana钱包使用 - Backpack 🎒

    Solana的钱包种类繁多,如众所周知的Phantom钱包。然而,在此我并不推荐使用Phantom,因为对于开发者来说,它并不够友好。

    - - + + \ No newline at end of file diff --git a/Solana-Co-Learn/tags/wallet/index.html b/Solana-Co-Learn/tags/wallet/index.html index 14f0da2a8..e86a657ad 100644 --- a/Solana-Co-Learn/tags/wallet/index.html +++ b/Solana-Co-Learn/tags/wallet/index.html @@ -9,13 +9,13 @@ - - + +
    Skip to main content

    One doc tagged with "wallet"

    View All Tags

    📱 在钱包中展示NFTs

    现在我们已经铸造了一个NFT,接下来我们要探索如何铸造一系列的NFT。我们将使用Candy Machine来完成这项任务,这是一款Solana程序,能让创作者方便地将他们的资产上链。虽然这不是创建系列的唯一方式,但在Solana上它已经成为标准,因为它具有诸如防机器人保护和安全随机化等有用的功能。你懂的,模板时间到了。然而,随着我们构建的项目越来越复杂,我们的模板也会变得更先进。这次我们将基于Solana dApp脚手架构建一个模板。与之前的模板一样,它是一个由create-next-app创建的Next.js应用程序。不过这次,它具有更多功能。不用担心!我们依然会使用相同的工具。

    - - + + \ No newline at end of file diff --git a/assets/js/00660f19.5bdf03b4.js b/assets/js/00660f19.f083cc67.js similarity index 98% rename from assets/js/00660f19.5bdf03b4.js rename to assets/js/00660f19.f083cc67.js index 1990b3b5e..78eb97178 100644 --- a/assets/js/00660f19.5bdf03b4.js +++ b/assets/js/00660f19.f083cc67.js @@ -1 +1 @@ -"use strict";(self.webpackChunkall_in_one_solana=self.webpackChunkall_in_one_solana||[]).push([[2203],{3905:(e,n,t)=>{t.d(n,{Zo:()=>d,kt:()=>m});var r=t(67294);function a(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function o(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);n&&(r=r.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,r)}return t}function l(e){for(var n=1;n=0||(a[t]=e[t]);return a}(e,n);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(a[t]=e[t])}return a}var s=r.createContext({}),c=function(e){var n=r.useContext(s),t=n;return e&&(t="function"==typeof e?e(n):l(l({},n),e)),t},d=function(e){var n=c(e.components);return r.createElement(s.Provider,{value:n},e.children)},p="mdxType",u={inlineCode:"code",wrapper:function(e){var n=e.children;return r.createElement(r.Fragment,{},n)}},f=r.forwardRef((function(e,n){var t=e.components,a=e.mdxType,o=e.originalType,s=e.parentName,d=i(e,["components","mdxType","originalType","parentName"]),p=c(t),f=a,m=p["".concat(s,".").concat(f)]||p[f]||u[f]||o;return t?r.createElement(m,l(l({ref:n},d),{},{components:t})):r.createElement(m,l({ref:n},d))}));function m(e,n){var t=arguments,a=n&&n.mdxType;if("string"==typeof e||a){var o=t.length,l=new Array(o);l[0]=f;var i={};for(var s in n)hasOwnProperty.call(n,s)&&(i[s]=n[s]);i.originalType=e,i[p]="string"==typeof e?e:a,l[1]=i;for(var c=2;c{t.r(n),t.d(n,{assets:()=>s,contentTitle:()=>l,default:()=>u,frontMatter:()=>o,metadata:()=>i,toc:()=>c});var r=t(87462),a=(t(67294),t(3905));const o={sidebar_position:16,sidebar_label:"\u94b1\u5305\u548c\u524d\u7aef",sidebar_class_name:"green"},l="\u94b1\u5305\u548c\u524d\u7aef",i={unversionedId:"module1/wallets-and-frontends/README",id:"module1/wallets-and-frontends/README",title:"\u94b1\u5305\u548c\u524d\u7aef",description:"- \ud83d\udd0c \u8fde\u63a5\u5230\u94b1\u5305",source:"@site/docs/Solana-Co-Learn/module1/wallets-and-frontends/README.md",sourceDirName:"module1/wallets-and-frontends",slug:"/module1/wallets-and-frontends/",permalink:"/solana-co-learn/Solana-Co-Learn/module1/wallets-and-frontends/",draft:!1,editUrl:"https://github.com/CreatorsDAO/solana-co-learn/tree/main/docs/Solana-Co-Learn/module1/wallets-and-frontends/README.md",tags:[],version:"current",lastUpdatedBy:"v1xingyue",lastUpdatedAt:1727439818,formattedLastUpdatedAt:"Sep 27, 2024",sidebarPosition:16,frontMatter:{sidebar_position:16,sidebar_label:"\u94b1\u5305\u548c\u524d\u7aef",sidebar_class_name:"green"},sidebar:"tutorialSidebar",previous:{title:"\ud83d\udcdd \u6784\u5efa\u4e00\u4e2a\u4ea4\u4e92\u811a\u672c",permalink:"/solana-co-learn/Solana-Co-Learn/module1/client-side-development/build-an-interaction-script/"},next:{title:"\ud83d\udd0c \u8fde\u63a5\u5230\u94b1\u5305",permalink:"/solana-co-learn/Solana-Co-Learn/module1/wallets-and-frontends/connecting-to-wallet/"}},s={},c=[],d={toc:c},p="wrapper";function u(e){let{components:n,...t}=e;return(0,a.kt)(p,(0,r.Z)({},d,t,{components:n,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"\u94b1\u5305\u548c\u524d\u7aef"},"\u94b1\u5305\u548c\u524d\u7aef"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module1/wallets-and-frontends/connecting-to-wallet/"},"\ud83d\udd0c \u8fde\u63a5\u5230\u94b1\u5305")),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module1/wallets-and-frontends/interact-with-a-program/"},"\ud83e\uddba \u4e0e\u7a0b\u5e8f\u8fdb\u884c\u4ea4\u4e92"))))}u.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunkall_in_one_solana=self.webpackChunkall_in_one_solana||[]).push([[2203],{3905:(e,n,t)=>{t.d(n,{Zo:()=>d,kt:()=>m});var r=t(67294);function a(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function o(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);n&&(r=r.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,r)}return t}function l(e){for(var n=1;n=0||(a[t]=e[t]);return a}(e,n);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(a[t]=e[t])}return a}var s=r.createContext({}),c=function(e){var n=r.useContext(s),t=n;return e&&(t="function"==typeof e?e(n):l(l({},n),e)),t},d=function(e){var n=c(e.components);return r.createElement(s.Provider,{value:n},e.children)},p="mdxType",u={inlineCode:"code",wrapper:function(e){var n=e.children;return r.createElement(r.Fragment,{},n)}},f=r.forwardRef((function(e,n){var t=e.components,a=e.mdxType,o=e.originalType,s=e.parentName,d=i(e,["components","mdxType","originalType","parentName"]),p=c(t),f=a,m=p["".concat(s,".").concat(f)]||p[f]||u[f]||o;return t?r.createElement(m,l(l({ref:n},d),{},{components:t})):r.createElement(m,l({ref:n},d))}));function m(e,n){var t=arguments,a=n&&n.mdxType;if("string"==typeof e||a){var o=t.length,l=new Array(o);l[0]=f;var i={};for(var s in n)hasOwnProperty.call(n,s)&&(i[s]=n[s]);i.originalType=e,i[p]="string"==typeof e?e:a,l[1]=i;for(var c=2;c{t.r(n),t.d(n,{assets:()=>s,contentTitle:()=>l,default:()=>u,frontMatter:()=>o,metadata:()=>i,toc:()=>c});var r=t(87462),a=(t(67294),t(3905));const o={sidebar_position:16,sidebar_label:"\u94b1\u5305\u548c\u524d\u7aef",sidebar_class_name:"green"},l="\u94b1\u5305\u548c\u524d\u7aef",i={unversionedId:"module1/wallets-and-frontends/README",id:"module1/wallets-and-frontends/README",title:"\u94b1\u5305\u548c\u524d\u7aef",description:"- \ud83d\udd0c \u8fde\u63a5\u5230\u94b1\u5305",source:"@site/docs/Solana-Co-Learn/module1/wallets-and-frontends/README.md",sourceDirName:"module1/wallets-and-frontends",slug:"/module1/wallets-and-frontends/",permalink:"/solana-co-learn/Solana-Co-Learn/module1/wallets-and-frontends/",draft:!1,editUrl:"https://github.com/CreatorsDAO/solana-co-learn/tree/main/docs/Solana-Co-Learn/module1/wallets-and-frontends/README.md",tags:[],version:"current",lastUpdatedBy:"v1xingyue",lastUpdatedAt:1727439952,formattedLastUpdatedAt:"Sep 27, 2024",sidebarPosition:16,frontMatter:{sidebar_position:16,sidebar_label:"\u94b1\u5305\u548c\u524d\u7aef",sidebar_class_name:"green"},sidebar:"tutorialSidebar",previous:{title:"\ud83d\udcdd \u6784\u5efa\u4e00\u4e2a\u4ea4\u4e92\u811a\u672c",permalink:"/solana-co-learn/Solana-Co-Learn/module1/client-side-development/build-an-interaction-script/"},next:{title:"\ud83d\udd0c \u8fde\u63a5\u5230\u94b1\u5305",permalink:"/solana-co-learn/Solana-Co-Learn/module1/wallets-and-frontends/connecting-to-wallet/"}},s={},c=[],d={toc:c},p="wrapper";function u(e){let{components:n,...t}=e;return(0,a.kt)(p,(0,r.Z)({},d,t,{components:n,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"\u94b1\u5305\u548c\u524d\u7aef"},"\u94b1\u5305\u548c\u524d\u7aef"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module1/wallets-and-frontends/connecting-to-wallet/"},"\ud83d\udd0c \u8fde\u63a5\u5230\u94b1\u5305")),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module1/wallets-and-frontends/interact-with-a-program/"},"\ud83e\uddba \u4e0e\u7a0b\u5e8f\u8fdb\u884c\u4ea4\u4e92"))))}u.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/02293396.085ff7af.js b/assets/js/02293396.56dae245.js similarity index 98% rename from assets/js/02293396.085ff7af.js rename to assets/js/02293396.56dae245.js index f0c7f8ca5..c2b2ab92f 100644 --- a/assets/js/02293396.085ff7af.js +++ b/assets/js/02293396.56dae245.js @@ -1 +1 @@ -"use strict";(self.webpackChunkall_in_one_solana=self.webpackChunkall_in_one_solana||[]).push([[1905],{3905:(e,n,t)=>{t.d(n,{Zo:()=>m,kt:()=>f});var a=t(67294);function r(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function o(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);n&&(a=a.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,a)}return t}function l(e){for(var n=1;n=0||(r[t]=e[t]);return r}(e,n);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(r[t]=e[t])}return r}var s=a.createContext({}),c=function(e){var n=a.useContext(s),t=n;return e&&(t="function"==typeof e?e(n):l(l({},n),e)),t},m=function(e){var n=c(e.components);return a.createElement(s.Provider,{value:n},e.children)},p="mdxType",d={inlineCode:"code",wrapper:function(e){var n=e.children;return a.createElement(a.Fragment,{},n)}},u=a.forwardRef((function(e,n){var t=e.components,r=e.mdxType,o=e.originalType,s=e.parentName,m=i(e,["components","mdxType","originalType","parentName"]),p=c(t),u=r,f=p["".concat(s,".").concat(u)]||p[u]||d[u]||o;return t?a.createElement(f,l(l({ref:n},m),{},{components:t})):a.createElement(f,l({ref:n},m))}));function f(e,n){var t=arguments,r=n&&n.mdxType;if("string"==typeof e||r){var o=t.length,l=new Array(o);l[0]=u;var i={};for(var s in n)hasOwnProperty.call(n,s)&&(i[s]=n[s]);i.originalType=e,i[p]="string"==typeof e?e:r,l[1]=i;for(var c=2;c{t.r(n),t.d(n,{assets:()=>s,contentTitle:()=>l,default:()=>d,frontMatter:()=>o,metadata:()=>i,toc:()=>c});var a=t(87462),r=(t(67294),t(3905));const o={sidebar_position:40,sidebar_label:"\u521b\u9020\u795e\u5947\u7684\u4e92\u8054\u7f51\u8d27\u5e01\u5e76\u51fa\u552eJPEG\u56fe\u7247",sidebar_class_name:"green"},l="\u521b\u9020\u795e\u5947\u7684\u4e92\u8054\u7f51\u8d27\u5e01\u5e76\u51fa\u552eJPEG\u56fe\u7247",i={unversionedId:"module2/make-magic-internet-money-and-sell-jepgs/README",id:"module2/make-magic-internet-money-and-sell-jepgs/README",title:"\u521b\u9020\u795e\u5947\u7684\u4e92\u8054\u7f51\u8d27\u5e01\u5e76\u51fa\u552eJPEG\u56fe\u7247",description:"- \ud83c\udfa8 \u521b\u5efa\u5956\u52b1\u4ee3\u5e01",source:"@site/docs/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/README.md",sourceDirName:"module2/make-magic-internet-money-and-sell-jepgs",slug:"/module2/make-magic-internet-money-and-sell-jepgs/",permalink:"/solana-co-learn/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/",draft:!1,editUrl:"https://github.com/CreatorsDAO/solana-co-learn/tree/main/docs/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/README.md",tags:[],version:"current",lastUpdatedBy:"v1xingyue",lastUpdatedAt:1727439818,formattedLastUpdatedAt:"Sep 27, 2024",sidebarPosition:40,frontMatter:{sidebar_position:40,sidebar_label:"\u521b\u9020\u795e\u5947\u7684\u4e92\u8054\u7f51\u8d27\u5e01\u5e76\u51fa\u552eJPEG\u56fe\u7247",sidebar_class_name:"green"},sidebar:"tutorialSidebar",previous:{title:"\ud83d\uddbc \u4ece\u7cd6\u679c\u673a\u5c55\u793aNFTs",permalink:"/solana-co-learn/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/displaying-nfts-from-a-candy-machine/"},next:{title:"\ud83c\udfa8 \u521b\u5efa\u5956\u52b1\u4ee3\u5e01",permalink:"/solana-co-learn/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-reward-tokens/"}},s={},c=[],m={toc:c},p="wrapper";function d(e){let{components:n,...t}=e;return(0,r.kt)(p,(0,a.Z)({},m,t,{components:n,mdxType:"MDXLayout"}),(0,r.kt)("h1",{id:"\u521b\u9020\u795e\u5947\u7684\u4e92\u8054\u7f51\u8d27\u5e01\u5e76\u51fa\u552ejpeg\u56fe\u7247"},"\u521b\u9020\u795e\u5947\u7684\u4e92\u8054\u7f51\u8d27\u5e01\u5e76\u51fa\u552eJPEG\u56fe\u7247"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-reward-tokens/"},"\ud83c\udfa8 \u521b\u5efa\u5956\u52b1\u4ee3\u5e01")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-candy-machine/"},"\ud83c\udf6c \u521b\u9020\u7cd6\u679c\u673a")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-the-minting-ui/"},"\ud83c\udfa8 \u521b\u5efa\u94f8\u5e01\u7528\u6237\u754c\u9762"))))}d.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunkall_in_one_solana=self.webpackChunkall_in_one_solana||[]).push([[1905],{3905:(e,n,t)=>{t.d(n,{Zo:()=>m,kt:()=>f});var a=t(67294);function r(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function o(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);n&&(a=a.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,a)}return t}function l(e){for(var n=1;n=0||(r[t]=e[t]);return r}(e,n);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(r[t]=e[t])}return r}var s=a.createContext({}),c=function(e){var n=a.useContext(s),t=n;return e&&(t="function"==typeof e?e(n):l(l({},n),e)),t},m=function(e){var n=c(e.components);return a.createElement(s.Provider,{value:n},e.children)},p="mdxType",d={inlineCode:"code",wrapper:function(e){var n=e.children;return a.createElement(a.Fragment,{},n)}},u=a.forwardRef((function(e,n){var t=e.components,r=e.mdxType,o=e.originalType,s=e.parentName,m=i(e,["components","mdxType","originalType","parentName"]),p=c(t),u=r,f=p["".concat(s,".").concat(u)]||p[u]||d[u]||o;return t?a.createElement(f,l(l({ref:n},m),{},{components:t})):a.createElement(f,l({ref:n},m))}));function f(e,n){var t=arguments,r=n&&n.mdxType;if("string"==typeof e||r){var o=t.length,l=new Array(o);l[0]=u;var i={};for(var s in n)hasOwnProperty.call(n,s)&&(i[s]=n[s]);i.originalType=e,i[p]="string"==typeof e?e:r,l[1]=i;for(var c=2;c{t.r(n),t.d(n,{assets:()=>s,contentTitle:()=>l,default:()=>d,frontMatter:()=>o,metadata:()=>i,toc:()=>c});var a=t(87462),r=(t(67294),t(3905));const o={sidebar_position:40,sidebar_label:"\u521b\u9020\u795e\u5947\u7684\u4e92\u8054\u7f51\u8d27\u5e01\u5e76\u51fa\u552eJPEG\u56fe\u7247",sidebar_class_name:"green"},l="\u521b\u9020\u795e\u5947\u7684\u4e92\u8054\u7f51\u8d27\u5e01\u5e76\u51fa\u552eJPEG\u56fe\u7247",i={unversionedId:"module2/make-magic-internet-money-and-sell-jepgs/README",id:"module2/make-magic-internet-money-and-sell-jepgs/README",title:"\u521b\u9020\u795e\u5947\u7684\u4e92\u8054\u7f51\u8d27\u5e01\u5e76\u51fa\u552eJPEG\u56fe\u7247",description:"- \ud83c\udfa8 \u521b\u5efa\u5956\u52b1\u4ee3\u5e01",source:"@site/docs/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/README.md",sourceDirName:"module2/make-magic-internet-money-and-sell-jepgs",slug:"/module2/make-magic-internet-money-and-sell-jepgs/",permalink:"/solana-co-learn/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/",draft:!1,editUrl:"https://github.com/CreatorsDAO/solana-co-learn/tree/main/docs/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/README.md",tags:[],version:"current",lastUpdatedBy:"v1xingyue",lastUpdatedAt:1727439952,formattedLastUpdatedAt:"Sep 27, 2024",sidebarPosition:40,frontMatter:{sidebar_position:40,sidebar_label:"\u521b\u9020\u795e\u5947\u7684\u4e92\u8054\u7f51\u8d27\u5e01\u5e76\u51fa\u552eJPEG\u56fe\u7247",sidebar_class_name:"green"},sidebar:"tutorialSidebar",previous:{title:"\ud83d\uddbc \u4ece\u7cd6\u679c\u673a\u5c55\u793aNFTs",permalink:"/solana-co-learn/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/displaying-nfts-from-a-candy-machine/"},next:{title:"\ud83c\udfa8 \u521b\u5efa\u5956\u52b1\u4ee3\u5e01",permalink:"/solana-co-learn/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-reward-tokens/"}},s={},c=[],m={toc:c},p="wrapper";function d(e){let{components:n,...t}=e;return(0,r.kt)(p,(0,a.Z)({},m,t,{components:n,mdxType:"MDXLayout"}),(0,r.kt)("h1",{id:"\u521b\u9020\u795e\u5947\u7684\u4e92\u8054\u7f51\u8d27\u5e01\u5e76\u51fa\u552ejpeg\u56fe\u7247"},"\u521b\u9020\u795e\u5947\u7684\u4e92\u8054\u7f51\u8d27\u5e01\u5e76\u51fa\u552eJPEG\u56fe\u7247"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-reward-tokens/"},"\ud83c\udfa8 \u521b\u5efa\u5956\u52b1\u4ee3\u5e01")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-candy-machine/"},"\ud83c\udf6c \u521b\u9020\u7cd6\u679c\u673a")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-the-minting-ui/"},"\ud83c\udfa8 \u521b\u5efa\u94f8\u5e01\u7528\u6237\u754c\u9762"))))}d.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/0231789f.e05c0aba.js b/assets/js/0231789f.5bc4fd8b.js similarity index 99% rename from assets/js/0231789f.e05c0aba.js rename to assets/js/0231789f.5bc4fd8b.js index 481a92149..a7381d682 100644 --- a/assets/js/0231789f.e05c0aba.js +++ b/assets/js/0231789f.5bc4fd8b.js @@ -1 +1 @@ -"use strict";(self.webpackChunkall_in_one_solana=self.webpackChunkall_in_one_solana||[]).push([[5932],{3905:(e,n,t)=>{t.d(n,{Zo:()=>u,kt:()=>k});var a=t(67294);function r(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function l(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);n&&(a=a.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,a)}return t}function i(e){for(var n=1;n=0||(r[t]=e[t]);return r}(e,n);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(r[t]=e[t])}return r}var p=a.createContext({}),s=function(e){var n=a.useContext(p),t=n;return e&&(t="function"==typeof e?e(n):i(i({},n),e)),t},u=function(e){var n=s(e.components);return a.createElement(p.Provider,{value:n},e.children)},d="mdxType",m={inlineCode:"code",wrapper:function(e){var n=e.children;return a.createElement(a.Fragment,{},n)}},c=a.forwardRef((function(e,n){var t=e.components,r=e.mdxType,l=e.originalType,p=e.parentName,u=o(e,["components","mdxType","originalType","parentName"]),d=s(t),c=r,k=d["".concat(p,".").concat(c)]||d[c]||m[c]||l;return t?a.createElement(k,i(i({ref:n},u),{},{components:t})):a.createElement(k,i({ref:n},u))}));function k(e,n){var t=arguments,r=n&&n.mdxType;if("string"==typeof e||r){var l=t.length,i=new Array(l);i[0]=c;var o={};for(var p in n)hasOwnProperty.call(n,p)&&(o[p]=n[p]);o.originalType=e,o[d]="string"==typeof e?e:r,i[1]=o;for(var s=2;s{t.r(n),t.d(n,{assets:()=>p,contentTitle:()=>i,default:()=>m,frontMatter:()=>l,metadata:()=>o,toc:()=>s});var a=t(87462),r=(t(67294),t(3905));const l={sidebar_position:55,sidebar_label:"\ud83c\udf82 Rust\u7684\u5206\u5c42\u86cb\u7cd5",sidebar_class_name:"green",tags:["native-solana-development","solana","native-solana-program","program","rust-layer-cake"]},i="\ud83c\udf82 Rust\u7684\u5206\u5c42\u86cb\u7cd5",o={unversionedId:"module3/native-solana-development/the-rust-layer-cake/README",id:"module3/native-solana-development/the-rust-layer-cake/README",title:"\ud83c\udf82 Rust\u7684\u5206\u5c42\u86cb\u7cd5",description:"\u6211\u4eec\u5c06\u5728\u6e38\u4e50\u573a\u4e0a\u5236\u4f5c\u4e00\u4e2a\u7b80\u5355\u7684Hello World\u7a0b\u5e8f\uff0c\u4ec5\u4ec5\u4f1a\u5728\u4ea4\u6613\u65e5\u5fd7\u4e2d\u8bb0\u5f55\u4e00\u6761\u6d88\u606f\u3002\u62db\u547c\u5df2\u7ecf\u6253\u8fc7\u4e86\u3002\u73b0\u5728\u662f\u65f6\u5019\u5b66\u4e60\u5982\u4f55\u5904\u7406\u6307\u4ee4\u6570\u636e\uff0c\u5c31\u50cf\u5728\u5ba2\u6237\u7aef\u5f00\u53d1\u4e2d\u4e00\u6837\u3002",source:"@site/docs/Solana-Co-Learn/module3/native-solana-development/the-rust-layer-cake/README.md",sourceDirName:"module3/native-solana-development/the-rust-layer-cake",slug:"/module3/native-solana-development/the-rust-layer-cake/",permalink:"/solana-co-learn/Solana-Co-Learn/module3/native-solana-development/the-rust-layer-cake/",draft:!1,editUrl:"https://github.com/CreatorsDAO/solana-co-learn/tree/main/docs/Solana-Co-Learn/module3/native-solana-development/the-rust-layer-cake/README.md",tags:[{label:"native-solana-development",permalink:"/solana-co-learn/Solana-Co-Learn/tags/native-solana-development"},{label:"solana",permalink:"/solana-co-learn/Solana-Co-Learn/tags/solana"},{label:"native-solana-program",permalink:"/solana-co-learn/Solana-Co-Learn/tags/native-solana-program"},{label:"program",permalink:"/solana-co-learn/Solana-Co-Learn/tags/program"},{label:"rust-layer-cake",permalink:"/solana-co-learn/Solana-Co-Learn/tags/rust-layer-cake"}],version:"current",lastUpdatedBy:"v1xingyue",lastUpdatedAt:1727439818,formattedLastUpdatedAt:"Sep 27, 2024",sidebarPosition:55,frontMatter:{sidebar_position:55,sidebar_label:"\ud83c\udf82 Rust\u7684\u5206\u5c42\u86cb\u7cd5",sidebar_class_name:"green",tags:["native-solana-development","solana","native-solana-program","program","rust-layer-cake"]},sidebar:"tutorialSidebar",previous:{title:"\u539f\u751fSOLANA\u5f00\u53d1",permalink:"/solana-co-learn/Solana-Co-Learn/module3/native-solana-development/"},next:{title:"\ud83c\udfa5 \u6784\u5efa\u4e00\u4e2a\u7535\u5f71\u8bc4\u8bba\u7a0b\u5e8f",permalink:"/solana-co-learn/Solana-Co-Learn/module3/native-solana-development/build-a-movie-review-program/"}},p={},s=[{value:"Rust\u7684\u5206\u5c42\u86cb\u7cd5",id:"rust\u7684\u5206\u5c42\u86cb\u7cd5",level:2},{value:"\ud83d\udc76 \u53d8\u91cf\u58f0\u660e\u548c\u53ef\u53d8\u6027",id:"-\u53d8\u91cf\u58f0\u660e\u548c\u53ef\u53d8\u6027",level:2},{value:"\ud83c\udf71 \u7ed3\u6784\u4f53",id:"-\u7ed3\u6784\u4f53",level:2},{value:"\ud83d\udcdc \u679a\u4e3e\u3001\u53d8\u4f53\u548c\u5339\u914d",id:"-\u679a\u4e3e\u53d8\u4f53\u548c\u5339\u914d",level:2},{value:"\ud83d\udce6 \u5b9e\u73b0",id:"-\u5b9e\u73b0",level:2},{value:"\ud83c\udf81 \u7279\u5f81\uff08Traits\uff09",id:"-\u7279\u5f81traits",level:2},{value:"\ud83c\udf82 \u628a\u6240\u6709\u5143\u7d20\u6574\u5408\u5728\u4e00\u8d77",id:"-\u628a\u6240\u6709\u5143\u7d20\u6574\u5408\u5728\u4e00\u8d77",level:2},{value:"\ud83d\ude80 \u7a0b\u5e8f\u903b\u8f91",id:"-\u7a0b\u5e8f\u903b\u8f91",level:2},{value:"\ud83d\udcc2 \u6587\u4ef6\u7ed3\u6784\u8bf4\u660e",id:"-\u6587\u4ef6\u7ed3\u6784\u8bf4\u660e",level:2}],u={toc:s},d="wrapper";function m(e){let{components:n,...l}=e;return(0,r.kt)(d,(0,a.Z)({},u,l,{components:n,mdxType:"MDXLayout"}),(0,r.kt)("h1",{id:"-rust\u7684\u5206\u5c42\u86cb\u7cd5"},"\ud83c\udf82 Rust\u7684\u5206\u5c42\u86cb\u7cd5"),(0,r.kt)("p",null,"\u6211\u4eec\u5c06\u5728\u6e38\u4e50\u573a\u4e0a\u5236\u4f5c\u4e00\u4e2a\u7b80\u5355\u7684",(0,r.kt)("inlineCode",{parentName:"p"},"Hello World"),"\u7a0b\u5e8f\uff0c\u4ec5\u4ec5\u4f1a\u5728\u4ea4\u6613\u65e5\u5fd7\u4e2d\u8bb0\u5f55\u4e00\u6761\u6d88\u606f\u3002\u62db\u547c\u5df2\u7ecf\u6253\u8fc7\u4e86\u3002\u73b0\u5728\u662f\u65f6\u5019\u5b66\u4e60\u5982\u4f55\u5904\u7406\u6307\u4ee4\u6570\u636e\uff0c\u5c31\u50cf\u5728\u5ba2\u6237\u7aef\u5f00\u53d1\u4e2d\u4e00\u6837\u3002"),(0,r.kt)("p",null,"\u5728\u5f00\u59cb\u6784\u5efa\u4e4b\u524d\uff0c\u6211\u60f3\u5148\u7ed9\u4f60\u4ecb\u7ecd\u4e00\u4e9b\u5373\u5c06\u4f7f\u7528\u7684\u6982\u5ff5\u3002\u8fd8\u8bb0\u5f97\u6211\u63d0\u5230\u7684\u89c4\u5219\u3001\u80fd\u529b\u548c\u4e92\u52a8\u5417\uff1f\u6211\u4f1a\u5e26\u4f60\u4e86\u89e3\u4e00\u4e0b\u7f16\u5199\u672c\u5730",(0,r.kt)("inlineCode",{parentName:"p"},"Solana"),"\u7a0b\u5e8f\u6240\u9700\u7684\u80fd\u529b\u548c\u89c4\u5219\u3002\u8fd9\u91cc\u7684\u201c\u672c\u5730\u201d\u975e\u5e38\u91cd\u8981 - \u6211\u4eec\u5c06\u5728\u540e\u7eed\u90e8\u5206\u501f\u52a9",(0,r.kt)("inlineCode",{parentName:"p"},"Anchor"),"\u6765\u5904\u7406\u6211\u4eec\u73b0\u5728\u6240\u5b66\u7684\u8bb8\u591a\u5185\u5bb9\u3002"),(0,r.kt)("p",null,"\u6211\u4eec\u5b66\u4e60\u539f\u751f\u5f00\u53d1\u7684\u539f\u56e0\u662f\u56e0\u4e3a\u4e86\u89e3\u5e95\u5c42\u5de5\u4f5c\u539f\u7406\u662f\u975e\u5e38\u91cd\u8981\u7684\u3002\u4e00\u65e6\u4f60\u7406\u89e3\u4e86\u4e8b\u7269\u662f\u5982\u4f55\u5728\u6700\u57fa\u672c\u7684\u5c42\u9762\u4e0a\u8fd0\u4f5c\u7684\uff0c\u5c31\u80fd\u591f\u501f\u52a9\u50cf",(0,r.kt)("inlineCode",{parentName:"p"},"Anchor"),"\u8fd9\u6837\u7684\u5de5\u5177\u6765\u6784\u5efa\u66f4\u5f3a\u5927\u7684\u7a0b\u5e8f\u3002\u4f60\u53ef\u4ee5\u628a\u8fd9\u4e2a\u8fc7\u7a0b\u60f3\u8c61\u6210\u4e0e\u4e0d\u540c\u7c7b\u578b\u7684\u654c\u4eba\u8fdb\u884c\u9996\u9886\u6218 \uff0c \u4f60\u9700\u8981\u5148\u5b66\u4f1a\u5982\u4f55\u9010\u4e00\u5bf9\u6297\u6bcf\u4e2a\u4e2a\u4f53\u602a\u7269\uff08\u4ee5\u53ca\u4e86\u89e3\u4f60\u81ea\u5df1\u7684\u80fd\u529b\uff09\u3002"),(0,r.kt)("p",null,"\u5f53\u6211\u521a\u5f00\u59cb\u5b66\u4e60\u7684\u65f6\u5019\uff0c\u6211\u53d1\u73b0\u5f88\u96be\u7406\u89e3\u81ea\u5df1\u7f3a\u5c11\u4e86\u4ec0\u4e48\u3002\u6240\u4ee5\u6211\u5c06\u5176\u5206\u89e3\u6210\u4e86\u201c\u5c42\u6b21\u201d\u3002\u6bcf\u4e00\u4e2a\u4f60\u5b66\u4e60\u7684\u4e3b\u9898\u90fd\u5efa\u7acb\u5728\u4e00\u5c42\u77e5\u8bc6\u7684\u57fa\u7840\u4e4b\u4e0a\u3002\u5982\u679c\u9047\u5230\u4e0d\u660e\u767d\u7684\u5730\u65b9\uff0c\u56de\u5230\u4e4b\u524d\u7684\u5c42\u6b21\uff0c\u786e\u4fdd\u4f60\u771f\u6b63\u7406\u89e3\u4e86\u5b83\u4eec\u3002"),(0,r.kt)("h2",{id:"rust\u7684\u5206\u5c42\u86cb\u7cd5"},"Rust\u7684\u5206\u5c42\u86cb\u7cd5"),(0,r.kt)("p",null,(0,r.kt)("img",{src:t(14397).Z,width:"3693",height:"2476"})),(0,r.kt)("p",null,"\u8fd9\u662f\u4e00\u4e2a\u7531",(0,r.kt)("inlineCode",{parentName:"p"},"Rust"),"\u5236\u4f5c\u7684\u86cb\u7cd5\u3002"),(0,r.kt)("admonition",{type:"caution"},(0,r.kt)("p",{parentName:"admonition"},"\u6ce8\u610f\uff1a\u56fe\u5c42\u4ee3\u8868\u91cd\u91cf\uff01")),(0,r.kt)("h2",{id:"-\u53d8\u91cf\u58f0\u660e\u548c\u53ef\u53d8\u6027"},"\ud83d\udc76 \u53d8\u91cf\u58f0\u660e\u548c\u53ef\u53d8\u6027"),(0,r.kt)("p",null,"\u53d8\u91cf\u3002\u4f60\u4e86\u89e3\u5b83\u4eec\u3002\u4f60\u4f7f\u7528\u8fc7\u5b83\u4eec\u3002\u4f60\u751a\u81f3\u53ef\u80fd\u62fc\u5199\u9519\u8bef\u8fc7\u5b83\u4eec\u3002\u5728",(0,r.kt)("inlineCode",{parentName:"p"},"Rust"),"\u4e2d\u5173\u4e8e\u53d8\u91cf\u552f\u4e00\u7684\u65b0\u6982\u5ff5\u5c31\u662f\u53ef\u53d8\u6027\u3002\u6240\u6709\u53d8\u91cf\u9ed8\u8ba4\u90fd\u662f\u4e0d\u53ef\u53d8\u7684 \uff0c \u4e00\u65e6\u58f0\u660e\u4e86\u53d8\u91cf\uff0c\u5c31\u4e0d\u80fd\u6539\u53d8\u5176\u503c\u3002\u4f60\u53ea\u9700\u901a\u8fc7\u6dfb\u52a0",(0,r.kt)("inlineCode",{parentName:"p"},"mut"),"\u5173\u952e\u5b57\u544a\u8bc9\u7f16\u8bd1\u5668\u4f60\u60f3\u8981\u4e00\u4e2a\u53ef\u53d8\u7684\u53d8\u91cf\u3002\u5c31\u662f\u8fd9\u4e48\u7b80\u5355\u3002\u5982\u679c\u6211\u4eec\u4e0d\u6307\u5b9a\u7c7b\u578b\uff0c\u7f16\u8bd1\u5668\u4f1a\u6839\u636e\u63d0\u4f9b\u7684\u6570\u636e\u8fdb\u884c\u63a8\u65ad\uff0c\u5e76\u5f3a\u5236\u6211\u4eec\u4fdd\u6301\u8be5\u7c7b\u578b\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"// compiler will throw error\nlet age = 33;\nage = 34;\n\n// this is allowed\nlet mut mutable_age = 33;\nmutable_age = 34;\n")),(0,r.kt)("h2",{id:"-\u7ed3\u6784\u4f53"},"\ud83c\udf71 \u7ed3\u6784\u4f53"),(0,r.kt)("p",null,"\u7ed3\u6784\u4f53\u662f\u81ea\u5b9a\u4e49\u7684\u6570\u636e\u7ed3\u6784\uff0c\u4e00\u79cd\u5c06\u6570\u636e\u7ec4\u7ec7\u5728\u4e00\u8d77\u7684\u65b9\u5f0f\u3002\u5b83\u4eec\u662f\u4f60\u5b9a\u4e49\u7684\u81ea\u5b9a\u4e49\u6570\u636e\u7c7b\u578b\uff0c\u7c7b\u4f3c\u4e8e",(0,r.kt)("inlineCode",{parentName:"p"},"JavaScript"),"\u4e2d\u7684\u5bf9\u8c61\u3002",(0,r.kt)("inlineCode",{parentName:"p"},"Rust"),"\u5e76\u4e0d\u662f\u5b8c\u5168\u9762\u5411\u5bf9\u8c61\u7684 - \u7ed3\u6784\u4f53\u672c\u8eab\u9664\u4e86\u4fdd\u5b58\u6709\u7ec4\u7ec7\u7684\u6570\u636e\u5916\uff0c\u65e0\u6cd5\u6267\u884c\u4efb\u4f55\u64cd\u4f5c\u3002\u4f46\u4f60\u53ef\u4ee5\u5411\u7ed3\u6784\u4f53\u6dfb\u52a0\u65b9\u6cd5\uff0c\u4f7f\u5176\u8868\u73b0\u5f97\u66f4\u50cf\u5bf9\u8c61\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},'Struct User {\n active: bool,\n email: String,\n age: u64\n}\n\nlet mut user1 = User {\n active: true,\n email: String::from("test@test.com"),\n age: 33\n};\n\nuser1.age = 34;\n')),(0,r.kt)("h2",{id:"-\u679a\u4e3e\u53d8\u4f53\u548c\u5339\u914d"},"\ud83d\udcdc \u679a\u4e3e\u3001\u53d8\u4f53\u548c\u5339\u914d"),(0,r.kt)("p",null,"\u679a\u4e3e\u5f88\u7b80\u5355 - \u5b83\u4eec\u5c31\u50cf\u4ee3\u7801\u4e2d\u7684\u4e0b\u62c9\u5217\u8868\u3002\u5b83\u4eec\u9650\u5236\u4f60\u4ece\u51e0\u4e2a\u53ef\u80fd\u7684\u53d8\u4f53\u4e2d\u9009\u62e9\u4e00\u4e2a\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"enum LightStatus {\n On,\n Off\n}\n")),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},'enum LightStatus {\n On {\n color: String\n },\n Off\n}\n\nlet light_status = LightStatus::On {\n color: String::from("red")\n};\n')),(0,r.kt)("p",null,(0,r.kt)("inlineCode",{parentName:"p"},"Rust"),"\u4e2d\u679a\u4e3e\u7684\u9177\u70ab\u4e4b\u5904\u5728\u4e8e\u4f60\u53ef\u4ee5\uff08\u53ef\u9009\u5730\uff09\u5411\u5176\u4e2d\u6dfb\u52a0\u6570\u636e\uff0c\u4f7f\u5176\u51e0\u4e4e\u50cf\u4e00\u4e2a\u8ff7\u4f60\u7684",(0,r.kt)("inlineCode",{parentName:"p"},"if"),"\u8bed\u53e5\u3002\u5728\u8fd9\u4e2a\u4f8b\u5b50\u4e2d\uff0c\u4f60\u6b63\u5728\u9009\u62e9\u4ea4\u901a\u4fe1\u53f7\u706f\u7684\u72b6\u6001\u3002\u5982\u679c\u5b83\u662f\u5f00\u542f\u7684\uff0c\u4f60\u9700\u8981\u6307\u5b9a\u989c\u8272 - \u662f\u7ea2\u8272\u3001\u9ec4\u8272\u8fd8\u662f\u7eff\u8272\uff1f"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"enum Coin {\n Penny,\n Nickel,\n Dime,\n Quarter,\n}\n\nfn value_in_cents(coin: Coin) -> u8 {\n match coin {\n Coin::Penny => 1,\n Coin::Nickel => 5,\n Coin::Dime => 10,\n Coin::Quarter => 25,\n }\n}\n")),(0,r.kt)("admonition",{type:"info"},(0,r.kt)("p",{parentName:"admonition"},"source code: ",(0,r.kt)("a",{parentName:"p",href:"https://kaisery.github.io/trpl-zh-cn/ch06-02-match.html"},"https://kaisery.github.io/trpl-zh-cn/ch06-02-match.html"))),(0,r.kt)("p",null,"\u5f53\u4e0e\u5339\u914d\u8bed\u53e5\u7ed3\u5408\u4f7f\u7528\u65f6\uff0c\u679a\u4e3e\u975e\u5e38\u6709\u7528\u3002\u5b83\u4eec\u662f\u4e00\u79cd\u68c0\u67e5\u53d8\u91cf\u503c\u5e76\u6839\u636e\u8be5\u503c\u6267\u884c\u4ee3\u7801\u7684\u65b9\u5f0f\uff0c\u4e0e",(0,r.kt)("inlineCode",{parentName:"p"},"JavaScript"),"\u4e2d\u7684",(0,r.kt)("inlineCode",{parentName:"p"},"switch"),"\u8bed\u53e5\u7c7b\u4f3c\u3002"),(0,r.kt)("h2",{id:"-\u5b9e\u73b0"},"\ud83d\udce6 \u5b9e\u73b0"),(0,r.kt)("p",null,"\u7ed3\u6784\u4f53\u672c\u8eab\u5f88\u6709\u7528\uff0c\u4f46\u5982\u679c\u4f60\u80fd\u4e3a\u5b83\u4eec\u6dfb\u52a0\u51fd\u6570\uff0c\u6548\u679c\u5c06\u5982\u4f55\u5462\uff1f\u4e0b\u9762\u6211\u4eec\u6765\u4ecb\u7ecd\u5b9e\u73b0\uff08",(0,r.kt)("inlineCode",{parentName:"p"},"Implementations"),"\uff09\uff0c\u5b83\u8ba9\u4f60\u53ef\u4ee5\u7ed9\u7ed3\u6784\u4f53\u6dfb\u52a0\u65b9\u6cd5\uff0c\u4f7f\u5176\u66f4\u63a5\u8fd1\u9762\u5411\u5bf9\u8c61\u7684\u8bbe\u8ba1\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},'#[derive(Debug)]\nstruct Rectangle {\n width: u32,\n height: u32,\n}\n\nimpl Rectangle {\n fn area(&self) -> u32 {\n self.width * self.height\n }\n}\n\nfn main() {\n let rect1 = Rectangle {\n width: 30,\n height: 50,\n };\n\n println!(\n "The area of the rectangle is {} square pixels.",\n rect1.area()\n );\n}\n')),(0,r.kt)("admonition",{type:"info"},(0,r.kt)("p",{parentName:"admonition"},"source code: ",(0,r.kt)("a",{parentName:"p",href:"https://kaisery.github.io/trpl-zh-cn/ch05-03-method-syntax.html"},"https://kaisery.github.io/trpl-zh-cn/ch05-03-method-syntax.html"))),(0,r.kt)("p",null,"\u5982\u679c\u4f60\u5bf9\u201c\u5411\u7ed3\u6784\u4f53\u6dfb\u52a0\u65b9\u6cd5\u201d\u611f\u5230\u56f0\u60d1\uff0c\u53ef\u4ee5\u7406\u89e3\u4e3a\u8d4b\u4e88\u7ed3\u6784\u4f53\u7279\u6b8a\u80fd\u529b\u3002\u4f8b\u5982\uff0c\u4f60\u53ef\u80fd\u6709\u4e00\u4e2a\u7b80\u5355\u7684",(0,r.kt)("inlineCode",{parentName:"p"},"user"),"\u7ed3\u6784\u4f53\uff0c\u62e5\u6709\u901f\u5ea6\u3001\u5065\u5eb7\u548c\u4f24\u5bb3\u5c5e\u6027\u3002\u901a\u8fc7\u4f7f\u7528 ",(0,r.kt)("inlineCode",{parentName:"p"},"impl")," \u5173\u952e\u5b57\u6dfb\u52a0\u4e00\u4e2a ",(0,r.kt)("inlineCode",{parentName:"p"},"wordPerMinute")," \u65b9\u6cd5\uff0c\u4f60\u5c31\u53ef\u4ee5\u8ba1\u7b97\u7528\u6237\u7684\u6253\u5b57\u901f\u5ea6\u2328\ufe0f\u3002"),(0,r.kt)("h2",{id:"-\u7279\u5f81traits"},"\ud83c\udf81 \u7279\u5f81\uff08",(0,r.kt)("inlineCode",{parentName:"h2"},"Traits"),"\uff09"),(0,r.kt)("p",null,"\u73b0\u5728\uff0c\u6211\u4eec\u6765\u8c08\u8c08\u8fd9\u4e2a\u201c\u86cb\u7cd5\u201d\u7684\u9876\u5c42\u90e8\u5206 - ",(0,r.kt)("inlineCode",{parentName:"p"},"Traits"),"\u3002",(0,r.kt)("inlineCode",{parentName:"p"},"Traits"),"\u548c\u5b9e\u73b0\u7c7b\u4f3c\uff0c\u4e5f\u662f\u4e3a\u7c7b\u578b\u589e\u6dfb\u529f\u80fd\u3002\u4f60\u53ef\u4ee5\u628a\u5b83\u770b\u4f5c\u7c7b\u578b\u80fd\u5177\u5907\u7684\u4e00\u79cd\u80fd\u529b\u3002"),(0,r.kt)("p",null,"\u56de\u5230\u6211\u4eec\u7684 ",(0,r.kt)("inlineCode",{parentName:"p"},"user")," \u7ed3\u6784\u4f53\u4f8b\u5b50\uff0c\u5982\u679c\u6211\u6dfb\u52a0\u4e86\u4e00\u4e2a\u540d\u4e3a ",(0,r.kt)("inlineCode",{parentName:"p"},"ThreeArms")," \u7684",(0,r.kt)("inlineCode",{parentName:"p"},"trait"),"\uff0c\u7528\u6237\u5c06\u80fd\u591f\u4ee5\u66f4\u5feb\u7684\u901f\u5ea6\u8f93\u5165\u6587\u5b57\uff0c\u56e0\u4e3a\u4ed6\u4eec\u5c06\u62e5\u6709\u989d\u5916\u7684\u624b\u81c2\uff01",(0,r.kt)("inlineCode",{parentName:"p"},"Traits"),"\u8fd9\u4e2a\u6982\u5ff5\u53ef\u80fd\u6709\u70b9\u62bd\u8c61\uff0c\u6240\u4ee5\u6211\u4eec\u6765\u770b\u4e00\u4e2a\u5177\u4f53\u7684\u4f8b\u5b50\uff1a"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"pub triat BorshDeserialize : Sized {\n fn deserialize(buf: &mut &[u8]) -> Result;\n fn try_from_slice(buf: &[u8]) -> Result { ... }\n}\n")),(0,r.kt)("p",null,"\u5982\u4f60\u6240\u77e5\uff0c\u6211\u4eec\u7684\u6307\u4ee4\u6570\u636e\u4ee5\u5b57\u8282\u6570\u7ec4\uff08\u7531",(0,r.kt)("inlineCode",{parentName:"p"},"1"),"\u548c",(0,r.kt)("inlineCode",{parentName:"p"},"0"),"\u7ec4\u6210\uff09\u7684\u5f62\u5f0f\u63d0\u4f9b\uff0c\u6211\u4eec\u9700\u8981\u5728\u7a0b\u5e8f\u4e2d\u5bf9\u5176\u8fdb\u884c\u53cd\u5e8f\u5217\u5316\uff08\u8f6c\u6362\u6210",(0,r.kt)("inlineCode",{parentName:"p"},"Rust"),"\u7c7b\u578b\uff09\u3002\u6211\u4eec\u5c06\u4f7f\u7528\u540d\u4e3a ",(0,r.kt)("inlineCode",{parentName:"p"},"BorshDeserialize")," \u7684",(0,r.kt)("inlineCode",{parentName:"p"},"Traits"),"\u6765\u5b8c\u6210\u8fd9\u4e00\u4efb\u52a1\uff1a\u5b83\u5305\u62ec\u4e00\u4e2a ",(0,r.kt)("inlineCode",{parentName:"p"},"deserialize")," \u65b9\u6cd5\uff0c\u53ef\u4ee5\u5c06\u6570\u636e\u8f6c\u6362\u4e3a\u6240\u9700\u7c7b\u578b\u3002\u8fd9\u610f\u5473\u7740\uff0c\u5982\u679c\u6211\u4eec\u5c06 ",(0,r.kt)("inlineCode",{parentName:"p"},"BorshDeserialize Traits")," \u6dfb\u52a0\u5230\u6307\u4ee4\u7ed3\u6784\u4f53\u4e2d\uff0c\u6211\u4eec\u5c31\u53ef\u4ee5\u4f7f\u7528 ",(0,r.kt)("inlineCode",{parentName:"p"},"deserialize")," \u65b9\u6cd5\u5c06\u6307\u4ee4\u6570\u636e\u5b9e\u4f8b\u8f6c\u6362\u4e3a",(0,r.kt)("inlineCode",{parentName:"p"},"Rust"),"\u7c7b\u578b\u3002"),(0,r.kt)("p",null,"\u5982\u679c\u4f60\u5bf9\u8fd9\u90e8\u5206\u5185\u5bb9\u611f\u5230\u56f0\u60d1\uff0c\u4e0d\u59a8\u518d\u8bfb\u4e00\u904d\uff0c\u6211\u81ea\u5df1\u4e5f\u82b1\u4e86\u4e00\u4e9b\u65f6\u95f4\u624d\u5f04\u6e05\u695a\u3002"),(0,r.kt)("p",null,"\u5b9e\u9645\u64cd\u4f5c\u793a\u4f8b\u5982\u4e0b\uff1a"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"#[derive(BorshDeserialize)]\nstruct InstructionData {\n input: String,\n}\n")),(0,r.kt)("admonition",{type:"caution"},(0,r.kt)("p",{parentName:"admonition"},"\u6ce8\u610f\uff1a\u53ef\u80fd\u6709\u4e00\u4e2a\u4f60\u5df2\u7ecf\u5ffd\u7565\u7684\u5c42\u9762\u2014\u2014\u5b8f\u3002\u5b83\u4eec\u7528\u6765\u751f\u6210\u4ee3\u7801\u3002")),(0,r.kt)("p",null,"\u5728\u6211\u4eec\u7684\u573a\u666f\u4e2d\uff0c\u7279\u8d28\u548c\u5b8f\u901a\u5e38\u4e00\u8d77\u4f7f\u7528\u3002\u4f8b\u5982\uff0c",(0,r.kt)("inlineCode",{parentName:"p"},"BorshDeserialize Traits"),"\u6709\u4e24\u4e2a\u5fc5\u987b\u5b9e\u73b0\u7684\u51fd\u6570\uff1a",(0,r.kt)("inlineCode",{parentName:"p"},"deserialize")," \u548c ",(0,r.kt)("inlineCode",{parentName:"p"},"try_from_slice"),"\u3002\u6211\u4eec\u53ef\u4ee5\u4f7f\u7528 ",(0,r.kt)("inlineCode",{parentName:"p"},"#[derive(BorshDeserialize)]")," \u5c5e\u6027\uff0c\u8ba9\u7f16\u8bd1\u5668\u5728\u7ed9\u5b9a\u7c7b\u578b\u4e0a\uff08\u5373\u6307\u4ee4\u6570\u636e\u7ed3\u6784\uff09\u4e3a\u6211\u4eec\u5b9e\u73b0\u8fd9\u4e24\u4e2a\u51fd\u6570\u3002\n\u6574\u4e2a\u6d41\u7a0b\u662f\u8fd9\u6837\u7684\uff1a"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"\u901a\u8fc7\u5b8f\u5c06",(0,r.kt)("inlineCode",{parentName:"li"},"Trait"),"\u6dfb\u52a0\u5230\u7ed3\u6784\u4f53\u4e2d"),(0,r.kt)("li",{parentName:"ul"},"\u7f16\u8bd1\u5668\u4f1a\u67e5\u627e",(0,r.kt)("inlineCode",{parentName:"li"},"Trait"),"\u7684\u5b9a\u4e49"),(0,r.kt)("li",{parentName:"ul"},"\u7f16\u8bd1\u5668\u4f1a\u4e3a\u8be5",(0,r.kt)("inlineCode",{parentName:"li"},"Trait"),"\u5b9e\u73b0\u5e95\u5c42\u51fd\u6570"),(0,r.kt)("li",{parentName:"ul"},"\u4f60\u7684\u7ed3\u6784\u4f53\u73b0\u5728\u5177\u5907\u4e86",(0,r.kt)("inlineCode",{parentName:"li"},"Trait"),"\u7684\u529f\u80fd")),(0,r.kt)("p",null,"\u5b9e\u9645\u4e0a\uff0c\u5b8f\u5728\u7f16\u8bd1\u65f6\u751f\u6210\u4e86\u7528\u4e8e\u53cd\u5e8f\u5217\u5316\u5b57\u7b26\u4e32\u7684\u51fd\u6570\u3002\u901a\u8fc7\u4f7f\u7528\u8fd9\u4e2a",(0,r.kt)("inlineCode",{parentName:"p"},"Trait"),"\uff0c\u6211\u4eec\u53ef\u4ee5\u544a\u8bc9",(0,r.kt)("inlineCode",{parentName:"p"},"Rust"),"\uff1a\u201c\u563f\uff0c\u6211\u60f3\u80fd\u53cd\u5e8f\u5217\u5316\u5b57\u7b26\u4e32\uff0c\u8bf7\u4e3a\u6211\u751f\u6210\u76f8\u5e94\u7684\u4ee3\u7801\u3002\u201d"),(0,r.kt)("p",null,"\u5bf9\u4e8e\u6211\u4eec\u7684\u60c5\u51b5\uff0c\u552f\u4e00\u7684\u8981\u6c42\u662f",(0,r.kt)("inlineCode",{parentName:"p"},"Borsh"),"\u5fc5\u987b\u652f\u6301\u6240\u6709\u7684\u7ed3\u6784\u6570\u636e\u7c7b\u578b\uff08\u5728\u6211\u4eec\u7684\u573a\u666f\u4e2d\u662f\u5b57\u7b26\u4e32\uff09\u3002\u5982\u679c\u4f60\u6709\u4e00\u4e2a",(0,r.kt)("inlineCode",{parentName:"p"},"Borsh"),"\u4e0d\u652f\u6301\u7684\u81ea\u5b9a\u4e49\u6570\u636e\u7c7b\u578b\uff0c\u5c31\u9700\u8981\u5728\u5b8f\u4e2d\u81ea\u5df1\u5b9e\u73b0\u8fd9\u4e9b\u529f\u80fd\u3002"),(0,r.kt)("p",null,"\u5982\u679c\u4f60\u8fd8\u672a\u5b8c\u5168\u7406\u89e3\uff0c\u4e0d\u7528\u62c5\u5fc3\uff01\u6211\u81ea\u5df1\u4e5f\u662f\u5728\u770b\u5230\u6574\u4e2a\u6d41\u7a0b\u540e\u624d\u7406\u89e3\u7684\uff0c\u6240\u4ee5\u73b0\u5728\u8ba9\u6211\u4eec\u4e00\u8d77\u5b9e\u8df5\u4e00\u4e0b\uff01"),(0,r.kt)("h2",{id:"-\u628a\u6240\u6709\u5143\u7d20\u6574\u5408\u5728\u4e00\u8d77"},"\ud83c\udf82 \u628a\u6240\u6709\u5143\u7d20\u6574\u5408\u5728\u4e00\u8d77"),(0,r.kt)("p",null,"\u6211\u4eec\u521a\u521a\u8ba8\u8bba\u4e86\u4e00\u7cfb\u5217\u76f8\u4e92\u5173\u8054\u7684\u62bd\u8c61\u4e3b\u9898\u3002\u5982\u679c\u53ea\u63cf\u8ff0\u6bcf\u4e00\u5c42\uff0c\u53ef\u80fd\u96be\u4ee5\u60f3\u8c61\u6574\u4e2a\u201c\u86cb\u7cd5\u201d\u7684\u6837\u5b50\uff0c\u6240\u4ee5\u6211\u4eec\u73b0\u5728\u5c31\u5c06\u5b83\u4eec\u6574\u5408\u8d77\u6765\u3002"),(0,r.kt)("p",null,"\u5047\u8bbe\u6211\u4eec\u6b63\u5728\u6784\u5efa\u4e00\u4e2a\u94fe\u4e0a\u7684\u7b14\u8bb0\u7a0b\u5e8f\uff0c\u6211\u4eec\u5c06\u4fdd\u6301\u5b83\u7684\u7b80\u5355\u6027\uff1a\u4f60\u53ea\u80fd\u521b\u5efa\u3001\u66f4\u65b0\u548c\u5220\u9664\u7b14\u8bb0\u3002\u6211\u4eec\u9700\u8981\u4e00\u6761\u6307\u4ee4\u6765\u5b8c\u6210\u8fd9\u4e9b\u64cd\u4f5c\uff0c\u6240\u4ee5\u8ba9\u6211\u4eec\u521b\u5efa\u4e00\u4e2a\u679a\u4e3e\u7c7b\u578b\u6765\u4ee3\u8868\u5b83\uff1a"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"enum NoteInstruction {\n CreateNote {\n id: u64,\n title: String,\n body: String\n },\n UpdateNote {\n id: u64,\n title: String,\n body: String\n },\n DeleteNote {\n id: u64\n }\n}\n")),(0,r.kt)("p",null,"\u6bcf\u4e2a\u6307\u4ee4\u53d8\u4f53\u7684\u5b57\u8282\u6570\u7ec4\u90fd\u6709\u81ea\u5df1\u7684\u6570\u636e\u7c7b\u578b\uff0c\u6211\u4eec\u5728\u8fd9\u91cc\u6709\u5b83\u4eec\uff01"),(0,r.kt)("p",null,"\u65e2\u7136\u6211\u4eec\u77e5\u9053\u6307\u4ee4\u6570\u636e\u7684\u6837\u5b50\uff0c\u6211\u4eec\u9700\u8981\u5c06\u5176\u4ece\u5b57\u8282\u8f6c\u6362\u4e3a\u8fd9\u4e9b\u7c7b\u578b\u3002\u7b2c\u4e00\u6b65\u662f\u53cd\u5e8f\u5217\u5316\uff0c\u6211\u4eec\u5c06\u4f7f\u7528\u4e00\u4e2a\u4e13\u95e8\u4e3a\u6709\u6548\u8d1f\u8f7d\u521b\u5efa\u7684\u65b0\u7ed3\u6784\u4f53\u4e0a\u7684 ",(0,r.kt)("inlineCode",{parentName:"p"},"BorshDeserialize Traits")," \u6765\u5b8c\u6210\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"#[derive(BorshDeserialize)]\nstruct NoteInstructionPayload {\n id: u64,\n title: String,\n body: String\n}\n")),(0,r.kt)("p",null,"\u6211\u4eec\u5728\u8fd9\u91cc\u5904\u7406\u4e86",(0,r.kt)("inlineCode",{parentName:"p"},"title"),"\u548c",(0,r.kt)("inlineCode",{parentName:"p"},"body"),"\uff0c\u8fd9\u5c31\u662f\u5b57\u8282\u6570\u7ec4\u4e2d\u7684\u5185\u5bb9\u3002",(0,r.kt)("inlineCode",{parentName:"p"},"Borsh"),"\u7684\u5de5\u4f5c\u4ec5\u4ec5\u662f\u6dfb\u52a0\u53cd\u5e8f\u5217\u5316\u7684\u652f\u6301\uff0c\u5b83\u5b9e\u9645\u4e0a\u5e76\u6ca1\u6709\u8fdb\u884c\u53cd\u5e8f\u5217\u5316\uff0c\u800c\u662f\u4ec5\u63d0\u4f9b\u4e86\u6211\u4eec\u53ef\u4ee5\u8c03\u7528\u7684\u53cd\u5e8f\u5217\u5316\u51fd\u6570\u3002"),(0,r.kt)("p",null,"\u4e0b\u4e00\u6b65\uff0c\u6211\u4eec\u8981\u5b9e\u9645\u4f7f\u7528\u8fd9\u4e9b\u51fd\u6570\u6765\u53cd\u5e8f\u5217\u5316\u6570\u636e\u3002\u6211\u4eec\u5c06\u5728\u4e00\u4e2a\u5b9e\u73b0\u4e2d\u5b9a\u4e49\u8fd9\u4e2a\u884c\u4e3a\uff0c\u8fd9\u662f\u4e00\u4e2a\u624b\u52a8\u7684\u8fc7\u7a0b\uff08\u81f3\u5c11\u6682\u65f6\u662f\u8fd9\u6837\uff09\uff01"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"impl NoteInstruction {\n // \u5c06\u4f20\u5165\u7684\u7f13\u51b2\u533a\u89e3\u5305\u5230\u76f8\u5173\u7684\u6307\u4ee4\n // \u8f93\u5165\u7684\u9884\u671f\u683c\u5f0f\u662f\u4e00\u4e2a\u7528Borsh\u5e8f\u5217\u5316\u7684\u5411\u91cf\n pub fn unpack(input: &[u8]) -> Result {\n // \u91c7\u7528\u7b2c\u4e00\u4e2a\u5b57\u8282\u4f5c\u4e3a\u53d8\u4f53\u6765\u786e\u5b9a\u8981\u6267\u884c\u54ea\u4e2a\u6307\u4ee4\n let (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;\n // \u4f7f\u7528\u4e34\u65f6\u6709\u6548\u8f7d\u8377\u7ed3\u6784\u4f53\u8fdb\u884c\u53cd\u5e8f\u5217\u5316\n let payload = NoteInstructionPayload::try_from_slice(rest).unwrap();\n // \u901a\u8fc7\u53d8\u4f53\u5339\u914d\uff0c\u786e\u5b9a\u51fd\u6570\u6240\u671f\u671b\u7684\u6570\u636e\u7ed3\u6784\uff0c\u7136\u540e\u8fd4\u56deTestStruct\u6216\u9519\u8bef\n Ok(match variant {\n 0 => Self::CreateNote {\n title: payload.title,\n body: payload.body,\n id: payload.id\n },\n 1 => Self::UpdateNote {\n title: payload.title,\n body: payload.body,\n id: payload.id\n },\n 2 => Self::DeleteNote {\n id: payload.id\n },\n _ => return Err(ProgramError::InvalidInstructionData)\n })\n }\n}\n")),(0,r.kt)("p",null,"\u8fd9\u90e8\u5206\u5185\u5bb9\u53ef\u80fd\u4e00\u5f00\u59cb\u770b\u8d77\u6765\u6709\u70b9\u5413\u4eba\uff0c\u4f46\u4f60\u5f88\u5feb\u5c31\u4f1a\u89c9\u5f97\u5b83\u5176\u5b9e\u975e\u5e38\u76f4\u63a5\u548c\u7b80\u5355\uff01\u8ba9\u6211\u4eec\u4e00\u8d77\u6765\u6df1\u5165\u5206\u6790\u4e00\u4e0b \ud83d\udd7a\ud83d\udc83\ud83d\udc6f\u200d\u2642\ufe0f"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"pub fn unpack(input: &[u8]) -> Result {\n")),(0,r.kt)("p",null,"\u6211\u4eec\u7684\u89e3\u5305\u51fd\u6570\u4ece\u6307\u4ee4\u4e2d\u83b7\u53d6\u5b57\u8282\uff0c\u5e76\u8fd4\u56de\u4e00\u4e2a",(0,r.kt)("inlineCode",{parentName:"p"},"NoteInstruction"),"\u7c7b\u578b\uff08\u5373 ",(0,r.kt)("inlineCode",{parentName:"p"},"Self"),"\uff09\u6216\u4e00\u4e2a",(0,r.kt)("inlineCode",{parentName:"p"},"ProgramError"),"\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"let (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;\n")),(0,r.kt)("p",null,"\u73b0\u5728\u662f\u65f6\u5019\u4ece\u5b57\u8282\u4e2d\u89e3\u5305\u6570\u636e\u5e76\u8c03\u7528\u53cd\u5e8f\u5217\u5316\u51fd\u6570\u4e86\u3002\u6211\u4eec\u7684\u6307\u4ee4\u6570\u636e\u7684\u7b2c\u4e00\u4e2a\u5b57\u8282\u662f\u4e00\u4e2a\u6574\u6570\uff0c\u5b83\u544a\u8bc9\u6211\u4eec\u6b63\u5728\u5904\u7406\u54ea\u4e2a\u6307\u4ee4\u3002\u6211\u4eec\u8fd9\u6837\u505a\u7684\u65b9\u5f0f\u662f\u4f7f\u7528",(0,r.kt)("inlineCode",{parentName:"p"},"Rust"),"\u7684\u5185\u7f6e\u51fd\u6570",(0,r.kt)("inlineCode",{parentName:"p"},"split_first"),"\u3002\u5982\u679c\u5207\u7247\u4e3a\u7a7a\uff0c",(0,r.kt)("inlineCode",{parentName:"p"},"ok_or"),"\u5c06\u8fd4\u56de",(0,r.kt)("inlineCode",{parentName:"p"},"ProgramError"),"\u679a\u4e3e\u4e2d\u7684",(0,r.kt)("inlineCode",{parentName:"p"},"InvalidInstructionData"),"\u9519\u8bef\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"let payload = NoteInstructionPayload::try_from_slice(rest).unwrap();\n")),(0,r.kt)("p",null,"\u73b0\u5728\u6211\u4eec\u6709\u4e86\u4e24\u4e2a\u53d8\u91cf\u8981\u5904\u7406\uff1a\u6307\u4ee4\u6307\u793a\u5668\u548c\u6307\u4ee4\u7684\u6709\u6548\u8f7d\u8377\uff08\u6570\u636e\uff09\u3002",(0,r.kt)("inlineCode",{parentName:"p"},"Borsh"),"\u5728\u6211\u4eec\u7684\u6709\u6548\u8f7d\u8377\u7ed3\u6784\u4e2d\u6dfb\u52a0\u4e86",(0,r.kt)("inlineCode",{parentName:"p"},"try_from_slice"),"\u51fd\u6570\uff0c\u4f7f\u6211\u4eec\u53ef\u4ee5\u5728\u6709\u6548\u8f7d\u8377\u53d8\u91cf",(0,r.kt)("inlineCode",{parentName:"p"},"rest"),"\u4e0a\u8c03\u7528\u5b83\u3002\u8fd9\u5c31\u662f\u53cd\u5e8f\u5217\u5316\u7684\u8fc7\u7a0b\uff01"),(0,r.kt)("p",null,"\u63a5\u4e0b\u6765\u7684\u6b65\u9aa4\u5305\u62ec\uff1a"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"\u5c06\u6307\u4ee4\u6570\u636e\u5b9a\u4e49\u4e3a",(0,r.kt)("inlineCode",{parentName:"li"},"Rust"),"\u7c7b\u578b\u4e2d\u7684\u679a\u4e3e\u3002"),(0,r.kt)("li",{parentName:"ul"},"\u5b9a\u4e49\u8d1f\u8f7d\u7ed3\u6784\u4f53\u3002"),(0,r.kt)("li",{parentName:"ul"},"\u5728\u8d1f\u8f7d\u7ed3\u6784\u4f53\u4e0a\u58f0\u660e",(0,r.kt)("inlineCode",{parentName:"li"},"BorshDeserialize"),"\u5b8f\u3002"),(0,r.kt)("li",{parentName:"ul"},"\u4e3a\u8d1f\u8f7d\u7ed3\u6784\u4f53\u521b\u5efa\u4e00\u4e2a\u5b9e\u73b0\uff08\u5b57\u8282 -> \u7ed3\u6784\u4f53\uff09\u3002"),(0,r.kt)("li",{parentName:"ul"},"\u521b\u5efa",(0,r.kt)("inlineCode",{parentName:"li"},"unpack"),"\u51fd\u6570\uff0c\u8be5\u51fd\u6570\u63a5\u6536\u6307\u4ee4\u6570\u636e\u5e76\u5bf9\u5176\u8fdb\u884c\u53cd\u5e8f\u5217\u5316\u3002")),(0,r.kt)("p",null,"\u6211\u4eec",(0,r.kt)("inlineCode",{parentName:"p"},"unpack"),"\u51fd\u6570\u7684\u6700\u540e\u4e00\u6b65\u662f\u5c06\u53cd\u5e8f\u5217\u5316\u7684\u6570\u636e\u8f6c\u6362\u4e3a\u679a\u4e3e\u53d8\u4f53\uff08\u5373\u6307\u4ee4\u6570\u636e\u7c7b\u578b\uff09\u3002\u6211\u4eec\u5c06\u4f7f\u7528\u5339\u914d\u8bed\u53e5\u6765\u5b8c\u6210\u8fd9\u4e2a\u4efb\u52a1\uff0c\u901a\u8fc7\u5339\u914d\u6307\u4ee4\u6307\u793a\u5668\uff0c\u6211\u4eec\u53ef\u4ee5\u8fd4\u56de\u679a\u4e3e\u7684\u6b63\u786e\u53d8\u4f53\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"Ok(match variant {\n 0 => Self::CreateNote {\n title: payload.title,\n body: payload.body,\n id: payload.id\n },\n 1 => Self::UpdateNote {\n title: payload.title,\n body: payload.body,\n id: payload.id\n },\n 2 => Self::DeleteNote {\n id: payload.id\n },\n _ => return Err(ProgramError::InvalidInstructionData)\n})\n")),(0,r.kt)("p",null,"\u73b0\u5728\u4f60\u5df2\u7ecf\u77e5\u9053\u4e86\u6574\u4e2a\u8fc7\u7a0b\uff01\u7406\u89e3\u8fd9\u4e00\u5207\u786e\u5b9e\u9700\u8981\u96c6\u4e2d\u7cbe\u529b\uff0c\u6240\u4ee5\u5982\u679c\u4f60\u9700\u8981\u591a\u8bfb\u51e0\u904d\uff0c\u4e5f\u5b8c\u5168\u6ca1\u5173\u7cfb\u3002"),(0,r.kt)("p",null,"\u8fd9\u90e8\u5206\u5185\u5bb9\u4fe1\u606f\u91cf\u8f83\u5927\uff0c\u53ef\u80fd\u4f1a\u8ba9\u4eba\u89c9\u5f97\u6709\u4e9b\u590d\u6742\u3002\u4f46\u522b\u62c5\u5fc3\uff0c\u6211\u4eec\u4f1a\u901a\u8fc7\u5927\u91cf\u7684\u7ec3\u4e60\u6765\u9010\u6e10\u719f\u6089\u5b83\u4eec\u3002\u968f\u7740\u65f6\u95f4\u7684\u63a8\u79fb\u548c\u53cd\u590d\u7ec3\u4e60\uff0c\u4f60\u4f1a\u53d1\u73b0\u8fd9\u4e9b\u5185\u5bb9\u5f00\u59cb\u53d8\u5f97\u66f4\u52a0\u76f4\u89c2\u548c\u6613\u61c2\u3002"),(0,r.kt)("h2",{id:"-\u7a0b\u5e8f\u903b\u8f91"},"\ud83d\ude80 \u7a0b\u5e8f\u903b\u8f91"),(0,r.kt)("p",null,"\u6211\u4eec\u5df2\u7ecf\u89e3\u538b\u4e86\u6307\u4ee4\u6570\u636e\uff0c\u51c6\u5907\u6295\u5165\u5b9e\u9645\u4f7f\u7528\u3002\u73b0\u5728\uff0c\u6211\u4eec\u9700\u8981\u9488\u5bf9\u6bcf\u4e2a\u6307\u4ee4\u7f16\u5199\u76f8\u5e94\u7684\u903b\u8f91\u5904\u7406\u3002\u8fd9\u90e8\u5206\u5176\u5b9e\u662f\u6700\u4e3a\u76f4\u89c2\u548c\u7b80\u5355\u7684\uff01\u76f8\u5bf9\u4e8e\u590d\u6742\u7684\u53cd\u5e8f\u5217\u5316\u5904\u7406\uff0c\u8fd9\u4e00\u90e8\u5206\u5c31\u50cf\u5403\u86cb\u7cd5\u4e00\u6837\u8f7b\u677e\u4e86\uff08",(0,r.kt)("inlineCode",{parentName:"p"},"Anchor"),"\u4f1a\u4e3a\u4f60\u5904\u7406\u5927\u90e8\u5206\u53cd\u5e8f\u5217\u5316\u5de5\u4f5c\uff09\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},'entrypoint!(process_instruction);\n\npub fn process_instruction(\n program_id: &Pubkey,\n accounts: &[AccountInfo],\n instruction_data: &[u8]\n) -> ProgramResult {\n\n // \u89e3\u538b\u6307\u4ee4\u6570\u636e\n let instruction = NoteInstruction::unpack(instruction_data)?;\n\n // \u5339\u914d\u6307\u4ee4\u5e76\u6267\u884c\u76f8\u5e94\u7684\u903b\u8f91\n match instruction {\n NoteInstruction::CreateNote { title, body, id } => {\n msg!("Instruction: CreateNote");\n create_note(accounts, title, body, id, program_id)\n }\n NoteInstruction::UpdateNote { title, body, id } => {\n msg!("Instruction: UpdateNote");\n update_note(accounts, title, body, id)\n }\n NoteInstruction::DeleteNote { id } => {\n msg!("Instruction: DeleteNote");\n delete_note(accounts, id)\n }\n }\n}\n')),(0,r.kt)("p",null,"\u9996\u5148\uff0c\u6211\u4eec\u8981\u505a\u7684\u662f\u5b9a\u4e49\u7a0b\u5e8f\u7684\u5165\u53e3\u51fd\u6570\u3002",(0,r.kt)("inlineCode",{parentName:"p"},"process_instruction")," \u51fd\u6570\u7684\u5b9a\u4e49\u4e0e\u6211\u4eec\u4e4b\u524d\u7684 \u201c",(0,r.kt)("inlineCode",{parentName:"p"},"Hello World"),"\u201d \u7a0b\u5e8f\u4e00\u6837\u3002\u63a5\u7740\uff0c\u6211\u4eec\u5c06\u5728 ",(0,r.kt)("inlineCode",{parentName:"p"},"NoteInstruction")," \u7684\u5b9e\u73b0\u4e2d\u4f7f\u7528 ",(0,r.kt)("inlineCode",{parentName:"p"},"unpack")," \u51fd\u6570\u6765\u63d0\u53d6\u6307\u4ee4\u6570\u636e\u3002\u7136\u540e\uff0c\u6211\u4eec\u53ef\u4ee5\u4f9d\u9760 ",(0,r.kt)("inlineCode",{parentName:"p"},"NoteInstruction")," \u679a\u4e3e\u6765\u786e\u5b9a\u6307\u4ee4\u7684\u5177\u4f53\u7c7b\u578b\u3002"),(0,r.kt)("p",null,"\u5728\u672c\u9636\u6bb5\uff0c\u6211\u4eec\u8fd8\u6ca1\u6709\u6d89\u53ca\u5177\u4f53\u7684\u903b\u8f91\u5904\u7406\uff0c\u771f\u6b63\u7684\u6784\u5efa\u5c06\u5728\u540e\u7eed\u9636\u6bb5\u5c55\u5f00\u3002"),(0,r.kt)("h2",{id:"-\u6587\u4ef6\u7ed3\u6784\u8bf4\u660e"},"\ud83d\udcc2 \u6587\u4ef6\u7ed3\u6784\u8bf4\u660e"),(0,r.kt)("p",null,"\u7f16\u5199\u81ea\u5b9a\u4e49\u7a0b\u5e8f\u65f6\uff0c\u5c06\u4ee3\u7801\u5212\u5206\u4e3a\u4e0d\u540c\u7684\u6587\u4ef6\u7ed3\u6784\u4f1a\u975e\u5e38\u6709\u52a9\u4e8e\u7ba1\u7406\u3002\u8fd9\u6837\u505a\u4e0d\u4ec5\u65b9\u4fbf\u4ee3\u7801\u91cd\u7528\uff0c\u8fd8\u80fd\u8ba9\u4f60\u66f4\u5feb\u901f\u5730\u627e\u5230\u6240\u9700\u7684\u5185\u5bb9\u3002"),(0,r.kt)("p",null,(0,r.kt)("img",{src:t(52228).Z,width:"1350",height:"582"})),(0,r.kt)("p",null,"\u9664\u4e86 ",(0,r.kt)("inlineCode",{parentName:"p"},"lib.rs")," \u6587\u4ef6\u5916\uff0c\u6211\u4eec\u8fd8\u4f1a\u628a\u7a0b\u5e8f\u7684\u5404\u4e2a\u90e8\u5206\u653e\u5165\u4e0d\u540c\u7684\u6587\u4ef6\u3002\u6700\u660e\u663e\u7684\u4e00\u4e2a\u4f8b\u5b50\u5c31\u662f ",(0,r.kt)("inlineCode",{parentName:"p"},"instruction.rs")," \u6587\u4ef6\u3002\u5728\u8fd9\u91cc\uff0c\u6211\u4eec\u5c06\u5b9a\u4e49\u6307\u4ee4\u6570\u636e\u7c7b\u578b\u5e76\u5b9e\u73b0\u5bf9\u6307\u4ee4\u6570\u636e\u7684\u89e3\u5305\u529f\u80fd\u3002"),(0,r.kt)("p",null,(0,r.kt)("strong",{parentName:"p"},"\u4f60\u505a\u5f97\u771f\u68d2\ud83d\udc4f\ud83d\udc4f\ud83d\udc4f")),(0,r.kt)("p",null,"\u6211\u60f3\u501f\u6b64\u673a\u4f1a\u5bf9\u4f60\u7684\u52aa\u529b\u4ed8\u51fa\u8868\u793a\u8d5e\u8d4f\u3002\u4f60\u6b63\u5728\u5b66\u4e60\u4e00\u4e9b\u5f3a\u5927\u4e14\u5b9e\u7528\u7684\u6280\u80fd\uff0c\u8fd9\u4e9b\u6280\u80fd\u4e0d\u4ec5\u5728 ",(0,r.kt)("inlineCode",{parentName:"p"},"Solana")," \u9886\u57df\u6709\u7528\uff0c",(0,r.kt)("inlineCode",{parentName:"p"},"Rust")," \u7684\u5e94\u7528\u4e5f\u975e\u5e38\u5e7f\u6cdb\u3002\u5c3d\u7ba1\u5b66\u4e60 ",(0,r.kt)("inlineCode",{parentName:"p"},"Solana")," \u53ef\u80fd\u4f1a\u6709\u4e00\u4e9b\u56f0\u96be\uff0c\u4f46\u8bf7\u8bb0\u4f4f\uff0c\u8fd9\u6837\u7684\u56f0\u96be\u4e5f\u6709\u4eba\u66fe\u7ecf\u7ecf\u5386\u5e76\u6218\u80dc\u3002\u4f8b\u5982\uff0c",(0,r.kt)("inlineCode",{parentName:"p"},"FormFunction")," \u7684\u521b\u59cb\u4eba\u5728\u5927\u7ea6\u4e00\u5e74\u524d\u7684\u63a8\u6587\u4e2d\u63d0\u5230\u4e86\u4ed6\u662f\u5982\u4f55\u627e\u5230\u56f0\u96be\u7684\uff1a"),(0,r.kt)("p",null,(0,r.kt)("img",{src:t(86337).Z,width:"1254",height:"778"})),(0,r.kt)("p",null,(0,r.kt)("inlineCode",{parentName:"p"},"FormFunction")," \u5df2\u7ecf\u7b79\u96c6\u4e86\u8d85\u8fc7 ",(0,r.kt)("inlineCode",{parentName:"p"},"470")," \u4e07\u7f8e\u5143\uff0c\u662f\u6211\u5fc3\u76ee\u4e2d ",(0,r.kt)("inlineCode",{parentName:"p"},"Solana")," \u4e0a\u6700\u4f18\u79c0\u7684 ",(0,r.kt)("inlineCode",{parentName:"p"},"1/1 NFT")," \u5e73\u53f0\u3002",(0,r.kt)("inlineCode",{parentName:"p"},"Matt")," \u51ed\u501f\u575a\u6301\u4e0d\u61c8\u7684\u52aa\u529b\u5efa\u7acb\u4e86\u4e00\u4e9b\u4ee4\u4eba\u96be\u4ee5\u7f6e\u4fe1\u7684\u4e1c\u897f\u3002\u8bd5\u60f3\u4e00\u4e0b\uff0c\u5982\u679c\u4f60\u638c\u63e1\u4e86\u8fd9\u4e9b\u6280\u80fd\uff0c\u4e00\u5e74\u540e\u4f60\u4f1a\u7ad9\u5728\u54ea\u91cc\u5462\uff1f"))}m.isMDXComponent=!0},52228:(e,n,t)=>{t.d(n,{Z:()=>a});const a=t.p+"assets/images/file-structure-eee2fba4bbd91380e0f837e4601d2b85.png"},86337:(e,n,t)=>{t.d(n,{Z:()=>a});const a=t.p+"assets/images/solana-learner-b7d0ee3d1fd3fa79f7d6c7825337e785.png"},14397:(e,n,t)=>{t.d(n,{Z:()=>a});const a=t.p+"assets/images/the-rust-layer-cake-ca50fcbc4c5288f3a4e41245ee64a4af.png"}}]); \ No newline at end of file +"use strict";(self.webpackChunkall_in_one_solana=self.webpackChunkall_in_one_solana||[]).push([[5932],{3905:(e,n,t)=>{t.d(n,{Zo:()=>u,kt:()=>k});var a=t(67294);function r(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function l(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);n&&(a=a.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,a)}return t}function i(e){for(var n=1;n=0||(r[t]=e[t]);return r}(e,n);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(r[t]=e[t])}return r}var p=a.createContext({}),s=function(e){var n=a.useContext(p),t=n;return e&&(t="function"==typeof e?e(n):i(i({},n),e)),t},u=function(e){var n=s(e.components);return a.createElement(p.Provider,{value:n},e.children)},d="mdxType",m={inlineCode:"code",wrapper:function(e){var n=e.children;return a.createElement(a.Fragment,{},n)}},c=a.forwardRef((function(e,n){var t=e.components,r=e.mdxType,l=e.originalType,p=e.parentName,u=o(e,["components","mdxType","originalType","parentName"]),d=s(t),c=r,k=d["".concat(p,".").concat(c)]||d[c]||m[c]||l;return t?a.createElement(k,i(i({ref:n},u),{},{components:t})):a.createElement(k,i({ref:n},u))}));function k(e,n){var t=arguments,r=n&&n.mdxType;if("string"==typeof e||r){var l=t.length,i=new Array(l);i[0]=c;var o={};for(var p in n)hasOwnProperty.call(n,p)&&(o[p]=n[p]);o.originalType=e,o[d]="string"==typeof e?e:r,i[1]=o;for(var s=2;s{t.r(n),t.d(n,{assets:()=>p,contentTitle:()=>i,default:()=>m,frontMatter:()=>l,metadata:()=>o,toc:()=>s});var a=t(87462),r=(t(67294),t(3905));const l={sidebar_position:55,sidebar_label:"\ud83c\udf82 Rust\u7684\u5206\u5c42\u86cb\u7cd5",sidebar_class_name:"green",tags:["native-solana-development","solana","native-solana-program","program","rust-layer-cake"]},i="\ud83c\udf82 Rust\u7684\u5206\u5c42\u86cb\u7cd5",o={unversionedId:"module3/native-solana-development/the-rust-layer-cake/README",id:"module3/native-solana-development/the-rust-layer-cake/README",title:"\ud83c\udf82 Rust\u7684\u5206\u5c42\u86cb\u7cd5",description:"\u6211\u4eec\u5c06\u5728\u6e38\u4e50\u573a\u4e0a\u5236\u4f5c\u4e00\u4e2a\u7b80\u5355\u7684Hello World\u7a0b\u5e8f\uff0c\u4ec5\u4ec5\u4f1a\u5728\u4ea4\u6613\u65e5\u5fd7\u4e2d\u8bb0\u5f55\u4e00\u6761\u6d88\u606f\u3002\u62db\u547c\u5df2\u7ecf\u6253\u8fc7\u4e86\u3002\u73b0\u5728\u662f\u65f6\u5019\u5b66\u4e60\u5982\u4f55\u5904\u7406\u6307\u4ee4\u6570\u636e\uff0c\u5c31\u50cf\u5728\u5ba2\u6237\u7aef\u5f00\u53d1\u4e2d\u4e00\u6837\u3002",source:"@site/docs/Solana-Co-Learn/module3/native-solana-development/the-rust-layer-cake/README.md",sourceDirName:"module3/native-solana-development/the-rust-layer-cake",slug:"/module3/native-solana-development/the-rust-layer-cake/",permalink:"/solana-co-learn/Solana-Co-Learn/module3/native-solana-development/the-rust-layer-cake/",draft:!1,editUrl:"https://github.com/CreatorsDAO/solana-co-learn/tree/main/docs/Solana-Co-Learn/module3/native-solana-development/the-rust-layer-cake/README.md",tags:[{label:"native-solana-development",permalink:"/solana-co-learn/Solana-Co-Learn/tags/native-solana-development"},{label:"solana",permalink:"/solana-co-learn/Solana-Co-Learn/tags/solana"},{label:"native-solana-program",permalink:"/solana-co-learn/Solana-Co-Learn/tags/native-solana-program"},{label:"program",permalink:"/solana-co-learn/Solana-Co-Learn/tags/program"},{label:"rust-layer-cake",permalink:"/solana-co-learn/Solana-Co-Learn/tags/rust-layer-cake"}],version:"current",lastUpdatedBy:"v1xingyue",lastUpdatedAt:1727439952,formattedLastUpdatedAt:"Sep 27, 2024",sidebarPosition:55,frontMatter:{sidebar_position:55,sidebar_label:"\ud83c\udf82 Rust\u7684\u5206\u5c42\u86cb\u7cd5",sidebar_class_name:"green",tags:["native-solana-development","solana","native-solana-program","program","rust-layer-cake"]},sidebar:"tutorialSidebar",previous:{title:"\u539f\u751fSOLANA\u5f00\u53d1",permalink:"/solana-co-learn/Solana-Co-Learn/module3/native-solana-development/"},next:{title:"\ud83c\udfa5 \u6784\u5efa\u4e00\u4e2a\u7535\u5f71\u8bc4\u8bba\u7a0b\u5e8f",permalink:"/solana-co-learn/Solana-Co-Learn/module3/native-solana-development/build-a-movie-review-program/"}},p={},s=[{value:"Rust\u7684\u5206\u5c42\u86cb\u7cd5",id:"rust\u7684\u5206\u5c42\u86cb\u7cd5",level:2},{value:"\ud83d\udc76 \u53d8\u91cf\u58f0\u660e\u548c\u53ef\u53d8\u6027",id:"-\u53d8\u91cf\u58f0\u660e\u548c\u53ef\u53d8\u6027",level:2},{value:"\ud83c\udf71 \u7ed3\u6784\u4f53",id:"-\u7ed3\u6784\u4f53",level:2},{value:"\ud83d\udcdc \u679a\u4e3e\u3001\u53d8\u4f53\u548c\u5339\u914d",id:"-\u679a\u4e3e\u53d8\u4f53\u548c\u5339\u914d",level:2},{value:"\ud83d\udce6 \u5b9e\u73b0",id:"-\u5b9e\u73b0",level:2},{value:"\ud83c\udf81 \u7279\u5f81\uff08Traits\uff09",id:"-\u7279\u5f81traits",level:2},{value:"\ud83c\udf82 \u628a\u6240\u6709\u5143\u7d20\u6574\u5408\u5728\u4e00\u8d77",id:"-\u628a\u6240\u6709\u5143\u7d20\u6574\u5408\u5728\u4e00\u8d77",level:2},{value:"\ud83d\ude80 \u7a0b\u5e8f\u903b\u8f91",id:"-\u7a0b\u5e8f\u903b\u8f91",level:2},{value:"\ud83d\udcc2 \u6587\u4ef6\u7ed3\u6784\u8bf4\u660e",id:"-\u6587\u4ef6\u7ed3\u6784\u8bf4\u660e",level:2}],u={toc:s},d="wrapper";function m(e){let{components:n,...l}=e;return(0,r.kt)(d,(0,a.Z)({},u,l,{components:n,mdxType:"MDXLayout"}),(0,r.kt)("h1",{id:"-rust\u7684\u5206\u5c42\u86cb\u7cd5"},"\ud83c\udf82 Rust\u7684\u5206\u5c42\u86cb\u7cd5"),(0,r.kt)("p",null,"\u6211\u4eec\u5c06\u5728\u6e38\u4e50\u573a\u4e0a\u5236\u4f5c\u4e00\u4e2a\u7b80\u5355\u7684",(0,r.kt)("inlineCode",{parentName:"p"},"Hello World"),"\u7a0b\u5e8f\uff0c\u4ec5\u4ec5\u4f1a\u5728\u4ea4\u6613\u65e5\u5fd7\u4e2d\u8bb0\u5f55\u4e00\u6761\u6d88\u606f\u3002\u62db\u547c\u5df2\u7ecf\u6253\u8fc7\u4e86\u3002\u73b0\u5728\u662f\u65f6\u5019\u5b66\u4e60\u5982\u4f55\u5904\u7406\u6307\u4ee4\u6570\u636e\uff0c\u5c31\u50cf\u5728\u5ba2\u6237\u7aef\u5f00\u53d1\u4e2d\u4e00\u6837\u3002"),(0,r.kt)("p",null,"\u5728\u5f00\u59cb\u6784\u5efa\u4e4b\u524d\uff0c\u6211\u60f3\u5148\u7ed9\u4f60\u4ecb\u7ecd\u4e00\u4e9b\u5373\u5c06\u4f7f\u7528\u7684\u6982\u5ff5\u3002\u8fd8\u8bb0\u5f97\u6211\u63d0\u5230\u7684\u89c4\u5219\u3001\u80fd\u529b\u548c\u4e92\u52a8\u5417\uff1f\u6211\u4f1a\u5e26\u4f60\u4e86\u89e3\u4e00\u4e0b\u7f16\u5199\u672c\u5730",(0,r.kt)("inlineCode",{parentName:"p"},"Solana"),"\u7a0b\u5e8f\u6240\u9700\u7684\u80fd\u529b\u548c\u89c4\u5219\u3002\u8fd9\u91cc\u7684\u201c\u672c\u5730\u201d\u975e\u5e38\u91cd\u8981 - \u6211\u4eec\u5c06\u5728\u540e\u7eed\u90e8\u5206\u501f\u52a9",(0,r.kt)("inlineCode",{parentName:"p"},"Anchor"),"\u6765\u5904\u7406\u6211\u4eec\u73b0\u5728\u6240\u5b66\u7684\u8bb8\u591a\u5185\u5bb9\u3002"),(0,r.kt)("p",null,"\u6211\u4eec\u5b66\u4e60\u539f\u751f\u5f00\u53d1\u7684\u539f\u56e0\u662f\u56e0\u4e3a\u4e86\u89e3\u5e95\u5c42\u5de5\u4f5c\u539f\u7406\u662f\u975e\u5e38\u91cd\u8981\u7684\u3002\u4e00\u65e6\u4f60\u7406\u89e3\u4e86\u4e8b\u7269\u662f\u5982\u4f55\u5728\u6700\u57fa\u672c\u7684\u5c42\u9762\u4e0a\u8fd0\u4f5c\u7684\uff0c\u5c31\u80fd\u591f\u501f\u52a9\u50cf",(0,r.kt)("inlineCode",{parentName:"p"},"Anchor"),"\u8fd9\u6837\u7684\u5de5\u5177\u6765\u6784\u5efa\u66f4\u5f3a\u5927\u7684\u7a0b\u5e8f\u3002\u4f60\u53ef\u4ee5\u628a\u8fd9\u4e2a\u8fc7\u7a0b\u60f3\u8c61\u6210\u4e0e\u4e0d\u540c\u7c7b\u578b\u7684\u654c\u4eba\u8fdb\u884c\u9996\u9886\u6218 \uff0c \u4f60\u9700\u8981\u5148\u5b66\u4f1a\u5982\u4f55\u9010\u4e00\u5bf9\u6297\u6bcf\u4e2a\u4e2a\u4f53\u602a\u7269\uff08\u4ee5\u53ca\u4e86\u89e3\u4f60\u81ea\u5df1\u7684\u80fd\u529b\uff09\u3002"),(0,r.kt)("p",null,"\u5f53\u6211\u521a\u5f00\u59cb\u5b66\u4e60\u7684\u65f6\u5019\uff0c\u6211\u53d1\u73b0\u5f88\u96be\u7406\u89e3\u81ea\u5df1\u7f3a\u5c11\u4e86\u4ec0\u4e48\u3002\u6240\u4ee5\u6211\u5c06\u5176\u5206\u89e3\u6210\u4e86\u201c\u5c42\u6b21\u201d\u3002\u6bcf\u4e00\u4e2a\u4f60\u5b66\u4e60\u7684\u4e3b\u9898\u90fd\u5efa\u7acb\u5728\u4e00\u5c42\u77e5\u8bc6\u7684\u57fa\u7840\u4e4b\u4e0a\u3002\u5982\u679c\u9047\u5230\u4e0d\u660e\u767d\u7684\u5730\u65b9\uff0c\u56de\u5230\u4e4b\u524d\u7684\u5c42\u6b21\uff0c\u786e\u4fdd\u4f60\u771f\u6b63\u7406\u89e3\u4e86\u5b83\u4eec\u3002"),(0,r.kt)("h2",{id:"rust\u7684\u5206\u5c42\u86cb\u7cd5"},"Rust\u7684\u5206\u5c42\u86cb\u7cd5"),(0,r.kt)("p",null,(0,r.kt)("img",{src:t(14397).Z,width:"3693",height:"2476"})),(0,r.kt)("p",null,"\u8fd9\u662f\u4e00\u4e2a\u7531",(0,r.kt)("inlineCode",{parentName:"p"},"Rust"),"\u5236\u4f5c\u7684\u86cb\u7cd5\u3002"),(0,r.kt)("admonition",{type:"caution"},(0,r.kt)("p",{parentName:"admonition"},"\u6ce8\u610f\uff1a\u56fe\u5c42\u4ee3\u8868\u91cd\u91cf\uff01")),(0,r.kt)("h2",{id:"-\u53d8\u91cf\u58f0\u660e\u548c\u53ef\u53d8\u6027"},"\ud83d\udc76 \u53d8\u91cf\u58f0\u660e\u548c\u53ef\u53d8\u6027"),(0,r.kt)("p",null,"\u53d8\u91cf\u3002\u4f60\u4e86\u89e3\u5b83\u4eec\u3002\u4f60\u4f7f\u7528\u8fc7\u5b83\u4eec\u3002\u4f60\u751a\u81f3\u53ef\u80fd\u62fc\u5199\u9519\u8bef\u8fc7\u5b83\u4eec\u3002\u5728",(0,r.kt)("inlineCode",{parentName:"p"},"Rust"),"\u4e2d\u5173\u4e8e\u53d8\u91cf\u552f\u4e00\u7684\u65b0\u6982\u5ff5\u5c31\u662f\u53ef\u53d8\u6027\u3002\u6240\u6709\u53d8\u91cf\u9ed8\u8ba4\u90fd\u662f\u4e0d\u53ef\u53d8\u7684 \uff0c \u4e00\u65e6\u58f0\u660e\u4e86\u53d8\u91cf\uff0c\u5c31\u4e0d\u80fd\u6539\u53d8\u5176\u503c\u3002\u4f60\u53ea\u9700\u901a\u8fc7\u6dfb\u52a0",(0,r.kt)("inlineCode",{parentName:"p"},"mut"),"\u5173\u952e\u5b57\u544a\u8bc9\u7f16\u8bd1\u5668\u4f60\u60f3\u8981\u4e00\u4e2a\u53ef\u53d8\u7684\u53d8\u91cf\u3002\u5c31\u662f\u8fd9\u4e48\u7b80\u5355\u3002\u5982\u679c\u6211\u4eec\u4e0d\u6307\u5b9a\u7c7b\u578b\uff0c\u7f16\u8bd1\u5668\u4f1a\u6839\u636e\u63d0\u4f9b\u7684\u6570\u636e\u8fdb\u884c\u63a8\u65ad\uff0c\u5e76\u5f3a\u5236\u6211\u4eec\u4fdd\u6301\u8be5\u7c7b\u578b\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"// compiler will throw error\nlet age = 33;\nage = 34;\n\n// this is allowed\nlet mut mutable_age = 33;\nmutable_age = 34;\n")),(0,r.kt)("h2",{id:"-\u7ed3\u6784\u4f53"},"\ud83c\udf71 \u7ed3\u6784\u4f53"),(0,r.kt)("p",null,"\u7ed3\u6784\u4f53\u662f\u81ea\u5b9a\u4e49\u7684\u6570\u636e\u7ed3\u6784\uff0c\u4e00\u79cd\u5c06\u6570\u636e\u7ec4\u7ec7\u5728\u4e00\u8d77\u7684\u65b9\u5f0f\u3002\u5b83\u4eec\u662f\u4f60\u5b9a\u4e49\u7684\u81ea\u5b9a\u4e49\u6570\u636e\u7c7b\u578b\uff0c\u7c7b\u4f3c\u4e8e",(0,r.kt)("inlineCode",{parentName:"p"},"JavaScript"),"\u4e2d\u7684\u5bf9\u8c61\u3002",(0,r.kt)("inlineCode",{parentName:"p"},"Rust"),"\u5e76\u4e0d\u662f\u5b8c\u5168\u9762\u5411\u5bf9\u8c61\u7684 - \u7ed3\u6784\u4f53\u672c\u8eab\u9664\u4e86\u4fdd\u5b58\u6709\u7ec4\u7ec7\u7684\u6570\u636e\u5916\uff0c\u65e0\u6cd5\u6267\u884c\u4efb\u4f55\u64cd\u4f5c\u3002\u4f46\u4f60\u53ef\u4ee5\u5411\u7ed3\u6784\u4f53\u6dfb\u52a0\u65b9\u6cd5\uff0c\u4f7f\u5176\u8868\u73b0\u5f97\u66f4\u50cf\u5bf9\u8c61\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},'Struct User {\n active: bool,\n email: String,\n age: u64\n}\n\nlet mut user1 = User {\n active: true,\n email: String::from("test@test.com"),\n age: 33\n};\n\nuser1.age = 34;\n')),(0,r.kt)("h2",{id:"-\u679a\u4e3e\u53d8\u4f53\u548c\u5339\u914d"},"\ud83d\udcdc \u679a\u4e3e\u3001\u53d8\u4f53\u548c\u5339\u914d"),(0,r.kt)("p",null,"\u679a\u4e3e\u5f88\u7b80\u5355 - \u5b83\u4eec\u5c31\u50cf\u4ee3\u7801\u4e2d\u7684\u4e0b\u62c9\u5217\u8868\u3002\u5b83\u4eec\u9650\u5236\u4f60\u4ece\u51e0\u4e2a\u53ef\u80fd\u7684\u53d8\u4f53\u4e2d\u9009\u62e9\u4e00\u4e2a\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"enum LightStatus {\n On,\n Off\n}\n")),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},'enum LightStatus {\n On {\n color: String\n },\n Off\n}\n\nlet light_status = LightStatus::On {\n color: String::from("red")\n};\n')),(0,r.kt)("p",null,(0,r.kt)("inlineCode",{parentName:"p"},"Rust"),"\u4e2d\u679a\u4e3e\u7684\u9177\u70ab\u4e4b\u5904\u5728\u4e8e\u4f60\u53ef\u4ee5\uff08\u53ef\u9009\u5730\uff09\u5411\u5176\u4e2d\u6dfb\u52a0\u6570\u636e\uff0c\u4f7f\u5176\u51e0\u4e4e\u50cf\u4e00\u4e2a\u8ff7\u4f60\u7684",(0,r.kt)("inlineCode",{parentName:"p"},"if"),"\u8bed\u53e5\u3002\u5728\u8fd9\u4e2a\u4f8b\u5b50\u4e2d\uff0c\u4f60\u6b63\u5728\u9009\u62e9\u4ea4\u901a\u4fe1\u53f7\u706f\u7684\u72b6\u6001\u3002\u5982\u679c\u5b83\u662f\u5f00\u542f\u7684\uff0c\u4f60\u9700\u8981\u6307\u5b9a\u989c\u8272 - \u662f\u7ea2\u8272\u3001\u9ec4\u8272\u8fd8\u662f\u7eff\u8272\uff1f"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"enum Coin {\n Penny,\n Nickel,\n Dime,\n Quarter,\n}\n\nfn value_in_cents(coin: Coin) -> u8 {\n match coin {\n Coin::Penny => 1,\n Coin::Nickel => 5,\n Coin::Dime => 10,\n Coin::Quarter => 25,\n }\n}\n")),(0,r.kt)("admonition",{type:"info"},(0,r.kt)("p",{parentName:"admonition"},"source code: ",(0,r.kt)("a",{parentName:"p",href:"https://kaisery.github.io/trpl-zh-cn/ch06-02-match.html"},"https://kaisery.github.io/trpl-zh-cn/ch06-02-match.html"))),(0,r.kt)("p",null,"\u5f53\u4e0e\u5339\u914d\u8bed\u53e5\u7ed3\u5408\u4f7f\u7528\u65f6\uff0c\u679a\u4e3e\u975e\u5e38\u6709\u7528\u3002\u5b83\u4eec\u662f\u4e00\u79cd\u68c0\u67e5\u53d8\u91cf\u503c\u5e76\u6839\u636e\u8be5\u503c\u6267\u884c\u4ee3\u7801\u7684\u65b9\u5f0f\uff0c\u4e0e",(0,r.kt)("inlineCode",{parentName:"p"},"JavaScript"),"\u4e2d\u7684",(0,r.kt)("inlineCode",{parentName:"p"},"switch"),"\u8bed\u53e5\u7c7b\u4f3c\u3002"),(0,r.kt)("h2",{id:"-\u5b9e\u73b0"},"\ud83d\udce6 \u5b9e\u73b0"),(0,r.kt)("p",null,"\u7ed3\u6784\u4f53\u672c\u8eab\u5f88\u6709\u7528\uff0c\u4f46\u5982\u679c\u4f60\u80fd\u4e3a\u5b83\u4eec\u6dfb\u52a0\u51fd\u6570\uff0c\u6548\u679c\u5c06\u5982\u4f55\u5462\uff1f\u4e0b\u9762\u6211\u4eec\u6765\u4ecb\u7ecd\u5b9e\u73b0\uff08",(0,r.kt)("inlineCode",{parentName:"p"},"Implementations"),"\uff09\uff0c\u5b83\u8ba9\u4f60\u53ef\u4ee5\u7ed9\u7ed3\u6784\u4f53\u6dfb\u52a0\u65b9\u6cd5\uff0c\u4f7f\u5176\u66f4\u63a5\u8fd1\u9762\u5411\u5bf9\u8c61\u7684\u8bbe\u8ba1\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},'#[derive(Debug)]\nstruct Rectangle {\n width: u32,\n height: u32,\n}\n\nimpl Rectangle {\n fn area(&self) -> u32 {\n self.width * self.height\n }\n}\n\nfn main() {\n let rect1 = Rectangle {\n width: 30,\n height: 50,\n };\n\n println!(\n "The area of the rectangle is {} square pixels.",\n rect1.area()\n );\n}\n')),(0,r.kt)("admonition",{type:"info"},(0,r.kt)("p",{parentName:"admonition"},"source code: ",(0,r.kt)("a",{parentName:"p",href:"https://kaisery.github.io/trpl-zh-cn/ch05-03-method-syntax.html"},"https://kaisery.github.io/trpl-zh-cn/ch05-03-method-syntax.html"))),(0,r.kt)("p",null,"\u5982\u679c\u4f60\u5bf9\u201c\u5411\u7ed3\u6784\u4f53\u6dfb\u52a0\u65b9\u6cd5\u201d\u611f\u5230\u56f0\u60d1\uff0c\u53ef\u4ee5\u7406\u89e3\u4e3a\u8d4b\u4e88\u7ed3\u6784\u4f53\u7279\u6b8a\u80fd\u529b\u3002\u4f8b\u5982\uff0c\u4f60\u53ef\u80fd\u6709\u4e00\u4e2a\u7b80\u5355\u7684",(0,r.kt)("inlineCode",{parentName:"p"},"user"),"\u7ed3\u6784\u4f53\uff0c\u62e5\u6709\u901f\u5ea6\u3001\u5065\u5eb7\u548c\u4f24\u5bb3\u5c5e\u6027\u3002\u901a\u8fc7\u4f7f\u7528 ",(0,r.kt)("inlineCode",{parentName:"p"},"impl")," \u5173\u952e\u5b57\u6dfb\u52a0\u4e00\u4e2a ",(0,r.kt)("inlineCode",{parentName:"p"},"wordPerMinute")," \u65b9\u6cd5\uff0c\u4f60\u5c31\u53ef\u4ee5\u8ba1\u7b97\u7528\u6237\u7684\u6253\u5b57\u901f\u5ea6\u2328\ufe0f\u3002"),(0,r.kt)("h2",{id:"-\u7279\u5f81traits"},"\ud83c\udf81 \u7279\u5f81\uff08",(0,r.kt)("inlineCode",{parentName:"h2"},"Traits"),"\uff09"),(0,r.kt)("p",null,"\u73b0\u5728\uff0c\u6211\u4eec\u6765\u8c08\u8c08\u8fd9\u4e2a\u201c\u86cb\u7cd5\u201d\u7684\u9876\u5c42\u90e8\u5206 - ",(0,r.kt)("inlineCode",{parentName:"p"},"Traits"),"\u3002",(0,r.kt)("inlineCode",{parentName:"p"},"Traits"),"\u548c\u5b9e\u73b0\u7c7b\u4f3c\uff0c\u4e5f\u662f\u4e3a\u7c7b\u578b\u589e\u6dfb\u529f\u80fd\u3002\u4f60\u53ef\u4ee5\u628a\u5b83\u770b\u4f5c\u7c7b\u578b\u80fd\u5177\u5907\u7684\u4e00\u79cd\u80fd\u529b\u3002"),(0,r.kt)("p",null,"\u56de\u5230\u6211\u4eec\u7684 ",(0,r.kt)("inlineCode",{parentName:"p"},"user")," \u7ed3\u6784\u4f53\u4f8b\u5b50\uff0c\u5982\u679c\u6211\u6dfb\u52a0\u4e86\u4e00\u4e2a\u540d\u4e3a ",(0,r.kt)("inlineCode",{parentName:"p"},"ThreeArms")," \u7684",(0,r.kt)("inlineCode",{parentName:"p"},"trait"),"\uff0c\u7528\u6237\u5c06\u80fd\u591f\u4ee5\u66f4\u5feb\u7684\u901f\u5ea6\u8f93\u5165\u6587\u5b57\uff0c\u56e0\u4e3a\u4ed6\u4eec\u5c06\u62e5\u6709\u989d\u5916\u7684\u624b\u81c2\uff01",(0,r.kt)("inlineCode",{parentName:"p"},"Traits"),"\u8fd9\u4e2a\u6982\u5ff5\u53ef\u80fd\u6709\u70b9\u62bd\u8c61\uff0c\u6240\u4ee5\u6211\u4eec\u6765\u770b\u4e00\u4e2a\u5177\u4f53\u7684\u4f8b\u5b50\uff1a"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"pub triat BorshDeserialize : Sized {\n fn deserialize(buf: &mut &[u8]) -> Result;\n fn try_from_slice(buf: &[u8]) -> Result { ... }\n}\n")),(0,r.kt)("p",null,"\u5982\u4f60\u6240\u77e5\uff0c\u6211\u4eec\u7684\u6307\u4ee4\u6570\u636e\u4ee5\u5b57\u8282\u6570\u7ec4\uff08\u7531",(0,r.kt)("inlineCode",{parentName:"p"},"1"),"\u548c",(0,r.kt)("inlineCode",{parentName:"p"},"0"),"\u7ec4\u6210\uff09\u7684\u5f62\u5f0f\u63d0\u4f9b\uff0c\u6211\u4eec\u9700\u8981\u5728\u7a0b\u5e8f\u4e2d\u5bf9\u5176\u8fdb\u884c\u53cd\u5e8f\u5217\u5316\uff08\u8f6c\u6362\u6210",(0,r.kt)("inlineCode",{parentName:"p"},"Rust"),"\u7c7b\u578b\uff09\u3002\u6211\u4eec\u5c06\u4f7f\u7528\u540d\u4e3a ",(0,r.kt)("inlineCode",{parentName:"p"},"BorshDeserialize")," \u7684",(0,r.kt)("inlineCode",{parentName:"p"},"Traits"),"\u6765\u5b8c\u6210\u8fd9\u4e00\u4efb\u52a1\uff1a\u5b83\u5305\u62ec\u4e00\u4e2a ",(0,r.kt)("inlineCode",{parentName:"p"},"deserialize")," \u65b9\u6cd5\uff0c\u53ef\u4ee5\u5c06\u6570\u636e\u8f6c\u6362\u4e3a\u6240\u9700\u7c7b\u578b\u3002\u8fd9\u610f\u5473\u7740\uff0c\u5982\u679c\u6211\u4eec\u5c06 ",(0,r.kt)("inlineCode",{parentName:"p"},"BorshDeserialize Traits")," \u6dfb\u52a0\u5230\u6307\u4ee4\u7ed3\u6784\u4f53\u4e2d\uff0c\u6211\u4eec\u5c31\u53ef\u4ee5\u4f7f\u7528 ",(0,r.kt)("inlineCode",{parentName:"p"},"deserialize")," \u65b9\u6cd5\u5c06\u6307\u4ee4\u6570\u636e\u5b9e\u4f8b\u8f6c\u6362\u4e3a",(0,r.kt)("inlineCode",{parentName:"p"},"Rust"),"\u7c7b\u578b\u3002"),(0,r.kt)("p",null,"\u5982\u679c\u4f60\u5bf9\u8fd9\u90e8\u5206\u5185\u5bb9\u611f\u5230\u56f0\u60d1\uff0c\u4e0d\u59a8\u518d\u8bfb\u4e00\u904d\uff0c\u6211\u81ea\u5df1\u4e5f\u82b1\u4e86\u4e00\u4e9b\u65f6\u95f4\u624d\u5f04\u6e05\u695a\u3002"),(0,r.kt)("p",null,"\u5b9e\u9645\u64cd\u4f5c\u793a\u4f8b\u5982\u4e0b\uff1a"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"#[derive(BorshDeserialize)]\nstruct InstructionData {\n input: String,\n}\n")),(0,r.kt)("admonition",{type:"caution"},(0,r.kt)("p",{parentName:"admonition"},"\u6ce8\u610f\uff1a\u53ef\u80fd\u6709\u4e00\u4e2a\u4f60\u5df2\u7ecf\u5ffd\u7565\u7684\u5c42\u9762\u2014\u2014\u5b8f\u3002\u5b83\u4eec\u7528\u6765\u751f\u6210\u4ee3\u7801\u3002")),(0,r.kt)("p",null,"\u5728\u6211\u4eec\u7684\u573a\u666f\u4e2d\uff0c\u7279\u8d28\u548c\u5b8f\u901a\u5e38\u4e00\u8d77\u4f7f\u7528\u3002\u4f8b\u5982\uff0c",(0,r.kt)("inlineCode",{parentName:"p"},"BorshDeserialize Traits"),"\u6709\u4e24\u4e2a\u5fc5\u987b\u5b9e\u73b0\u7684\u51fd\u6570\uff1a",(0,r.kt)("inlineCode",{parentName:"p"},"deserialize")," \u548c ",(0,r.kt)("inlineCode",{parentName:"p"},"try_from_slice"),"\u3002\u6211\u4eec\u53ef\u4ee5\u4f7f\u7528 ",(0,r.kt)("inlineCode",{parentName:"p"},"#[derive(BorshDeserialize)]")," \u5c5e\u6027\uff0c\u8ba9\u7f16\u8bd1\u5668\u5728\u7ed9\u5b9a\u7c7b\u578b\u4e0a\uff08\u5373\u6307\u4ee4\u6570\u636e\u7ed3\u6784\uff09\u4e3a\u6211\u4eec\u5b9e\u73b0\u8fd9\u4e24\u4e2a\u51fd\u6570\u3002\n\u6574\u4e2a\u6d41\u7a0b\u662f\u8fd9\u6837\u7684\uff1a"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"\u901a\u8fc7\u5b8f\u5c06",(0,r.kt)("inlineCode",{parentName:"li"},"Trait"),"\u6dfb\u52a0\u5230\u7ed3\u6784\u4f53\u4e2d"),(0,r.kt)("li",{parentName:"ul"},"\u7f16\u8bd1\u5668\u4f1a\u67e5\u627e",(0,r.kt)("inlineCode",{parentName:"li"},"Trait"),"\u7684\u5b9a\u4e49"),(0,r.kt)("li",{parentName:"ul"},"\u7f16\u8bd1\u5668\u4f1a\u4e3a\u8be5",(0,r.kt)("inlineCode",{parentName:"li"},"Trait"),"\u5b9e\u73b0\u5e95\u5c42\u51fd\u6570"),(0,r.kt)("li",{parentName:"ul"},"\u4f60\u7684\u7ed3\u6784\u4f53\u73b0\u5728\u5177\u5907\u4e86",(0,r.kt)("inlineCode",{parentName:"li"},"Trait"),"\u7684\u529f\u80fd")),(0,r.kt)("p",null,"\u5b9e\u9645\u4e0a\uff0c\u5b8f\u5728\u7f16\u8bd1\u65f6\u751f\u6210\u4e86\u7528\u4e8e\u53cd\u5e8f\u5217\u5316\u5b57\u7b26\u4e32\u7684\u51fd\u6570\u3002\u901a\u8fc7\u4f7f\u7528\u8fd9\u4e2a",(0,r.kt)("inlineCode",{parentName:"p"},"Trait"),"\uff0c\u6211\u4eec\u53ef\u4ee5\u544a\u8bc9",(0,r.kt)("inlineCode",{parentName:"p"},"Rust"),"\uff1a\u201c\u563f\uff0c\u6211\u60f3\u80fd\u53cd\u5e8f\u5217\u5316\u5b57\u7b26\u4e32\uff0c\u8bf7\u4e3a\u6211\u751f\u6210\u76f8\u5e94\u7684\u4ee3\u7801\u3002\u201d"),(0,r.kt)("p",null,"\u5bf9\u4e8e\u6211\u4eec\u7684\u60c5\u51b5\uff0c\u552f\u4e00\u7684\u8981\u6c42\u662f",(0,r.kt)("inlineCode",{parentName:"p"},"Borsh"),"\u5fc5\u987b\u652f\u6301\u6240\u6709\u7684\u7ed3\u6784\u6570\u636e\u7c7b\u578b\uff08\u5728\u6211\u4eec\u7684\u573a\u666f\u4e2d\u662f\u5b57\u7b26\u4e32\uff09\u3002\u5982\u679c\u4f60\u6709\u4e00\u4e2a",(0,r.kt)("inlineCode",{parentName:"p"},"Borsh"),"\u4e0d\u652f\u6301\u7684\u81ea\u5b9a\u4e49\u6570\u636e\u7c7b\u578b\uff0c\u5c31\u9700\u8981\u5728\u5b8f\u4e2d\u81ea\u5df1\u5b9e\u73b0\u8fd9\u4e9b\u529f\u80fd\u3002"),(0,r.kt)("p",null,"\u5982\u679c\u4f60\u8fd8\u672a\u5b8c\u5168\u7406\u89e3\uff0c\u4e0d\u7528\u62c5\u5fc3\uff01\u6211\u81ea\u5df1\u4e5f\u662f\u5728\u770b\u5230\u6574\u4e2a\u6d41\u7a0b\u540e\u624d\u7406\u89e3\u7684\uff0c\u6240\u4ee5\u73b0\u5728\u8ba9\u6211\u4eec\u4e00\u8d77\u5b9e\u8df5\u4e00\u4e0b\uff01"),(0,r.kt)("h2",{id:"-\u628a\u6240\u6709\u5143\u7d20\u6574\u5408\u5728\u4e00\u8d77"},"\ud83c\udf82 \u628a\u6240\u6709\u5143\u7d20\u6574\u5408\u5728\u4e00\u8d77"),(0,r.kt)("p",null,"\u6211\u4eec\u521a\u521a\u8ba8\u8bba\u4e86\u4e00\u7cfb\u5217\u76f8\u4e92\u5173\u8054\u7684\u62bd\u8c61\u4e3b\u9898\u3002\u5982\u679c\u53ea\u63cf\u8ff0\u6bcf\u4e00\u5c42\uff0c\u53ef\u80fd\u96be\u4ee5\u60f3\u8c61\u6574\u4e2a\u201c\u86cb\u7cd5\u201d\u7684\u6837\u5b50\uff0c\u6240\u4ee5\u6211\u4eec\u73b0\u5728\u5c31\u5c06\u5b83\u4eec\u6574\u5408\u8d77\u6765\u3002"),(0,r.kt)("p",null,"\u5047\u8bbe\u6211\u4eec\u6b63\u5728\u6784\u5efa\u4e00\u4e2a\u94fe\u4e0a\u7684\u7b14\u8bb0\u7a0b\u5e8f\uff0c\u6211\u4eec\u5c06\u4fdd\u6301\u5b83\u7684\u7b80\u5355\u6027\uff1a\u4f60\u53ea\u80fd\u521b\u5efa\u3001\u66f4\u65b0\u548c\u5220\u9664\u7b14\u8bb0\u3002\u6211\u4eec\u9700\u8981\u4e00\u6761\u6307\u4ee4\u6765\u5b8c\u6210\u8fd9\u4e9b\u64cd\u4f5c\uff0c\u6240\u4ee5\u8ba9\u6211\u4eec\u521b\u5efa\u4e00\u4e2a\u679a\u4e3e\u7c7b\u578b\u6765\u4ee3\u8868\u5b83\uff1a"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"enum NoteInstruction {\n CreateNote {\n id: u64,\n title: String,\n body: String\n },\n UpdateNote {\n id: u64,\n title: String,\n body: String\n },\n DeleteNote {\n id: u64\n }\n}\n")),(0,r.kt)("p",null,"\u6bcf\u4e2a\u6307\u4ee4\u53d8\u4f53\u7684\u5b57\u8282\u6570\u7ec4\u90fd\u6709\u81ea\u5df1\u7684\u6570\u636e\u7c7b\u578b\uff0c\u6211\u4eec\u5728\u8fd9\u91cc\u6709\u5b83\u4eec\uff01"),(0,r.kt)("p",null,"\u65e2\u7136\u6211\u4eec\u77e5\u9053\u6307\u4ee4\u6570\u636e\u7684\u6837\u5b50\uff0c\u6211\u4eec\u9700\u8981\u5c06\u5176\u4ece\u5b57\u8282\u8f6c\u6362\u4e3a\u8fd9\u4e9b\u7c7b\u578b\u3002\u7b2c\u4e00\u6b65\u662f\u53cd\u5e8f\u5217\u5316\uff0c\u6211\u4eec\u5c06\u4f7f\u7528\u4e00\u4e2a\u4e13\u95e8\u4e3a\u6709\u6548\u8d1f\u8f7d\u521b\u5efa\u7684\u65b0\u7ed3\u6784\u4f53\u4e0a\u7684 ",(0,r.kt)("inlineCode",{parentName:"p"},"BorshDeserialize Traits")," \u6765\u5b8c\u6210\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"#[derive(BorshDeserialize)]\nstruct NoteInstructionPayload {\n id: u64,\n title: String,\n body: String\n}\n")),(0,r.kt)("p",null,"\u6211\u4eec\u5728\u8fd9\u91cc\u5904\u7406\u4e86",(0,r.kt)("inlineCode",{parentName:"p"},"title"),"\u548c",(0,r.kt)("inlineCode",{parentName:"p"},"body"),"\uff0c\u8fd9\u5c31\u662f\u5b57\u8282\u6570\u7ec4\u4e2d\u7684\u5185\u5bb9\u3002",(0,r.kt)("inlineCode",{parentName:"p"},"Borsh"),"\u7684\u5de5\u4f5c\u4ec5\u4ec5\u662f\u6dfb\u52a0\u53cd\u5e8f\u5217\u5316\u7684\u652f\u6301\uff0c\u5b83\u5b9e\u9645\u4e0a\u5e76\u6ca1\u6709\u8fdb\u884c\u53cd\u5e8f\u5217\u5316\uff0c\u800c\u662f\u4ec5\u63d0\u4f9b\u4e86\u6211\u4eec\u53ef\u4ee5\u8c03\u7528\u7684\u53cd\u5e8f\u5217\u5316\u51fd\u6570\u3002"),(0,r.kt)("p",null,"\u4e0b\u4e00\u6b65\uff0c\u6211\u4eec\u8981\u5b9e\u9645\u4f7f\u7528\u8fd9\u4e9b\u51fd\u6570\u6765\u53cd\u5e8f\u5217\u5316\u6570\u636e\u3002\u6211\u4eec\u5c06\u5728\u4e00\u4e2a\u5b9e\u73b0\u4e2d\u5b9a\u4e49\u8fd9\u4e2a\u884c\u4e3a\uff0c\u8fd9\u662f\u4e00\u4e2a\u624b\u52a8\u7684\u8fc7\u7a0b\uff08\u81f3\u5c11\u6682\u65f6\u662f\u8fd9\u6837\uff09\uff01"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"impl NoteInstruction {\n // \u5c06\u4f20\u5165\u7684\u7f13\u51b2\u533a\u89e3\u5305\u5230\u76f8\u5173\u7684\u6307\u4ee4\n // \u8f93\u5165\u7684\u9884\u671f\u683c\u5f0f\u662f\u4e00\u4e2a\u7528Borsh\u5e8f\u5217\u5316\u7684\u5411\u91cf\n pub fn unpack(input: &[u8]) -> Result {\n // \u91c7\u7528\u7b2c\u4e00\u4e2a\u5b57\u8282\u4f5c\u4e3a\u53d8\u4f53\u6765\u786e\u5b9a\u8981\u6267\u884c\u54ea\u4e2a\u6307\u4ee4\n let (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;\n // \u4f7f\u7528\u4e34\u65f6\u6709\u6548\u8f7d\u8377\u7ed3\u6784\u4f53\u8fdb\u884c\u53cd\u5e8f\u5217\u5316\n let payload = NoteInstructionPayload::try_from_slice(rest).unwrap();\n // \u901a\u8fc7\u53d8\u4f53\u5339\u914d\uff0c\u786e\u5b9a\u51fd\u6570\u6240\u671f\u671b\u7684\u6570\u636e\u7ed3\u6784\uff0c\u7136\u540e\u8fd4\u56deTestStruct\u6216\u9519\u8bef\n Ok(match variant {\n 0 => Self::CreateNote {\n title: payload.title,\n body: payload.body,\n id: payload.id\n },\n 1 => Self::UpdateNote {\n title: payload.title,\n body: payload.body,\n id: payload.id\n },\n 2 => Self::DeleteNote {\n id: payload.id\n },\n _ => return Err(ProgramError::InvalidInstructionData)\n })\n }\n}\n")),(0,r.kt)("p",null,"\u8fd9\u90e8\u5206\u5185\u5bb9\u53ef\u80fd\u4e00\u5f00\u59cb\u770b\u8d77\u6765\u6709\u70b9\u5413\u4eba\uff0c\u4f46\u4f60\u5f88\u5feb\u5c31\u4f1a\u89c9\u5f97\u5b83\u5176\u5b9e\u975e\u5e38\u76f4\u63a5\u548c\u7b80\u5355\uff01\u8ba9\u6211\u4eec\u4e00\u8d77\u6765\u6df1\u5165\u5206\u6790\u4e00\u4e0b \ud83d\udd7a\ud83d\udc83\ud83d\udc6f\u200d\u2642\ufe0f"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"pub fn unpack(input: &[u8]) -> Result {\n")),(0,r.kt)("p",null,"\u6211\u4eec\u7684\u89e3\u5305\u51fd\u6570\u4ece\u6307\u4ee4\u4e2d\u83b7\u53d6\u5b57\u8282\uff0c\u5e76\u8fd4\u56de\u4e00\u4e2a",(0,r.kt)("inlineCode",{parentName:"p"},"NoteInstruction"),"\u7c7b\u578b\uff08\u5373 ",(0,r.kt)("inlineCode",{parentName:"p"},"Self"),"\uff09\u6216\u4e00\u4e2a",(0,r.kt)("inlineCode",{parentName:"p"},"ProgramError"),"\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"let (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;\n")),(0,r.kt)("p",null,"\u73b0\u5728\u662f\u65f6\u5019\u4ece\u5b57\u8282\u4e2d\u89e3\u5305\u6570\u636e\u5e76\u8c03\u7528\u53cd\u5e8f\u5217\u5316\u51fd\u6570\u4e86\u3002\u6211\u4eec\u7684\u6307\u4ee4\u6570\u636e\u7684\u7b2c\u4e00\u4e2a\u5b57\u8282\u662f\u4e00\u4e2a\u6574\u6570\uff0c\u5b83\u544a\u8bc9\u6211\u4eec\u6b63\u5728\u5904\u7406\u54ea\u4e2a\u6307\u4ee4\u3002\u6211\u4eec\u8fd9\u6837\u505a\u7684\u65b9\u5f0f\u662f\u4f7f\u7528",(0,r.kt)("inlineCode",{parentName:"p"},"Rust"),"\u7684\u5185\u7f6e\u51fd\u6570",(0,r.kt)("inlineCode",{parentName:"p"},"split_first"),"\u3002\u5982\u679c\u5207\u7247\u4e3a\u7a7a\uff0c",(0,r.kt)("inlineCode",{parentName:"p"},"ok_or"),"\u5c06\u8fd4\u56de",(0,r.kt)("inlineCode",{parentName:"p"},"ProgramError"),"\u679a\u4e3e\u4e2d\u7684",(0,r.kt)("inlineCode",{parentName:"p"},"InvalidInstructionData"),"\u9519\u8bef\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"let payload = NoteInstructionPayload::try_from_slice(rest).unwrap();\n")),(0,r.kt)("p",null,"\u73b0\u5728\u6211\u4eec\u6709\u4e86\u4e24\u4e2a\u53d8\u91cf\u8981\u5904\u7406\uff1a\u6307\u4ee4\u6307\u793a\u5668\u548c\u6307\u4ee4\u7684\u6709\u6548\u8f7d\u8377\uff08\u6570\u636e\uff09\u3002",(0,r.kt)("inlineCode",{parentName:"p"},"Borsh"),"\u5728\u6211\u4eec\u7684\u6709\u6548\u8f7d\u8377\u7ed3\u6784\u4e2d\u6dfb\u52a0\u4e86",(0,r.kt)("inlineCode",{parentName:"p"},"try_from_slice"),"\u51fd\u6570\uff0c\u4f7f\u6211\u4eec\u53ef\u4ee5\u5728\u6709\u6548\u8f7d\u8377\u53d8\u91cf",(0,r.kt)("inlineCode",{parentName:"p"},"rest"),"\u4e0a\u8c03\u7528\u5b83\u3002\u8fd9\u5c31\u662f\u53cd\u5e8f\u5217\u5316\u7684\u8fc7\u7a0b\uff01"),(0,r.kt)("p",null,"\u63a5\u4e0b\u6765\u7684\u6b65\u9aa4\u5305\u62ec\uff1a"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"\u5c06\u6307\u4ee4\u6570\u636e\u5b9a\u4e49\u4e3a",(0,r.kt)("inlineCode",{parentName:"li"},"Rust"),"\u7c7b\u578b\u4e2d\u7684\u679a\u4e3e\u3002"),(0,r.kt)("li",{parentName:"ul"},"\u5b9a\u4e49\u8d1f\u8f7d\u7ed3\u6784\u4f53\u3002"),(0,r.kt)("li",{parentName:"ul"},"\u5728\u8d1f\u8f7d\u7ed3\u6784\u4f53\u4e0a\u58f0\u660e",(0,r.kt)("inlineCode",{parentName:"li"},"BorshDeserialize"),"\u5b8f\u3002"),(0,r.kt)("li",{parentName:"ul"},"\u4e3a\u8d1f\u8f7d\u7ed3\u6784\u4f53\u521b\u5efa\u4e00\u4e2a\u5b9e\u73b0\uff08\u5b57\u8282 -> \u7ed3\u6784\u4f53\uff09\u3002"),(0,r.kt)("li",{parentName:"ul"},"\u521b\u5efa",(0,r.kt)("inlineCode",{parentName:"li"},"unpack"),"\u51fd\u6570\uff0c\u8be5\u51fd\u6570\u63a5\u6536\u6307\u4ee4\u6570\u636e\u5e76\u5bf9\u5176\u8fdb\u884c\u53cd\u5e8f\u5217\u5316\u3002")),(0,r.kt)("p",null,"\u6211\u4eec",(0,r.kt)("inlineCode",{parentName:"p"},"unpack"),"\u51fd\u6570\u7684\u6700\u540e\u4e00\u6b65\u662f\u5c06\u53cd\u5e8f\u5217\u5316\u7684\u6570\u636e\u8f6c\u6362\u4e3a\u679a\u4e3e\u53d8\u4f53\uff08\u5373\u6307\u4ee4\u6570\u636e\u7c7b\u578b\uff09\u3002\u6211\u4eec\u5c06\u4f7f\u7528\u5339\u914d\u8bed\u53e5\u6765\u5b8c\u6210\u8fd9\u4e2a\u4efb\u52a1\uff0c\u901a\u8fc7\u5339\u914d\u6307\u4ee4\u6307\u793a\u5668\uff0c\u6211\u4eec\u53ef\u4ee5\u8fd4\u56de\u679a\u4e3e\u7684\u6b63\u786e\u53d8\u4f53\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"Ok(match variant {\n 0 => Self::CreateNote {\n title: payload.title,\n body: payload.body,\n id: payload.id\n },\n 1 => Self::UpdateNote {\n title: payload.title,\n body: payload.body,\n id: payload.id\n },\n 2 => Self::DeleteNote {\n id: payload.id\n },\n _ => return Err(ProgramError::InvalidInstructionData)\n})\n")),(0,r.kt)("p",null,"\u73b0\u5728\u4f60\u5df2\u7ecf\u77e5\u9053\u4e86\u6574\u4e2a\u8fc7\u7a0b\uff01\u7406\u89e3\u8fd9\u4e00\u5207\u786e\u5b9e\u9700\u8981\u96c6\u4e2d\u7cbe\u529b\uff0c\u6240\u4ee5\u5982\u679c\u4f60\u9700\u8981\u591a\u8bfb\u51e0\u904d\uff0c\u4e5f\u5b8c\u5168\u6ca1\u5173\u7cfb\u3002"),(0,r.kt)("p",null,"\u8fd9\u90e8\u5206\u5185\u5bb9\u4fe1\u606f\u91cf\u8f83\u5927\uff0c\u53ef\u80fd\u4f1a\u8ba9\u4eba\u89c9\u5f97\u6709\u4e9b\u590d\u6742\u3002\u4f46\u522b\u62c5\u5fc3\uff0c\u6211\u4eec\u4f1a\u901a\u8fc7\u5927\u91cf\u7684\u7ec3\u4e60\u6765\u9010\u6e10\u719f\u6089\u5b83\u4eec\u3002\u968f\u7740\u65f6\u95f4\u7684\u63a8\u79fb\u548c\u53cd\u590d\u7ec3\u4e60\uff0c\u4f60\u4f1a\u53d1\u73b0\u8fd9\u4e9b\u5185\u5bb9\u5f00\u59cb\u53d8\u5f97\u66f4\u52a0\u76f4\u89c2\u548c\u6613\u61c2\u3002"),(0,r.kt)("h2",{id:"-\u7a0b\u5e8f\u903b\u8f91"},"\ud83d\ude80 \u7a0b\u5e8f\u903b\u8f91"),(0,r.kt)("p",null,"\u6211\u4eec\u5df2\u7ecf\u89e3\u538b\u4e86\u6307\u4ee4\u6570\u636e\uff0c\u51c6\u5907\u6295\u5165\u5b9e\u9645\u4f7f\u7528\u3002\u73b0\u5728\uff0c\u6211\u4eec\u9700\u8981\u9488\u5bf9\u6bcf\u4e2a\u6307\u4ee4\u7f16\u5199\u76f8\u5e94\u7684\u903b\u8f91\u5904\u7406\u3002\u8fd9\u90e8\u5206\u5176\u5b9e\u662f\u6700\u4e3a\u76f4\u89c2\u548c\u7b80\u5355\u7684\uff01\u76f8\u5bf9\u4e8e\u590d\u6742\u7684\u53cd\u5e8f\u5217\u5316\u5904\u7406\uff0c\u8fd9\u4e00\u90e8\u5206\u5c31\u50cf\u5403\u86cb\u7cd5\u4e00\u6837\u8f7b\u677e\u4e86\uff08",(0,r.kt)("inlineCode",{parentName:"p"},"Anchor"),"\u4f1a\u4e3a\u4f60\u5904\u7406\u5927\u90e8\u5206\u53cd\u5e8f\u5217\u5316\u5de5\u4f5c\uff09\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},'entrypoint!(process_instruction);\n\npub fn process_instruction(\n program_id: &Pubkey,\n accounts: &[AccountInfo],\n instruction_data: &[u8]\n) -> ProgramResult {\n\n // \u89e3\u538b\u6307\u4ee4\u6570\u636e\n let instruction = NoteInstruction::unpack(instruction_data)?;\n\n // \u5339\u914d\u6307\u4ee4\u5e76\u6267\u884c\u76f8\u5e94\u7684\u903b\u8f91\n match instruction {\n NoteInstruction::CreateNote { title, body, id } => {\n msg!("Instruction: CreateNote");\n create_note(accounts, title, body, id, program_id)\n }\n NoteInstruction::UpdateNote { title, body, id } => {\n msg!("Instruction: UpdateNote");\n update_note(accounts, title, body, id)\n }\n NoteInstruction::DeleteNote { id } => {\n msg!("Instruction: DeleteNote");\n delete_note(accounts, id)\n }\n }\n}\n')),(0,r.kt)("p",null,"\u9996\u5148\uff0c\u6211\u4eec\u8981\u505a\u7684\u662f\u5b9a\u4e49\u7a0b\u5e8f\u7684\u5165\u53e3\u51fd\u6570\u3002",(0,r.kt)("inlineCode",{parentName:"p"},"process_instruction")," \u51fd\u6570\u7684\u5b9a\u4e49\u4e0e\u6211\u4eec\u4e4b\u524d\u7684 \u201c",(0,r.kt)("inlineCode",{parentName:"p"},"Hello World"),"\u201d \u7a0b\u5e8f\u4e00\u6837\u3002\u63a5\u7740\uff0c\u6211\u4eec\u5c06\u5728 ",(0,r.kt)("inlineCode",{parentName:"p"},"NoteInstruction")," \u7684\u5b9e\u73b0\u4e2d\u4f7f\u7528 ",(0,r.kt)("inlineCode",{parentName:"p"},"unpack")," \u51fd\u6570\u6765\u63d0\u53d6\u6307\u4ee4\u6570\u636e\u3002\u7136\u540e\uff0c\u6211\u4eec\u53ef\u4ee5\u4f9d\u9760 ",(0,r.kt)("inlineCode",{parentName:"p"},"NoteInstruction")," \u679a\u4e3e\u6765\u786e\u5b9a\u6307\u4ee4\u7684\u5177\u4f53\u7c7b\u578b\u3002"),(0,r.kt)("p",null,"\u5728\u672c\u9636\u6bb5\uff0c\u6211\u4eec\u8fd8\u6ca1\u6709\u6d89\u53ca\u5177\u4f53\u7684\u903b\u8f91\u5904\u7406\uff0c\u771f\u6b63\u7684\u6784\u5efa\u5c06\u5728\u540e\u7eed\u9636\u6bb5\u5c55\u5f00\u3002"),(0,r.kt)("h2",{id:"-\u6587\u4ef6\u7ed3\u6784\u8bf4\u660e"},"\ud83d\udcc2 \u6587\u4ef6\u7ed3\u6784\u8bf4\u660e"),(0,r.kt)("p",null,"\u7f16\u5199\u81ea\u5b9a\u4e49\u7a0b\u5e8f\u65f6\uff0c\u5c06\u4ee3\u7801\u5212\u5206\u4e3a\u4e0d\u540c\u7684\u6587\u4ef6\u7ed3\u6784\u4f1a\u975e\u5e38\u6709\u52a9\u4e8e\u7ba1\u7406\u3002\u8fd9\u6837\u505a\u4e0d\u4ec5\u65b9\u4fbf\u4ee3\u7801\u91cd\u7528\uff0c\u8fd8\u80fd\u8ba9\u4f60\u66f4\u5feb\u901f\u5730\u627e\u5230\u6240\u9700\u7684\u5185\u5bb9\u3002"),(0,r.kt)("p",null,(0,r.kt)("img",{src:t(52228).Z,width:"1350",height:"582"})),(0,r.kt)("p",null,"\u9664\u4e86 ",(0,r.kt)("inlineCode",{parentName:"p"},"lib.rs")," \u6587\u4ef6\u5916\uff0c\u6211\u4eec\u8fd8\u4f1a\u628a\u7a0b\u5e8f\u7684\u5404\u4e2a\u90e8\u5206\u653e\u5165\u4e0d\u540c\u7684\u6587\u4ef6\u3002\u6700\u660e\u663e\u7684\u4e00\u4e2a\u4f8b\u5b50\u5c31\u662f ",(0,r.kt)("inlineCode",{parentName:"p"},"instruction.rs")," \u6587\u4ef6\u3002\u5728\u8fd9\u91cc\uff0c\u6211\u4eec\u5c06\u5b9a\u4e49\u6307\u4ee4\u6570\u636e\u7c7b\u578b\u5e76\u5b9e\u73b0\u5bf9\u6307\u4ee4\u6570\u636e\u7684\u89e3\u5305\u529f\u80fd\u3002"),(0,r.kt)("p",null,(0,r.kt)("strong",{parentName:"p"},"\u4f60\u505a\u5f97\u771f\u68d2\ud83d\udc4f\ud83d\udc4f\ud83d\udc4f")),(0,r.kt)("p",null,"\u6211\u60f3\u501f\u6b64\u673a\u4f1a\u5bf9\u4f60\u7684\u52aa\u529b\u4ed8\u51fa\u8868\u793a\u8d5e\u8d4f\u3002\u4f60\u6b63\u5728\u5b66\u4e60\u4e00\u4e9b\u5f3a\u5927\u4e14\u5b9e\u7528\u7684\u6280\u80fd\uff0c\u8fd9\u4e9b\u6280\u80fd\u4e0d\u4ec5\u5728 ",(0,r.kt)("inlineCode",{parentName:"p"},"Solana")," \u9886\u57df\u6709\u7528\uff0c",(0,r.kt)("inlineCode",{parentName:"p"},"Rust")," \u7684\u5e94\u7528\u4e5f\u975e\u5e38\u5e7f\u6cdb\u3002\u5c3d\u7ba1\u5b66\u4e60 ",(0,r.kt)("inlineCode",{parentName:"p"},"Solana")," \u53ef\u80fd\u4f1a\u6709\u4e00\u4e9b\u56f0\u96be\uff0c\u4f46\u8bf7\u8bb0\u4f4f\uff0c\u8fd9\u6837\u7684\u56f0\u96be\u4e5f\u6709\u4eba\u66fe\u7ecf\u7ecf\u5386\u5e76\u6218\u80dc\u3002\u4f8b\u5982\uff0c",(0,r.kt)("inlineCode",{parentName:"p"},"FormFunction")," \u7684\u521b\u59cb\u4eba\u5728\u5927\u7ea6\u4e00\u5e74\u524d\u7684\u63a8\u6587\u4e2d\u63d0\u5230\u4e86\u4ed6\u662f\u5982\u4f55\u627e\u5230\u56f0\u96be\u7684\uff1a"),(0,r.kt)("p",null,(0,r.kt)("img",{src:t(86337).Z,width:"1254",height:"778"})),(0,r.kt)("p",null,(0,r.kt)("inlineCode",{parentName:"p"},"FormFunction")," \u5df2\u7ecf\u7b79\u96c6\u4e86\u8d85\u8fc7 ",(0,r.kt)("inlineCode",{parentName:"p"},"470")," \u4e07\u7f8e\u5143\uff0c\u662f\u6211\u5fc3\u76ee\u4e2d ",(0,r.kt)("inlineCode",{parentName:"p"},"Solana")," \u4e0a\u6700\u4f18\u79c0\u7684 ",(0,r.kt)("inlineCode",{parentName:"p"},"1/1 NFT")," \u5e73\u53f0\u3002",(0,r.kt)("inlineCode",{parentName:"p"},"Matt")," \u51ed\u501f\u575a\u6301\u4e0d\u61c8\u7684\u52aa\u529b\u5efa\u7acb\u4e86\u4e00\u4e9b\u4ee4\u4eba\u96be\u4ee5\u7f6e\u4fe1\u7684\u4e1c\u897f\u3002\u8bd5\u60f3\u4e00\u4e0b\uff0c\u5982\u679c\u4f60\u638c\u63e1\u4e86\u8fd9\u4e9b\u6280\u80fd\uff0c\u4e00\u5e74\u540e\u4f60\u4f1a\u7ad9\u5728\u54ea\u91cc\u5462\uff1f"))}m.isMDXComponent=!0},52228:(e,n,t)=>{t.d(n,{Z:()=>a});const a=t.p+"assets/images/file-structure-eee2fba4bbd91380e0f837e4601d2b85.png"},86337:(e,n,t)=>{t.d(n,{Z:()=>a});const a=t.p+"assets/images/solana-learner-b7d0ee3d1fd3fa79f7d6c7825337e785.png"},14397:(e,n,t)=>{t.d(n,{Z:()=>a});const a=t.p+"assets/images/the-rust-layer-cake-ca50fcbc4c5288f3a4e41245ee64a4af.png"}}]); \ No newline at end of file diff --git a/assets/js/0279d735.864e1ffb.js b/assets/js/0279d735.70fbf4d6.js similarity index 99% rename from assets/js/0279d735.864e1ffb.js rename to assets/js/0279d735.70fbf4d6.js index f0eb976a3..81ac7dad1 100644 --- a/assets/js/0279d735.864e1ffb.js +++ b/assets/js/0279d735.70fbf4d6.js @@ -1 +1 @@ -"use strict";(self.webpackChunkall_in_one_solana=self.webpackChunkall_in_one_solana||[]).push([[9673],{3905:(e,a,n)=>{n.d(a,{Zo:()=>m,kt:()=>f});var t=n(67294);function r(e,a,n){return a in e?Object.defineProperty(e,a,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[a]=n,e}function l(e,a){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var t=Object.getOwnPropertySymbols(e);a&&(t=t.filter((function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.push.apply(n,t)}return n}function o(e){for(var a=1;a=0||(r[n]=e[n]);return r}(e,a);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);for(t=0;t=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}var s=t.createContext({}),p=function(e){var a=t.useContext(s),n=a;return e&&(n="function"==typeof e?e(a):o(o({},a),e)),n},m=function(e){var a=p(e.components);return t.createElement(s.Provider,{value:a},e.children)},u="mdxType",c={inlineCode:"code",wrapper:function(e){var a=e.children;return t.createElement(t.Fragment,{},a)}},d=t.forwardRef((function(e,a){var n=e.components,r=e.mdxType,l=e.originalType,s=e.parentName,m=i(e,["components","mdxType","originalType","parentName"]),u=p(n),d=r,f=u["".concat(s,".").concat(d)]||u[d]||c[d]||l;return n?t.createElement(f,o(o({ref:a},m),{},{components:n})):t.createElement(f,o({ref:a},m))}));function f(e,a){var n=arguments,r=a&&a.mdxType;if("string"==typeof e||r){var l=n.length,o=new Array(l);o[0]=d;var i={};for(var s in a)hasOwnProperty.call(a,s)&&(i[s]=a[s]);i.originalType=e,i[u]="string"==typeof e?e:r,o[1]=i;for(var p=2;p{n.r(a),n.d(a,{assets:()=>s,contentTitle:()=>o,default:()=>c,frontMatter:()=>l,metadata:()=>i,toc:()=>p});var t=n(87462),r=(n(67294),n(3905));const l={sidebar_position:26,sidebar_label:"Solana \u8fdb\u9636 (SPL token\u4e0eNFT)",sidebar_class_name:"green"},o="Solana \u8fdb\u9636 (SPL token\u4e0eNFT)",i={unversionedId:"module2/README",id:"module2/README",title:"Solana \u8fdb\u9636 (SPL token\u4e0eNFT)",description:"SPL\u4ee3\u5e01",source:"@site/docs/Solana-Co-Learn/module2/README.md",sourceDirName:"module2",slug:"/module2/",permalink:"/solana-co-learn/Solana-Co-Learn/module2/",draft:!1,editUrl:"https://github.com/CreatorsDAO/solana-co-learn/tree/main/docs/Solana-Co-Learn/module2/README.md",tags:[],version:"current",lastUpdatedBy:"v1xingyue",lastUpdatedAt:1727439818,formattedLastUpdatedAt:"Sep 27, 2024",sidebarPosition:26,frontMatter:{sidebar_position:26,sidebar_label:"Solana \u8fdb\u9636 (SPL token\u4e0eNFT)",sidebar_class_name:"green"},sidebar:"tutorialSidebar",previous:{title:"Solana\u94b1\u5305\u4f7f\u7528 - Backpack \ud83c\udf92",permalink:"/solana-co-learn/Solana-Co-Learn/module1/wallet-usage/"},next:{title:"SPL token",permalink:"/solana-co-learn/Solana-Co-Learn/module2/spl-token/"}},s={},p=[{value:"SPL\u4ee3\u5e01",id:"spl\u4ee3\u5e01",level:2},{value:"NFTS + METAPLEX\u94f8\u9020",id:"nfts--metaplex\u94f8\u9020",level:2},{value:"\u5728\u7528\u6237\u754c\u9762\u4e2d\u5c55\u793aNFTS",id:"\u5728\u7528\u6237\u754c\u9762\u4e2d\u5c55\u793anfts",level:2},{value:"\u521b\u9020\u795e\u5947\u7684\u4e92\u8054\u7f51\u8d27\u5e01\u5e76\u51fa\u552eJPEG\u56fe\u7247",id:"\u521b\u9020\u795e\u5947\u7684\u4e92\u8054\u7f51\u8d27\u5e01\u5e76\u51fa\u552ejpeg\u56fe\u7247",level:2}],m={toc:p},u="wrapper";function c(e){let{components:a,...n}=e;return(0,r.kt)(u,(0,t.Z)({},m,n,{components:a,mdxType:"MDXLayout"}),(0,r.kt)("h1",{id:"solana-\u8fdb\u9636-spl-token\u4e0enft"},"Solana \u8fdb\u9636 (SPL token\u4e0eNFT)"),(0,r.kt)("h2",{id:"spl\u4ee3\u5e01"},"SPL\u4ee3\u5e01"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/spl-token/the-token-program/"},"\ud83d\udcb5 ",(0,r.kt)("inlineCode",{parentName:"a"},"The Token Program"))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/spl-token/mint-token-on-solana/"},"\ud83c\udfe7 \u5728",(0,r.kt)("inlineCode",{parentName:"a"},"Solana"),"\u4e0a\u94f8\u9020\u4ee3\u5e01")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/spl-token/token-metadata/"},"\ud83e\uddee \u4ee4\u724c\u5143\u6570\u636e")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/spl-token/give-your-token-an-identity/"}," \ud83e\uddec \u4e3a\u4f60\u7684\u4ee3\u5e01\u8d4b\u4e88\u8eab\u4efd"))),(0,r.kt)("h2",{id:"nfts--metaplex\u94f8\u9020"},"NFTS + METAPLEX\u94f8\u9020"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/nfts-and-minting-with-metaplex/nfts-one-solana/"},"\ud83c\udfa8 ",(0,r.kt)("inlineCode",{parentName:"a"},"Solana"),"\u4e0a\u7684",(0,r.kt)("inlineCode",{parentName:"a"},"NFT"))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/nfts-and-minting-with-metaplex/nft-your-face/"},"\ud83e\udd28 ",(0,r.kt)("inlineCode",{parentName:"a"},"NFT"),"\u4f60\u7684\u8138")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/nfts-and-minting-with-metaplex/candy-machine-and-the-sugar-cli/"},"\ud83c\udf6d \u7cd6\u679c\u673a\u548cSugar CLI"))),(0,r.kt)("h2",{id:"\u5728\u7528\u6237\u754c\u9762\u4e2d\u5c55\u793anfts"},"\u5728\u7528\u6237\u754c\u9762\u4e2d\u5c55\u793aNFTS"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/displaying-nfts/"},"\u5c55\u793a",(0,r.kt)("inlineCode",{parentName:"a"},"NFTs")," \ud83d\udc83")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/displaying-nfts-from-a-wallet/"},"\ud83d\udcf1 \u5728\u94b1\u5305\u4e2d\u5c55\u793a",(0,r.kt)("inlineCode",{parentName:"a"},"NFTs"))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/displaying-nfts-from-a-candy-machine/"},"\ud83d\uddbc \u4ece\u7cd6\u679c\u673a\u5c55\u793a",(0,r.kt)("inlineCode",{parentName:"a"},"NFTs")))),(0,r.kt)("h2",{id:"\u521b\u9020\u795e\u5947\u7684\u4e92\u8054\u7f51\u8d27\u5e01\u5e76\u51fa\u552ejpeg\u56fe\u7247"},"\u521b\u9020\u795e\u5947\u7684\u4e92\u8054\u7f51\u8d27\u5e01\u5e76\u51fa\u552eJPEG\u56fe\u7247"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-reward-tokens/"},"\ud83c\udfa8 \u521b\u5efa\u5956\u52b1\u4ee3\u5e01")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-candy-machine/"},"\ud83c\udf6c \u521b\u9020\u7cd6\u679c\u673a")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-the-minting-ui/"},"\ud83c\udfa8 \u521b\u5efa\u94f8\u5e01\u7528\u6237\u754c\u9762"))))}c.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunkall_in_one_solana=self.webpackChunkall_in_one_solana||[]).push([[9673],{3905:(e,a,n)=>{n.d(a,{Zo:()=>m,kt:()=>f});var t=n(67294);function r(e,a,n){return a in e?Object.defineProperty(e,a,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[a]=n,e}function l(e,a){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var t=Object.getOwnPropertySymbols(e);a&&(t=t.filter((function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.push.apply(n,t)}return n}function o(e){for(var a=1;a=0||(r[n]=e[n]);return r}(e,a);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);for(t=0;t=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}var s=t.createContext({}),p=function(e){var a=t.useContext(s),n=a;return e&&(n="function"==typeof e?e(a):o(o({},a),e)),n},m=function(e){var a=p(e.components);return t.createElement(s.Provider,{value:a},e.children)},u="mdxType",c={inlineCode:"code",wrapper:function(e){var a=e.children;return t.createElement(t.Fragment,{},a)}},d=t.forwardRef((function(e,a){var n=e.components,r=e.mdxType,l=e.originalType,s=e.parentName,m=i(e,["components","mdxType","originalType","parentName"]),u=p(n),d=r,f=u["".concat(s,".").concat(d)]||u[d]||c[d]||l;return n?t.createElement(f,o(o({ref:a},m),{},{components:n})):t.createElement(f,o({ref:a},m))}));function f(e,a){var n=arguments,r=a&&a.mdxType;if("string"==typeof e||r){var l=n.length,o=new Array(l);o[0]=d;var i={};for(var s in a)hasOwnProperty.call(a,s)&&(i[s]=a[s]);i.originalType=e,i[u]="string"==typeof e?e:r,o[1]=i;for(var p=2;p{n.r(a),n.d(a,{assets:()=>s,contentTitle:()=>o,default:()=>c,frontMatter:()=>l,metadata:()=>i,toc:()=>p});var t=n(87462),r=(n(67294),n(3905));const l={sidebar_position:26,sidebar_label:"Solana \u8fdb\u9636 (SPL token\u4e0eNFT)",sidebar_class_name:"green"},o="Solana \u8fdb\u9636 (SPL token\u4e0eNFT)",i={unversionedId:"module2/README",id:"module2/README",title:"Solana \u8fdb\u9636 (SPL token\u4e0eNFT)",description:"SPL\u4ee3\u5e01",source:"@site/docs/Solana-Co-Learn/module2/README.md",sourceDirName:"module2",slug:"/module2/",permalink:"/solana-co-learn/Solana-Co-Learn/module2/",draft:!1,editUrl:"https://github.com/CreatorsDAO/solana-co-learn/tree/main/docs/Solana-Co-Learn/module2/README.md",tags:[],version:"current",lastUpdatedBy:"v1xingyue",lastUpdatedAt:1727439952,formattedLastUpdatedAt:"Sep 27, 2024",sidebarPosition:26,frontMatter:{sidebar_position:26,sidebar_label:"Solana \u8fdb\u9636 (SPL token\u4e0eNFT)",sidebar_class_name:"green"},sidebar:"tutorialSidebar",previous:{title:"Solana\u94b1\u5305\u4f7f\u7528 - Backpack \ud83c\udf92",permalink:"/solana-co-learn/Solana-Co-Learn/module1/wallet-usage/"},next:{title:"SPL token",permalink:"/solana-co-learn/Solana-Co-Learn/module2/spl-token/"}},s={},p=[{value:"SPL\u4ee3\u5e01",id:"spl\u4ee3\u5e01",level:2},{value:"NFTS + METAPLEX\u94f8\u9020",id:"nfts--metaplex\u94f8\u9020",level:2},{value:"\u5728\u7528\u6237\u754c\u9762\u4e2d\u5c55\u793aNFTS",id:"\u5728\u7528\u6237\u754c\u9762\u4e2d\u5c55\u793anfts",level:2},{value:"\u521b\u9020\u795e\u5947\u7684\u4e92\u8054\u7f51\u8d27\u5e01\u5e76\u51fa\u552eJPEG\u56fe\u7247",id:"\u521b\u9020\u795e\u5947\u7684\u4e92\u8054\u7f51\u8d27\u5e01\u5e76\u51fa\u552ejpeg\u56fe\u7247",level:2}],m={toc:p},u="wrapper";function c(e){let{components:a,...n}=e;return(0,r.kt)(u,(0,t.Z)({},m,n,{components:a,mdxType:"MDXLayout"}),(0,r.kt)("h1",{id:"solana-\u8fdb\u9636-spl-token\u4e0enft"},"Solana \u8fdb\u9636 (SPL token\u4e0eNFT)"),(0,r.kt)("h2",{id:"spl\u4ee3\u5e01"},"SPL\u4ee3\u5e01"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/spl-token/the-token-program/"},"\ud83d\udcb5 ",(0,r.kt)("inlineCode",{parentName:"a"},"The Token Program"))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/spl-token/mint-token-on-solana/"},"\ud83c\udfe7 \u5728",(0,r.kt)("inlineCode",{parentName:"a"},"Solana"),"\u4e0a\u94f8\u9020\u4ee3\u5e01")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/spl-token/token-metadata/"},"\ud83e\uddee \u4ee4\u724c\u5143\u6570\u636e")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/spl-token/give-your-token-an-identity/"}," \ud83e\uddec \u4e3a\u4f60\u7684\u4ee3\u5e01\u8d4b\u4e88\u8eab\u4efd"))),(0,r.kt)("h2",{id:"nfts--metaplex\u94f8\u9020"},"NFTS + METAPLEX\u94f8\u9020"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/nfts-and-minting-with-metaplex/nfts-one-solana/"},"\ud83c\udfa8 ",(0,r.kt)("inlineCode",{parentName:"a"},"Solana"),"\u4e0a\u7684",(0,r.kt)("inlineCode",{parentName:"a"},"NFT"))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/nfts-and-minting-with-metaplex/nft-your-face/"},"\ud83e\udd28 ",(0,r.kt)("inlineCode",{parentName:"a"},"NFT"),"\u4f60\u7684\u8138")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/nfts-and-minting-with-metaplex/candy-machine-and-the-sugar-cli/"},"\ud83c\udf6d \u7cd6\u679c\u673a\u548cSugar CLI"))),(0,r.kt)("h2",{id:"\u5728\u7528\u6237\u754c\u9762\u4e2d\u5c55\u793anfts"},"\u5728\u7528\u6237\u754c\u9762\u4e2d\u5c55\u793aNFTS"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/displaying-nfts/"},"\u5c55\u793a",(0,r.kt)("inlineCode",{parentName:"a"},"NFTs")," \ud83d\udc83")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/displaying-nfts-from-a-wallet/"},"\ud83d\udcf1 \u5728\u94b1\u5305\u4e2d\u5c55\u793a",(0,r.kt)("inlineCode",{parentName:"a"},"NFTs"))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/displayings-nfts-in-a-ui/displaying-nfts-from-a-candy-machine/"},"\ud83d\uddbc \u4ece\u7cd6\u679c\u673a\u5c55\u793a",(0,r.kt)("inlineCode",{parentName:"a"},"NFTs")))),(0,r.kt)("h2",{id:"\u521b\u9020\u795e\u5947\u7684\u4e92\u8054\u7f51\u8d27\u5e01\u5e76\u51fa\u552ejpeg\u56fe\u7247"},"\u521b\u9020\u795e\u5947\u7684\u4e92\u8054\u7f51\u8d27\u5e01\u5e76\u51fa\u552eJPEG\u56fe\u7247"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-reward-tokens/"},"\ud83c\udfa8 \u521b\u5efa\u5956\u52b1\u4ee3\u5e01")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-candy-machine/"},"\ud83c\udf6c \u521b\u9020\u7cd6\u679c\u673a")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/solana-co-learn/Solana-Co-Learn/module2/make-magic-internet-money-and-sell-jepgs/create-the-minting-ui/"},"\ud83c\udfa8 \u521b\u5efa\u94f8\u5e01\u7528\u6237\u754c\u9762"))))}c.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/030b84d0.7fcb7f51.js b/assets/js/030b84d0.17c1adc8.js similarity index 99% rename from assets/js/030b84d0.7fcb7f51.js rename to assets/js/030b84d0.17c1adc8.js index c5203387f..f1bee6a04 100644 --- a/assets/js/030b84d0.7fcb7f51.js +++ b/assets/js/030b84d0.17c1adc8.js @@ -1 +1 @@ -"use strict";(self.webpackChunkall_in_one_solana=self.webpackChunkall_in_one_solana||[]).push([[5920],{3905:(e,n,t)=>{t.d(n,{Zo:()=>s,kt:()=>m});var a=t(67294);function r(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function o(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);n&&(a=a.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,a)}return t}function l(e){for(var n=1;n=0||(r[t]=e[t]);return r}(e,n);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(r[t]=e[t])}return r}var i=a.createContext({}),c=function(e){var n=a.useContext(i),t=n;return e&&(t="function"==typeof e?e(n):l(l({},n),e)),t},s=function(e){var n=c(e.components);return a.createElement(i.Provider,{value:n},e.children)},u="mdxType",d={inlineCode:"code",wrapper:function(e){var n=e.children;return a.createElement(a.Fragment,{},n)}},k=a.forwardRef((function(e,n){var t=e.components,r=e.mdxType,o=e.originalType,i=e.parentName,s=p(e,["components","mdxType","originalType","parentName"]),u=c(t),k=r,m=u["".concat(i,".").concat(k)]||u[k]||d[k]||o;return t?a.createElement(m,l(l({ref:n},s),{},{components:t})):a.createElement(m,l({ref:n},s))}));function m(e,n){var t=arguments,r=n&&n.mdxType;if("string"==typeof e||r){var o=t.length,l=new Array(o);l[0]=k;var p={};for(var i in n)hasOwnProperty.call(n,i)&&(p[i]=n[i]);p.originalType=e,p[u]="string"==typeof e?e:r,l[1]=p;for(var c=2;c{t.r(n),t.d(n,{assets:()=>i,contentTitle:()=>l,default:()=>d,frontMatter:()=>o,metadata:()=>p,toc:()=>c});var a=t(87462),r=(t(67294),t(3905));const o={sidebar_position:91,sidebar_label:"\ud83e\udd69 \u4f7f\u7528Anchor\u8fdb\u884cNFT\u7684\u8d28\u62bc",sidebar_class_name:"green"},l="\ud83e\udd69 \u4f7f\u7528Anchor\u8fdb\u884cNFT\u7684\u8d28\u62bc",p={unversionedId:"module5/a-full-stack-anchor-app/staking-with-anchor/README",id:"module5/a-full-stack-anchor-app/staking-with-anchor/README",title:"\ud83e\udd69 \u4f7f\u7528Anchor\u8fdb\u884cNFT\u7684\u8d28\u62bc",description:"\u73b0\u5728\u662f\u65f6\u5019\u5c06\u4f60\u7684NFT\u8d28\u62bc\u8ba1\u5212\u53ca\u7528\u6237\u754c\u9762\u8fc1\u79fb\u5230Anchor\u4e0a\u4e86\uff01\u4f60\u4e00\u76f4\u8f9b\u82e6\u5f00\u53d1\u7684buildoor\u9879\u76ee\u5df2\u7ecf\u76f8\u5f53\u51fa\u8272\uff0c\u4f46\u5c06\u5176\u8fc1\u79fb\u5230Anchor\u4e0a\u5c06\u4f7f\u672a\u6765\u7684\u5de5\u4f5c\u53d8\u5f97\u66f4\u52a0\u7b80\u6d01\u3002\u8bf7\u5229\u7528\u4f60\u6240\u638c\u63e1\u7684\u77e5\u8bc6\uff0c\u5b8c\u6210\u4e0b\u8ff0\u4efb\u52a1\uff1a",source:"@site/docs/Solana-Co-Learn/module5/a-full-stack-anchor-app/staking-with-anchor/README.md",sourceDirName:"module5/a-full-stack-anchor-app/staking-with-anchor",slug:"/module5/a-full-stack-anchor-app/staking-with-anchor/",permalink:"/solana-co-learn/Solana-Co-Learn/module5/a-full-stack-anchor-app/staking-with-anchor/",draft:!1,editUrl:"https://github.com/CreatorsDAO/solana-co-learn/tree/main/docs/Solana-Co-Learn/module5/a-full-stack-anchor-app/staking-with-anchor/README.md",tags:[],version:"current",lastUpdatedBy:"v1xingyue",lastUpdatedAt:1727439818,formattedLastUpdatedAt:"Sep 27, 2024",sidebarPosition:91,frontMatter:{sidebar_position:91,sidebar_label:"\ud83e\udd69 \u4f7f\u7528Anchor\u8fdb\u884cNFT\u7684\u8d28\u62bc",sidebar_class_name:"green"},sidebar:"tutorialSidebar",previous:{title:"\u4e00\u4e2a\u5168\u6808\u7684Anchor\u5e94\u7528\u7a0b\u5e8f",permalink:"/solana-co-learn/Solana-Co-Learn/module5/a-full-stack-anchor-app/"},next:{title:"\ud83d\udcb8 \u4f7f\u7528Anchor\u8d4e\u56de",permalink:"/solana-co-learn/Solana-Co-Learn/module5/a-full-stack-anchor-app/redeeming-with-anchor/"}},i={},c=[],s={toc:c},u="wrapper";function d(e){let{components:n,...t}=e;return(0,r.kt)(u,(0,a.Z)({},s,t,{components:n,mdxType:"MDXLayout"}),(0,r.kt)("h1",{id:"-\u4f7f\u7528anchor\u8fdb\u884cnft\u7684\u8d28\u62bc"},"\ud83e\udd69 \u4f7f\u7528Anchor\u8fdb\u884cNFT\u7684\u8d28\u62bc"),(0,r.kt)("p",null,"\u73b0\u5728\u662f\u65f6\u5019\u5c06\u4f60\u7684",(0,r.kt)("inlineCode",{parentName:"p"},"NFT"),"\u8d28\u62bc\u8ba1\u5212\u53ca\u7528\u6237\u754c\u9762\u8fc1\u79fb\u5230",(0,r.kt)("inlineCode",{parentName:"p"},"Anchor"),"\u4e0a\u4e86\uff01\u4f60\u4e00\u76f4\u8f9b\u82e6\u5f00\u53d1\u7684",(0,r.kt)("inlineCode",{parentName:"p"},"buildoor"),"\u9879\u76ee\u5df2\u7ecf\u76f8\u5f53\u51fa\u8272\uff0c\u4f46\u5c06\u5176\u8fc1\u79fb\u5230",(0,r.kt)("inlineCode",{parentName:"p"},"Anchor"),"\u4e0a\u5c06\u4f7f\u672a\u6765\u7684\u5de5\u4f5c\u53d8\u5f97\u66f4\u52a0\u7b80\u6d01\u3002\u8bf7\u5229\u7528\u4f60\u6240\u638c\u63e1\u7684\u77e5\u8bc6\uff0c\u5b8c\u6210\u4e0b\u8ff0\u4efb\u52a1\uff1a"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"\u4f7f\u7528",(0,r.kt)("inlineCode",{parentName:"li"},"Anchor"),"\u4ece\u5934\u5f00\u59cb\u91cd\u65b0\u7f16\u5199\u4ee3\u7801\u3002"),(0,r.kt)("li",{parentName:"ul"},"\u589e\u52a0\u4e00\u4e9b\u53ef\u9760\u7684\u6d4b\u8bd5\u8986\u76d6\uff0c\u4ee5\u786e\u4fdd\u4f60\u80fd\u591f\u4e25\u5bc6\u6355\u6349\u4efb\u4f55\u5b89\u5168\u98ce\u9669\u3002"),(0,r.kt)("li",{parentName:"ul"},"\u7528",(0,r.kt)("inlineCode",{parentName:"li"},"Anchor"),"\u7684\u65b9\u6cd5\u6784\u5efa\u5668\u6765\u66ff\u6362\u590d\u6742\u7684",(0,r.kt)("inlineCode",{parentName:"li"},"UI"),"\u4ee3\u7801\u3002")),(0,r.kt)("p",null,"\u8fd9\u9879\u4efb\u52a1\u53ef\u80fd\u6709\u4e9b\u590d\u6742\uff0c\u9700\u8981\u4f60\u6295\u5165\u4e00\u4e9b\u65f6\u95f4\u72ec\u7acb\u8fdb\u884c\u5c1d\u8bd5\u3002\u5982\u679c\u51e0\u4e2a\u5c0f\u65f6\u540e\u4f60\u611f\u5230\u56f0\u60d1\uff0c\u968f\u65f6\u53ef\u4ee5\u89c2\u770b\u6211\u4eec\u63d0\u4f9b\u7684\u89c6\u9891\u6f14\u793a\u89e3\u51b3\u65b9\u6848\u3002"),(0,r.kt)("p",null,"\u6211\u4eec\u6765\u5171\u540c\u5b8c\u6210\u8fd9\u4e2a\u4efb\u52a1\uff0c\u5e76\u67e5\u770b\u6211\u4eec\u7684\u6210\u679c\u3002\u6211\u4eec\u4e0d\u662f\u5728\u589e\u52a0\u65b0\u529f\u80fd\uff0c\u800c\u662f\u8981\u5b8c\u5168\u7528",(0,r.kt)("inlineCode",{parentName:"p"},"Anchor"),"\u66ff\u6362\u6211\u4eec\u4e00\u76f4\u5728\u52aa\u529b\u5f00\u53d1\u7684\u8d28\u62bc\u8ba1\u5212\u3002"),(0,r.kt)("p",null,"\u9996\u5148\uff0c\u901a\u8fc7\u8fd0\u884c ",(0,r.kt)("inlineCode",{parentName:"p"},"anchor init anchor-nft-staking")," \u521b\u5efa\u4e00\u4e2a\u65b0\u9879\u76ee\uff0c\u6216\u8005\u53ef\u4ee5\u9009\u62e9\u81ea\u5df1\u7684\u540d\u5b57\u3002\u7136\u540e\u6253\u5f00 ",(0,r.kt)("inlineCode",{parentName:"p"},"Anchor.toml")," \u6587\u4ef6\uff0c\u5c06\u79cd\u5b50\u8bbe\u7f6e\u4e3a ",(0,r.kt)("inlineCode",{parentName:"p"},"true"),"\uff0c\u96c6\u7fa4\u8bbe\u7f6e\u4e3a ",(0,r.kt)("inlineCode",{parentName:"p"},"devnet"),"\u3002"),(0,r.kt)("p",null,"\u63a5\u4e0b\u6765\uff0c\u8df3\u8f6c\u5230 ",(0,r.kt)("inlineCode",{parentName:"p"},"/programs/anchor-nft-staking/Cargo.toml"),"\uff0c\u5e76\u6dfb\u52a0\u4ee5\u4e0b\u4f9d\u8d56\u9879\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-toml"},'anchor-lang = { version="0.25.0", features = ["init-if-needed"] }\nanchor-spl = "0.25.0"\nmpl-token-metadata = { version="1.4.1", features=["no-entrypoint"] }\n')),(0,r.kt)("p",null,"\u597d\u7684\uff0c\u6253\u5f00 ",(0,r.kt)("inlineCode",{parentName:"p"},"lib.rs")," \u6587\u4ef6\uff0c\u6211\u4eec\u6765\u6784\u5efa\u57fa\u672c\u7684\u6846\u67b6\u3002"),(0,r.kt)("p",null,"\u6211\u4eec\u9700\u8981\u6dfb\u52a0\u4ee5\u4e0b\u5bfc\u5165\u3002\u968f\u7740\u6211\u4eec\u7684\u5de5\u4f5c\u8fdb\u5c55\uff0c\u6bcf\u4e2a\u5bfc\u5165\u7684\u5fc5\u8981\u6027\u5c06\u9010\u6e10\u663e\u73b0\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"use anchor_lang::solana_program::program::invoke_signed;\nuse anchor_spl::token;\nuse anchor_spl::{\n associated_token::AssociatedToken,\n token::{Approve, Mint, MintTo, Revoke, Token, TokenAccount},\n};\nuse mpl_token_metadata::{\n instruction::{freeze_delegated_account, thaw_delegated_account},\n ID as MetadataTokenId,\n};\n")),(0,r.kt)("p",null,"\u9996\u5148\uff0c\u6211\u4eec\u5c06\u9ed8\u8ba4\u51fd\u6570\u7684\u540d\u79f0\u66f4\u6539\u4e3a ",(0,r.kt)("inlineCode",{parentName:"p"},"stake"),"\uff0c\u5e76\u66f4\u6539\u5176\u76f8\u5173\u4e0a\u4e0b\u6587\u4e3a\u7c7b\u578b",(0,r.kt)("inlineCode",{parentName:"p"},"Stake"),"\u3002"),(0,r.kt)("p",null,"\u7136\u540e\u6dfb\u52a0\u540d\u4e3a ",(0,r.kt)("inlineCode",{parentName:"p"},"redeem")," \u7684\u51fd\u6570\uff0c\u4e0a\u4e0b\u6587\u7c7b\u578b\u4e3a ",(0,r.kt)("inlineCode",{parentName:"p"},"Redeem"),"\u3002"),(0,r.kt)("p",null,"\u6700\u540e\uff0c\u5bf9\u4e8e ",(0,r.kt)("inlineCode",{parentName:"p"},"unstake"),"\uff0c\u4f7f\u7528\u4e0a\u4e0b\u6587\u7c7b\u578b ",(0,r.kt)("inlineCode",{parentName:"p"},"Unstake")," \u8fdb\u884c\u76f8\u540c\u64cd\u4f5c\u3002"),(0,r.kt)("p",null,"\u63a5\u4e0b\u6765\u6211\u4eec\u8981\u6784\u5efa\u7684\u662f",(0,r.kt)("inlineCode",{parentName:"p"},"Stake"),"\u7684\u7ed3\u6784\u3002\u6211\u4eec\u9700\u8981\u4e00\u4e2a",(0,r.kt)("inlineCode",{parentName:"p"},"PDA"),"\u6765\u5b58\u50a8",(0,r.kt)("inlineCode",{parentName:"p"},"UserStakeInfo"),"\uff0c\u5e76\u4e14\u9700\u8981\u4e00\u4e2a",(0,r.kt)("inlineCode",{parentName:"p"},"StakeState"),"\u679a\u4e3e\u6765\u8868\u793a",(0,r.kt)("inlineCode",{parentName:"p"},"PDA"),"\u7684\u5176\u4e2d\u4e00\u4e2a\u5b57\u6bb5\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"#[account]\npub struct UserStakeInfo {\n pub token_account: Pubkey,\n pub stake_start_time: i64,\n pub last_stake_redeem: i64,\n pub user_pubkey: Pubkey,\n pub stake_state: StakeState,\n pub is_initialized: bool,\n}\n\n#[derive(Debug, PartialEq, AnchorDeserialize, AnchorSerialize, Clone)]\npub enum StakeState {\n Unstaked,\n Staked,\n}\n")),(0,r.kt)("p",null,"\u4e3a",(0,r.kt)("inlineCode",{parentName:"p"},"StakeState"),"\u6dfb\u52a0\u4e00\u4e2a\u9ed8\u8ba4\u503c\uff0c\u8bbe\u4e3a\u672a\u62b5\u62bc\u72b6\u6001\u3002"),(0,r.kt)("p",null,"\u7531\u4e8e\u6211\u4eec\u5c06\u4f7f\u7528\u7684\u5143\u6570\u636e\u7a0b\u5e8f\u76f8\u5bf9\u8f83\u65b0\uff0c\u951a\u5b9a\u7a0b\u5e8f\u4e2d\u8fd8\u6ca1\u6709\u76f8\u5e94\u7684\u7c7b\u578b\u3002\u4e3a\u4e86\u50cf\u5176\u4ed6\u7a0b\u5e8f\uff08\u4f8b\u5982\u7cfb\u7edf\u7a0b\u5e8f\u3001\u4ee4\u724c\u7a0b\u5e8f\u7b49\uff09\u4e00\u6837\u4f7f\u7528\u5b83\uff0c\u6211\u4eec\u5c06\u4e3a\u5176\u521b\u5efa\u4e00\u4e2a\u7ed3\u6784\u4f53\uff0c\u5e76\u6dfb\u52a0\u4e00\u4e2a\u540d\u4e3a ",(0,r.kt)("inlineCode",{parentName:"p"},"id")," \u7684\u5b9e\u73b0\uff0c\u8fd4\u56de\u4e00\u4e2a ",(0,r.kt)("inlineCode",{parentName:"p"},"Pubkey"),"\uff0c\u5b83\u5bf9\u5e94\u4e8e ",(0,r.kt)("inlineCode",{parentName:"p"},"MetadataTokenId"),"\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"#[derive(Clone)]\npub struct Metadata;\n\nimpl anchor_lang::Id for Metadata {\n fn id() -> Pubkey {\n MetadataTokenId\n }\n}\n")),(0,r.kt)("p",null,"\u597d\u7684\uff0c\u73b0\u5728\u6211\u4eec\u53ef\u4ee5\u5f00\u59cb\u5904\u7406\u8d28\u62bc\u90e8\u5206\u3002\u4e0b\u9762\u662f\u7ed3\u6784\u4f53\u6240\u9700\u7684\u4e5d\u4e2a\u8d26\u6237\uff0c\u4ee5\u53ca\u4e00\u4e9b\u503c\u5f97\u6ce8\u610f\u7684\u4e8b\u9879\u3002"),(0,r.kt)("p",null,"\u9996\u5148\uff0c\u4f60\u4f1a\u770b\u5230 ",(0,r.kt)("inlineCode",{parentName:"p"},"nft_edition")," \u662f\u4e00\u4e2a ",(0,r.kt)("inlineCode",{parentName:"p"},"Unchecked")," \u8d26\u6237\uff0c\u8fd9\u662f\u56e0\u4e3a\u7cfb\u7edf\u4e2d\u8fd8\u672a\u4e3a\u8fd9\u79cd\u7c7b\u578b\u7684\u8d26\u6237\u521b\u5efa\u3002\u6240\u6709\u672a\u7ecf\u6838\u5b9e\u7684\u8d26\u6237\u90fd\u9700\u9644\u5e26\u4e00\u6761\u5907\u6ce8\uff0c\u4ee5\u4fbf\u7cfb\u7edf\u77e5\u9053\u4f60\u5c06\u8fdb\u884c\u624b\u52a8\u5b89\u5168\u68c0\u67e5\u3002\u4f60\u4f1a\u5728\u4e0b\u65b9\u770b\u5230 ",(0,r.kt)("inlineCode",{parentName:"p"},"CHECK: Manual validation"),"\u3002"),(0,r.kt)("p",null,"\u9700\u8981\u63d0\u9192\u7684\u662f\uff0c\u6bcf\u4e2a\u8d26\u6237\u4e0a\u7684\u5c5e\u6027\u90fd\u662f\u4e00\u79cd\u5b89\u5168\u68c0\u67e5\uff0c\u4ee5\u786e\u4fdd\u8d26\u6237\u662f\u6b63\u786e\u7684\u7c7b\u578b\u5e76\u80fd\u6267\u884c\u7279\u5b9a\u529f\u80fd\u3002\u7531\u4e8e\u7528\u6237\u9700\u8981\u4ed8\u8d39\uff0c\u5e76\u4e14",(0,r.kt)("inlineCode",{parentName:"p"},"NFT"),"\u4ee3\u5e01\u8d26\u6237\u5c06\u88ab\u4fee\u6539\uff0c\u6240\u4ee5\u4e24\u8005\u90fd\u5177\u6709",(0,r.kt)("inlineCode",{parentName:"p"},"mut"),"\u5c5e\u6027\u3002\u67d0\u4e9b\u8d26\u6237\u8fd8\u9700\u8981\u79cd\u5b50\uff0c\u5982\u4e0b\u6240\u793a\u3002"),(0,r.kt)("p",null,"\u81f3\u4e8e\u5176\u4ed6\u6ca1\u6709\u4efb\u4f55\u5c5e\u6027\u7684\u8d26\u6237\uff0c\u5b83\u4eec\u5728",(0,r.kt)("inlineCode",{parentName:"p"},"Anchor"),"\u4e2d\u90fd\u6709\u81ea\u5df1\u5fc5\u9700\u7684\u5b89\u5168\u68c0\u67e5\uff0c\u6240\u4ee5\u6211\u4eec\u4e0d\u9700\u8981\u6dfb\u52a0\u4efb\u4f55\u5c5e\u6027\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"#[derive(Accounts)]\npub struct Stake<'info> {\n #[account(mut)]\n pub user: Signer<'info>,\n #[account(\n mut,\n associated_token::mint=nft_mint,\n associated_token::authority=user\n )]\n pub nft_token_account: Account<'info, TokenAccount>,\n pub nft_mint: Account<'info, Mint>,\n /// CHECK: Manual validation\n #[account(owner=MetadataTokenId)]\n pub nft_edition: UncheckedAccount<'info>,\n #[account(\n init_if_needed,\n payer=user,\n space = std::mem::size_of::() + 8,\n seeds = [user.key().as_ref(), nft_token_account.key().as_ref()],\n bump\n )]\n pub stake_state: Account<'info, UserStakeInfo>,\n /// CHECK: Manual validation\n #[account(mut, seeds=[\"authority\".as_bytes().as_ref()], bump)]\n pub program_authority: UncheckedAccount<'info>,\n pub token_program: Program<'info, Token>,\n pub system_program: Program<'info, System>,\n pub metadata_program: Program<'info, Metadata>,\n}\n")),(0,r.kt)("p",null,"\u5728\u7ee7\u7eed\u64cd\u4f5c\u4e4b\u524d\uff0c\u5148\u8fd0\u884c",(0,r.kt)("inlineCode",{parentName:"p"},"anchor build"),"\uff0c\u8fd9\u6837\u6211\u4eec\u7684\u7b2c\u4e00\u4e2a\u6784\u5efa\u5c31\u53ef\u4ee5\u5f00\u59cb\u4e86\u3002\u8bf7\u8bb0\u4f4f\uff0c\u8fd9\u662f\u6211\u4eec\u7684\u7b2c\u4e00\u6b21\u6784\u5efa\uff0c\u5b83\u4f1a\u751f\u6210\u6211\u4eec\u7684\u7a0b\u5e8f",(0,r.kt)("inlineCode",{parentName:"p"},"ID"),"\u3002"),(0,r.kt)("p",null,"\u5728\u6784\u5efa\u7684\u540c\u65f6\uff0c\u5728",(0,r.kt)("inlineCode",{parentName:"p"},"tests"),"\u76ee\u5f55\u4e2d\u521b\u5efa\u4e00\u4e2a\u540d\u4e3a",(0,r.kt)("inlineCode",{parentName:"p"},"utils"),"\u7684\u65b0\u6587\u4ef6\u5939\u3002\u5728\u5176\u4e2d\u521b\u5efa\u4e00\u4e2a\u540d\u4e3a",(0,r.kt)("inlineCode",{parentName:"p"},"setupNft.ts"),"\u7684\u6587\u4ef6\uff0c\u5e76\u5c06\u4e0b\u9762\u7684\u4ee3\u7801\u7c98\u8d34\u8fdb\u53bb\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},'import {\n bundlrStorage,\n keypairIdentity,\n Metaplex,\n} from "@metaplex-foundation/js"\nimport { createMint, getAssociatedTokenAddress } from "@solana/spl-token"\nimport * as anchor from "@project-serum/anchor"\n\nexport const setupNft = async (program, payer) => {\n const metaplex = Metaplex.make(program.provider.connection)\n .use(keypairIdentity(payer))\n .use(bundlrStorage())\n\n const nft = await metaplex\n .nfts()\n .create({\n uri: "",\n name: "Test nft",\n sellerFeeBasisPoints: 0,\n })\n\n console.log("nft metadata pubkey: ", nft.metadataAddress.toBase58())\n console.log("nft token address: ", nft.tokenAddress.toBase58())\n const [delegatedAuthPda] = await anchor.web3.PublicKey.findProgramAddress(\n [Buffer.from("authority")],\n program.programId\n )\n const [stakeStatePda] = await anchor.web3.PublicKey.findProgramAddress(\n [payer.publicKey.toBuffer(), nft.tokenAddress.toBuffer()],\n program.programId\n )\n\n console.log("delegated authority pda: ", delegatedAuthPda.toBase58())\n console.log("stake state pda: ", stakeStatePda.toBase58())\n const [mintAuth] = await anchor.web3.PublicKey.findProgramAddress(\n [Buffer.from("mint")],\n program.programId\n )\n\n const mint = await createMint(\n program.provider.connection,\n payer,\n mintAuth,\n null,\n 2\n )\n console.log("Mint pubkey: ", mint.toBase58())\n\n const tokenAddress = await getAssociatedTokenAddress(mint, payer.publicKey)\n\n return {\n nft: nft,\n delegatedAuthPda: delegatedAuthPda,\n stakeStatePda: stakeStatePda,\n mint: mint,\n mintAuth: mintAuth,\n tokenAddress: tokenAddress,\n }\n}\n\n')),(0,r.kt)("p",null,"\u7136\u540e\uff0c\u8fd0\u884c",(0,r.kt)("inlineCode",{parentName:"p"},"npm install @metaplex-foundation/js"),"\u3002"),(0,r.kt)("p",null,"\u6700\u540e\uff0c\u8f6c\u5230",(0,r.kt)("inlineCode",{parentName:"p"},"anchor-nft-staking.ts"),"\u76ee\u5f55\u3002\u8fd9\u662f",(0,r.kt)("inlineCode",{parentName:"p"},"Anchor"),"\u521b\u5efa\u7684\u9ed8\u8ba4\u6587\u4ef6\u3002"),(0,r.kt)("p",null,"\u4f60\u9700\u8981\u5c06\u63d0\u4f9b\u8005\u7684\u9ed8\u8ba4\u884c\u5206\u4e3a\u4e24\u90e8\u5206\uff0c\u4ee5\u4fbf\u5728\u4ee5\u540e\u9700\u8981\u65f6\u80fd\u591f\u8bbf\u95ee\u63d0\u4f9b\u8005\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},"const provider = anchor.AnchorProvider.env();\nanchor.setProvider(provider);\n")),(0,r.kt)("p",null,"\u8ba9\u6211\u4eec\u5f15\u5165\u94b1\u5305\uff0c\u8fd9\u5c06\u4f7f\u6211\u4eec\u80fd\u591f\u516c\u5f00\u4ed8\u6b3e\u4eba\u4e3a\u4ea4\u6613\u7b7e\u540d\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},"const wallet = anchor.workspace.AnchorNftStaking.provider.wallet;\n")),(0,r.kt)("p",null,"\u68c0\u67e5\u4f60\u7684\u7f16\u8bd1\u60c5\u51b5\uff0c\u5982\u679c\u4e00\u5207\u987a\u5229\uff0c\u8bf7\u8fd0\u884c ",(0,r.kt)("inlineCode",{parentName:"p"},"anchor deploy"),"\u3002\u5982\u679c\u51fa\u73b0\u95ee\u9898\uff0c\u4f60\u53ef\u80fd\u9700\u8981\u4e3a\u81ea\u5df1\u7a7a\u6295\u4e00\u4e9bSOL\u3002"),(0,r.kt)("p",null,"\u7f16\u8bd1\u5b8c\u6210\u540e\uff0c\u8fd0\u884c ",(0,r.kt)("inlineCode",{parentName:"p"},"anchor keys list")," \u5e76\u83b7\u53d6\u7a0b\u5e8f",(0,r.kt)("inlineCode",{parentName:"p"},"ID"),"\uff0c\u7136\u540e\u5c06\u5176\u653e\u5165 ",(0,r.kt)("inlineCode",{parentName:"p"},"lib.rs")," \u548c ",(0,r.kt)("inlineCode",{parentName:"p"},"Anchor.toml")," \u6587\u4ef6\u4e2d\u3002\u5982\u679c\u7f16\u8bd1\u82b1\u8d39\u4e00\u4e9b\u65f6\u95f4\uff0c\u4f60\u53ef\u80fd\u9700\u8981\u56de\u5230\u8fd9\u4e00\u6b65\u3002"),(0,r.kt)("p",null,"\u56de\u5230\u6d4b\u8bd5\u6587\u4ef6\u3002"),(0,r.kt)("p",null,"\u8ba9\u6211\u4eec\u5b9a\u4e49\u4e00\u4e9b\u6d4b\u8bd5\u4e2d\u9700\u8981\u4f7f\u7528\u7684\u53d8\u91cf\u7c7b\u578b\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},"let delegatedAuthPda: anchor.web3.PublicKey;\nlet stakeStatePda: anchor.web3.PublicKey;\nlet nft: any;\nlet mintAuth: anchor.web3.PublicKey;\nlet mint: anchor.web3.PublicKey;\nlet tokenAddress: anchor.web3.PublicKey;\n")),(0,r.kt)("p",null,"\u73b0\u5728\uff0c\u6211\u4eec\u6dfb\u52a0\u4e00\u4e2a ",(0,r.kt)("inlineCode",{parentName:"p"},"before"),' \u51fd\u6570\uff0c\u8be5\u51fd\u6570\u4f1a\u5728\u6d4b\u8bd5\u8fd0\u884c\u4e4b\u524d\u88ab\u8c03\u7528\u3002\u6ce8\u610f"',(0,r.kt)("inlineCode",{parentName:"p"},";"),'"\u8bed\u6cd5\uff0c\u5b83\u4f1a\u89e3\u6784\u8fd4\u56de\u503c\u5e76\u4e3a\u6240\u6709\u8fd9\u4e9b\u503c\u8fdb\u884c\u8bbe\u7f6e\u3002'),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},"before(async () => {\n ;({ nft, delegatedAuthPda, stakeStatePda, mint, mintAuth, tokenAddress } =\n await setupNft(program, wallet.payer));\n });\n")),(0,r.kt)("p",null,"\u8f6c\u5230\u9ed8\u8ba4\u6d4b\u8bd5\uff0c\u5c06\u5176\u66f4\u6539\u4e3a ",(0,r.kt)("inlineCode",{parentName:"p"},'it("Stakes"'),"\u3002\u9996\u5148\uff0c\u6211\u4eec\u53ea\u662f\u786e\u8ba4\u51fd\u6570\u88ab\u6210\u529f\u8c03\u7528\u3002\u6211\u4eec\u8fd8\u6ca1\u6709\u6784\u5efa\u5b9e\u9645\u7684\u62b5\u62bc\u51fd\u6570\uff0c\u6240\u4ee5\u6682\u65f6\u4e0d\u4f1a\u8fdb\u884c\u4efb\u4f55\u903b\u8f91\u6d4b\u8bd5\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},'it("Stakes", async () => {\n // \u5728\u6b64\u6dfb\u52a0\u4f60\u7684\u6d4b\u8bd5\u3002\n await program.methods\n .stake()\n .accounts({\n nftTokenAccount: nft.tokenAddress,\n nftMint: nft.mintAddress,\n nftEdition: nft.masterEditionAddress,\n metadataProgram: METADATA_PROGRAM_ID,\n })\n .rpc();\n });\n')),(0,r.kt)("p",null,"\u73b0\u5728\uff0c\u8fd0\u884c ",(0,r.kt)("inlineCode",{parentName:"p"},"anchor test"),"\u3002\u5982\u679c\u5b83\u901a\u8fc7\u4e86\uff0c\u8fd9\u610f\u5473\u7740\u6211\u4eec\u901a\u8fc7\u4e86\u5728",(0,r.kt)("inlineCode",{parentName:"p"},"Stake"),"\u7ed3\u6784\u4e2d\u521b\u5efa\u8d26\u6237\u7684\u9a8c\u8bc1\u3002"),(0,r.kt)("p",null,"\u56de\u5230\u903b\u8f91\u90e8\u5206\uff0c\u4e0b\u9762\u662f\u62b5\u62bc\u5de5\u4f5c\u6240\u9700\u7684\u9010\u6b65\u64cd\u4f5c\u3002\u6211\u4eec\u9700\u8981\u83b7\u53d6\u65f6\u949f\u8bbf\u95ee\u6743\u9650\uff0c\u786e\u4fdd\u62b5\u62bc\u72b6\u6001\u5df2\u521d\u59cb\u5316\uff0c\u5e76\u786e\u8ba4\u5c1a\u672a\u62b5\u62bc\u3002"),(0,r.kt)("p",null,"\u5728",(0,r.kt)("inlineCode",{parentName:"p"},"stake"),"\u51fd\u6570\u4e2d\uff0c\u6211\u4eec\u9996\u5148\u83b7\u53d6\u65f6\u949f\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"let clock = Clock::get().unwrap();\n")),(0,r.kt)("p",null,"\u63a5\u4e0b\u6765\uff0c\u6211\u4eec\u521b\u5efa\u4e00\u4e2a",(0,r.kt)("inlineCode",{parentName:"p"},"CPI"),"\u6765\u59d4\u6258\u8be5\u7a0b\u5e8f\u4f5c\u4e3a\u51bb\u7ed3\u6216\u89e3\u51bb\u6211\u4eec\u7684",(0,r.kt)("inlineCode",{parentName:"p"},"NFT"),"\u7684\u6743\u9650\u3002\u9996\u5148\uff0c\u6211\u4eec\u8bbe\u7f6e",(0,r.kt)("inlineCode",{parentName:"p"},"CPI"),"\uff0c\u7136\u540e\u786e\u5b9a\u6211\u4eec\u8981\u4f7f\u7528\u7684\u8d26\u6237\uff0c\u6700\u540e\u8bbe\u5b9a\u6743\u9650\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},'msg!("Approving delegate");\n\nlet cpi_approve_program = ctx.accounts.token_program.to_account_info();\nlet cpi_approve_accounts = Approve {\n to: ctx.accounts.nft_token_account.to_account_info(),\n delegate: ctx.accounts.program_authority.to_account_info(),\n authority: ctx.accounts.user.to_account_info(),\n};\n\nlet cpi_approve_ctx = CpiContext::new(cpi_approve_program, cpi_approve_accounts);\ntoken::approve(cpi_approve_ctx, 1)?;\n')),(0,r.kt)("p",null,"\u7136\u540e\u6211\u4eec\u5f00\u59cb\u51bb\u7ed3\u4ee3\u5e01\u3002\u9996\u5148\u8bbe\u7f6e\u6743\u9650\u63d0\u5347\uff0c\u7136\u540e\u8c03\u7528",(0,r.kt)("inlineCode",{parentName:"p"},"invoke_signed"),"\u51fd\u6570\uff0c\u4f20\u5165\u6240\u6709\u5fc5\u8981\u7684\u8d26\u6237\u548c\u8d26\u6237\u4fe1\u606f\u6570\u7ec4\uff0c\u6700\u540e\u662f\u79cd\u5b50\u548c\u63d0\u5347\u503c\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},'msg!("Freezing token account");\nlet authority_bump = *ctx.bumps.get("program_authority").unwrap();\ninvoke_signed(\n &freeze_delegated_account(\n ctx.accounts.metadata_program.key(),\n ctx.accounts.program_authority.key(),\n ctx.accounts.nft_token_account.key(),\n ctx.accounts.nft_edition.key(),\n ctx.accounts.nft_mint.key(),\n ),\n &[\n ctx.accounts.program_authority.to_account_info(),\n ctx.accounts.nft_token_account.to_account_info(),\n ctx.accounts.nft_edition.to_account_info(),\n ctx.accounts.nft_mint.to_account_info(),\n ctx.accounts.metadata_program.to_account_info(),\n ],\n &[&[b"authority", &[authority_bump]]],\n)?;\n')),(0,r.kt)("p",null,"\u73b0\u5728\uff0c\u6211\u4eec\u5728\u80a1\u6743\u8d26\u6237\u4e0a\u8bbe\u7f6e\u6570\u636e\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"ctx.accounts.stake_state.token_account = ctx.accounts.nft_token_account.key();\nctx.accounts.stake_state.user_pubkey = ctx.accounts.user.key();\nctx.accounts.stake_state.stake_state = StakeState::Staked;\nctx.accounts.stake_state.stake_start_time = clock.unix_timestamp;\nctx.accounts.stake_state.last_stake_redeem = clock.unix_timestamp;\nctx.accounts.stake_state.is_initialized = true;\n")),(0,r.kt)("p",null,"\u54ce\u5440\uff0c\u8ba9\u6211\u4eec\u8df3\u5230\u6587\u4ef6\u5f00\u59cb\u90e8\u5206\u5e76\u6dfb\u52a0\u4e00\u4e2a\u5b89\u5168\u68c0\u67e5\uff0c\u6211\u4eec\u8fd8\u9700\u8981\u4e00\u4e2a\u81ea\u5b9a\u4e49\u9519\u8bef\u3002\u4e0b\u9762\u662f\u4e24\u6bb5\u4ee3\u7801\uff0c\u4f46\u662f\u5c06\u81ea\u5b9a\u4e49\u9519\u8bef\u4ee3\u7801\u653e\u5728\u6587\u4ef6\u5e95\u90e8\uff0c\u8fd9\u6837\u4e0d\u4f1a\u5f71\u54cd\u903b\u8f91\u548c\u7ed3\u6784\u7684\u9605\u8bfb\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"require!(\n ctx.accounts.stake_state.stake_state == StakeState::Unstaked,\n StakeError::AlreadyStaked\n);\n")),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},'#[error_code]\npub enum StakeError {\n #[msg("NFT already staked")]\n AlreadyStaked,\n}\n')),(0,r.kt)("p",null,"\u5728\u518d\u6b21\u6d4b\u8bd5\u4e4b\u524d\uff0c\u4e0d\u8981\u5fd8\u8bb0\u5145\u5b9e\u4f60\u7684",(0,r.kt)("inlineCode",{parentName:"p"},"SOL"),"\u4f59\u989d\u3002"),(0,r.kt)("p",null,"\u597d\u7684\uff0c\u5c31\u8fd9\u6837\uff0c\u8ba9\u6211\u4eec\u56de\u5230\u6d4b\u8bd5\u4e2d\uff0c\u4e3a\u6211\u4eec\u7684\u8d28\u62bc\u6d4b\u8bd5\u6dfb\u52a0\u4e00\u4e9b\u529f\u80fd\uff0c\u4ee5\u68c0\u67e5\u8d28\u62bc\u72b6\u6001\u662f\u5426\u6b63\u786e\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},'const account = await program.account.userStakeInfo.fetch(stakeStatePda);\nexpect(account.stakeState === "Staked");\n')),(0,r.kt)("p",null,"\u518d\u6b21\u8fd0\u884c\u6d4b\u8bd5\uff0c\u5e0c\u671b\u4e00\u5207\u90fd\u987a\u5229\uff01\ud83e\udd1e"),(0,r.kt)("p",null,"\u5c31\u8fd9\u6837\uff0c\u6211\u4eec\u7684\u7b2c\u4e00\u4e2a\u6307\u4ee4\u5df2\u7ecf\u843d\u5730\u751f\u6548\u3002\u5728\u63a5\u4e0b\u6765\u7684\u90e8\u5206\uff0c\u6211\u4eec\u5c06\u5904\u7406\u5176\u4f59\u4e24\u4e2a\u6307\u4ee4\uff0c\u7136\u540e\u7ec8\u4e8e\u5f00\u59cb\u5904\u7406\u5ba2\u6237\u7aef\u4ea4\u6613\u7684\u4e8b\u5b9c\u3002"))}d.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunkall_in_one_solana=self.webpackChunkall_in_one_solana||[]).push([[5920],{3905:(e,n,t)=>{t.d(n,{Zo:()=>s,kt:()=>m});var a=t(67294);function r(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function o(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);n&&(a=a.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,a)}return t}function l(e){for(var n=1;n=0||(r[t]=e[t]);return r}(e,n);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(r[t]=e[t])}return r}var i=a.createContext({}),c=function(e){var n=a.useContext(i),t=n;return e&&(t="function"==typeof e?e(n):l(l({},n),e)),t},s=function(e){var n=c(e.components);return a.createElement(i.Provider,{value:n},e.children)},u="mdxType",d={inlineCode:"code",wrapper:function(e){var n=e.children;return a.createElement(a.Fragment,{},n)}},k=a.forwardRef((function(e,n){var t=e.components,r=e.mdxType,o=e.originalType,i=e.parentName,s=p(e,["components","mdxType","originalType","parentName"]),u=c(t),k=r,m=u["".concat(i,".").concat(k)]||u[k]||d[k]||o;return t?a.createElement(m,l(l({ref:n},s),{},{components:t})):a.createElement(m,l({ref:n},s))}));function m(e,n){var t=arguments,r=n&&n.mdxType;if("string"==typeof e||r){var o=t.length,l=new Array(o);l[0]=k;var p={};for(var i in n)hasOwnProperty.call(n,i)&&(p[i]=n[i]);p.originalType=e,p[u]="string"==typeof e?e:r,l[1]=p;for(var c=2;c{t.r(n),t.d(n,{assets:()=>i,contentTitle:()=>l,default:()=>d,frontMatter:()=>o,metadata:()=>p,toc:()=>c});var a=t(87462),r=(t(67294),t(3905));const o={sidebar_position:91,sidebar_label:"\ud83e\udd69 \u4f7f\u7528Anchor\u8fdb\u884cNFT\u7684\u8d28\u62bc",sidebar_class_name:"green"},l="\ud83e\udd69 \u4f7f\u7528Anchor\u8fdb\u884cNFT\u7684\u8d28\u62bc",p={unversionedId:"module5/a-full-stack-anchor-app/staking-with-anchor/README",id:"module5/a-full-stack-anchor-app/staking-with-anchor/README",title:"\ud83e\udd69 \u4f7f\u7528Anchor\u8fdb\u884cNFT\u7684\u8d28\u62bc",description:"\u73b0\u5728\u662f\u65f6\u5019\u5c06\u4f60\u7684NFT\u8d28\u62bc\u8ba1\u5212\u53ca\u7528\u6237\u754c\u9762\u8fc1\u79fb\u5230Anchor\u4e0a\u4e86\uff01\u4f60\u4e00\u76f4\u8f9b\u82e6\u5f00\u53d1\u7684buildoor\u9879\u76ee\u5df2\u7ecf\u76f8\u5f53\u51fa\u8272\uff0c\u4f46\u5c06\u5176\u8fc1\u79fb\u5230Anchor\u4e0a\u5c06\u4f7f\u672a\u6765\u7684\u5de5\u4f5c\u53d8\u5f97\u66f4\u52a0\u7b80\u6d01\u3002\u8bf7\u5229\u7528\u4f60\u6240\u638c\u63e1\u7684\u77e5\u8bc6\uff0c\u5b8c\u6210\u4e0b\u8ff0\u4efb\u52a1\uff1a",source:"@site/docs/Solana-Co-Learn/module5/a-full-stack-anchor-app/staking-with-anchor/README.md",sourceDirName:"module5/a-full-stack-anchor-app/staking-with-anchor",slug:"/module5/a-full-stack-anchor-app/staking-with-anchor/",permalink:"/solana-co-learn/Solana-Co-Learn/module5/a-full-stack-anchor-app/staking-with-anchor/",draft:!1,editUrl:"https://github.com/CreatorsDAO/solana-co-learn/tree/main/docs/Solana-Co-Learn/module5/a-full-stack-anchor-app/staking-with-anchor/README.md",tags:[],version:"current",lastUpdatedBy:"v1xingyue",lastUpdatedAt:1727439952,formattedLastUpdatedAt:"Sep 27, 2024",sidebarPosition:91,frontMatter:{sidebar_position:91,sidebar_label:"\ud83e\udd69 \u4f7f\u7528Anchor\u8fdb\u884cNFT\u7684\u8d28\u62bc",sidebar_class_name:"green"},sidebar:"tutorialSidebar",previous:{title:"\u4e00\u4e2a\u5168\u6808\u7684Anchor\u5e94\u7528\u7a0b\u5e8f",permalink:"/solana-co-learn/Solana-Co-Learn/module5/a-full-stack-anchor-app/"},next:{title:"\ud83d\udcb8 \u4f7f\u7528Anchor\u8d4e\u56de",permalink:"/solana-co-learn/Solana-Co-Learn/module5/a-full-stack-anchor-app/redeeming-with-anchor/"}},i={},c=[],s={toc:c},u="wrapper";function d(e){let{components:n,...t}=e;return(0,r.kt)(u,(0,a.Z)({},s,t,{components:n,mdxType:"MDXLayout"}),(0,r.kt)("h1",{id:"-\u4f7f\u7528anchor\u8fdb\u884cnft\u7684\u8d28\u62bc"},"\ud83e\udd69 \u4f7f\u7528Anchor\u8fdb\u884cNFT\u7684\u8d28\u62bc"),(0,r.kt)("p",null,"\u73b0\u5728\u662f\u65f6\u5019\u5c06\u4f60\u7684",(0,r.kt)("inlineCode",{parentName:"p"},"NFT"),"\u8d28\u62bc\u8ba1\u5212\u53ca\u7528\u6237\u754c\u9762\u8fc1\u79fb\u5230",(0,r.kt)("inlineCode",{parentName:"p"},"Anchor"),"\u4e0a\u4e86\uff01\u4f60\u4e00\u76f4\u8f9b\u82e6\u5f00\u53d1\u7684",(0,r.kt)("inlineCode",{parentName:"p"},"buildoor"),"\u9879\u76ee\u5df2\u7ecf\u76f8\u5f53\u51fa\u8272\uff0c\u4f46\u5c06\u5176\u8fc1\u79fb\u5230",(0,r.kt)("inlineCode",{parentName:"p"},"Anchor"),"\u4e0a\u5c06\u4f7f\u672a\u6765\u7684\u5de5\u4f5c\u53d8\u5f97\u66f4\u52a0\u7b80\u6d01\u3002\u8bf7\u5229\u7528\u4f60\u6240\u638c\u63e1\u7684\u77e5\u8bc6\uff0c\u5b8c\u6210\u4e0b\u8ff0\u4efb\u52a1\uff1a"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"\u4f7f\u7528",(0,r.kt)("inlineCode",{parentName:"li"},"Anchor"),"\u4ece\u5934\u5f00\u59cb\u91cd\u65b0\u7f16\u5199\u4ee3\u7801\u3002"),(0,r.kt)("li",{parentName:"ul"},"\u589e\u52a0\u4e00\u4e9b\u53ef\u9760\u7684\u6d4b\u8bd5\u8986\u76d6\uff0c\u4ee5\u786e\u4fdd\u4f60\u80fd\u591f\u4e25\u5bc6\u6355\u6349\u4efb\u4f55\u5b89\u5168\u98ce\u9669\u3002"),(0,r.kt)("li",{parentName:"ul"},"\u7528",(0,r.kt)("inlineCode",{parentName:"li"},"Anchor"),"\u7684\u65b9\u6cd5\u6784\u5efa\u5668\u6765\u66ff\u6362\u590d\u6742\u7684",(0,r.kt)("inlineCode",{parentName:"li"},"UI"),"\u4ee3\u7801\u3002")),(0,r.kt)("p",null,"\u8fd9\u9879\u4efb\u52a1\u53ef\u80fd\u6709\u4e9b\u590d\u6742\uff0c\u9700\u8981\u4f60\u6295\u5165\u4e00\u4e9b\u65f6\u95f4\u72ec\u7acb\u8fdb\u884c\u5c1d\u8bd5\u3002\u5982\u679c\u51e0\u4e2a\u5c0f\u65f6\u540e\u4f60\u611f\u5230\u56f0\u60d1\uff0c\u968f\u65f6\u53ef\u4ee5\u89c2\u770b\u6211\u4eec\u63d0\u4f9b\u7684\u89c6\u9891\u6f14\u793a\u89e3\u51b3\u65b9\u6848\u3002"),(0,r.kt)("p",null,"\u6211\u4eec\u6765\u5171\u540c\u5b8c\u6210\u8fd9\u4e2a\u4efb\u52a1\uff0c\u5e76\u67e5\u770b\u6211\u4eec\u7684\u6210\u679c\u3002\u6211\u4eec\u4e0d\u662f\u5728\u589e\u52a0\u65b0\u529f\u80fd\uff0c\u800c\u662f\u8981\u5b8c\u5168\u7528",(0,r.kt)("inlineCode",{parentName:"p"},"Anchor"),"\u66ff\u6362\u6211\u4eec\u4e00\u76f4\u5728\u52aa\u529b\u5f00\u53d1\u7684\u8d28\u62bc\u8ba1\u5212\u3002"),(0,r.kt)("p",null,"\u9996\u5148\uff0c\u901a\u8fc7\u8fd0\u884c ",(0,r.kt)("inlineCode",{parentName:"p"},"anchor init anchor-nft-staking")," \u521b\u5efa\u4e00\u4e2a\u65b0\u9879\u76ee\uff0c\u6216\u8005\u53ef\u4ee5\u9009\u62e9\u81ea\u5df1\u7684\u540d\u5b57\u3002\u7136\u540e\u6253\u5f00 ",(0,r.kt)("inlineCode",{parentName:"p"},"Anchor.toml")," \u6587\u4ef6\uff0c\u5c06\u79cd\u5b50\u8bbe\u7f6e\u4e3a ",(0,r.kt)("inlineCode",{parentName:"p"},"true"),"\uff0c\u96c6\u7fa4\u8bbe\u7f6e\u4e3a ",(0,r.kt)("inlineCode",{parentName:"p"},"devnet"),"\u3002"),(0,r.kt)("p",null,"\u63a5\u4e0b\u6765\uff0c\u8df3\u8f6c\u5230 ",(0,r.kt)("inlineCode",{parentName:"p"},"/programs/anchor-nft-staking/Cargo.toml"),"\uff0c\u5e76\u6dfb\u52a0\u4ee5\u4e0b\u4f9d\u8d56\u9879\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-toml"},'anchor-lang = { version="0.25.0", features = ["init-if-needed"] }\nanchor-spl = "0.25.0"\nmpl-token-metadata = { version="1.4.1", features=["no-entrypoint"] }\n')),(0,r.kt)("p",null,"\u597d\u7684\uff0c\u6253\u5f00 ",(0,r.kt)("inlineCode",{parentName:"p"},"lib.rs")," \u6587\u4ef6\uff0c\u6211\u4eec\u6765\u6784\u5efa\u57fa\u672c\u7684\u6846\u67b6\u3002"),(0,r.kt)("p",null,"\u6211\u4eec\u9700\u8981\u6dfb\u52a0\u4ee5\u4e0b\u5bfc\u5165\u3002\u968f\u7740\u6211\u4eec\u7684\u5de5\u4f5c\u8fdb\u5c55\uff0c\u6bcf\u4e2a\u5bfc\u5165\u7684\u5fc5\u8981\u6027\u5c06\u9010\u6e10\u663e\u73b0\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"use anchor_lang::solana_program::program::invoke_signed;\nuse anchor_spl::token;\nuse anchor_spl::{\n associated_token::AssociatedToken,\n token::{Approve, Mint, MintTo, Revoke, Token, TokenAccount},\n};\nuse mpl_token_metadata::{\n instruction::{freeze_delegated_account, thaw_delegated_account},\n ID as MetadataTokenId,\n};\n")),(0,r.kt)("p",null,"\u9996\u5148\uff0c\u6211\u4eec\u5c06\u9ed8\u8ba4\u51fd\u6570\u7684\u540d\u79f0\u66f4\u6539\u4e3a ",(0,r.kt)("inlineCode",{parentName:"p"},"stake"),"\uff0c\u5e76\u66f4\u6539\u5176\u76f8\u5173\u4e0a\u4e0b\u6587\u4e3a\u7c7b\u578b",(0,r.kt)("inlineCode",{parentName:"p"},"Stake"),"\u3002"),(0,r.kt)("p",null,"\u7136\u540e\u6dfb\u52a0\u540d\u4e3a ",(0,r.kt)("inlineCode",{parentName:"p"},"redeem")," \u7684\u51fd\u6570\uff0c\u4e0a\u4e0b\u6587\u7c7b\u578b\u4e3a ",(0,r.kt)("inlineCode",{parentName:"p"},"Redeem"),"\u3002"),(0,r.kt)("p",null,"\u6700\u540e\uff0c\u5bf9\u4e8e ",(0,r.kt)("inlineCode",{parentName:"p"},"unstake"),"\uff0c\u4f7f\u7528\u4e0a\u4e0b\u6587\u7c7b\u578b ",(0,r.kt)("inlineCode",{parentName:"p"},"Unstake")," \u8fdb\u884c\u76f8\u540c\u64cd\u4f5c\u3002"),(0,r.kt)("p",null,"\u63a5\u4e0b\u6765\u6211\u4eec\u8981\u6784\u5efa\u7684\u662f",(0,r.kt)("inlineCode",{parentName:"p"},"Stake"),"\u7684\u7ed3\u6784\u3002\u6211\u4eec\u9700\u8981\u4e00\u4e2a",(0,r.kt)("inlineCode",{parentName:"p"},"PDA"),"\u6765\u5b58\u50a8",(0,r.kt)("inlineCode",{parentName:"p"},"UserStakeInfo"),"\uff0c\u5e76\u4e14\u9700\u8981\u4e00\u4e2a",(0,r.kt)("inlineCode",{parentName:"p"},"StakeState"),"\u679a\u4e3e\u6765\u8868\u793a",(0,r.kt)("inlineCode",{parentName:"p"},"PDA"),"\u7684\u5176\u4e2d\u4e00\u4e2a\u5b57\u6bb5\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"#[account]\npub struct UserStakeInfo {\n pub token_account: Pubkey,\n pub stake_start_time: i64,\n pub last_stake_redeem: i64,\n pub user_pubkey: Pubkey,\n pub stake_state: StakeState,\n pub is_initialized: bool,\n}\n\n#[derive(Debug, PartialEq, AnchorDeserialize, AnchorSerialize, Clone)]\npub enum StakeState {\n Unstaked,\n Staked,\n}\n")),(0,r.kt)("p",null,"\u4e3a",(0,r.kt)("inlineCode",{parentName:"p"},"StakeState"),"\u6dfb\u52a0\u4e00\u4e2a\u9ed8\u8ba4\u503c\uff0c\u8bbe\u4e3a\u672a\u62b5\u62bc\u72b6\u6001\u3002"),(0,r.kt)("p",null,"\u7531\u4e8e\u6211\u4eec\u5c06\u4f7f\u7528\u7684\u5143\u6570\u636e\u7a0b\u5e8f\u76f8\u5bf9\u8f83\u65b0\uff0c\u951a\u5b9a\u7a0b\u5e8f\u4e2d\u8fd8\u6ca1\u6709\u76f8\u5e94\u7684\u7c7b\u578b\u3002\u4e3a\u4e86\u50cf\u5176\u4ed6\u7a0b\u5e8f\uff08\u4f8b\u5982\u7cfb\u7edf\u7a0b\u5e8f\u3001\u4ee4\u724c\u7a0b\u5e8f\u7b49\uff09\u4e00\u6837\u4f7f\u7528\u5b83\uff0c\u6211\u4eec\u5c06\u4e3a\u5176\u521b\u5efa\u4e00\u4e2a\u7ed3\u6784\u4f53\uff0c\u5e76\u6dfb\u52a0\u4e00\u4e2a\u540d\u4e3a ",(0,r.kt)("inlineCode",{parentName:"p"},"id")," \u7684\u5b9e\u73b0\uff0c\u8fd4\u56de\u4e00\u4e2a ",(0,r.kt)("inlineCode",{parentName:"p"},"Pubkey"),"\uff0c\u5b83\u5bf9\u5e94\u4e8e ",(0,r.kt)("inlineCode",{parentName:"p"},"MetadataTokenId"),"\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"#[derive(Clone)]\npub struct Metadata;\n\nimpl anchor_lang::Id for Metadata {\n fn id() -> Pubkey {\n MetadataTokenId\n }\n}\n")),(0,r.kt)("p",null,"\u597d\u7684\uff0c\u73b0\u5728\u6211\u4eec\u53ef\u4ee5\u5f00\u59cb\u5904\u7406\u8d28\u62bc\u90e8\u5206\u3002\u4e0b\u9762\u662f\u7ed3\u6784\u4f53\u6240\u9700\u7684\u4e5d\u4e2a\u8d26\u6237\uff0c\u4ee5\u53ca\u4e00\u4e9b\u503c\u5f97\u6ce8\u610f\u7684\u4e8b\u9879\u3002"),(0,r.kt)("p",null,"\u9996\u5148\uff0c\u4f60\u4f1a\u770b\u5230 ",(0,r.kt)("inlineCode",{parentName:"p"},"nft_edition")," \u662f\u4e00\u4e2a ",(0,r.kt)("inlineCode",{parentName:"p"},"Unchecked")," \u8d26\u6237\uff0c\u8fd9\u662f\u56e0\u4e3a\u7cfb\u7edf\u4e2d\u8fd8\u672a\u4e3a\u8fd9\u79cd\u7c7b\u578b\u7684\u8d26\u6237\u521b\u5efa\u3002\u6240\u6709\u672a\u7ecf\u6838\u5b9e\u7684\u8d26\u6237\u90fd\u9700\u9644\u5e26\u4e00\u6761\u5907\u6ce8\uff0c\u4ee5\u4fbf\u7cfb\u7edf\u77e5\u9053\u4f60\u5c06\u8fdb\u884c\u624b\u52a8\u5b89\u5168\u68c0\u67e5\u3002\u4f60\u4f1a\u5728\u4e0b\u65b9\u770b\u5230 ",(0,r.kt)("inlineCode",{parentName:"p"},"CHECK: Manual validation"),"\u3002"),(0,r.kt)("p",null,"\u9700\u8981\u63d0\u9192\u7684\u662f\uff0c\u6bcf\u4e2a\u8d26\u6237\u4e0a\u7684\u5c5e\u6027\u90fd\u662f\u4e00\u79cd\u5b89\u5168\u68c0\u67e5\uff0c\u4ee5\u786e\u4fdd\u8d26\u6237\u662f\u6b63\u786e\u7684\u7c7b\u578b\u5e76\u80fd\u6267\u884c\u7279\u5b9a\u529f\u80fd\u3002\u7531\u4e8e\u7528\u6237\u9700\u8981\u4ed8\u8d39\uff0c\u5e76\u4e14",(0,r.kt)("inlineCode",{parentName:"p"},"NFT"),"\u4ee3\u5e01\u8d26\u6237\u5c06\u88ab\u4fee\u6539\uff0c\u6240\u4ee5\u4e24\u8005\u90fd\u5177\u6709",(0,r.kt)("inlineCode",{parentName:"p"},"mut"),"\u5c5e\u6027\u3002\u67d0\u4e9b\u8d26\u6237\u8fd8\u9700\u8981\u79cd\u5b50\uff0c\u5982\u4e0b\u6240\u793a\u3002"),(0,r.kt)("p",null,"\u81f3\u4e8e\u5176\u4ed6\u6ca1\u6709\u4efb\u4f55\u5c5e\u6027\u7684\u8d26\u6237\uff0c\u5b83\u4eec\u5728",(0,r.kt)("inlineCode",{parentName:"p"},"Anchor"),"\u4e2d\u90fd\u6709\u81ea\u5df1\u5fc5\u9700\u7684\u5b89\u5168\u68c0\u67e5\uff0c\u6240\u4ee5\u6211\u4eec\u4e0d\u9700\u8981\u6dfb\u52a0\u4efb\u4f55\u5c5e\u6027\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"#[derive(Accounts)]\npub struct Stake<'info> {\n #[account(mut)]\n pub user: Signer<'info>,\n #[account(\n mut,\n associated_token::mint=nft_mint,\n associated_token::authority=user\n )]\n pub nft_token_account: Account<'info, TokenAccount>,\n pub nft_mint: Account<'info, Mint>,\n /// CHECK: Manual validation\n #[account(owner=MetadataTokenId)]\n pub nft_edition: UncheckedAccount<'info>,\n #[account(\n init_if_needed,\n payer=user,\n space = std::mem::size_of::() + 8,\n seeds = [user.key().as_ref(), nft_token_account.key().as_ref()],\n bump\n )]\n pub stake_state: Account<'info, UserStakeInfo>,\n /// CHECK: Manual validation\n #[account(mut, seeds=[\"authority\".as_bytes().as_ref()], bump)]\n pub program_authority: UncheckedAccount<'info>,\n pub token_program: Program<'info, Token>,\n pub system_program: Program<'info, System>,\n pub metadata_program: Program<'info, Metadata>,\n}\n")),(0,r.kt)("p",null,"\u5728\u7ee7\u7eed\u64cd\u4f5c\u4e4b\u524d\uff0c\u5148\u8fd0\u884c",(0,r.kt)("inlineCode",{parentName:"p"},"anchor build"),"\uff0c\u8fd9\u6837\u6211\u4eec\u7684\u7b2c\u4e00\u4e2a\u6784\u5efa\u5c31\u53ef\u4ee5\u5f00\u59cb\u4e86\u3002\u8bf7\u8bb0\u4f4f\uff0c\u8fd9\u662f\u6211\u4eec\u7684\u7b2c\u4e00\u6b21\u6784\u5efa\uff0c\u5b83\u4f1a\u751f\u6210\u6211\u4eec\u7684\u7a0b\u5e8f",(0,r.kt)("inlineCode",{parentName:"p"},"ID"),"\u3002"),(0,r.kt)("p",null,"\u5728\u6784\u5efa\u7684\u540c\u65f6\uff0c\u5728",(0,r.kt)("inlineCode",{parentName:"p"},"tests"),"\u76ee\u5f55\u4e2d\u521b\u5efa\u4e00\u4e2a\u540d\u4e3a",(0,r.kt)("inlineCode",{parentName:"p"},"utils"),"\u7684\u65b0\u6587\u4ef6\u5939\u3002\u5728\u5176\u4e2d\u521b\u5efa\u4e00\u4e2a\u540d\u4e3a",(0,r.kt)("inlineCode",{parentName:"p"},"setupNft.ts"),"\u7684\u6587\u4ef6\uff0c\u5e76\u5c06\u4e0b\u9762\u7684\u4ee3\u7801\u7c98\u8d34\u8fdb\u53bb\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},'import {\n bundlrStorage,\n keypairIdentity,\n Metaplex,\n} from "@metaplex-foundation/js"\nimport { createMint, getAssociatedTokenAddress } from "@solana/spl-token"\nimport * as anchor from "@project-serum/anchor"\n\nexport const setupNft = async (program, payer) => {\n const metaplex = Metaplex.make(program.provider.connection)\n .use(keypairIdentity(payer))\n .use(bundlrStorage())\n\n const nft = await metaplex\n .nfts()\n .create({\n uri: "",\n name: "Test nft",\n sellerFeeBasisPoints: 0,\n })\n\n console.log("nft metadata pubkey: ", nft.metadataAddress.toBase58())\n console.log("nft token address: ", nft.tokenAddress.toBase58())\n const [delegatedAuthPda] = await anchor.web3.PublicKey.findProgramAddress(\n [Buffer.from("authority")],\n program.programId\n )\n const [stakeStatePda] = await anchor.web3.PublicKey.findProgramAddress(\n [payer.publicKey.toBuffer(), nft.tokenAddress.toBuffer()],\n program.programId\n )\n\n console.log("delegated authority pda: ", delegatedAuthPda.toBase58())\n console.log("stake state pda: ", stakeStatePda.toBase58())\n const [mintAuth] = await anchor.web3.PublicKey.findProgramAddress(\n [Buffer.from("mint")],\n program.programId\n )\n\n const mint = await createMint(\n program.provider.connection,\n payer,\n mintAuth,\n null,\n 2\n )\n console.log("Mint pubkey: ", mint.toBase58())\n\n const tokenAddress = await getAssociatedTokenAddress(mint, payer.publicKey)\n\n return {\n nft: nft,\n delegatedAuthPda: delegatedAuthPda,\n stakeStatePda: stakeStatePda,\n mint: mint,\n mintAuth: mintAuth,\n tokenAddress: tokenAddress,\n }\n}\n\n')),(0,r.kt)("p",null,"\u7136\u540e\uff0c\u8fd0\u884c",(0,r.kt)("inlineCode",{parentName:"p"},"npm install @metaplex-foundation/js"),"\u3002"),(0,r.kt)("p",null,"\u6700\u540e\uff0c\u8f6c\u5230",(0,r.kt)("inlineCode",{parentName:"p"},"anchor-nft-staking.ts"),"\u76ee\u5f55\u3002\u8fd9\u662f",(0,r.kt)("inlineCode",{parentName:"p"},"Anchor"),"\u521b\u5efa\u7684\u9ed8\u8ba4\u6587\u4ef6\u3002"),(0,r.kt)("p",null,"\u4f60\u9700\u8981\u5c06\u63d0\u4f9b\u8005\u7684\u9ed8\u8ba4\u884c\u5206\u4e3a\u4e24\u90e8\u5206\uff0c\u4ee5\u4fbf\u5728\u4ee5\u540e\u9700\u8981\u65f6\u80fd\u591f\u8bbf\u95ee\u63d0\u4f9b\u8005\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},"const provider = anchor.AnchorProvider.env();\nanchor.setProvider(provider);\n")),(0,r.kt)("p",null,"\u8ba9\u6211\u4eec\u5f15\u5165\u94b1\u5305\uff0c\u8fd9\u5c06\u4f7f\u6211\u4eec\u80fd\u591f\u516c\u5f00\u4ed8\u6b3e\u4eba\u4e3a\u4ea4\u6613\u7b7e\u540d\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},"const wallet = anchor.workspace.AnchorNftStaking.provider.wallet;\n")),(0,r.kt)("p",null,"\u68c0\u67e5\u4f60\u7684\u7f16\u8bd1\u60c5\u51b5\uff0c\u5982\u679c\u4e00\u5207\u987a\u5229\uff0c\u8bf7\u8fd0\u884c ",(0,r.kt)("inlineCode",{parentName:"p"},"anchor deploy"),"\u3002\u5982\u679c\u51fa\u73b0\u95ee\u9898\uff0c\u4f60\u53ef\u80fd\u9700\u8981\u4e3a\u81ea\u5df1\u7a7a\u6295\u4e00\u4e9bSOL\u3002"),(0,r.kt)("p",null,"\u7f16\u8bd1\u5b8c\u6210\u540e\uff0c\u8fd0\u884c ",(0,r.kt)("inlineCode",{parentName:"p"},"anchor keys list")," \u5e76\u83b7\u53d6\u7a0b\u5e8f",(0,r.kt)("inlineCode",{parentName:"p"},"ID"),"\uff0c\u7136\u540e\u5c06\u5176\u653e\u5165 ",(0,r.kt)("inlineCode",{parentName:"p"},"lib.rs")," \u548c ",(0,r.kt)("inlineCode",{parentName:"p"},"Anchor.toml")," \u6587\u4ef6\u4e2d\u3002\u5982\u679c\u7f16\u8bd1\u82b1\u8d39\u4e00\u4e9b\u65f6\u95f4\uff0c\u4f60\u53ef\u80fd\u9700\u8981\u56de\u5230\u8fd9\u4e00\u6b65\u3002"),(0,r.kt)("p",null,"\u56de\u5230\u6d4b\u8bd5\u6587\u4ef6\u3002"),(0,r.kt)("p",null,"\u8ba9\u6211\u4eec\u5b9a\u4e49\u4e00\u4e9b\u6d4b\u8bd5\u4e2d\u9700\u8981\u4f7f\u7528\u7684\u53d8\u91cf\u7c7b\u578b\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},"let delegatedAuthPda: anchor.web3.PublicKey;\nlet stakeStatePda: anchor.web3.PublicKey;\nlet nft: any;\nlet mintAuth: anchor.web3.PublicKey;\nlet mint: anchor.web3.PublicKey;\nlet tokenAddress: anchor.web3.PublicKey;\n")),(0,r.kt)("p",null,"\u73b0\u5728\uff0c\u6211\u4eec\u6dfb\u52a0\u4e00\u4e2a ",(0,r.kt)("inlineCode",{parentName:"p"},"before"),' \u51fd\u6570\uff0c\u8be5\u51fd\u6570\u4f1a\u5728\u6d4b\u8bd5\u8fd0\u884c\u4e4b\u524d\u88ab\u8c03\u7528\u3002\u6ce8\u610f"',(0,r.kt)("inlineCode",{parentName:"p"},";"),'"\u8bed\u6cd5\uff0c\u5b83\u4f1a\u89e3\u6784\u8fd4\u56de\u503c\u5e76\u4e3a\u6240\u6709\u8fd9\u4e9b\u503c\u8fdb\u884c\u8bbe\u7f6e\u3002'),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},"before(async () => {\n ;({ nft, delegatedAuthPda, stakeStatePda, mint, mintAuth, tokenAddress } =\n await setupNft(program, wallet.payer));\n });\n")),(0,r.kt)("p",null,"\u8f6c\u5230\u9ed8\u8ba4\u6d4b\u8bd5\uff0c\u5c06\u5176\u66f4\u6539\u4e3a ",(0,r.kt)("inlineCode",{parentName:"p"},'it("Stakes"'),"\u3002\u9996\u5148\uff0c\u6211\u4eec\u53ea\u662f\u786e\u8ba4\u51fd\u6570\u88ab\u6210\u529f\u8c03\u7528\u3002\u6211\u4eec\u8fd8\u6ca1\u6709\u6784\u5efa\u5b9e\u9645\u7684\u62b5\u62bc\u51fd\u6570\uff0c\u6240\u4ee5\u6682\u65f6\u4e0d\u4f1a\u8fdb\u884c\u4efb\u4f55\u903b\u8f91\u6d4b\u8bd5\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},'it("Stakes", async () => {\n // \u5728\u6b64\u6dfb\u52a0\u4f60\u7684\u6d4b\u8bd5\u3002\n await program.methods\n .stake()\n .accounts({\n nftTokenAccount: nft.tokenAddress,\n nftMint: nft.mintAddress,\n nftEdition: nft.masterEditionAddress,\n metadataProgram: METADATA_PROGRAM_ID,\n })\n .rpc();\n });\n')),(0,r.kt)("p",null,"\u73b0\u5728\uff0c\u8fd0\u884c ",(0,r.kt)("inlineCode",{parentName:"p"},"anchor test"),"\u3002\u5982\u679c\u5b83\u901a\u8fc7\u4e86\uff0c\u8fd9\u610f\u5473\u7740\u6211\u4eec\u901a\u8fc7\u4e86\u5728",(0,r.kt)("inlineCode",{parentName:"p"},"Stake"),"\u7ed3\u6784\u4e2d\u521b\u5efa\u8d26\u6237\u7684\u9a8c\u8bc1\u3002"),(0,r.kt)("p",null,"\u56de\u5230\u903b\u8f91\u90e8\u5206\uff0c\u4e0b\u9762\u662f\u62b5\u62bc\u5de5\u4f5c\u6240\u9700\u7684\u9010\u6b65\u64cd\u4f5c\u3002\u6211\u4eec\u9700\u8981\u83b7\u53d6\u65f6\u949f\u8bbf\u95ee\u6743\u9650\uff0c\u786e\u4fdd\u62b5\u62bc\u72b6\u6001\u5df2\u521d\u59cb\u5316\uff0c\u5e76\u786e\u8ba4\u5c1a\u672a\u62b5\u62bc\u3002"),(0,r.kt)("p",null,"\u5728",(0,r.kt)("inlineCode",{parentName:"p"},"stake"),"\u51fd\u6570\u4e2d\uff0c\u6211\u4eec\u9996\u5148\u83b7\u53d6\u65f6\u949f\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"let clock = Clock::get().unwrap();\n")),(0,r.kt)("p",null,"\u63a5\u4e0b\u6765\uff0c\u6211\u4eec\u521b\u5efa\u4e00\u4e2a",(0,r.kt)("inlineCode",{parentName:"p"},"CPI"),"\u6765\u59d4\u6258\u8be5\u7a0b\u5e8f\u4f5c\u4e3a\u51bb\u7ed3\u6216\u89e3\u51bb\u6211\u4eec\u7684",(0,r.kt)("inlineCode",{parentName:"p"},"NFT"),"\u7684\u6743\u9650\u3002\u9996\u5148\uff0c\u6211\u4eec\u8bbe\u7f6e",(0,r.kt)("inlineCode",{parentName:"p"},"CPI"),"\uff0c\u7136\u540e\u786e\u5b9a\u6211\u4eec\u8981\u4f7f\u7528\u7684\u8d26\u6237\uff0c\u6700\u540e\u8bbe\u5b9a\u6743\u9650\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},'msg!("Approving delegate");\n\nlet cpi_approve_program = ctx.accounts.token_program.to_account_info();\nlet cpi_approve_accounts = Approve {\n to: ctx.accounts.nft_token_account.to_account_info(),\n delegate: ctx.accounts.program_authority.to_account_info(),\n authority: ctx.accounts.user.to_account_info(),\n};\n\nlet cpi_approve_ctx = CpiContext::new(cpi_approve_program, cpi_approve_accounts);\ntoken::approve(cpi_approve_ctx, 1)?;\n')),(0,r.kt)("p",null,"\u7136\u540e\u6211\u4eec\u5f00\u59cb\u51bb\u7ed3\u4ee3\u5e01\u3002\u9996\u5148\u8bbe\u7f6e\u6743\u9650\u63d0\u5347\uff0c\u7136\u540e\u8c03\u7528",(0,r.kt)("inlineCode",{parentName:"p"},"invoke_signed"),"\u51fd\u6570\uff0c\u4f20\u5165\u6240\u6709\u5fc5\u8981\u7684\u8d26\u6237\u548c\u8d26\u6237\u4fe1\u606f\u6570\u7ec4\uff0c\u6700\u540e\u662f\u79cd\u5b50\u548c\u63d0\u5347\u503c\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},'msg!("Freezing token account");\nlet authority_bump = *ctx.bumps.get("program_authority").unwrap();\ninvoke_signed(\n &freeze_delegated_account(\n ctx.accounts.metadata_program.key(),\n ctx.accounts.program_authority.key(),\n ctx.accounts.nft_token_account.key(),\n ctx.accounts.nft_edition.key(),\n ctx.accounts.nft_mint.key(),\n ),\n &[\n ctx.accounts.program_authority.to_account_info(),\n ctx.accounts.nft_token_account.to_account_info(),\n ctx.accounts.nft_edition.to_account_info(),\n ctx.accounts.nft_mint.to_account_info(),\n ctx.accounts.metadata_program.to_account_info(),\n ],\n &[&[b"authority", &[authority_bump]]],\n)?;\n')),(0,r.kt)("p",null,"\u73b0\u5728\uff0c\u6211\u4eec\u5728\u80a1\u6743\u8d26\u6237\u4e0a\u8bbe\u7f6e\u6570\u636e\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"ctx.accounts.stake_state.token_account = ctx.accounts.nft_token_account.key();\nctx.accounts.stake_state.user_pubkey = ctx.accounts.user.key();\nctx.accounts.stake_state.stake_state = StakeState::Staked;\nctx.accounts.stake_state.stake_start_time = clock.unix_timestamp;\nctx.accounts.stake_state.last_stake_redeem = clock.unix_timestamp;\nctx.accounts.stake_state.is_initialized = true;\n")),(0,r.kt)("p",null,"\u54ce\u5440\uff0c\u8ba9\u6211\u4eec\u8df3\u5230\u6587\u4ef6\u5f00\u59cb\u90e8\u5206\u5e76\u6dfb\u52a0\u4e00\u4e2a\u5b89\u5168\u68c0\u67e5\uff0c\u6211\u4eec\u8fd8\u9700\u8981\u4e00\u4e2a\u81ea\u5b9a\u4e49\u9519\u8bef\u3002\u4e0b\u9762\u662f\u4e24\u6bb5\u4ee3\u7801\uff0c\u4f46\u662f\u5c06\u81ea\u5b9a\u4e49\u9519\u8bef\u4ee3\u7801\u653e\u5728\u6587\u4ef6\u5e95\u90e8\uff0c\u8fd9\u6837\u4e0d\u4f1a\u5f71\u54cd\u903b\u8f91\u548c\u7ed3\u6784\u7684\u9605\u8bfb\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},"require!(\n ctx.accounts.stake_state.stake_state == StakeState::Unstaked,\n StakeError::AlreadyStaked\n);\n")),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-rust"},'#[error_code]\npub enum StakeError {\n #[msg("NFT already staked")]\n AlreadyStaked,\n}\n')),(0,r.kt)("p",null,"\u5728\u518d\u6b21\u6d4b\u8bd5\u4e4b\u524d\uff0c\u4e0d\u8981\u5fd8\u8bb0\u5145\u5b9e\u4f60\u7684",(0,r.kt)("inlineCode",{parentName:"p"},"SOL"),"\u4f59\u989d\u3002"),(0,r.kt)("p",null,"\u597d\u7684\uff0c\u5c31\u8fd9\u6837\uff0c\u8ba9\u6211\u4eec\u56de\u5230\u6d4b\u8bd5\u4e2d\uff0c\u4e3a\u6211\u4eec\u7684\u8d28\u62bc\u6d4b\u8bd5\u6dfb\u52a0\u4e00\u4e9b\u529f\u80fd\uff0c\u4ee5\u68c0\u67e5\u8d28\u62bc\u72b6\u6001\u662f\u5426\u6b63\u786e\u3002"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},'const account = await program.account.userStakeInfo.fetch(stakeStatePda);\nexpect(account.stakeState === "Staked");\n')),(0,r.kt)("p",null,"\u518d\u6b21\u8fd0\u884c\u6d4b\u8bd5\uff0c\u5e0c\u671b\u4e00\u5207\u90fd\u987a\u5229\uff01\ud83e\udd1e"),(0,r.kt)("p",null,"\u5c31\u8fd9\u6837\uff0c\u6211\u4eec\u7684\u7b2c\u4e00\u4e2a\u6307\u4ee4\u5df2\u7ecf\u843d\u5730\u751f\u6548\u3002\u5728\u63a5\u4e0b\u6765\u7684\u90e8\u5206\uff0c\u6211\u4eec\u5c06\u5904\u7406\u5176\u4f59\u4e24\u4e2a\u6307\u4ee4\uff0c\u7136\u540e\u7ec8\u4e8e\u5f00\u59cb\u5904\u7406\u5ba2\u6237\u7aef\u4ea4\u6613\u7684\u4e8b\u5b9c\u3002"))}d.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/03b4e199.0fa902d7.js b/assets/js/03b4e199.033527c4.js similarity index 99% rename from assets/js/03b4e199.0fa902d7.js rename to assets/js/03b4e199.033527c4.js index 8e4626160..1d7b62f1f 100644 --- a/assets/js/03b4e199.0fa902d7.js +++ b/assets/js/03b4e199.033527c4.js @@ -1 +1 @@ -"use strict";(self.webpackChunkall_in_one_solana=self.webpackChunkall_in_one_solana||[]).push([[3366],{3905:(e,a,t)=>{t.d(a,{Zo:()=>p,kt:()=>b});var n=t(67294);function l(e,a,t){return a in e?Object.defineProperty(e,a,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[a]=t,e}function r(e,a){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);a&&(n=n.filter((function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),t.push.apply(t,n)}return t}function s(e){for(var a=1;a=0||(l[t]=e[t]);return l}(e,a);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(l[t]=e[t])}return l}var i=n.createContext({}),u=function(e){var a=n.useContext(i),t=a;return e&&(t="function"==typeof e?e(a):s(s({},a),e)),t},p=function(e){var a=u(e.components);return n.createElement(i.Provider,{value:a},e.children)},c="mdxType",m={inlineCode:"code",wrapper:function(e){var a=e.children;return n.createElement(n.Fragment,{},a)}},d=n.forwardRef((function(e,a){var t=e.components,l=e.mdxType,r=e.originalType,i=e.parentName,p=o(e,["components","mdxType","originalType","parentName"]),c=u(t),d=l,b=c["".concat(i,".").concat(d)]||c[d]||m[d]||r;return t?n.createElement(b,s(s({ref:a},p),{},{components:t})):n.createElement(b,s({ref:a},p))}));function b(e,a){var t=arguments,l=a&&a.mdxType;if("string"==typeof e||l){var r=t.length,s=new Array(r);s[0]=d;var o={};for(var i in a)hasOwnProperty.call(a,i)&&(o[i]=a[i]);o.originalType=e,o[c]="string"==typeof e?e:l,s[1]=o;for(var u=2;u{t.r(a),t.d(a,{assets:()=>p,contentTitle:()=>i,default:()=>b,frontMatter:()=>o,metadata:()=>u,toc:()=>c});var n=t(87462),l=(t(67294),t(3905)),r=t(74866),s=t(85162);const o={title:"\u5b89\u88c5",sidebar_position:2,tags:["solana-cook-book","web3.js"]},i="\u5b89\u88c5",u={unversionedId:"getting-started/installation",id:"getting-started/installation",title:"\u5b89\u88c5",description:"\u5b89\u88c5Web3.js",source:"@site/docs/cookbook-zh/getting-started/installation.md",sourceDirName:"getting-started",slug:"/getting-started/installation",permalink:"/solana-co-learn/cookbook-zh/getting-started/installation",draft:!1,editUrl:"https://github.com/CreatorsDAO/solana-co-learn/tree/main/docs/cookbook-zh/getting-started/installation.md",tags:[{label:"solana-cook-book",permalink:"/solana-co-learn/cookbook-zh/tags/solana-cook-book"},{label:"web3.js",permalink:"/solana-co-learn/cookbook-zh/tags/web-3-js"}],version:"current",lastUpdatedBy:"v1xingyue",lastUpdatedAt:1727439818,formattedLastUpdatedAt:"Sep 27, 2024",sidebarPosition:2,frontMatter:{title:"\u5b89\u88c5",sidebar_position:2,tags:["solana-cook-book","web3.js"]},sidebar:"tutorialSidebar",previous:{title:"\u5f00\u59cb",permalink:"/solana-co-learn/cookbook-zh/getting-started/"},next:{title:"\u8d21\u732e",permalink:"/solana-co-learn/cookbook-zh/getting-started/contributing"}},p={},c=[{value:"\u5b89\u88c5Web3.js",id:"\u5b89\u88c5web3js",level:2},{value:"Web3.js",id:"web3js",level:3},{value:"SPL\u4ee3\u5e01\uff08SPL-Token\uff09",id:"spl\u4ee3\u5e01spl-token",level:3},{value:"\u94b1\u5305\u9002\u914d\u5668\uff08Wallet-Adapter\uff09",id:"\u94b1\u5305\u9002\u914d\u5668wallet-adapter",level:3},{value:"\u5b89\u88c5Rust",id:"\u5b89\u88c5rust",level:2},{value:"\u5b89\u88c5\u547d\u4ee4\u884c\u5de5\u5177",id:"\u5b89\u88c5\u547d\u4ee4\u884c\u5de5\u5177",level:2},{value:"macOS & Linux",id:"macos--linux",level:3},{value:"\u4e0b\u8f7d\u9884\u7f16\u8bd1\u4e8c\u8fdb\u5236\u6587\u4ef6 \uff08linux\uff09",id:"\u4e0b\u8f7d\u9884\u7f16\u8bd1\u4e8c\u8fdb\u5236\u6587\u4ef6-linux",level:4},{value:"\u4e0b\u8f7d\u9884\u7f16\u8bd1\u4e8c\u8fdb\u5236\u6587\u4ef6 \uff08macOS\uff09",id:"\u4e0b\u8f7d\u9884\u7f16\u8bd1\u4e8c\u8fdb\u5236\u6587\u4ef6-macos",level:4},{value:"Windows",id:"windows",level:3},{value:"\u4e0b\u8f7d\u9884\u7f16\u8bd1\u4e8c\u8fdb\u5236\u6587\u4ef6",id:"\u4e0b\u8f7d\u9884\u7f16\u8bd1\u4e8c\u8fdb\u5236\u6587\u4ef6",level:4},{value:"\u4ece\u6e90\u7801\u7f16\u8bd1",id:"\u4ece\u6e90\u7801\u7f16\u8bd1",level:3}],m={toc:c},d="wrapper";function b(e){let{components:a,...t}=e;return(0,l.kt)(d,(0,n.Z)({},m,t,{components:a,mdxType:"MDXLayout"}),(0,l.kt)("h1",{id:"\u5b89\u88c5"},"\u5b89\u88c5"),(0,l.kt)("h2",{id:"\u5b89\u88c5web3js"},"\u5b89\u88c5Web3.js"),(0,l.kt)("p",null,"\u7528JavaScript\u6216\u8005TypeScript\u8fdb\u884cSolana\u7f16\u7a0b\u65f6\uff0c\u4f60\u4f1a\u7528\u5230\u4e0b\u9762\u7684\u51e0\u4e2a\u5e93\u3002",(0,l.kt)("br",null)),(0,l.kt)("h3",{id:"web3js"},"Web3.js"),(0,l.kt)("p",null,(0,l.kt)("a",{parentName:"p",href:"https://solana-labs.github.io/solana-web3.js/"},(0,l.kt)("inlineCode",{parentName:"a"},"@solana/web3.js")),"\n\u8fd9\u4e2a\u5e93\u63d0\u4f9b\u4e86\u5f88\u591a\u7528\u4e8e\u4e0eSolana\u4ea4\u4e92\uff0c\u53d1\u9001\u4ea4\u6613\uff0c\u4ece\u533a\u5757\u94fe\u4e0a\u8bfb\u53d6\u6570\u636e\u7b49\u64cd\u4f5c\u7684\u57fa\u7840\u529f\u80fd\u3002"),(0,l.kt)("p",null,"\u53ef\u4ee5\u7528\u4ee5\u4e0b\u547d\u4ee4\u5b89\u88c5\uff1a"),(0,l.kt)(r.Z,{mdxType:"Tabs"},(0,l.kt)(s.Z,{value:"Yarn",label:"Yarn",mdxType:"TabItem"},(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"yarn add @solana/web3.js\n"))),(0,l.kt)(s.Z,{value:"NPM",label:"Npm",mdxType:"TabItem"},(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"npm install --save @solana/web3.js\n"))),(0,l.kt)(s.Z,{value:"browser",label:"browser",mdxType:"TabItem"},(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-html"},'\x3c!-- Development (un-minified) --\x3e\n - - +Orbis: Use our open social protocol to add web3 social features to your project in just a few lines of code. + + \ No newline at end of file diff --git a/blog/ada-and-pda/index.html b/blog/ada-and-pda/index.html index 720d3ff57..b752b09c3 100644 --- a/blog/ada-and-pda/index.html +++ b/blog/ada-and-pda/index.html @@ -9,8 +9,8 @@ - - + +
    @@ -18,7 +18,7 @@ 通常由特定程序(通常是一个智能合约)关联额外的账户。该账号没有私钥,故除程序本身外,无法完成数据签名,无法完成完整的数据交易。

    • ADA (Account Derived Account)

    由 createWithSeed 方法产生。 有一个账号公钥派生出来的关联账户,数据签名权限属于主账号。也即,需要主账号的签名才能完成完整的数据交易。

    solana中,根据数据签名,决定了数据的真实所有权。即 我的数据我做主

    本文主要分析这两种账号的异同。

    地址生成逻辑介绍如下

    • PDA 地址生成规则
    1. buffer = [seed,programId,"ProgramDerivedAddress"]
    2. 对buffer 取 sha256
    3. 如果在曲线上,那么抛出error, 如果不在,那么直接返回作为 使用地址

    createProgramAddressSync

    • ADA 生成
    1. buffer=[fromPublicKey,seed,programId]
    2. buffer 取 sha256, 直接返回

    createWithSeed

    区别在于,数据的托管使用逻辑.

    • ADA 数据签名权限,在于账户本身。即 我的数据我做主,未经允许(我未签名)不能修改。
    • PDA 数据签名权限在于合约。经过程序签名,可以修改 account 的数据和提取其中的sol。

    ADA 账号使用

    数据操作,有配套的函数对应,内部包含 xxxxWithSeedParams 类型的参数,完成对应的操作。 操作数据,需要 主账户的签名,这一点决定了,账号的真实所有权。

    • SystemProgram.createAccountWithSeed 初始化账号
    • SystemProgram.assign 重新分配owner
    • SystemProgram.allocate 分配空间
    • SystemProgram.transfer 转移SOL

    PDA 账号使用

    • 客户端只用于账户地址推导,不能初始化。初始化过程在合约内部完成。
    • 因其签名权限,必须在合约内部完成。他的操作权限完全属于智能合约。

    ADA 账号使用 example

      const seed = "ada.creator";

    // 初始化ada 账户
    let ada_account = await web3.PublicKey.createWithSeed(
    signer.publicKey,
    seed,
    program
    );
    console.log("ada_account address: ", ada_account.toBase58());

    let ada_info = await connection.getAccountInfo(ada_account);

    // 根据是否存在账号,决定是否初始化
    if (ada_info) {
    console.log(ada_info);
    } else {
    console.log("ada account not found");
    const transaction = new web3.Transaction().add(
    web3.SystemProgram.createAccountWithSeed({
    newAccountPubkey: ada_account,
    fromPubkey: signer.publicKey,
    basePubkey: signer.publicKey,
    programId: program,
    seed,
    lamports: web3.LAMPORTS_PER_SOL,
    space: 20,
    })
    );

    PDA 使用 example

    客户端部分代码逻辑

    const pda_seed = "pda.creator";

    const obj = new Model();

    const [pda, bump_seed] = web3.PublicKey.findProgramAddressSync(
    [signer.publicKey.toBuffer(), new TextEncoder().encode(movie.title)],
    program
    );

    console.log("pda address : ", pda.toBase58());

    const instruction = new web3.TransactionInstruction({
    keys: [
    {
    // 付钱的账户
    pubkey: signer.publicKey,
    isSigner: true,
    isWritable: false,
    },
    {
    // PDA将存储数据
    pubkey: pda,
    isSigner: false,
    isWritable: true,
    },
    {
    // 系统程序将用于创建PDA
    pubkey: web3.SystemProgram.programId,
    isSigner: false,
    isWritable: false,
    },
    ],
    // 传输数据
    data: obj.serialize(),
    programId: program,
    });

    const transaction = new web3.Transaction().add(instruction);

    const signature = await web3.sendAndConfirmTransaction(
    connection,
    transaction,
    [signer]
    );

    console.log(signature);

    合约部分代码逻辑

    // 获取账户迭代器
    let account_info_iter = &mut accounts.iter();

    // 获取账户
    let initializer = next_account_info(account_info_iter)?;
    let pda_account = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    // 构造PDA账户
    let (pda, bump_seed) =
    Pubkey::find_program_address(&[initializer.key.as_ref(), title.as_bytes()], program_id);

    // 和客户端比对
    if pda != *pda_account.key {
    msg!("Invalid seeds for PDA");
    return Err(ProgramError::InvalidArgument);
    }

    // 计算所需的租金
    let rent = Rent::get()?;
    let rent_lamports = rent.minimum_balance(total_len);

    // 创建账户
    invoke_signed(
    &system_instruction::create_account(
    initializer.key,
    pda_account.key,
    rent_lamports,
    total_len
    .try_into()
    .map_err(|_| Error::ConvertUsizeToU64Failed)?,
    program_id,
    ),
    &[
    initializer.clone(),
    pda_account.clone(),
    system_program.clone(),
    ],
    &[&[initializer.key.as_ref(), title.as_bytes(), &[bump_seed]]],
    )?;

    // MovieAccountState 定义的state类型
    let mut account_data =
    try_from_slice_unchecked::<MovieAccountState>(&pda_account.data.borrow()).unwrap();

    account_data.title = title;
    account_data.rating = rating;
    account_data.description = description;
    account_data.is_initialized = true;

    // 写入pda 数据本身
    account_data.serialize(&mut &mut pda_account.data.borrow_mut()[..])?;

    参考资料

    - - + + \ No newline at end of file diff --git a/blog/archive/index.html b/blog/archive/index.html index 2fcef7558..03a57a4cf 100644 --- a/blog/archive/index.html +++ b/blog/archive/index.html @@ -9,13 +9,13 @@ - - + + - - + + \ No newline at end of file diff --git a/blog/cloudbreak/index.html b/blog/cloudbreak/index.html index fbe426c53..5d6a9e294 100644 --- a/blog/cloudbreak/index.html +++ b/blog/cloudbreak/index.html @@ -9,13 +9,13 @@ - - + +

    Cloudbreak Solana Horizontally Scaled State Architecture

    · 7 min read
    Davirain

    在这篇博文中,我们将介绍 Cloudbreak,Solana 的水平扩展状态架构

    概述:RAM、SSD 和线程

    当在不进行分片的情况下扩展区块链时,仅扩展计算是不够的。用于跟踪帐户的内存很快就会成为大小和访问速度的瓶颈。例如:人们普遍认为,许多现代链使用的本地数据库引擎 LevelDB 在单台机器上无法支持超过 5,000 TPS。这是因为虚拟机无法通过数据库抽象利用对帐户状态的并发读写访问。

    一个简单的解决方案是在 RAM 中维护全局状态。然而,期望消费级机器有足够的 RAM 来存储全局状态是不合理的。下一个选择是使用 SSD。虽然 SSD 将每字节成本降低了 30 倍或更多,但它们比 RAM 慢 1000 倍。以下是最新三星 SSD 的数据表,它是市场上最快的 SSD 之一。

    单笔交易需要读取 2 个账户并写入 1 个账户。账户密钥是加密公钥,完全随机,没有真实的数据局部性。用户的钱包会有很多账户地址,每个地址的位与任何其他地址完全无关。由于帐户之间不存在局部性,因此我们不可能将它们放置在内存中以使它们可能彼此接近。

    每秒最多 15,000 次唯一读取,使用单个 SSD 的帐户数据库的简单单线程实现将支持每秒最多 7,500 个事务。现代 SSD 支持 32 个并发线程,因此可以支持每秒 370,000 次读取,或每秒大约 185,000 个事务。

    Cloudbreak 破云

    Solana 的指导设计原则是设计不妨碍硬件的软件,以实现 100% 的利用率。

    组织帐户数据库以便在 32 个线程之间可以进行并发读取和写入是一项挑战。像 LevelDB 这样的普通开源数据库会导致瓶颈,因为它们没有针对区块链设置中的这一特定挑战进行优化。 Solana 不使用传统数据库来解决这些问题。相反,我们使用操作系统使用的几种机制。

    首先,我们利用内存映射文件。内存映射文件是其字节被映射到进程的虚拟地址空间的文件。一旦文件被映射,它的行为就像任何其他内存一样。内核可能会将部分内存缓存在 RAM 中,或者不将其缓存在 RAM 中,但物理内存的数量受到磁盘大小的限制,而不是 RAM 的大小。读取和写入仍然明显受到磁盘性能的限制。

    第二个重要的设计考虑因素是顺序操作比随机操作快得多。这不仅适用于 SSD,也适用于整个虚拟内存堆栈。 CPU 擅长预取按顺序访问的内存,而操作系统则擅长处理连续页错误。为了利用这种行为,我们将帐户数据结构大致分解如下:

    1. 账户和分叉的索引存储在 RAM 中。

    2. 帐户存储在最大 4MB 的内存映射文件中。

    3. 每个内存映射仅存储来自单个提议分叉的帐户。

    4. 地图随机分布在尽可能多的可用 SSD 上。

    5. 使用写时复制语义。

    6. 写入会附加到同一分叉的随机内存映射中。

    7. 每次写入完成后都会更新索引。

    由于帐户更新是写时复制并附加到随机 SSD,因此 Solana 获得了顺序写入和跨多个 SSD 进行横向写入以进行并发事务的好处。读取仍然是随机访问,但由于任何给定的分叉状态更新都分布在许多 SSD 上,因此读取最终也会水平扩展。

    Cloudbreak 还执行某种形式的垃圾收集。随着分叉在回滚之外最终确定并且帐户被更新,旧的无效帐户将被垃圾收集,并且内存将被放弃。

    这种架构至少还有一个更大的好处:计算任何给定分叉的状态更新的 Merkle 根可以通过跨 SSD 水平扩展的顺序读取来完成。这种方法的缺点是失去了数据的通用性。由于这是一个自定义数据结构,具有自定义布局,因此我们无法使用通用数据库抽象来查询和操作数据。我们必须从头开始构建一切。幸运的是,现在已经完成了。

    Benchmarking Cloudbreak Cloudbreak 基准测试

    虽然帐户数据库位于 RAM 中,但我们看到吞吐量与 RAM 访问时间相匹配,同时随可用内核数量进行扩展。当帐户数量达到 1000 万时,数据库不再适合 RAM。然而,我们仍然看到单个 SSD 上每秒读取或写入的性能接近 1m。

    - - + + \ No newline at end of file diff --git a/blog/error-after-running-anchor-build/index.html b/blog/error-after-running-anchor-build/index.html index dd8f6108d..0502a0a19 100644 --- a/blog/error-after-running-anchor-build/index.html +++ b/blog/error-after-running-anchor-build/index.html @@ -9,13 +9,13 @@ - - + +

    使用anchor build 依賴衝突

    · 3 min read
    YanAemons

    報錯日志

    在使用solana-cli時候,鑑於一些依賴版本限制,會用到cli14.xx(主網版本),而不是16.xx(測試網版本)

    例如,在使用solana-cli版本爲1.14.17, anchor版本爲0.26.0的環境中, anchor init創建一個新項目後運行 anchor build會發生以下錯誤:

    error: package constant_time_eq v0.3.0 cannot be built because it requires rustc 1.66.0 or newer, while the currently active rustc version is 1.62.0-dev

    報錯原因

    使用的solana-cli版本在14.xxx, cli內自帶的rustc版本過老,無法編譯較新的依賴

    解決方案

    1. 升級solana-cli至最新版本

    solana-install update

    2.指定依賴包版本

    需要在Cargo.toml文件下指定以下依賴版本

    getrandom = { version = "0.2.9", features = ["custom"] }  
    solana-program = "=1.14.17"
    winnow="=0.4.1"
    toml_datetime="=0.6.1"
    blake3 = "=1.3.1"

    運行cargo clean後重新運行anchor build即可解決

    監聽程序log監聽到兩次

    在使用program.addEventListener()有可能聽到兩次相同的事件,其中一次的txSign會是“1111111111111111111111111111111111111111111111111111111111111111”, 這是因爲監聽到了模擬時的交易哈系,我們只需要在監聽到該交易哈系時拋棄即可

    program.addEventListener("eventName", (event, slot, signature) => {
    if (signature === '1111111111111111111111111111111111111111111111111111111111111111') return

    // do ur stuff
    })

    然而,有時websocket訂閱也會多次返回實際簽名。如果是這種情況,您可以使用一些緩存解決方案。例如,創建一個具有一定長度限制的集合,在此處添加簽名並檢查該集合中是否存在新簽名:

    const handledSignatures = new Set<string>()
    const maxHandledSignaturesLen = 100

    program.addEventListener("eventName", (event, slot, signature) => {
    if (signature === '1111111111111111111111111111111111111111111111111111111111111111') return
    if (handledSignatures.has(signature)) return

    // do ur stuff

    handledSignatures.add(signature)
    if (handledSignatures.size > maxHandledSignaturesLen) {
    handledSignatures.delete(handledSignatures.values().next().value)
    }
    })
    - - + + \ No newline at end of file diff --git a/blog/first-blog-post/index.html b/blog/first-blog-post/index.html index ed0f928b5..37113eff6 100644 --- a/blog/first-blog-post/index.html +++ b/blog/first-blog-post/index.html @@ -9,13 +9,13 @@ - - + +

    Solana共学教程

    · 3 min read
    Davirain

    欢迎来到Solana共学,这是一个精心设计的教程系列,供任何对Solana感兴趣的人深入学习。无论你是初学者还是有经验的开发者,这些模块都会引导你了解Solana区块链开发的基本内容。

    模块1:Solana基础

    • 区块链基本概念介绍
    • 本地程序开发环境配置
      • 原始Solana合约实现《hello, World》
      • Anchor合约框架实现《hello, World》
      • 使用Solang编译器编译solidity合约实现《hello, World》
    • BackPack钱包使用
    • 客户端开发
    • 钱包和前端
    • 自定义指令
    • 开始你自己的定制项目

    模块2:Solana高级主题

    • SPL token
    • NFTs + 使用Metaplex进行铸造
    • 在用户界面中展示NFTs
    • 创造神奇的网络货币并出售JPEG图片

    更深入的模块:深入了解Solana

    • 模块3:Rust入门,原生Solana开发,安全性,NFT质押
    • 模块4:本地环境,跨程序调用,测试,质押应用开发
    • 模块5:Anchor入门,全栈Anchor应用开发
    • 模块6:发布周,随机性,完善

    特别主题:超越基础

    • Solana程序中的环境变量
    • Solana支付,版本化事务,Rust宏
    • Solana程序安全:签名授权,所有者检查,重新初始化攻击,PDA共享等
    • 使用Solidity编写Solana合约
    • 发行Token2020,压缩NFT
    • 在Solana中使用The Graph,Oracles Pyth SDK
    • TipLink使用,如何在Quicknode和Helius申请RPC endpoint
    • 等等...

    和我们一起,在这全面的指南中探索Solana的每一个方面。从最基本的内容到安全和合约开发的复杂方面,Solana共学为每一位Solana爱好者提供了内容。

    敬请期待,如果有任何问题或需要进一步的协助,请随时与我们联系。欢迎来到Solana共学!

    - - + + \ No newline at end of file diff --git a/blog/gulf-stream/index.html b/blog/gulf-stream/index.html index 799d06584..7ec987ad8 100644 --- a/blog/gulf-stream/index.html +++ b/blog/gulf-stream/index.html @@ -9,13 +9,13 @@ - - + +

    Gulf Stream Solana Mempool-less Transaction Forwarding Protocol

    · 7 min read
    Davirain

    在这篇博文中,我们将探讨 Gulf Stream,这是 Solana 用于高性能对抗网络的内存池管理解决方案。在进一步的博客文章中,我们将列出所有 7 个关键创新。

    内存池解释

    内存池是一组已提交但尚未被网络处理的交易。您现在可以看到比特币和以太坊内存池。

    30 天的比特币内存池(以字节为单位)。

    以交易量衡量的 30 天以太坊内存池

    对于比特币和以太坊来说,未经确认的交易数量通常约为 20K-100K,如上所示。内存池的大小(通常以未确认交易的数量来衡量)取决于区块空间的供需。即使在区块链时代的早期,当内存池上升时,这也会对整个网络产生显着的瓶颈效应。

    那么,Solana 如何做得更好呢?在不增加网络吞吐量的情况下,Solana 验证器可以管理 100,000 的内存池大小。这意味着在网络吞吐量为 50,000 TPS 的情况下,100,000 个交易内存池只需几秒钟即可执行。这就是 Solana 成为世界上性能最高的无需许可区块链的原因。

    令人印象深刻,对吧?但这个简单的分析忽略了很多重要因素……

    以太坊和比特币中的内存池使用八卦协议以点对点方式在随机节点之间传播。网络中的节点定期构建代表本地内存池的布隆过滤器,并向网络上的其他节点请求与该过滤器不匹配的任何交易(以及其他一些交易,例如最低费用)。将单个事务传播到网络的其余部分将至少需要 log(N) 步骤,消耗过滤它所需的带宽、内存和计算资源。

    当基准客户端开始每秒生成 100,000 个事务时,八卦协议就会不堪重负。计算过滤器以及在机器之间应用过滤器同时维护内存中的所有事务的成本变得非常高。领导者(区块生产者)还必须在区块中重新传输相同的交易,这意味着每笔交易至少通过网络传播两次。这既不高效也不实用。

    Introducing Gulf Stream 墨西哥湾流简介

    我们在 Solana 网络上解决这个问题的解决方案是将事务缓存和转发推到网络边缘。我们称之为湾流。由于每个验证者都知道即将到来的领导者的顺序,因此客户端和验证者会提前将交易转发给预期的领导者。这使得验证者可以提前执行交易,减少确认时间,更快地切换领导者,并减少未确认交易池对验证者的内存压力。该解决方案在具有非确定性领导者的网络中是不可能的

    那么它是怎样工作的?客户端(例如钱包)签署引用特定区块哈希的交易。客户端选择一个最近的、已被网络完全确认的区块哈希值。区块大约每 800 毫秒提议一次,并且每增加一个区块就需要指数级增加的超时时间来展开。使用我们的默认超时曲线,在最坏的情况下,完全确认的块哈希值是 32 个块旧的。该交易仅在引用块的子块中有效,并且仅对 X 个块有效。虽然 X 尚未最终确定,但我们预计区块哈希的 TTL(生存时间)约为 32 个区块。假设区块时间为 800 毫秒,相当于 24 秒。

    一旦交易被转发给任何验证者,验证者就会将其转发给即将到来的领导者之一。客户可以订阅来自验证器的交易确认。客户知道区块哈希会在有限的时间内过期,或者交易已被网络确认。这允许客户签署保证执行或失败的交易。一旦网络越过回滚点,使得交易引用的区块哈希过期,客户端就可以保证交易现在无效并且永远不会在链上执行。

    https://podcasts.apple.com/us/podcast/anatoly-yakovenko-ceo-co-founder-solana-what-sharding/id1434060078?i=1000439218245&source=post_page-----d342e72186ad--------------------------------

    这种架构固有的许多积极的副作用。首先,负载下的验证器可以提前执行交易并丢弃任何失败的交易。其次,领导者可以根据转发交易的验证器的权益权重来优先处理交易。这允许网络在大规模拒绝服务期间正常降级。

    到目前为止,很明显,区块链网络的功能只有在其内存池最小的情况下才能发挥作用。虽然交易吞吐量有限的网络承担着尝试改造全新扩展技术以解决不断增长的内存池的崇高努力,但 Solana 自构思以来一直通过历史证明、湾流和海平面等优化来解决第一代的问题区块链网络并实现巨大的交易吞吐量。从一开始,这就是全球范围内的惊人速度,也是为世界各地的企业、经济和人民创建功能强大的去中心化基础设施的根本性发展。

    - - + + \ No newline at end of file diff --git a/blog/index.html b/blog/index.html index 3adeb9f5f..df45f0314 100644 --- a/blog/index.html +++ b/blog/index.html @@ -9,8 +9,8 @@ - - + +
    @@ -19,7 +19,7 @@ 通常由特定程序(通常是一个智能合约)关联额外的账户。该账号没有私钥,故除程序本身外,无法完成数据签名,无法完成完整的数据交易。

    • ADA (Account Derived Account)

    由 createWithSeed 方法产生。 有一个账号公钥派生出来的关联账户,数据签名权限属于主账号。也即,需要主账号的签名才能完成完整的数据交易。

    solana中,根据数据签名,决定了数据的真实所有权。即 我的数据我做主

    本文主要分析这两种账号的异同。

    地址生成逻辑介绍如下

    • PDA 地址生成规则
    1. buffer = [seed,programId,"ProgramDerivedAddress"]
    2. 对buffer 取 sha256
    3. 如果在曲线上,那么抛出error, 如果不在,那么直接返回作为 使用地址

    createProgramAddressSync

    • ADA 生成
    1. buffer=[fromPublicKey,seed,programId]
    2. buffer 取 sha256, 直接返回

    createWithSeed

    区别在于,数据的托管使用逻辑.

    • ADA 数据签名权限,在于账户本身。即 我的数据我做主,未经允许(我未签名)不能修改。
    • PDA 数据签名权限在于合约。经过程序签名,可以修改 account 的数据和提取其中的sol。

    ADA 账号使用

    数据操作,有配套的函数对应,内部包含 xxxxWithSeedParams 类型的参数,完成对应的操作。 操作数据,需要 主账户的签名,这一点决定了,账号的真实所有权。

    • SystemProgram.createAccountWithSeed 初始化账号
    • SystemProgram.assign 重新分配owner
    • SystemProgram.allocate 分配空间
    • SystemProgram.transfer 转移SOL

    PDA 账号使用

    • 客户端只用于账户地址推导,不能初始化。初始化过程在合约内部完成。
    • 因其签名权限,必须在合约内部完成。他的操作权限完全属于智能合约。

    ADA 账号使用 example

      const seed = "ada.creator";

    // 初始化ada 账户
    let ada_account = await web3.PublicKey.createWithSeed(
    signer.publicKey,
    seed,
    program
    );
    console.log("ada_account address: ", ada_account.toBase58());

    let ada_info = await connection.getAccountInfo(ada_account);

    // 根据是否存在账号,决定是否初始化
    if (ada_info) {
    console.log(ada_info);
    } else {
    console.log("ada account not found");
    const transaction = new web3.Transaction().add(
    web3.SystemProgram.createAccountWithSeed({
    newAccountPubkey: ada_account,
    fromPubkey: signer.publicKey,
    basePubkey: signer.publicKey,
    programId: program,
    seed,
    lamports: web3.LAMPORTS_PER_SOL,
    space: 20,
    })
    );

    PDA 使用 example

    客户端部分代码逻辑

    const pda_seed = "pda.creator";

    const obj = new Model();

    const [pda, bump_seed] = web3.PublicKey.findProgramAddressSync(
    [signer.publicKey.toBuffer(), new TextEncoder().encode(movie.title)],
    program
    );

    console.log("pda address : ", pda.toBase58());

    const instruction = new web3.TransactionInstruction({
    keys: [
    {
    // 付钱的账户
    pubkey: signer.publicKey,
    isSigner: true,
    isWritable: false,
    },
    {
    // PDA将存储数据
    pubkey: pda,
    isSigner: false,
    isWritable: true,
    },
    {
    // 系统程序将用于创建PDA
    pubkey: web3.SystemProgram.programId,
    isSigner: false,
    isWritable: false,
    },
    ],
    // 传输数据
    data: obj.serialize(),
    programId: program,
    });

    const transaction = new web3.Transaction().add(instruction);

    const signature = await web3.sendAndConfirmTransaction(
    connection,
    transaction,
    [signer]
    );

    console.log(signature);

    合约部分代码逻辑

    // 获取账户迭代器
    let account_info_iter = &mut accounts.iter();

    // 获取账户
    let initializer = next_account_info(account_info_iter)?;
    let pda_account = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    // 构造PDA账户
    let (pda, bump_seed) =
    Pubkey::find_program_address(&[initializer.key.as_ref(), title.as_bytes()], program_id);

    // 和客户端比对
    if pda != *pda_account.key {
    msg!("Invalid seeds for PDA");
    return Err(ProgramError::InvalidArgument);
    }

    // 计算所需的租金
    let rent = Rent::get()?;
    let rent_lamports = rent.minimum_balance(total_len);

    // 创建账户
    invoke_signed(
    &system_instruction::create_account(
    initializer.key,
    pda_account.key,
    rent_lamports,
    total_len
    .try_into()
    .map_err(|_| Error::ConvertUsizeToU64Failed)?,
    program_id,
    ),
    &[
    initializer.clone(),
    pda_account.clone(),
    system_program.clone(),
    ],
    &[&[initializer.key.as_ref(), title.as_bytes(), &[bump_seed]]],
    )?;

    // MovieAccountState 定义的state类型
    let mut account_data =
    try_from_slice_unchecked::<MovieAccountState>(&pda_account.data.borrow()).unwrap();

    account_data.title = title;
    account_data.rating = rating;
    account_data.description = description;
    account_data.is_initialized = true;

    // 写入pda 数据本身
    account_data.serialize(&mut &mut pda_account.data.borrow_mut()[..])?;

    参考资料

    - - + + \ No newline at end of file diff --git a/blog/page/2/index.html b/blog/page/2/index.html index 74d6a2c78..e7c6f05a8 100644 --- a/blog/page/2/index.html +++ b/blog/page/2/index.html @@ -9,13 +9,13 @@ - - + +

    · 3 min read
    YanAemons

    報錯日志

    在使用solana-cli時候,鑑於一些依賴版本限制,會用到cli14.xx(主網版本),而不是16.xx(測試網版本)

    例如,在使用solana-cli版本爲1.14.17, anchor版本爲0.26.0的環境中, anchor init創建一個新項目後運行 anchor build會發生以下錯誤:

    error: package constant_time_eq v0.3.0 cannot be built because it requires rustc 1.66.0 or newer, while the currently active rustc version is 1.62.0-dev

    報錯原因

    使用的solana-cli版本在14.xxx, cli內自帶的rustc版本過老,無法編譯較新的依賴

    解決方案

    1. 升級solana-cli至最新版本

    solana-install update

    2.指定依賴包版本

    需要在Cargo.toml文件下指定以下依賴版本

    getrandom = { version = "0.2.9", features = ["custom"] }  
    solana-program = "=1.14.17"
    winnow="=0.4.1"
    toml_datetime="=0.6.1"
    blake3 = "=1.3.1"

    運行cargo clean後重新運行anchor build即可解決

    監聽程序log監聽到兩次

    在使用program.addEventListener()有可能聽到兩次相同的事件,其中一次的txSign會是“1111111111111111111111111111111111111111111111111111111111111111”, 這是因爲監聽到了模擬時的交易哈系,我們只需要在監聽到該交易哈系時拋棄即可

    program.addEventListener("eventName", (event, slot, signature) => {
    if (signature === '1111111111111111111111111111111111111111111111111111111111111111') return

    // do ur stuff
    })

    然而,有時websocket訂閱也會多次返回實際簽名。如果是這種情況,您可以使用一些緩存解決方案。例如,創建一個具有一定長度限制的集合,在此處添加簽名並檢查該集合中是否存在新簽名:

    const handledSignatures = new Set<string>()
    const maxHandledSignaturesLen = 100

    program.addEventListener("eventName", (event, slot, signature) => {
    if (signature === '1111111111111111111111111111111111111111111111111111111111111111') return
    if (handledSignatures.has(signature)) return

    // do ur stuff

    handledSignatures.add(signature)
    if (handledSignatures.size > maxHandledSignaturesLen) {
    handledSignatures.delete(handledSignatures.values().next().value)
    }
    })

    · 17 min read
    Davirain

    Solana上,状态压缩是一种创建离链数据的“指纹”(或哈希)并将该指纹存储在链上以进行安全验证的方法。有效地利用Solana账本的安全性来安全验证离链数据,以确保其未被篡改。

    这种“压缩”方法使得Solana的程序和dApps能够使用廉价的区块链账本空间来安全存储数据,而不是更昂贵的账户空间。

    这是通过使用一种特殊的二叉树结构,称为并发默克尔树,对每个数据片段(称为 leaf )创建哈希,将它们哈希在一起,并仅将最终哈希存储在链上来实现的。

    什么是状态压缩?

    简单来说,状态压缩使用“树”结构将链外数据以确定性的方式进行加密哈希,计算出一个最终的哈希值,并将其存储在链上。

    这些树是通过这个“确定性”过程创建的:

    • 获取任何数据
    • 创建这些数据的哈希值
    • 将此哈希值存储为树底部的 leaf
    • 每个 leaf 对都会被一起哈希,创建一个 branch
    • 每个 branch 然后一起哈希
    • 不断攀爬树木并将相邻的树枝连接在一起
    • 树顶上一旦到达,就会产生最后的 root hash

    这个 root hash 然后存储在链上,作为每个叶子节点中所有数据的可验证证据。这样任何人都可以通过加密验证树中所有离链数据,而实际上只需在链上存储少量数据。因此,由于这种"状态压缩",大大降低了存储/证明大量数据的成本。

    默克尔树和并发默克尔树

    Solana的状态压缩使用了一种特殊类型的默克尔树,允许对任何给定的树进行多次更改,同时仍然保持树的完整性和有效性。

    这棵特殊的树被称为“并发默克尔树”,有效地在链上保留了树的“更改日志”。允许在一个证明失效之前对同一棵树进行多次快速更改(即在同一个区块中)。

    默克尔树是什么?

    默克尔树,有时也被称为“哈希树”,是一种基于哈希的二叉树结构,其中每个leaf节点都被表示为其内部数据的加密哈希。而每个非叶节点,也被称为“branch节点”,则被表示为其子叶节点哈希的哈希值。

    每个分支也被哈希在一起,沿着树向上爬,直到最后只剩下一个哈希。这个最终的哈希,称为 root hash 或者"根",可以与一个"证明路径"结合使用,来验证存储在叶节点中的任何数据。

    一旦计算出最终的根哈希值(root hash),可以通过重新计算特定叶子(leaf)节点的数据和每个相邻分支的哈希标签(称为“证明路径”)来验证存储在节点中的任何数据。将这个“重新哈希”与根哈希值进行比较,可以验证底层叶子数据的准确性。如果它们匹配,数据就被验证为准确的。如果它们不匹配,叶子数据已被更改。

    只要需要,原始叶子数据可以通过对新的叶子数据进行哈希运算并重新计算根哈希值来进行更改,方法与原始根哈希值的计算方式相同。然后,这个新的根哈希值用于验证任何数据,并且有效地使之前的根哈希值和证明无效。因此,对这些传统的默克尔树的每一次更改都需要按顺序执行。

    info

    当使用默克尔树时,更改叶子数据并计算新的根哈希的过程可能是非常常见的事情!虽然这是树的设计要点之一,但它可能导致最显著的缺点之一:快速变化。

    什么是并发默克尔树?

    在高吞吐量的应用中,比如在Solana运行时中,对于链上传统Merkle树的更改请求可能会相对快速地连续接收到验证者(例如在同一个槽中)。每个叶子数据的更改仍然需要按顺序执行。这导致每个后续的更改请求都会失败,因为根哈希和证明已经被同一槽中之前的更改请求无效化了。

    进入,并发默克尔树。

    并发默克尔树存储了最近更改的安全日志、它们的根哈希以及用于推导根哈希的证明。这个日志缓冲区存储在链上的每个树对应的特定账户中,最大记录数为(也称为 maxBufferSize )。

    当同一时隙内的验证者收到多个叶子数据变更请求时,链上并发 Merkle 树可以将这个“变更日志缓冲区”作为更可接受的证明的真实来源。有效地允许在同一时隙内对同一棵树进行多达 maxBufferSize 次变更。大幅提升吞吐量。

    并发默克尔树的大小调整

    创建这种链上树时,有三个值将决定您的树的大小、创建树的成本以及对树的并发更改数量:

    1. max depth 最大深度
    2. max buffer size 最大缓冲区大小
    3. canopy depth

    max depth

    树的“最大深度”是从任何数据 leaf 到树的 root 所需的最大跳数。

    由于默克尔树是二叉树,每个叶子节点只与另一个叶子节点相连;存在于一个 leaf pair 中。

    因此,树的 maxDepth 被用来确定可以通过简单的计算存储在树中的最大节点数(也称为数据或 leafs

    nodes_count = 2 ^ maxDepth

    由于树的深度必须在创建树时设置,您必须决定您希望树存储多少个数据。然后使用上述简单的计算,您可以确定存储数据的最低 maxDepth

    示例1:铸造100个NFTs

    如果你想创建一个用于存储100个压缩NFT的树,我们至少需要"100个叶子"或"100个节点"。

    // maxDepth=6 -> 64 nodes
    2^6 = 64

    // maxDepth=7 -> 128 nodes
    2^7 = 128

    因此,我们需要一个最大深度为 7 的树,以存储 100 个数据。

    例子2:铸造15000个NFTs

    如果你想创建一个用于存储15000个压缩NFT的树,我们将需要至少"15000个叶子"或"15000个节点"。

    // maxDepth=13 -> 8192 nodes
    2^13 = 8192

    // maxDepth=14 -> 16384 nodes
    2^14 = 16384

    因此,我们需要一个最大深度为 14 的树,以存储 15000 个数据。

    最大深度越高,成本越高

    创建树时, maxDepth 值将是成本的主要驱动因素之一,因为您将在创建树时支付这笔成本。最大树深度越高,您可以存储的数据指纹(也称为哈希)越多,成本就越高。

    max buffer size

    max buffer size” 实际上是树上可以发生的最大变化数量,同时仍然有效的 root hash

    由于根哈希有效地是所有叶子数据的单一哈希,改变任何一个叶子将使得所有后续尝试改变常规树的叶子所需的证明无效。

    但是使用并发树,对于这些证明来说,实际上有一个更新的日志。这个日志缓冲区的大小和设置是通过这个 maxBufferSize 值在树创建时完成的。

    Canopy depth

    Canopy depth”,有时也称为Canopy大小,是指在任何给定的证明路径上缓存/存储在链上的证明节点数量。

    在对 leaf 执行更新操作时,例如转让所有权(例如出售压缩的NFT),必须使用完整的证明路径来验证叶子节点的原始所有权,从而允许进行更新操作。此验证是使用完整的证明路径来正确计算当前的 root hash (或通过链上的“并发缓冲区”缓存的任何 root hash )来执行的。

    树的最大深度越大,执行此验证所需的证明节点就越多。例如,如果您的最大深度是 14 ,则需要使用 14 个总的证明节点进行验证。随着树的增大,完整的证明路径也会变得更长。

    通常情况下,每个这些证明节点都需要在每个树更新事务中包含。由于每个证明节点的值在事务中占用 32 bytes (类似于提供公钥),较大的树很快就会超过最大事务大小限制。

    进入CanopyCanopy可以在链上存储一定数量的验证节点(对于任何给定的验证路径)。这样可以在每个更新交易中包含较少的验证节点,从而保持整体交易大小低于限制。

    例如,深度为 14 的树需要 14 个总的验证节点。而有 10Canopy的情况下,每个更新事务只需要提交 4 个验证节点。

    Canopy深度值越大,成本越高

    canopyDepth 值也是创建树时成本的主要因素,因为您将在树的创建时支付这个成本。canopyDepth越高,链上存储的数据证明节点越多,成本也越高。

    较小的Canopy限制了可组合性

    虽然树的创建成本随着Canopy的高度而增加,但较低的Canopy将需要在每个更新事务中包含更多的证明节点。所需提交的节点越多,事务的大小就越大,因此超过事务大小限制就越容易。

    这也适用于任何其他试图与您的树/叶子进行交互的Solana程序或dApp。如果您的树需要太多的证明节点(因为Canopy深度较低),那么任何其他链上程序可能提供的额外操作都将受到其特定指令大小加上您的证明节点列表大小的限制。这限制了可组合性,并限制了您的特定树的潜在附加效用。

    例如,如果您的树被用于压缩的非同质化代币(NFTs),并且Canopy深度非常低,一个NFT市场可能只能支持简单的NFT转移,而无法支持链上竞标系统。

    创建一棵树的成本

    创建并发 Merkle 树的成本基于树的大小参数: maxDepthmaxBufferSizecanopyDepth 。这些值都用于计算在链上存在树所需的链上存储空间(以字节为单位)。

    一旦计算出所需的空间(以字节为单位),并使用 getMinimumBalanceForRentExemption RPC方法,请求在链上分配这些字节所需的费用(以lamports为单位)。

    在JavaScript中计算树木成本

    @solana/spl-account-compression 包中,开发人员可以使用 getConcurrentMerkleTreeAccountSize 函数来计算给定树大小参数所需的空间。

    然后使用 getMinimumBalanceForRentExemption 函数来获取在链上分配所需空间的最终成本(以lamports计算)。

    然后确定以lamports计算的成本,使得这个大小的账户免除租金,与其他账户创建类似。

    // calculate the space required for the tree
    const requiredSpace = getConcurrentMerkleTreeAccountSize(
    maxDepth,
    maxBufferSize,
    canopyDepth,
    );

    // get the cost (in lamports) to store the tree on-chain
    const storageCost = await connection.getMinimumBalanceForRentExemption(
    requiredSpace,
    );

    示例费用

    以下是几个不同树大小的示例成本,包括每个树可能的叶节点数量:

    例子 #1:16,384个节点,成本为0.222 SOL

    • 最大深度为 14 ,最大缓冲区大小为 64
    • 叶节点的最大数量: 16,384
    • 创建 0.222 SOLCanopy深度大约需要 0 的成本

    例子 #2:16,384个节点,成本为1.134 SOL

    • 最大深度为 14 ,最大缓冲区大小为 64
    • 叶节点的最大数量: 16,384
    • 创建 1.134 SOLCanopy深度大约需要 11 的成本

    示例 #3:1,048,576个节点,成本为1.673 SOL

    • 最大深度为 20 ,最大缓冲区大小为 256
    • 叶节点的最大数量: 1,048,576
    • 创建 1.673 SOLCanopy深度大约需要 10 的成本

    示例#4:1,048,576个节点,成本为15.814 SOL

    • 最大深度为 20 ,最大缓冲区大小为 256
    • 叶节点的最大数量: 1,048,576
    • 创建 15.814 SOLCanopy深度大约需要 15 的成本

    压缩的NFTs

    压缩的NFT是Solana上状态压缩的最受欢迎的应用之一。通过压缩,一个拥有一百万个NFT的收藏品可以以 ~50 SOL 的价格铸造,而不是其未压缩的等价收藏品。

    开发者指南:

    阅读我们的开发者指南,了解如何铸造和转移压缩的NFT

    · 3 min read
    Davirain

    欢迎来到Solana共学,这是一个精心设计的教程系列,供任何对Solana感兴趣的人深入学习。无论你是初学者还是有经验的开发者,这些模块都会引导你了解Solana区块链开发的基本内容。

    模块1:Solana基础

    • 区块链基本概念介绍
    • 本地程序开发环境配置
      • 原始Solana合约实现《hello, World》
      • Anchor合约框架实现《hello, World》
      • 使用Solang编译器编译solidity合约实现《hello, World》
    • BackPack钱包使用
    • 客户端开发
    • 钱包和前端
    • 自定义指令
    • 开始你自己的定制项目

    模块2:Solana高级主题

    • SPL token
    • NFTs + 使用Metaplex进行铸造
    • 在用户界面中展示NFTs
    • 创造神奇的网络货币并出售JPEG图片

    更深入的模块:深入了解Solana

    • 模块3:Rust入门,原生Solana开发,安全性,NFT质押
    • 模块4:本地环境,跨程序调用,测试,质押应用开发
    • 模块5:Anchor入门,全栈Anchor应用开发
    • 模块6:发布周,随机性,完善

    特别主题:超越基础

    • Solana程序中的环境变量
    • Solana支付,版本化事务,Rust宏
    • Solana程序安全:签名授权,所有者检查,重新初始化攻击,PDA共享等
    • 使用Solidity编写Solana合约
    • 发行Token2020,压缩NFT
    • 在Solana中使用The Graph,Oracles Pyth SDK
    • TipLink使用,如何在Quicknode和Helius申请RPC endpoint
    • 等等...

    和我们一起,在这全面的指南中探索Solana的每一个方面。从最基本的内容到安全和合约开发的复杂方面,Solana共学为每一位Solana爱好者提供了内容。

    敬请期待,如果有任何问题或需要进一步的协助,请随时与我们联系。欢迎来到Solana共学!

    - - + + \ No newline at end of file diff --git a/blog/pileline/index.html b/blog/pileline/index.html index a70bf90e9..deba3154d 100644 --- a/blog/pileline/index.html +++ b/blog/pileline/index.html @@ -9,13 +9,13 @@ - - + +

    Pipelining in Solana The Transaction Processing Unit

    · 6 min read
    Davirain

    为了达到亚秒级的确认时间和 Solana 成为世界上第一个网络规模区块链所需的交易能力,仅仅快速达成共识是不够的。该团队必须开发一种方法来快速验证大量交易块,同时在网络上快速复制它们。为了实现这一目标,Solana 网络上的事务验证过程广泛使用了 CPU 设计中常见的一种称为管道的优化。

    当存在需要通过一系列步骤处理的输入数据流并且每个步骤都有不同的硬件负责时,流水线是一个合适的过程。解释这一点的典型比喻是洗衣机和烘干机,它按顺序洗涤/烘干/折叠多批衣物。清洗必须在干燥之前进行,干燥之前必须进行折叠,但这三个操作中的每一个都由单独的单元执行。

    为了最大限度地提高效率,人们创建了一系列阶段的管道。我们将洗衣机称为第一阶段,烘干机称为第二阶段,折叠过程称为第三阶段。为了运行管道,需要在第一批衣物添加到烘干机后立即将第二批衣物添加到洗衣机中。同样,第三个负载在第二个负载放入烘干机并且第一个负载被折叠之后添加到洗衣机。通过这种方式,人们可以同时处理三批衣物。给定无限负载,管道将始终以管道中最慢阶段的速率完成负载。

    “我们需要找到一种方法让所有硬件始终保持忙碌状态。这就是网卡、CPU 核心和所有 GPU 核心。为此,我们借鉴了 CPU 设计的经验”,Solana 创始人兼首席技术官 Greg Fitzgerald 解释道。 “我们在软件中创建了一个四级交易处理器。我们称之为 TPU,我们的交易处理单元。”

    在 Solana 网络上,管道机制——交易处理单元——通过内核级别的数据获取、GPU 级别的签名验证、CPU 级别的存储和内核空间的写入来进行。当 TPU 开始向验证器发送块时,它已经在下一组数据包中获取,验证了它们的签名,并开始记入令牌。

    验证器节点同时运行两个管道进程,一个用于领导者模式,称为 TPU,另一个用于验证器模式,称为 TVU。在这两种情况下,管道化的硬件是相同的:网络输入、GPU 卡、CPU 内核、磁盘写入和网络输出。它对该硬件的作用是不同的。 TPU 的存在是为了创建分类帐条目,而 TVU 的存在是为了验证它们。

    “我们知道签名验证将成为瓶颈,但我们也可以将这种与上下文无关的操作卸载到 GPU,”Fitzgersald 说道。 “即使卸载了这一最昂贵的操作,仍然存在许多额外的瓶颈,例如与网络驱动程序交互以及管理限制并发性的智能合约中的数据依赖性。”

    在这个四级管道中的 GPU 并行化之间,在任何给定时刻,Solana TPU 都可以同时处理 50,000 个事务。 “这一切都可以通过一台现成的计算机来实现,价格不到 5000 美元,”Fitzgerland 解释道。 “不是超级计算机。”

    通过将 GPU 卸载到 Solana 的事务处理单元上,网络可以影响单个节点的效率。实现这一目标一直是 Solana 自成立以来的目标。

    “下一个挑战是以某种方式将块从领导节点发送到所有验证节点,并且以一种不会拥塞网络并导致吞吐量缓慢的方式进行,”Fitzgerald 继续说道。 “为此,我们提出了一种称为 Turbine 的区块传播策略。

    “通过 Turbine,我们将验证器节点构建为多个级别,其中每个级别的大小至少是其上一级的两倍。通过这种结构,这些不同的级别,确认时间最终与树的高度成正比,而不是与树中的节点数量成正比,后者要大得多。每当网络规模扩大一倍时,您都会看到确认时间略有增加,但仅此而已。”

    - - + + \ No newline at end of file diff --git a/blog/proof-of-history/index.html b/blog/proof-of-history/index.html index c58c76651..ad0aaf750 100644 --- a/blog/proof-of-history/index.html +++ b/blog/proof-of-history/index.html @@ -9,13 +9,13 @@ - - + +

    Proof of History A Clock for Blockchain

    · 9 min read
    Davirain

    分布式系统中最困难的问题之一是时间一致性。事实上,一些人认为比特币的工作量证明算法最重要的功能是充当系统的去中心化时钟。在 Solana,我们相信历史证明提供了这个解决方案,并且我们已经基于它构建了一个区块链。

    去中心化网络通过可信的集中式计时解决方案解决了这个问题。例如,谷歌的 Spanner 在其数据中心之间使用同步原子钟。谷歌的工程师以非常高的精度同步这些时钟并不断维护它们。

    在区块链等对抗性系统中,这个问题更加困难。网络中的节点不能信任外部时间源或消息中出现的任何时间戳。例如,哈希图通过“中值”时间戳解决了这个问题。网络看到的每条消息都由网络的绝大多数人签名和时间戳。消息的时间戳中位数就是 Hashgraph 所说的“公平”排序。每条消息都必须传播到系统中的绝大多数节点,然后在消息收集到足够的签名后,整个集合需要传播到整个网络。正如您可以想象的那样,这确实很慢。

    如果您可以简单地信任编码到消息中的时间戳怎么办?大量的分布式系统优化将突然可供您使用。例如。

    info

    同步时钟很有趣,因为它们可以用来提高分布式算法的性能。它们使得用本地计算取代通信成为可能。

    — Liskov, B. 分布式系统中同步时钟的实际应用

    在我们的例子中,这意味着高吞吐量、高性能的区块链

    历史证明

    如果您可以证明消息是在事件之前和之后的某个时间发生的,而不是信任时间戳,该怎么办?当您拍摄《纽约时报》封面的照片时,您正在创建一个证据,证明您的照片是在该报纸出版后拍摄的,或者您有某种方式影响《纽约时报》的出版内容。通过历史证明,您可以创建历史记录,证明事件在特定时刻发生。

    历史时间戳证明

    历史证明是一种高频可验证延迟函数。可验证延迟函数需要特定数量的连续步骤来进行评估,但会产生可以有效且公开验证的独特输出。

    我们的具体实现使用顺序原像抗散列,该散列连续地运行在自身上,并将先前的输出用作下一个输入。定期记录计数和当前输出。

    对于 SHA256 哈希函数,如果不使用 2^2⁸ 核心进行强力攻击,则该过程不可能并行化。

    然后我们可以确定每个计数器在生成时已经经过了实时时间,并且每个计数器记录的顺序与实时时的顺序相同。

    时间上限

    将消息记录到历史证明序列中

    通过将数据的散列附加到先前生成的状态,可以将数据插入到序列中。状态、输入数据和计数均已发布。附加输入会导致所有未来的输出发生不可预测的变化。并行化仍然是不可能的,并且只要散列函数是原像和抗碰撞的,就不可能创建一个在未来生成所需散列的输入,或者创建具有相同散列的替代历史记录。我们可以证明任意两个追加操作之间经过的时间。我们可以证明数据是在附加之前的某个时间创建的。就像我们知道《纽约时报》上刊登的事件发生在报纸撰写之前。

    时间下限

    历史证明的时间下限

    历史证明的输入可以引用历史证明本身。反向引用可以作为带有用户签名的签名消息的一部分插入,因此如果没有用户私钥就无法对其进行修改。这就像以《纽约时报》为背景拍照一样。因为此消息包含 0xdeadc0de 哈希值,所以我们知道它是在创建计数 510144806912 之后生成的。

    但由于该消息也被插入回历史证明流中,就好像您以《纽约时报》为背景拍了一张照片,第二天《纽约时报》发布了这张照片。我们知道该照片的内容在特定日期之前和之后存在。

    确认

    虽然记录的序列只能在单个 CPU 内核上生成,但可以并行验证输出。

    并行验证

    每个记录的切片都可以在单独的核心上从头到尾进行验证,所需时间仅为生成时间的 1/(核心数)。因此,具有 4000 个核心的现代 GPU 可以在 0.25 毫秒内验证一秒。

    ASICS 亚瑟士

    是不是每个 CPU 都不同,有些 CPU 比其他 CPU 快得多?您如何真正相信我们的 SHA256 循环生成的“时间”是准确的?

    这个主题值得单独写一篇文章,但长话短说,我们不太关心某些 CPU 是否比其他 CPU 更快,以及 ASIC 是否可以比网络可用的 CPU 更快。最重要的是 ASIC 的速度是有限的。

    我们正在使用 SHA256,并且感谢比特币,在使这种加密哈希函数变得更快方面进行了大量研究。该功能不可能通过使用更大的芯片区域(例如查找表)或在不影响时钟速度的情况下展开它来加速。 Intel 和 AMD 都发布了可以在 1.75 个周期内完成一轮 SHA256 的消费类芯片。

    因此,我们非常确定定制 ASIC 的速度不会快 100 倍,更不用说 1000 倍了,而且很可能会在网络可用速度的 30% 以内。我们可以构建利用这个界限的协议,并且只允许攻击者有非常有限的、容易检测到的、短暂的拒绝服务攻击机会。下一篇文章将详细介绍这一点!

    代码

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

    - - + + \ No newline at end of file diff --git a/blog/solana-sealevel-runtime/index.html b/blog/solana-sealevel-runtime/index.html index 82abb48ed..8cf57f25d 100644 --- a/blog/solana-sealevel-runtime/index.html +++ b/blog/solana-sealevel-runtime/index.html @@ -9,13 +9,13 @@ - - + +

    SeaLevel Parallel Processing Thousands of Smart Contracts

    · 7 min read
    Davirain

    在这篇博文中,我们将探讨 Solana 的并行智能合约运行时 Sealevel。在开始之前,需要考虑的一件事是 EVM 和 EOS 基于 WASM 的运行时都是单线程的。这意味着一次一个合约会修改区块链状态。我们在 Solana 中构建的是一个运行时,可以使用验证器可用的尽可能多的内核并行处理数万个合约。

    Solana 之所以能够并行处理事务,是因为 Solana 事务描述了事务在执行时将读取或写入的所有状态。这不仅允许非重叠事务并发执行,还允许仅读取相同状态的事务并发执行。

    程序和帐户

    Cloudbreak,我们的帐户数据库,是公钥到帐户的映射。账户维护余额和数据,其中数据是字节向量。帐户有一个“所有者”字段。所有者是管理帐户状态转换的程序的公钥。程序是代码,没有状态。他们依赖分配给他们的账户中的数据向量来进行状态转换。

    1. 程序只能更改其拥有的帐户的数据。

    2. 程序只能借记其拥有的账户。

    3. 任何程序都可以存入任何帐户。

    4. 任何程序都可以读取任何帐户。

    默认情况下,所有帐户一开始均由系统程序拥有。

    1. 系统程序是唯一可以分配帐户所有权的程序。

    2. 系统程序是唯一可以分配零初始化数据的程序。

    3. 帐户所有权的分配在帐户的生命周期内只能发生一次。

    用户定义的程序由加载程序加载。加载程序能够将帐户中的数据标记为可执行。用户执行以下事务来加载自定义程序:

    1. 创建一个新的公钥。

    2. 将硬币转移到钥匙上。

    3. 告诉系统程序分配内存。

    4. 告诉系统程序将帐户分配给加载程序。

    5. 将字节码分块上传到内存中。

    6. 告诉 Loader 程序将内存标记为可执行文件。

    此时,加载器对字节码进行验证,字节码加载到的账户就可以作为可执行程序了。新帐户可以标记为由用户定义的程序拥有。

    这里的关键见解是程序是代码,并且在我们的键值存储中,存在程序的某些键子集,并且只有该程序具有写访问权限。

    交易

    事务指定一个指令向量。每条指令都包含程序、程序指令以及交易想要读写的账户列表。该接口的灵感来自于设备的低级操作系统接口:

    size_t readv(int d, const struct iovec *iov, int iovcnt);

    struct iovec {
    char *iov_base; /* Base address. */
    size_t iov_len; /* Length. */
    };

    readv 或 writev 等接口提前告诉内核用户想要读取或写入的所有内存。这允许操作系统预取、准备设备,并在设备允许的情况下并发执行操作。

    在 Solana 上,每条指令都会提前告诉虚拟机要读取和写入哪些帐户。这就是我们对VM进行优化的根源。

    1. 对数以百万计的待处理交易进行排序。

    2. 并行安排所有非重叠事务。

    更重要的是,我们可以利用 CPU 和 GPU 硬件的设计方式。

    SIMD 指令允许在多个数据流上执行一段代码。这意味着 Sealevel 可以执行额外的优化,这是 Solana 设计所独有的:

    1. 按程序 ID 对所有指令进行排序。

    2. 同时在所有帐户上运行相同的程序。

    要了解为什么这是一个如此强大的优化,请查看 CUDA 开发人员指南

    info

    CUDA 架构是围绕可扩展的多线程流多处理器 (SM) 阵列构建的。当主机 CPU 上的 CUDA 程序调用内核网格时,网格的块将被枚举并分配给具有可用执行能力的多处理器。

    现代 Nvidia GPU 拥有 4000 个 CUDA 核心,但大约有 50 个多处理器。虽然多处理器一次只能执行一条程序指令,但它可以并行执行超过 80 个不同输入的指令。因此,如果 Sealvel 加载的传入事务都调用相同的程序指令(例如 CryptoKitties::BreedCats),Solana 可以在所有可用的 CUDA 核心上同时执行所有事务。

    性能方面没有免费的午餐,因此为了使 SIMD 优化可行,执行的指令应该包含少量分支,并且都应该采用相同的分支。多处理器受到批处理中执行速度最慢的路径的限制。即使考虑到这一点,与单线程运行时相比,通过 Sealevel 进行的并行处理在区块链网络的运行方式方面呈现出基础性的发展,从而实现了极高的吞吐量和可用性。

    - - + + \ No newline at end of file diff --git a/blog/solana-state-compression/index.html b/blog/solana-state-compression/index.html index 1bf1a28fc..62227ec8e 100644 --- a/blog/solana-state-compression/index.html +++ b/blog/solana-state-compression/index.html @@ -9,13 +9,13 @@ - - + +

    翻译 Solana 的状态压缩

    · 17 min read
    Davirain

    Solana上,状态压缩是一种创建离链数据的“指纹”(或哈希)并将该指纹存储在链上以进行安全验证的方法。有效地利用Solana账本的安全性来安全验证离链数据,以确保其未被篡改。

    这种“压缩”方法使得Solana的程序和dApps能够使用廉价的区块链账本空间来安全存储数据,而不是更昂贵的账户空间。

    这是通过使用一种特殊的二叉树结构,称为并发默克尔树,对每个数据片段(称为 leaf )创建哈希,将它们哈希在一起,并仅将最终哈希存储在链上来实现的。

    什么是状态压缩?

    简单来说,状态压缩使用“树”结构将链外数据以确定性的方式进行加密哈希,计算出一个最终的哈希值,并将其存储在链上。

    这些树是通过这个“确定性”过程创建的:

    • 获取任何数据
    • 创建这些数据的哈希值
    • 将此哈希值存储为树底部的 leaf
    • 每个 leaf 对都会被一起哈希,创建一个 branch
    • 每个 branch 然后一起哈希
    • 不断攀爬树木并将相邻的树枝连接在一起
    • 树顶上一旦到达,就会产生最后的 root hash

    这个 root hash 然后存储在链上,作为每个叶子节点中所有数据的可验证证据。这样任何人都可以通过加密验证树中所有离链数据,而实际上只需在链上存储少量数据。因此,由于这种"状态压缩",大大降低了存储/证明大量数据的成本。

    默克尔树和并发默克尔树

    Solana的状态压缩使用了一种特殊类型的默克尔树,允许对任何给定的树进行多次更改,同时仍然保持树的完整性和有效性。

    这棵特殊的树被称为“并发默克尔树”,有效地在链上保留了树的“更改日志”。允许在一个证明失效之前对同一棵树进行多次快速更改(即在同一个区块中)。

    默克尔树是什么?

    默克尔树,有时也被称为“哈希树”,是一种基于哈希的二叉树结构,其中每个leaf节点都被表示为其内部数据的加密哈希。而每个非叶节点,也被称为“branch节点”,则被表示为其子叶节点哈希的哈希值。

    每个分支也被哈希在一起,沿着树向上爬,直到最后只剩下一个哈希。这个最终的哈希,称为 root hash 或者"根",可以与一个"证明路径"结合使用,来验证存储在叶节点中的任何数据。

    一旦计算出最终的根哈希值(root hash),可以通过重新计算特定叶子(leaf)节点的数据和每个相邻分支的哈希标签(称为“证明路径”)来验证存储在节点中的任何数据。将这个“重新哈希”与根哈希值进行比较,可以验证底层叶子数据的准确性。如果它们匹配,数据就被验证为准确的。如果它们不匹配,叶子数据已被更改。

    只要需要,原始叶子数据可以通过对新的叶子数据进行哈希运算并重新计算根哈希值来进行更改,方法与原始根哈希值的计算方式相同。然后,这个新的根哈希值用于验证任何数据,并且有效地使之前的根哈希值和证明无效。因此,对这些传统的默克尔树的每一次更改都需要按顺序执行。

    info

    当使用默克尔树时,更改叶子数据并计算新的根哈希的过程可能是非常常见的事情!虽然这是树的设计要点之一,但它可能导致最显著的缺点之一:快速变化。

    什么是并发默克尔树?

    在高吞吐量的应用中,比如在Solana运行时中,对于链上传统Merkle树的更改请求可能会相对快速地连续接收到验证者(例如在同一个槽中)。每个叶子数据的更改仍然需要按顺序执行。这导致每个后续的更改请求都会失败,因为根哈希和证明已经被同一槽中之前的更改请求无效化了。

    进入,并发默克尔树。

    并发默克尔树存储了最近更改的安全日志、它们的根哈希以及用于推导根哈希的证明。这个日志缓冲区存储在链上的每个树对应的特定账户中,最大记录数为(也称为 maxBufferSize )。

    当同一时隙内的验证者收到多个叶子数据变更请求时,链上并发 Merkle 树可以将这个“变更日志缓冲区”作为更可接受的证明的真实来源。有效地允许在同一时隙内对同一棵树进行多达 maxBufferSize 次变更。大幅提升吞吐量。

    并发默克尔树的大小调整

    创建这种链上树时,有三个值将决定您的树的大小、创建树的成本以及对树的并发更改数量:

    1. max depth 最大深度
    2. max buffer size 最大缓冲区大小
    3. canopy depth

    max depth

    树的“最大深度”是从任何数据 leaf 到树的 root 所需的最大跳数。

    由于默克尔树是二叉树,每个叶子节点只与另一个叶子节点相连;存在于一个 leaf pair 中。

    因此,树的 maxDepth 被用来确定可以通过简单的计算存储在树中的最大节点数(也称为数据或 leafs

    nodes_count = 2 ^ maxDepth

    由于树的深度必须在创建树时设置,您必须决定您希望树存储多少个数据。然后使用上述简单的计算,您可以确定存储数据的最低 maxDepth

    示例1:铸造100个NFTs

    如果你想创建一个用于存储100个压缩NFT的树,我们至少需要"100个叶子"或"100个节点"。

    // maxDepth=6 -> 64 nodes
    2^6 = 64

    // maxDepth=7 -> 128 nodes
    2^7 = 128

    因此,我们需要一个最大深度为 7 的树,以存储 100 个数据。

    例子2:铸造15000个NFTs

    如果你想创建一个用于存储15000个压缩NFT的树,我们将需要至少"15000个叶子"或"15000个节点"。

    // maxDepth=13 -> 8192 nodes
    2^13 = 8192

    // maxDepth=14 -> 16384 nodes
    2^14 = 16384

    因此,我们需要一个最大深度为 14 的树,以存储 15000 个数据。

    最大深度越高,成本越高

    创建树时, maxDepth 值将是成本的主要驱动因素之一,因为您将在创建树时支付这笔成本。最大树深度越高,您可以存储的数据指纹(也称为哈希)越多,成本就越高。

    max buffer size

    max buffer size” 实际上是树上可以发生的最大变化数量,同时仍然有效的 root hash

    由于根哈希有效地是所有叶子数据的单一哈希,改变任何一个叶子将使得所有后续尝试改变常规树的叶子所需的证明无效。

    但是使用并发树,对于这些证明来说,实际上有一个更新的日志。这个日志缓冲区的大小和设置是通过这个 maxBufferSize 值在树创建时完成的。

    Canopy depth

    Canopy depth”,有时也称为Canopy大小,是指在任何给定的证明路径上缓存/存储在链上的证明节点数量。

    在对 leaf 执行更新操作时,例如转让所有权(例如出售压缩的NFT),必须使用完整的证明路径来验证叶子节点的原始所有权,从而允许进行更新操作。此验证是使用完整的证明路径来正确计算当前的 root hash (或通过链上的“并发缓冲区”缓存的任何 root hash )来执行的。

    树的最大深度越大,执行此验证所需的证明节点就越多。例如,如果您的最大深度是 14 ,则需要使用 14 个总的证明节点进行验证。随着树的增大,完整的证明路径也会变得更长。

    通常情况下,每个这些证明节点都需要在每个树更新事务中包含。由于每个证明节点的值在事务中占用 32 bytes (类似于提供公钥),较大的树很快就会超过最大事务大小限制。

    进入CanopyCanopy可以在链上存储一定数量的验证节点(对于任何给定的验证路径)。这样可以在每个更新交易中包含较少的验证节点,从而保持整体交易大小低于限制。

    例如,深度为 14 的树需要 14 个总的验证节点。而有 10Canopy的情况下,每个更新事务只需要提交 4 个验证节点。

    Canopy深度值越大,成本越高

    canopyDepth 值也是创建树时成本的主要因素,因为您将在树的创建时支付这个成本。canopyDepth越高,链上存储的数据证明节点越多,成本也越高。

    较小的Canopy限制了可组合性

    虽然树的创建成本随着Canopy的高度而增加,但较低的Canopy将需要在每个更新事务中包含更多的证明节点。所需提交的节点越多,事务的大小就越大,因此超过事务大小限制就越容易。

    这也适用于任何其他试图与您的树/叶子进行交互的Solana程序或dApp。如果您的树需要太多的证明节点(因为Canopy深度较低),那么任何其他链上程序可能提供的额外操作都将受到其特定指令大小加上您的证明节点列表大小的限制。这限制了可组合性,并限制了您的特定树的潜在附加效用。

    例如,如果您的树被用于压缩的非同质化代币(NFTs),并且Canopy深度非常低,一个NFT市场可能只能支持简单的NFT转移,而无法支持链上竞标系统。

    创建一棵树的成本

    创建并发 Merkle 树的成本基于树的大小参数: maxDepthmaxBufferSizecanopyDepth 。这些值都用于计算在链上存在树所需的链上存储空间(以字节为单位)。

    一旦计算出所需的空间(以字节为单位),并使用 getMinimumBalanceForRentExemption RPC方法,请求在链上分配这些字节所需的费用(以lamports为单位)。

    在JavaScript中计算树木成本

    @solana/spl-account-compression 包中,开发人员可以使用 getConcurrentMerkleTreeAccountSize 函数来计算给定树大小参数所需的空间。

    然后使用 getMinimumBalanceForRentExemption 函数来获取在链上分配所需空间的最终成本(以lamports计算)。

    然后确定以lamports计算的成本,使得这个大小的账户免除租金,与其他账户创建类似。

    // calculate the space required for the tree
    const requiredSpace = getConcurrentMerkleTreeAccountSize(
    maxDepth,
    maxBufferSize,
    canopyDepth,
    );

    // get the cost (in lamports) to store the tree on-chain
    const storageCost = await connection.getMinimumBalanceForRentExemption(
    requiredSpace,
    );

    示例费用

    以下是几个不同树大小的示例成本,包括每个树可能的叶节点数量:

    例子 #1:16,384个节点,成本为0.222 SOL

    • 最大深度为 14 ,最大缓冲区大小为 64
    • 叶节点的最大数量: 16,384
    • 创建 0.222 SOLCanopy深度大约需要 0 的成本

    例子 #2:16,384个节点,成本为1.134 SOL

    • 最大深度为 14 ,最大缓冲区大小为 64
    • 叶节点的最大数量: 16,384
    • 创建 1.134 SOLCanopy深度大约需要 11 的成本

    示例 #3:1,048,576个节点,成本为1.673 SOL

    • 最大深度为 20 ,最大缓冲区大小为 256
    • 叶节点的最大数量: 1,048,576
    • 创建 1.673 SOLCanopy深度大约需要 10 的成本

    示例#4:1,048,576个节点,成本为15.814 SOL

    • 最大深度为 20 ,最大缓冲区大小为 256
    • 叶节点的最大数量: 1,048,576
    • 创建 15.814 SOLCanopy深度大约需要 15 的成本

    压缩的NFTs

    压缩的NFT是Solana上状态压缩的最受欢迎的应用之一。通过压缩,一个拥有一百万个NFT的收藏品可以以 ~50 SOL 的价格铸造,而不是其未压缩的等价收藏品。

    开发者指南:

    阅读我们的开发者指南,了解如何铸造和转移压缩的NFT

    - - + + \ No newline at end of file diff --git a/blog/tags/anchor/index.html b/blog/tags/anchor/index.html index b44338ce0..81fc92d4f 100644 --- a/blog/tags/anchor/index.html +++ b/blog/tags/anchor/index.html @@ -9,14 +9,14 @@ - - + +

    2 posts tagged with "anchor"

    View All Tags

    · 11 min read
    Davirain

    今天,开始我们学习从Solana上开发智能合约,这里我打算先从Anchor开始。因为Anchor也是Solana上现在如今用的最多的开发框架,哦,这里主要用的也是Rust语言,对于Anchor还支持的Solidity语法来写合约,暂时我先不考虑。也希望有🧍‍♂️能一起完善。

    那今天就简单的介绍下Anchor上如何从项目的初始化,到后面如何部署合约以及前端如何来调用这个简单的Example合约。

    先来介绍下什么是Anchor吧

    这里我先引用下官方的介绍

    Anchor是一个快速构建安全Solana程序的框架。

    使用Anchor,您可以快速构建程序,因为它会为您编写各种样板代码,例如账户和指令数据的(反)序列化。

    由于Anchor为您处理了某些安全检查,因此您可以更轻松地构建安全的程序。除此之外,它还允许您简洁地定义额外的检查,并将其与业务逻辑分开。

    这两个方面意味着,你不必再花费时间在繁琐的Solana原始程序上,而是可以更多地投入到最重要的事情上,即你的产品。

    简单的点来说,就是Anchor做为一个Solana上的合约开发框架,对于原生使用Rust开发来说的话,anchor 提供了对于一些模版代码,或者说公共代码操作的抽象,使得开发者更加具体的专注与自己的业务逻辑。

    简单的Anchor介绍完了,我们看看如何来初始化一个合约以及部署合约到本地测试网。本地测试网的部署查看这个教程完成✅。

    一个简单的Anchor合约的部署测试

    对于要使用Anchor来开发他需要一些前置的环境配置,例如你需要先安装Rust环境,第二个是安装Solana-cli工具。因为这里Anchor要使用solana cli的 solana-keygen new 命令来生成一个本地册测试账户。最后一个是Yarn。这里是Anchor官方给出的安装教程,按照这个安装即可。

    下面是具体的anchor如何安装

    官方推荐的是avm,一个Anchor的多版本管理器。前面我们已经安装了Rust语言,我们就可以使用cargo来安装这个工具。

    通过执行这个命令,我们就可以安装avm了。

    cargo install --git https://github.com/coral-xyz/anchor avm --locked --force

    按照完之后我们就可以使用avm选择一个具体的版本安装,下面者一个命令我们安装的Anchor版本是最新的Anchor。

    avm install latest
    avm use latest

    验证安装成功的我们可以执行anchor --version命令,我们可以看到有版本号输出,说明我们安装成功了。

    一个anchor项目的结构

    通过执行anchor init new-workspace-name 我们就可以初始化一个solana program。

    下面是通过执行anchor init hello-world的输出。

    ls --tree . --depth 1
    .
    ├──  .git
    ├──  .gitignore
    ├──  .prettierignore
    ├──  Anchor.toml
    ├──  app
    ├──  Cargo.toml
    ├──  migrations
    ├──  node_modules
    ├──  package-lock.json
    ├──  package.json
    ├──  programs
    ├──  target
    ├──  tests
    ├──  tsconfig.json
    └──  yarn-error.log
    • app 文件夹:初始化之后是一个空文件夹,这里可以用来存放自己的前端代码。
    • programs 文件夹:此文件夹包含程序代码。它可以包含多个文件,但最初只包含与 <new-workspace-name> 相同名称的程序。并且这个program中已经包含了一些示例代码,在lib.rs中可以看到。
    • tests 文件夹:包含您的端到端测试的文件夹。它已经包含一个测试 programs/<new-workspace-name> 中示例代码的文件,这里面的测试都是使用typescript写✍️的代码。当执行anchor test的时候会在本地启动一个solana的测试节点,执行里面的测试代码。
    • migrations 文件夹:在这个文件夹中,保存程序的部署和迁移脚本。
    • Anchor.toml 文件:此文件配置了程序的工作区范围设置。
      • 程序在本地网络上的地址( [programs.localnet]
      • 程序可以推送到的注册表 ( [registry] )
      • 一个可以在你的测试中使用的也就是通过solana-keygen new 生成的私钥文件路径 ( [provider] )
    • .anchor 这个文件是只有在执行anchor test之后才生成的文件夹,里面包含了最新的程序日志和用于测试的本地账本。

    这个是在执行anchor test之后的文件内容。

    ls --tree . --depth 1
    .
    ├──  .anchor
    ├──  .git
    ├──  .gitignore
    ├──  .prettierignore
    ├──  Anchor.toml
    ├──  app
    ├──  Cargo.lock
    ├──  Cargo.toml
    ├──  migrations
    ├──  node_modules
    ├──  package-lock.json
    ├──  package.json
    ├──  programs
    ├──  target
    ├──  tests
    ├──  tsconfig.json
    └──  yarn-error.log

    下面这个是执行anchor test之后.anchor里面生成的日志内容。

    好说了这么多,我们看下如何使用anchor打印一个hello world, 目前先只通过anchor test 来观察打印,后面在做介绍如何通过前端调用打印。

    初始化一个 hello world program

    通过执行anchor init hello-world, 会为我们创建一个solana program的样板代码。

    use anchor_lang::prelude::*;

    declare_id!("2HvxNpAdkkWitSQyDy9vMvJDpRsvtrhZ6JNqsXzGRi3i");

    #[program]
    pub mod counter {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    Ok(())
    }
    }

    #[derive(Accounts)]
    pub struct Initialize {}

    上面这段代码就是通过anchor init hello-world 创建出来的代码,文件存放在hello-world/programs/hello-world/src/lib.rs中。

    下面我们就通过简单的修改下这个简单的代码,在里面添加一个打印hello, world!的消息。

    use anchor_lang::prelude::*;

    declare_id!("2HvxNpAdkkWitSQyDy9vMvJDpRsvtrhZ6JNqsXzGRi3i");

    #[program]
    pub mod counter {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    msg!("hello, world!");
    Ok(())
    }
    }

    #[derive(Accounts)]
    pub struct Initialize {}

    这个是添加了msg!这段代码,msg!主要做的事情,类似于在rust中打印内容到标准输出的println!, 因为是solana program,他是链上代码,我们不可能打印到标准输出的,所以这我们就通过使用msg!这个宏记录自己需要打印的东西。

    在Solana中,由于智能合约在执行时是在分布式网络中运行的,无法直接使用传统的标准输出来打印消息。为了在智能合约中输出调试信息或日志,Solana提供了msg!宏。

    msg!宏的使用方式与println!宏类似,你可以在智能合约中使用它来打印消息。这些消息将被记录并作为日志输出到Solana节点的日志文件中。

    需要注意的是,msg!宏只在Solana智能合约中可用,用于在智能合约执行过程中输出消息。它与Rust中的println!宏略有不同,因为它将消息记录到Solana节点的日志文件中,而不是直接输出到控制台。

    观察👀 hello,world!消息

    想要观察是否打印了hello, world!这个消息,我们可以通过运行anchor test。这个会记录📝program在测试执行的内容。

    我们可以看到通过执行anchor test已经将我们打印的hello,world! 记录下来了。

    来看下执行的这个测试脚本吧。这里是执行的程序的initialize执行的调用。我们在这个指令中添加了打印hello, world的代码。

    import * as anchor from "@coral-xyz/anchor";
    import { Program } from "@coral-xyz/anchor";
    import { Hello } from "../target/types/hello";

    describe("hello", () => {
    // Configure the client to use the local cluster.
    anchor.setProvider(anchor.AnchorProvider.env());

    const program = anchor.workspace.Hello as Program<Hello>;

    it("Is initialized!", async () => {
    // Add your test here.
    const tx = await program.methods.initialize().rpc();
    console.log("Your transaction signature", tx);
    });
    });

    anchor.setProvider(anchor.AnchorProvider.env()); 这段代码是通过读取的Anchor.toml中的配置初始化了Anchor的provider。

    [features]
    seeds = false
    skip-lint = false
    [programs.localnet]
    hello = "2HvxNpAdkkWitSQyDy9vMvJDpRsvtrhZ6JNqsXzGRi3i"

    [registry]
    url = "https://api.apr.dev"

    [provider]
    cluster = "Localnet"
    wallet = "/Users/davirain/.config/solana/id.json"

    [scripts]
    test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

    我们可以看到这里的provider是localnet,wallet是自己本地的私钥路径。

    const program = anchor.workspace.Hello as Program<Hello>;

    这一步是我们初始化了一个solana 的program 实例,通过Hello这个IDL文件。

    在测试中,使用it函数定义了一个测试用例,名称为"Is initialized!"。在这个测试用例中,调用了program.methods.initialize().rpc()方法,该方法是调用合约中的initialize方法,并通过RPC方式发送交易。然后,使用console.log打印出交易的签名。

    这段代码的目的是测试hello程序是否能够成功初始化。通过调用initialize方法并打印交易签名,可以验证初始化过程是否成功。

    这就是一个简单的Anchor合约的入门。

    · 3 min read
    YanAemons

    報錯日志

    在使用solana-cli時候,鑑於一些依賴版本限制,會用到cli14.xx(主網版本),而不是16.xx(測試網版本)

    例如,在使用solana-cli版本爲1.14.17, anchor版本爲0.26.0的環境中, anchor init創建一個新項目後運行 anchor build會發生以下錯誤:

    error: package constant_time_eq v0.3.0 cannot be built because it requires rustc 1.66.0 or newer, while the currently active rustc version is 1.62.0-dev

    報錯原因

    使用的solana-cli版本在14.xxx, cli內自帶的rustc版本過老,無法編譯較新的依賴

    解決方案

    1. 升級solana-cli至最新版本

    solana-install update

    2.指定依賴包版本

    需要在Cargo.toml文件下指定以下依賴版本

    getrandom = { version = "0.2.9", features = ["custom"] }  
    solana-program = "=1.14.17"
    winnow="=0.4.1"
    toml_datetime="=0.6.1"
    blake3 = "=1.3.1"

    運行cargo clean後重新運行anchor build即可解決

    監聽程序log監聽到兩次

    在使用program.addEventListener()有可能聽到兩次相同的事件,其中一次的txSign會是“1111111111111111111111111111111111111111111111111111111111111111”, 這是因爲監聽到了模擬時的交易哈系,我們只需要在監聽到該交易哈系時拋棄即可

    program.addEventListener("eventName", (event, slot, signature) => {
    if (signature === '1111111111111111111111111111111111111111111111111111111111111111') return

    // do ur stuff
    })

    然而,有時websocket訂閱也會多次返回實際簽名。如果是這種情況,您可以使用一些緩存解決方案。例如,創建一個具有一定長度限制的集合,在此處添加簽名並檢查該集合中是否存在新簽名:

    const handledSignatures = new Set<string>()
    const maxHandledSignaturesLen = 100

    program.addEventListener("eventName", (event, slot, signature) => {
    if (signature === '1111111111111111111111111111111111111111111111111111111111111111') return
    if (handledSignatures.has(signature)) return

    // do ur stuff

    handledSignatures.add(signature)
    if (handledSignatures.size > maxHandledSignaturesLen) {
    handledSignatures.delete(handledSignatures.values().next().value)
    }
    })
    - - + + \ No newline at end of file diff --git a/blog/tags/blockchain/index.html b/blog/tags/blockchain/index.html index 503cb04ed..81bc8cf5c 100644 --- a/blog/tags/blockchain/index.html +++ b/blog/tags/blockchain/index.html @@ -9,8 +9,8 @@ - - + +
    @@ -19,7 +19,7 @@ 通常由特定程序(通常是一个智能合约)关联额外的账户。该账号没有私钥,故除程序本身外,无法完成数据签名,无法完成完整的数据交易。

    • ADA (Account Derived Account)

    由 createWithSeed 方法产生。 有一个账号公钥派生出来的关联账户,数据签名权限属于主账号。也即,需要主账号的签名才能完成完整的数据交易。

    solana中,根据数据签名,决定了数据的真实所有权。即 我的数据我做主

    本文主要分析这两种账号的异同。

    地址生成逻辑介绍如下

    • PDA 地址生成规则
    1. buffer = [seed,programId,"ProgramDerivedAddress"]
    2. 对buffer 取 sha256
    3. 如果在曲线上,那么抛出error, 如果不在,那么直接返回作为 使用地址

    createProgramAddressSync

    • ADA 生成
    1. buffer=[fromPublicKey,seed,programId]
    2. buffer 取 sha256, 直接返回

    createWithSeed

    区别在于,数据的托管使用逻辑.

    • ADA 数据签名权限,在于账户本身。即 我的数据我做主,未经允许(我未签名)不能修改。
    • PDA 数据签名权限在于合约。经过程序签名,可以修改 account 的数据和提取其中的sol。

    ADA 账号使用

    数据操作,有配套的函数对应,内部包含 xxxxWithSeedParams 类型的参数,完成对应的操作。 操作数据,需要 主账户的签名,这一点决定了,账号的真实所有权。

    • SystemProgram.createAccountWithSeed 初始化账号
    • SystemProgram.assign 重新分配owner
    • SystemProgram.allocate 分配空间
    • SystemProgram.transfer 转移SOL

    PDA 账号使用

    • 客户端只用于账户地址推导,不能初始化。初始化过程在合约内部完成。
    • 因其签名权限,必须在合约内部完成。他的操作权限完全属于智能合约。

    ADA 账号使用 example

      const seed = "ada.creator";

    // 初始化ada 账户
    let ada_account = await web3.PublicKey.createWithSeed(
    signer.publicKey,
    seed,
    program
    );
    console.log("ada_account address: ", ada_account.toBase58());

    let ada_info = await connection.getAccountInfo(ada_account);

    // 根据是否存在账号,决定是否初始化
    if (ada_info) {
    console.log(ada_info);
    } else {
    console.log("ada account not found");
    const transaction = new web3.Transaction().add(
    web3.SystemProgram.createAccountWithSeed({
    newAccountPubkey: ada_account,
    fromPubkey: signer.publicKey,
    basePubkey: signer.publicKey,
    programId: program,
    seed,
    lamports: web3.LAMPORTS_PER_SOL,
    space: 20,
    })
    );

    PDA 使用 example

    客户端部分代码逻辑

    const pda_seed = "pda.creator";

    const obj = new Model();

    const [pda, bump_seed] = web3.PublicKey.findProgramAddressSync(
    [signer.publicKey.toBuffer(), new TextEncoder().encode(movie.title)],
    program
    );

    console.log("pda address : ", pda.toBase58());

    const instruction = new web3.TransactionInstruction({
    keys: [
    {
    // 付钱的账户
    pubkey: signer.publicKey,
    isSigner: true,
    isWritable: false,
    },
    {
    // PDA将存储数据
    pubkey: pda,
    isSigner: false,
    isWritable: true,
    },
    {
    // 系统程序将用于创建PDA
    pubkey: web3.SystemProgram.programId,
    isSigner: false,
    isWritable: false,
    },
    ],
    // 传输数据
    data: obj.serialize(),
    programId: program,
    });

    const transaction = new web3.Transaction().add(instruction);

    const signature = await web3.sendAndConfirmTransaction(
    connection,
    transaction,
    [signer]
    );

    console.log(signature);

    合约部分代码逻辑

    // 获取账户迭代器
    let account_info_iter = &mut accounts.iter();

    // 获取账户
    let initializer = next_account_info(account_info_iter)?;
    let pda_account = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    // 构造PDA账户
    let (pda, bump_seed) =
    Pubkey::find_program_address(&[initializer.key.as_ref(), title.as_bytes()], program_id);

    // 和客户端比对
    if pda != *pda_account.key {
    msg!("Invalid seeds for PDA");
    return Err(ProgramError::InvalidArgument);
    }

    // 计算所需的租金
    let rent = Rent::get()?;
    let rent_lamports = rent.minimum_balance(total_len);

    // 创建账户
    invoke_signed(
    &system_instruction::create_account(
    initializer.key,
    pda_account.key,
    rent_lamports,
    total_len
    .try_into()
    .map_err(|_| Error::ConvertUsizeToU64Failed)?,
    program_id,
    ),
    &[
    initializer.clone(),
    pda_account.clone(),
    system_program.clone(),
    ],
    &[&[initializer.key.as_ref(), title.as_bytes(), &[bump_seed]]],
    )?;

    // MovieAccountState 定义的state类型
    let mut account_data =
    try_from_slice_unchecked::<MovieAccountState>(&pda_account.data.borrow()).unwrap();

    account_data.title = title;
    account_data.rating = rating;
    account_data.description = description;
    account_data.is_initialized = true;

    // 写入pda 数据本身
    account_data.serialize(&mut &mut pda_account.data.borrow_mut()[..])?;

    参考资料

    - - + + \ No newline at end of file diff --git a/blog/tags/blockchain/page/2/index.html b/blog/tags/blockchain/page/2/index.html index cce6f100b..a324a5031 100644 --- a/blog/tags/blockchain/page/2/index.html +++ b/blog/tags/blockchain/page/2/index.html @@ -9,13 +9,13 @@ - - + +

    13 posts tagged with "blockchain"

    View All Tags

    · 3 min read
    YanAemons

    報錯日志

    在使用solana-cli時候,鑑於一些依賴版本限制,會用到cli14.xx(主網版本),而不是16.xx(測試網版本)

    例如,在使用solana-cli版本爲1.14.17, anchor版本爲0.26.0的環境中, anchor init創建一個新項目後運行 anchor build會發生以下錯誤:

    error: package constant_time_eq v0.3.0 cannot be built because it requires rustc 1.66.0 or newer, while the currently active rustc version is 1.62.0-dev

    報錯原因

    使用的solana-cli版本在14.xxx, cli內自帶的rustc版本過老,無法編譯較新的依賴

    解決方案

    1. 升級solana-cli至最新版本

    solana-install update

    2.指定依賴包版本

    需要在Cargo.toml文件下指定以下依賴版本

    getrandom = { version = "0.2.9", features = ["custom"] }  
    solana-program = "=1.14.17"
    winnow="=0.4.1"
    toml_datetime="=0.6.1"
    blake3 = "=1.3.1"

    運行cargo clean後重新運行anchor build即可解決

    監聽程序log監聽到兩次

    在使用program.addEventListener()有可能聽到兩次相同的事件,其中一次的txSign會是“1111111111111111111111111111111111111111111111111111111111111111”, 這是因爲監聽到了模擬時的交易哈系,我們只需要在監聽到該交易哈系時拋棄即可

    program.addEventListener("eventName", (event, slot, signature) => {
    if (signature === '1111111111111111111111111111111111111111111111111111111111111111') return

    // do ur stuff
    })

    然而,有時websocket訂閱也會多次返回實際簽名。如果是這種情況,您可以使用一些緩存解決方案。例如,創建一個具有一定長度限制的集合,在此處添加簽名並檢查該集合中是否存在新簽名:

    const handledSignatures = new Set<string>()
    const maxHandledSignaturesLen = 100

    program.addEventListener("eventName", (event, slot, signature) => {
    if (signature === '1111111111111111111111111111111111111111111111111111111111111111') return
    if (handledSignatures.has(signature)) return

    // do ur stuff

    handledSignatures.add(signature)
    if (handledSignatures.size > maxHandledSignaturesLen) {
    handledSignatures.delete(handledSignatures.values().next().value)
    }
    })

    · 17 min read
    Davirain

    Solana上,状态压缩是一种创建离链数据的“指纹”(或哈希)并将该指纹存储在链上以进行安全验证的方法。有效地利用Solana账本的安全性来安全验证离链数据,以确保其未被篡改。

    这种“压缩”方法使得Solana的程序和dApps能够使用廉价的区块链账本空间来安全存储数据,而不是更昂贵的账户空间。

    这是通过使用一种特殊的二叉树结构,称为并发默克尔树,对每个数据片段(称为 leaf )创建哈希,将它们哈希在一起,并仅将最终哈希存储在链上来实现的。

    什么是状态压缩?

    简单来说,状态压缩使用“树”结构将链外数据以确定性的方式进行加密哈希,计算出一个最终的哈希值,并将其存储在链上。

    这些树是通过这个“确定性”过程创建的:

    • 获取任何数据
    • 创建这些数据的哈希值
    • 将此哈希值存储为树底部的 leaf
    • 每个 leaf 对都会被一起哈希,创建一个 branch
    • 每个 branch 然后一起哈希
    • 不断攀爬树木并将相邻的树枝连接在一起
    • 树顶上一旦到达,就会产生最后的 root hash

    这个 root hash 然后存储在链上,作为每个叶子节点中所有数据的可验证证据。这样任何人都可以通过加密验证树中所有离链数据,而实际上只需在链上存储少量数据。因此,由于这种"状态压缩",大大降低了存储/证明大量数据的成本。

    默克尔树和并发默克尔树

    Solana的状态压缩使用了一种特殊类型的默克尔树,允许对任何给定的树进行多次更改,同时仍然保持树的完整性和有效性。

    这棵特殊的树被称为“并发默克尔树”,有效地在链上保留了树的“更改日志”。允许在一个证明失效之前对同一棵树进行多次快速更改(即在同一个区块中)。

    默克尔树是什么?

    默克尔树,有时也被称为“哈希树”,是一种基于哈希的二叉树结构,其中每个leaf节点都被表示为其内部数据的加密哈希。而每个非叶节点,也被称为“branch节点”,则被表示为其子叶节点哈希的哈希值。

    每个分支也被哈希在一起,沿着树向上爬,直到最后只剩下一个哈希。这个最终的哈希,称为 root hash 或者"根",可以与一个"证明路径"结合使用,来验证存储在叶节点中的任何数据。

    一旦计算出最终的根哈希值(root hash),可以通过重新计算特定叶子(leaf)节点的数据和每个相邻分支的哈希标签(称为“证明路径”)来验证存储在节点中的任何数据。将这个“重新哈希”与根哈希值进行比较,可以验证底层叶子数据的准确性。如果它们匹配,数据就被验证为准确的。如果它们不匹配,叶子数据已被更改。

    只要需要,原始叶子数据可以通过对新的叶子数据进行哈希运算并重新计算根哈希值来进行更改,方法与原始根哈希值的计算方式相同。然后,这个新的根哈希值用于验证任何数据,并且有效地使之前的根哈希值和证明无效。因此,对这些传统的默克尔树的每一次更改都需要按顺序执行。

    info

    当使用默克尔树时,更改叶子数据并计算新的根哈希的过程可能是非常常见的事情!虽然这是树的设计要点之一,但它可能导致最显著的缺点之一:快速变化。

    什么是并发默克尔树?

    在高吞吐量的应用中,比如在Solana运行时中,对于链上传统Merkle树的更改请求可能会相对快速地连续接收到验证者(例如在同一个槽中)。每个叶子数据的更改仍然需要按顺序执行。这导致每个后续的更改请求都会失败,因为根哈希和证明已经被同一槽中之前的更改请求无效化了。

    进入,并发默克尔树。

    并发默克尔树存储了最近更改的安全日志、它们的根哈希以及用于推导根哈希的证明。这个日志缓冲区存储在链上的每个树对应的特定账户中,最大记录数为(也称为 maxBufferSize )。

    当同一时隙内的验证者收到多个叶子数据变更请求时,链上并发 Merkle 树可以将这个“变更日志缓冲区”作为更可接受的证明的真实来源。有效地允许在同一时隙内对同一棵树进行多达 maxBufferSize 次变更。大幅提升吞吐量。

    并发默克尔树的大小调整

    创建这种链上树时,有三个值将决定您的树的大小、创建树的成本以及对树的并发更改数量:

    1. max depth 最大深度
    2. max buffer size 最大缓冲区大小
    3. canopy depth

    max depth

    树的“最大深度”是从任何数据 leaf 到树的 root 所需的最大跳数。

    由于默克尔树是二叉树,每个叶子节点只与另一个叶子节点相连;存在于一个 leaf pair 中。

    因此,树的 maxDepth 被用来确定可以通过简单的计算存储在树中的最大节点数(也称为数据或 leafs

    nodes_count = 2 ^ maxDepth

    由于树的深度必须在创建树时设置,您必须决定您希望树存储多少个数据。然后使用上述简单的计算,您可以确定存储数据的最低 maxDepth

    示例1:铸造100个NFTs

    如果你想创建一个用于存储100个压缩NFT的树,我们至少需要"100个叶子"或"100个节点"。

    // maxDepth=6 -> 64 nodes
    2^6 = 64

    // maxDepth=7 -> 128 nodes
    2^7 = 128

    因此,我们需要一个最大深度为 7 的树,以存储 100 个数据。

    例子2:铸造15000个NFTs

    如果你想创建一个用于存储15000个压缩NFT的树,我们将需要至少"15000个叶子"或"15000个节点"。

    // maxDepth=13 -> 8192 nodes
    2^13 = 8192

    // maxDepth=14 -> 16384 nodes
    2^14 = 16384

    因此,我们需要一个最大深度为 14 的树,以存储 15000 个数据。

    最大深度越高,成本越高

    创建树时, maxDepth 值将是成本的主要驱动因素之一,因为您将在创建树时支付这笔成本。最大树深度越高,您可以存储的数据指纹(也称为哈希)越多,成本就越高。

    max buffer size

    max buffer size” 实际上是树上可以发生的最大变化数量,同时仍然有效的 root hash

    由于根哈希有效地是所有叶子数据的单一哈希,改变任何一个叶子将使得所有后续尝试改变常规树的叶子所需的证明无效。

    但是使用并发树,对于这些证明来说,实际上有一个更新的日志。这个日志缓冲区的大小和设置是通过这个 maxBufferSize 值在树创建时完成的。

    Canopy depth

    Canopy depth”,有时也称为Canopy大小,是指在任何给定的证明路径上缓存/存储在链上的证明节点数量。

    在对 leaf 执行更新操作时,例如转让所有权(例如出售压缩的NFT),必须使用完整的证明路径来验证叶子节点的原始所有权,从而允许进行更新操作。此验证是使用完整的证明路径来正确计算当前的 root hash (或通过链上的“并发缓冲区”缓存的任何 root hash )来执行的。

    树的最大深度越大,执行此验证所需的证明节点就越多。例如,如果您的最大深度是 14 ,则需要使用 14 个总的证明节点进行验证。随着树的增大,完整的证明路径也会变得更长。

    通常情况下,每个这些证明节点都需要在每个树更新事务中包含。由于每个证明节点的值在事务中占用 32 bytes (类似于提供公钥),较大的树很快就会超过最大事务大小限制。

    进入CanopyCanopy可以在链上存储一定数量的验证节点(对于任何给定的验证路径)。这样可以在每个更新交易中包含较少的验证节点,从而保持整体交易大小低于限制。

    例如,深度为 14 的树需要 14 个总的验证节点。而有 10Canopy的情况下,每个更新事务只需要提交 4 个验证节点。

    Canopy深度值越大,成本越高

    canopyDepth 值也是创建树时成本的主要因素,因为您将在树的创建时支付这个成本。canopyDepth越高,链上存储的数据证明节点越多,成本也越高。

    较小的Canopy限制了可组合性

    虽然树的创建成本随着Canopy的高度而增加,但较低的Canopy将需要在每个更新事务中包含更多的证明节点。所需提交的节点越多,事务的大小就越大,因此超过事务大小限制就越容易。

    这也适用于任何其他试图与您的树/叶子进行交互的Solana程序或dApp。如果您的树需要太多的证明节点(因为Canopy深度较低),那么任何其他链上程序可能提供的额外操作都将受到其特定指令大小加上您的证明节点列表大小的限制。这限制了可组合性,并限制了您的特定树的潜在附加效用。

    例如,如果您的树被用于压缩的非同质化代币(NFTs),并且Canopy深度非常低,一个NFT市场可能只能支持简单的NFT转移,而无法支持链上竞标系统。

    创建一棵树的成本

    创建并发 Merkle 树的成本基于树的大小参数: maxDepthmaxBufferSizecanopyDepth 。这些值都用于计算在链上存在树所需的链上存储空间(以字节为单位)。

    一旦计算出所需的空间(以字节为单位),并使用 getMinimumBalanceForRentExemption RPC方法,请求在链上分配这些字节所需的费用(以lamports为单位)。

    在JavaScript中计算树木成本

    @solana/spl-account-compression 包中,开发人员可以使用 getConcurrentMerkleTreeAccountSize 函数来计算给定树大小参数所需的空间。

    然后使用 getMinimumBalanceForRentExemption 函数来获取在链上分配所需空间的最终成本(以lamports计算)。

    然后确定以lamports计算的成本,使得这个大小的账户免除租金,与其他账户创建类似。

    // calculate the space required for the tree
    const requiredSpace = getConcurrentMerkleTreeAccountSize(
    maxDepth,
    maxBufferSize,
    canopyDepth,
    );

    // get the cost (in lamports) to store the tree on-chain
    const storageCost = await connection.getMinimumBalanceForRentExemption(
    requiredSpace,
    );

    示例费用

    以下是几个不同树大小的示例成本,包括每个树可能的叶节点数量:

    例子 #1:16,384个节点,成本为0.222 SOL

    • 最大深度为 14 ,最大缓冲区大小为 64
    • 叶节点的最大数量: 16,384
    • 创建 0.222 SOLCanopy深度大约需要 0 的成本

    例子 #2:16,384个节点,成本为1.134 SOL

    • 最大深度为 14 ,最大缓冲区大小为 64
    • 叶节点的最大数量: 16,384
    • 创建 1.134 SOLCanopy深度大约需要 11 的成本

    示例 #3:1,048,576个节点,成本为1.673 SOL

    • 最大深度为 20 ,最大缓冲区大小为 256
    • 叶节点的最大数量: 1,048,576
    • 创建 1.673 SOLCanopy深度大约需要 10 的成本

    示例#4:1,048,576个节点,成本为15.814 SOL

    • 最大深度为 20 ,最大缓冲区大小为 256
    • 叶节点的最大数量: 1,048,576
    • 创建 15.814 SOLCanopy深度大约需要 15 的成本

    压缩的NFTs

    压缩的NFT是Solana上状态压缩的最受欢迎的应用之一。通过压缩,一个拥有一百万个NFT的收藏品可以以 ~50 SOL 的价格铸造,而不是其未压缩的等价收藏品。

    开发者指南:

    阅读我们的开发者指南,了解如何铸造和转移压缩的NFT

    · 3 min read
    Davirain

    欢迎来到Solana共学,这是一个精心设计的教程系列,供任何对Solana感兴趣的人深入学习。无论你是初学者还是有经验的开发者,这些模块都会引导你了解Solana区块链开发的基本内容。

    模块1:Solana基础

    • 区块链基本概念介绍
    • 本地程序开发环境配置
      • 原始Solana合约实现《hello, World》
      • Anchor合约框架实现《hello, World》
      • 使用Solang编译器编译solidity合约实现《hello, World》
    • BackPack钱包使用
    • 客户端开发
    • 钱包和前端
    • 自定义指令
    • 开始你自己的定制项目

    模块2:Solana高级主题

    • SPL token
    • NFTs + 使用Metaplex进行铸造
    • 在用户界面中展示NFTs
    • 创造神奇的网络货币并出售JPEG图片

    更深入的模块:深入了解Solana

    • 模块3:Rust入门,原生Solana开发,安全性,NFT质押
    • 模块4:本地环境,跨程序调用,测试,质押应用开发
    • 模块5:Anchor入门,全栈Anchor应用开发
    • 模块6:发布周,随机性,完善

    特别主题:超越基础

    • Solana程序中的环境变量
    • Solana支付,版本化事务,Rust宏
    • Solana程序安全:签名授权,所有者检查,重新初始化攻击,PDA共享等
    • 使用Solidity编写Solana合约
    • 发行Token2020,压缩NFT
    • 在Solana中使用The Graph,Oracles Pyth SDK
    • TipLink使用,如何在Quicknode和Helius申请RPC endpoint
    • 等等...

    和我们一起,在这全面的指南中探索Solana的每一个方面。从最基本的内容到安全和合约开发的复杂方面,Solana共学为每一位Solana爱好者提供了内容。

    敬请期待,如果有任何问题或需要进一步的协助,请随时与我们联系。欢迎来到Solana共学!

    - - + + \ No newline at end of file diff --git a/blog/tags/blog/index.html b/blog/tags/blog/index.html index 8723c479d..fba8d4179 100644 --- a/blog/tags/blog/index.html +++ b/blog/tags/blog/index.html @@ -9,8 +9,8 @@ - - + +
    @@ -19,7 +19,7 @@ 通常由特定程序(通常是一个智能合约)关联额外的账户。该账号没有私钥,故除程序本身外,无法完成数据签名,无法完成完整的数据交易。

    • ADA (Account Derived Account)

    由 createWithSeed 方法产生。 有一个账号公钥派生出来的关联账户,数据签名权限属于主账号。也即,需要主账号的签名才能完成完整的数据交易。

    solana中,根据数据签名,决定了数据的真实所有权。即 我的数据我做主

    本文主要分析这两种账号的异同。

    地址生成逻辑介绍如下

    • PDA 地址生成规则
    1. buffer = [seed,programId,"ProgramDerivedAddress"]
    2. 对buffer 取 sha256
    3. 如果在曲线上,那么抛出error, 如果不在,那么直接返回作为 使用地址

    createProgramAddressSync

    • ADA 生成
    1. buffer=[fromPublicKey,seed,programId]
    2. buffer 取 sha256, 直接返回

    createWithSeed

    区别在于,数据的托管使用逻辑.

    • ADA 数据签名权限,在于账户本身。即 我的数据我做主,未经允许(我未签名)不能修改。
    • PDA 数据签名权限在于合约。经过程序签名,可以修改 account 的数据和提取其中的sol。

    ADA 账号使用

    数据操作,有配套的函数对应,内部包含 xxxxWithSeedParams 类型的参数,完成对应的操作。 操作数据,需要 主账户的签名,这一点决定了,账号的真实所有权。

    • SystemProgram.createAccountWithSeed 初始化账号
    • SystemProgram.assign 重新分配owner
    • SystemProgram.allocate 分配空间
    • SystemProgram.transfer 转移SOL

    PDA 账号使用

    • 客户端只用于账户地址推导,不能初始化。初始化过程在合约内部完成。
    • 因其签名权限,必须在合约内部完成。他的操作权限完全属于智能合约。

    ADA 账号使用 example

      const seed = "ada.creator";

    // 初始化ada 账户
    let ada_account = await web3.PublicKey.createWithSeed(
    signer.publicKey,
    seed,
    program
    );
    console.log("ada_account address: ", ada_account.toBase58());

    let ada_info = await connection.getAccountInfo(ada_account);

    // 根据是否存在账号,决定是否初始化
    if (ada_info) {
    console.log(ada_info);
    } else {
    console.log("ada account not found");
    const transaction = new web3.Transaction().add(
    web3.SystemProgram.createAccountWithSeed({
    newAccountPubkey: ada_account,
    fromPubkey: signer.publicKey,
    basePubkey: signer.publicKey,
    programId: program,
    seed,
    lamports: web3.LAMPORTS_PER_SOL,
    space: 20,
    })
    );

    PDA 使用 example

    客户端部分代码逻辑

    const pda_seed = "pda.creator";

    const obj = new Model();

    const [pda, bump_seed] = web3.PublicKey.findProgramAddressSync(
    [signer.publicKey.toBuffer(), new TextEncoder().encode(movie.title)],
    program
    );

    console.log("pda address : ", pda.toBase58());

    const instruction = new web3.TransactionInstruction({
    keys: [
    {
    // 付钱的账户
    pubkey: signer.publicKey,
    isSigner: true,
    isWritable: false,
    },
    {
    // PDA将存储数据
    pubkey: pda,
    isSigner: false,
    isWritable: true,
    },
    {
    // 系统程序将用于创建PDA
    pubkey: web3.SystemProgram.programId,
    isSigner: false,
    isWritable: false,
    },
    ],
    // 传输数据
    data: obj.serialize(),
    programId: program,
    });

    const transaction = new web3.Transaction().add(instruction);

    const signature = await web3.sendAndConfirmTransaction(
    connection,
    transaction,
    [signer]
    );

    console.log(signature);

    合约部分代码逻辑

    // 获取账户迭代器
    let account_info_iter = &mut accounts.iter();

    // 获取账户
    let initializer = next_account_info(account_info_iter)?;
    let pda_account = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    // 构造PDA账户
    let (pda, bump_seed) =
    Pubkey::find_program_address(&[initializer.key.as_ref(), title.as_bytes()], program_id);

    // 和客户端比对
    if pda != *pda_account.key {
    msg!("Invalid seeds for PDA");
    return Err(ProgramError::InvalidArgument);
    }

    // 计算所需的租金
    let rent = Rent::get()?;
    let rent_lamports = rent.minimum_balance(total_len);

    // 创建账户
    invoke_signed(
    &system_instruction::create_account(
    initializer.key,
    pda_account.key,
    rent_lamports,
    total_len
    .try_into()
    .map_err(|_| Error::ConvertUsizeToU64Failed)?,
    program_id,
    ),
    &[
    initializer.clone(),
    pda_account.clone(),
    system_program.clone(),
    ],
    &[&[initializer.key.as_ref(), title.as_bytes(), &[bump_seed]]],
    )?;

    // MovieAccountState 定义的state类型
    let mut account_data =
    try_from_slice_unchecked::<MovieAccountState>(&pda_account.data.borrow()).unwrap();

    account_data.title = title;
    account_data.rating = rating;
    account_data.description = description;
    account_data.is_initialized = true;

    // 写入pda 数据本身
    account_data.serialize(&mut &mut pda_account.data.borrow_mut()[..])?;

    参考资料

    - - + + \ No newline at end of file diff --git a/blog/tags/blog/page/2/index.html b/blog/tags/blog/page/2/index.html index 6a7a07281..d7aa9c72a 100644 --- a/blog/tags/blog/page/2/index.html +++ b/blog/tags/blog/page/2/index.html @@ -9,13 +9,13 @@ - - + +

    13 posts tagged with "blog"

    View All Tags

    · 3 min read
    YanAemons

    報錯日志

    在使用solana-cli時候,鑑於一些依賴版本限制,會用到cli14.xx(主網版本),而不是16.xx(測試網版本)

    例如,在使用solana-cli版本爲1.14.17, anchor版本爲0.26.0的環境中, anchor init創建一個新項目後運行 anchor build會發生以下錯誤:

    error: package constant_time_eq v0.3.0 cannot be built because it requires rustc 1.66.0 or newer, while the currently active rustc version is 1.62.0-dev

    報錯原因

    使用的solana-cli版本在14.xxx, cli內自帶的rustc版本過老,無法編譯較新的依賴

    解決方案

    1. 升級solana-cli至最新版本

    solana-install update

    2.指定依賴包版本

    需要在Cargo.toml文件下指定以下依賴版本

    getrandom = { version = "0.2.9", features = ["custom"] }  
    solana-program = "=1.14.17"
    winnow="=0.4.1"
    toml_datetime="=0.6.1"
    blake3 = "=1.3.1"

    運行cargo clean後重新運行anchor build即可解決

    監聽程序log監聽到兩次

    在使用program.addEventListener()有可能聽到兩次相同的事件,其中一次的txSign會是“1111111111111111111111111111111111111111111111111111111111111111”, 這是因爲監聽到了模擬時的交易哈系,我們只需要在監聽到該交易哈系時拋棄即可

    program.addEventListener("eventName", (event, slot, signature) => {
    if (signature === '1111111111111111111111111111111111111111111111111111111111111111') return

    // do ur stuff
    })

    然而,有時websocket訂閱也會多次返回實際簽名。如果是這種情況,您可以使用一些緩存解決方案。例如,創建一個具有一定長度限制的集合,在此處添加簽名並檢查該集合中是否存在新簽名:

    const handledSignatures = new Set<string>()
    const maxHandledSignaturesLen = 100

    program.addEventListener("eventName", (event, slot, signature) => {
    if (signature === '1111111111111111111111111111111111111111111111111111111111111111') return
    if (handledSignatures.has(signature)) return

    // do ur stuff

    handledSignatures.add(signature)
    if (handledSignatures.size > maxHandledSignaturesLen) {
    handledSignatures.delete(handledSignatures.values().next().value)
    }
    })

    · 17 min read
    Davirain

    Solana上,状态压缩是一种创建离链数据的“指纹”(或哈希)并将该指纹存储在链上以进行安全验证的方法。有效地利用Solana账本的安全性来安全验证离链数据,以确保其未被篡改。

    这种“压缩”方法使得Solana的程序和dApps能够使用廉价的区块链账本空间来安全存储数据,而不是更昂贵的账户空间。

    这是通过使用一种特殊的二叉树结构,称为并发默克尔树,对每个数据片段(称为 leaf )创建哈希,将它们哈希在一起,并仅将最终哈希存储在链上来实现的。

    什么是状态压缩?

    简单来说,状态压缩使用“树”结构将链外数据以确定性的方式进行加密哈希,计算出一个最终的哈希值,并将其存储在链上。

    这些树是通过这个“确定性”过程创建的:

    • 获取任何数据
    • 创建这些数据的哈希值
    • 将此哈希值存储为树底部的 leaf
    • 每个 leaf 对都会被一起哈希,创建一个 branch
    • 每个 branch 然后一起哈希
    • 不断攀爬树木并将相邻的树枝连接在一起
    • 树顶上一旦到达,就会产生最后的 root hash

    这个 root hash 然后存储在链上,作为每个叶子节点中所有数据的可验证证据。这样任何人都可以通过加密验证树中所有离链数据,而实际上只需在链上存储少量数据。因此,由于这种"状态压缩",大大降低了存储/证明大量数据的成本。

    默克尔树和并发默克尔树

    Solana的状态压缩使用了一种特殊类型的默克尔树,允许对任何给定的树进行多次更改,同时仍然保持树的完整性和有效性。

    这棵特殊的树被称为“并发默克尔树”,有效地在链上保留了树的“更改日志”。允许在一个证明失效之前对同一棵树进行多次快速更改(即在同一个区块中)。

    默克尔树是什么?

    默克尔树,有时也被称为“哈希树”,是一种基于哈希的二叉树结构,其中每个leaf节点都被表示为其内部数据的加密哈希。而每个非叶节点,也被称为“branch节点”,则被表示为其子叶节点哈希的哈希值。

    每个分支也被哈希在一起,沿着树向上爬,直到最后只剩下一个哈希。这个最终的哈希,称为 root hash 或者"根",可以与一个"证明路径"结合使用,来验证存储在叶节点中的任何数据。

    一旦计算出最终的根哈希值(root hash),可以通过重新计算特定叶子(leaf)节点的数据和每个相邻分支的哈希标签(称为“证明路径”)来验证存储在节点中的任何数据。将这个“重新哈希”与根哈希值进行比较,可以验证底层叶子数据的准确性。如果它们匹配,数据就被验证为准确的。如果它们不匹配,叶子数据已被更改。

    只要需要,原始叶子数据可以通过对新的叶子数据进行哈希运算并重新计算根哈希值来进行更改,方法与原始根哈希值的计算方式相同。然后,这个新的根哈希值用于验证任何数据,并且有效地使之前的根哈希值和证明无效。因此,对这些传统的默克尔树的每一次更改都需要按顺序执行。

    info

    当使用默克尔树时,更改叶子数据并计算新的根哈希的过程可能是非常常见的事情!虽然这是树的设计要点之一,但它可能导致最显著的缺点之一:快速变化。

    什么是并发默克尔树?

    在高吞吐量的应用中,比如在Solana运行时中,对于链上传统Merkle树的更改请求可能会相对快速地连续接收到验证者(例如在同一个槽中)。每个叶子数据的更改仍然需要按顺序执行。这导致每个后续的更改请求都会失败,因为根哈希和证明已经被同一槽中之前的更改请求无效化了。

    进入,并发默克尔树。

    并发默克尔树存储了最近更改的安全日志、它们的根哈希以及用于推导根哈希的证明。这个日志缓冲区存储在链上的每个树对应的特定账户中,最大记录数为(也称为 maxBufferSize )。

    当同一时隙内的验证者收到多个叶子数据变更请求时,链上并发 Merkle 树可以将这个“变更日志缓冲区”作为更可接受的证明的真实来源。有效地允许在同一时隙内对同一棵树进行多达 maxBufferSize 次变更。大幅提升吞吐量。

    并发默克尔树的大小调整

    创建这种链上树时,有三个值将决定您的树的大小、创建树的成本以及对树的并发更改数量:

    1. max depth 最大深度
    2. max buffer size 最大缓冲区大小
    3. canopy depth

    max depth

    树的“最大深度”是从任何数据 leaf 到树的 root 所需的最大跳数。

    由于默克尔树是二叉树,每个叶子节点只与另一个叶子节点相连;存在于一个 leaf pair 中。

    因此,树的 maxDepth 被用来确定可以通过简单的计算存储在树中的最大节点数(也称为数据或 leafs

    nodes_count = 2 ^ maxDepth

    由于树的深度必须在创建树时设置,您必须决定您希望树存储多少个数据。然后使用上述简单的计算,您可以确定存储数据的最低 maxDepth

    示例1:铸造100个NFTs

    如果你想创建一个用于存储100个压缩NFT的树,我们至少需要"100个叶子"或"100个节点"。

    // maxDepth=6 -> 64 nodes
    2^6 = 64

    // maxDepth=7 -> 128 nodes
    2^7 = 128

    因此,我们需要一个最大深度为 7 的树,以存储 100 个数据。

    例子2:铸造15000个NFTs

    如果你想创建一个用于存储15000个压缩NFT的树,我们将需要至少"15000个叶子"或"15000个节点"。

    // maxDepth=13 -> 8192 nodes
    2^13 = 8192

    // maxDepth=14 -> 16384 nodes
    2^14 = 16384

    因此,我们需要一个最大深度为 14 的树,以存储 15000 个数据。

    最大深度越高,成本越高

    创建树时, maxDepth 值将是成本的主要驱动因素之一,因为您将在创建树时支付这笔成本。最大树深度越高,您可以存储的数据指纹(也称为哈希)越多,成本就越高。

    max buffer size

    max buffer size” 实际上是树上可以发生的最大变化数量,同时仍然有效的 root hash

    由于根哈希有效地是所有叶子数据的单一哈希,改变任何一个叶子将使得所有后续尝试改变常规树的叶子所需的证明无效。

    但是使用并发树,对于这些证明来说,实际上有一个更新的日志。这个日志缓冲区的大小和设置是通过这个 maxBufferSize 值在树创建时完成的。

    Canopy depth

    Canopy depth”,有时也称为Canopy大小,是指在任何给定的证明路径上缓存/存储在链上的证明节点数量。

    在对 leaf 执行更新操作时,例如转让所有权(例如出售压缩的NFT),必须使用完整的证明路径来验证叶子节点的原始所有权,从而允许进行更新操作。此验证是使用完整的证明路径来正确计算当前的 root hash (或通过链上的“并发缓冲区”缓存的任何 root hash )来执行的。

    树的最大深度越大,执行此验证所需的证明节点就越多。例如,如果您的最大深度是 14 ,则需要使用 14 个总的证明节点进行验证。随着树的增大,完整的证明路径也会变得更长。

    通常情况下,每个这些证明节点都需要在每个树更新事务中包含。由于每个证明节点的值在事务中占用 32 bytes (类似于提供公钥),较大的树很快就会超过最大事务大小限制。

    进入CanopyCanopy可以在链上存储一定数量的验证节点(对于任何给定的验证路径)。这样可以在每个更新交易中包含较少的验证节点,从而保持整体交易大小低于限制。

    例如,深度为 14 的树需要 14 个总的验证节点。而有 10Canopy的情况下,每个更新事务只需要提交 4 个验证节点。

    Canopy深度值越大,成本越高

    canopyDepth 值也是创建树时成本的主要因素,因为您将在树的创建时支付这个成本。canopyDepth越高,链上存储的数据证明节点越多,成本也越高。

    较小的Canopy限制了可组合性

    虽然树的创建成本随着Canopy的高度而增加,但较低的Canopy将需要在每个更新事务中包含更多的证明节点。所需提交的节点越多,事务的大小就越大,因此超过事务大小限制就越容易。

    这也适用于任何其他试图与您的树/叶子进行交互的Solana程序或dApp。如果您的树需要太多的证明节点(因为Canopy深度较低),那么任何其他链上程序可能提供的额外操作都将受到其特定指令大小加上您的证明节点列表大小的限制。这限制了可组合性,并限制了您的特定树的潜在附加效用。

    例如,如果您的树被用于压缩的非同质化代币(NFTs),并且Canopy深度非常低,一个NFT市场可能只能支持简单的NFT转移,而无法支持链上竞标系统。

    创建一棵树的成本

    创建并发 Merkle 树的成本基于树的大小参数: maxDepthmaxBufferSizecanopyDepth 。这些值都用于计算在链上存在树所需的链上存储空间(以字节为单位)。

    一旦计算出所需的空间(以字节为单位),并使用 getMinimumBalanceForRentExemption RPC方法,请求在链上分配这些字节所需的费用(以lamports为单位)。

    在JavaScript中计算树木成本

    @solana/spl-account-compression 包中,开发人员可以使用 getConcurrentMerkleTreeAccountSize 函数来计算给定树大小参数所需的空间。

    然后使用 getMinimumBalanceForRentExemption 函数来获取在链上分配所需空间的最终成本(以lamports计算)。

    然后确定以lamports计算的成本,使得这个大小的账户免除租金,与其他账户创建类似。

    // calculate the space required for the tree
    const requiredSpace = getConcurrentMerkleTreeAccountSize(
    maxDepth,
    maxBufferSize,
    canopyDepth,
    );

    // get the cost (in lamports) to store the tree on-chain
    const storageCost = await connection.getMinimumBalanceForRentExemption(
    requiredSpace,
    );

    示例费用

    以下是几个不同树大小的示例成本,包括每个树可能的叶节点数量:

    例子 #1:16,384个节点,成本为0.222 SOL

    • 最大深度为 14 ,最大缓冲区大小为 64
    • 叶节点的最大数量: 16,384
    • 创建 0.222 SOLCanopy深度大约需要 0 的成本

    例子 #2:16,384个节点,成本为1.134 SOL

    • 最大深度为 14 ,最大缓冲区大小为 64
    • 叶节点的最大数量: 16,384
    • 创建 1.134 SOLCanopy深度大约需要 11 的成本

    示例 #3:1,048,576个节点,成本为1.673 SOL

    • 最大深度为 20 ,最大缓冲区大小为 256
    • 叶节点的最大数量: 1,048,576
    • 创建 1.673 SOLCanopy深度大约需要 10 的成本

    示例#4:1,048,576个节点,成本为15.814 SOL

    • 最大深度为 20 ,最大缓冲区大小为 256
    • 叶节点的最大数量: 1,048,576
    • 创建 15.814 SOLCanopy深度大约需要 15 的成本

    压缩的NFTs

    压缩的NFT是Solana上状态压缩的最受欢迎的应用之一。通过压缩,一个拥有一百万个NFT的收藏品可以以 ~50 SOL 的价格铸造,而不是其未压缩的等价收藏品。

    开发者指南:

    阅读我们的开发者指南,了解如何铸造和转移压缩的NFT

    · 3 min read
    Davirain

    欢迎来到Solana共学,这是一个精心设计的教程系列,供任何对Solana感兴趣的人深入学习。无论你是初学者还是有经验的开发者,这些模块都会引导你了解Solana区块链开发的基本内容。

    模块1:Solana基础

    • 区块链基本概念介绍
    • 本地程序开发环境配置
      • 原始Solana合约实现《hello, World》
      • Anchor合约框架实现《hello, World》
      • 使用Solang编译器编译solidity合约实现《hello, World》
    • BackPack钱包使用
    • 客户端开发
    • 钱包和前端
    • 自定义指令
    • 开始你自己的定制项目

    模块2:Solana高级主题

    • SPL token
    • NFTs + 使用Metaplex进行铸造
    • 在用户界面中展示NFTs
    • 创造神奇的网络货币并出售JPEG图片

    更深入的模块:深入了解Solana

    • 模块3:Rust入门,原生Solana开发,安全性,NFT质押
    • 模块4:本地环境,跨程序调用,测试,质押应用开发
    • 模块5:Anchor入门,全栈Anchor应用开发
    • 模块6:发布周,随机性,完善

    特别主题:超越基础

    • Solana程序中的环境变量
    • Solana支付,版本化事务,Rust宏
    • Solana程序安全:签名授权,所有者检查,重新初始化攻击,PDA共享等
    • 使用Solidity编写Solana合约
    • 发行Token2020,压缩NFT
    • 在Solana中使用The Graph,Oracles Pyth SDK
    • TipLink使用,如何在Quicknode和Helius申请RPC endpoint
    • 等等...

    和我们一起,在这全面的指南中探索Solana的每一个方面。从最基本的内容到安全和合约开发的复杂方面,Solana共学为每一位Solana爱好者提供了内容。

    敬请期待,如果有任何问题或需要进一步的协助,请随时与我们联系。欢迎来到Solana共学!

    - - + + \ No newline at end of file diff --git a/blog/tags/cloudbreak/index.html b/blog/tags/cloudbreak/index.html index 760fcd62c..27cb30589 100644 --- a/blog/tags/cloudbreak/index.html +++ b/blog/tags/cloudbreak/index.html @@ -9,13 +9,13 @@ - - + +

    One post tagged with "cloudbreak"

    View All Tags

    · 7 min read
    Davirain

    在这篇博文中,我们将介绍 Cloudbreak,Solana 的水平扩展状态架构

    概述:RAM、SSD 和线程

    当在不进行分片的情况下扩展区块链时,仅扩展计算是不够的。用于跟踪帐户的内存很快就会成为大小和访问速度的瓶颈。例如:人们普遍认为,许多现代链使用的本地数据库引擎 LevelDB 在单台机器上无法支持超过 5,000 TPS。这是因为虚拟机无法通过数据库抽象利用对帐户状态的并发读写访问。

    一个简单的解决方案是在 RAM 中维护全局状态。然而,期望消费级机器有足够的 RAM 来存储全局状态是不合理的。下一个选择是使用 SSD。虽然 SSD 将每字节成本降低了 30 倍或更多,但它们比 RAM 慢 1000 倍。以下是最新三星 SSD 的数据表,它是市场上最快的 SSD 之一。

    单笔交易需要读取 2 个账户并写入 1 个账户。账户密钥是加密公钥,完全随机,没有真实的数据局部性。用户的钱包会有很多账户地址,每个地址的位与任何其他地址完全无关。由于帐户之间不存在局部性,因此我们不可能将它们放置在内存中以使它们可能彼此接近。

    每秒最多 15,000 次唯一读取,使用单个 SSD 的帐户数据库的简单单线程实现将支持每秒最多 7,500 个事务。现代 SSD 支持 32 个并发线程,因此可以支持每秒 370,000 次读取,或每秒大约 185,000 个事务。

    Cloudbreak 破云

    Solana 的指导设计原则是设计不妨碍硬件的软件,以实现 100% 的利用率。

    组织帐户数据库以便在 32 个线程之间可以进行并发读取和写入是一项挑战。像 LevelDB 这样的普通开源数据库会导致瓶颈,因为它们没有针对区块链设置中的这一特定挑战进行优化。 Solana 不使用传统数据库来解决这些问题。相反,我们使用操作系统使用的几种机制。

    首先,我们利用内存映射文件。内存映射文件是其字节被映射到进程的虚拟地址空间的文件。一旦文件被映射,它的行为就像任何其他内存一样。内核可能会将部分内存缓存在 RAM 中,或者不将其缓存在 RAM 中,但物理内存的数量受到磁盘大小的限制,而不是 RAM 的大小。读取和写入仍然明显受到磁盘性能的限制。

    第二个重要的设计考虑因素是顺序操作比随机操作快得多。这不仅适用于 SSD,也适用于整个虚拟内存堆栈。 CPU 擅长预取按顺序访问的内存,而操作系统则擅长处理连续页错误。为了利用这种行为,我们将帐户数据结构大致分解如下:

    1. 账户和分叉的索引存储在 RAM 中。

    2. 帐户存储在最大 4MB 的内存映射文件中。

    3. 每个内存映射仅存储来自单个提议分叉的帐户。

    4. 地图随机分布在尽可能多的可用 SSD 上。

    5. 使用写时复制语义。

    6. 写入会附加到同一分叉的随机内存映射中。

    7. 每次写入完成后都会更新索引。

    由于帐户更新是写时复制并附加到随机 SSD,因此 Solana 获得了顺序写入和跨多个 SSD 进行横向写入以进行并发事务的好处。读取仍然是随机访问,但由于任何给定的分叉状态更新都分布在许多 SSD 上,因此读取最终也会水平扩展。

    Cloudbreak 还执行某种形式的垃圾收集。随着分叉在回滚之外最终确定并且帐户被更新,旧的无效帐户将被垃圾收集,并且内存将被放弃。

    这种架构至少还有一个更大的好处:计算任何给定分叉的状态更新的 Merkle 根可以通过跨 SSD 水平扩展的顺序读取来完成。这种方法的缺点是失去了数据的通用性。由于这是一个自定义数据结构,具有自定义布局,因此我们无法使用通用数据库抽象来查询和操作数据。我们必须从头开始构建一切。幸运的是,现在已经完成了。

    Benchmarking Cloudbreak Cloudbreak 基准测试

    虽然帐户数据库位于 RAM 中,但我们看到吞吐量与 RAM 访问时间相匹配,同时随可用内核数量进行扩展。当帐户数量达到 1000 万时,数据库不再适合 RAM。然而,我们仍然看到单个 SSD 上每秒读取或写入的性能接近 1m。

    - - + + \ No newline at end of file diff --git a/blog/tags/co-learn/index.html b/blog/tags/co-learn/index.html index 9eba18a65..fe80bd1bc 100644 --- a/blog/tags/co-learn/index.html +++ b/blog/tags/co-learn/index.html @@ -9,8 +9,8 @@ - - + +
    @@ -18,7 +18,7 @@ 通常由特定程序(通常是一个智能合约)关联额外的账户。该账号没有私钥,故除程序本身外,无法完成数据签名,无法完成完整的数据交易。

    • ADA (Account Derived Account)

    由 createWithSeed 方法产生。 有一个账号公钥派生出来的关联账户,数据签名权限属于主账号。也即,需要主账号的签名才能完成完整的数据交易。

    solana中,根据数据签名,决定了数据的真实所有权。即 我的数据我做主

    本文主要分析这两种账号的异同。

    地址生成逻辑介绍如下

    • PDA 地址生成规则
    1. buffer = [seed,programId,"ProgramDerivedAddress"]
    2. 对buffer 取 sha256
    3. 如果在曲线上,那么抛出error, 如果不在,那么直接返回作为 使用地址

    createProgramAddressSync

    • ADA 生成
    1. buffer=[fromPublicKey,seed,programId]
    2. buffer 取 sha256, 直接返回

    createWithSeed

    区别在于,数据的托管使用逻辑.

    • ADA 数据签名权限,在于账户本身。即 我的数据我做主,未经允许(我未签名)不能修改。
    • PDA 数据签名权限在于合约。经过程序签名,可以修改 account 的数据和提取其中的sol。

    ADA 账号使用

    数据操作,有配套的函数对应,内部包含 xxxxWithSeedParams 类型的参数,完成对应的操作。 操作数据,需要 主账户的签名,这一点决定了,账号的真实所有权。

    • SystemProgram.createAccountWithSeed 初始化账号
    • SystemProgram.assign 重新分配owner
    • SystemProgram.allocate 分配空间
    • SystemProgram.transfer 转移SOL

    PDA 账号使用

    • 客户端只用于账户地址推导,不能初始化。初始化过程在合约内部完成。
    • 因其签名权限,必须在合约内部完成。他的操作权限完全属于智能合约。

    ADA 账号使用 example

      const seed = "ada.creator";

    // 初始化ada 账户
    let ada_account = await web3.PublicKey.createWithSeed(
    signer.publicKey,
    seed,
    program
    );
    console.log("ada_account address: ", ada_account.toBase58());

    let ada_info = await connection.getAccountInfo(ada_account);

    // 根据是否存在账号,决定是否初始化
    if (ada_info) {
    console.log(ada_info);
    } else {
    console.log("ada account not found");
    const transaction = new web3.Transaction().add(
    web3.SystemProgram.createAccountWithSeed({
    newAccountPubkey: ada_account,
    fromPubkey: signer.publicKey,
    basePubkey: signer.publicKey,
    programId: program,
    seed,
    lamports: web3.LAMPORTS_PER_SOL,
    space: 20,
    })
    );

    PDA 使用 example

    客户端部分代码逻辑

    const pda_seed = "pda.creator";

    const obj = new Model();

    const [pda, bump_seed] = web3.PublicKey.findProgramAddressSync(
    [signer.publicKey.toBuffer(), new TextEncoder().encode(movie.title)],
    program
    );

    console.log("pda address : ", pda.toBase58());

    const instruction = new web3.TransactionInstruction({
    keys: [
    {
    // 付钱的账户
    pubkey: signer.publicKey,
    isSigner: true,
    isWritable: false,
    },
    {
    // PDA将存储数据
    pubkey: pda,
    isSigner: false,
    isWritable: true,
    },
    {
    // 系统程序将用于创建PDA
    pubkey: web3.SystemProgram.programId,
    isSigner: false,
    isWritable: false,
    },
    ],
    // 传输数据
    data: obj.serialize(),
    programId: program,
    });

    const transaction = new web3.Transaction().add(instruction);

    const signature = await web3.sendAndConfirmTransaction(
    connection,
    transaction,
    [signer]
    );

    console.log(signature);

    合约部分代码逻辑

    // 获取账户迭代器
    let account_info_iter = &mut accounts.iter();

    // 获取账户
    let initializer = next_account_info(account_info_iter)?;
    let pda_account = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    // 构造PDA账户
    let (pda, bump_seed) =
    Pubkey::find_program_address(&[initializer.key.as_ref(), title.as_bytes()], program_id);

    // 和客户端比对
    if pda != *pda_account.key {
    msg!("Invalid seeds for PDA");
    return Err(ProgramError::InvalidArgument);
    }

    // 计算所需的租金
    let rent = Rent::get()?;
    let rent_lamports = rent.minimum_balance(total_len);

    // 创建账户
    invoke_signed(
    &system_instruction::create_account(
    initializer.key,
    pda_account.key,
    rent_lamports,
    total_len
    .try_into()
    .map_err(|_| Error::ConvertUsizeToU64Failed)?,
    program_id,
    ),
    &[
    initializer.clone(),
    pda_account.clone(),
    system_program.clone(),
    ],
    &[&[initializer.key.as_ref(), title.as_bytes(), &[bump_seed]]],
    )?;

    // MovieAccountState 定义的state类型
    let mut account_data =
    try_from_slice_unchecked::<MovieAccountState>(&pda_account.data.borrow()).unwrap();

    account_data.title = title;
    account_data.rating = rating;
    account_data.description = description;
    account_data.is_initialized = true;

    // 写入pda 数据本身
    account_data.serialize(&mut &mut pda_account.data.borrow_mut()[..])?;

    参考资料

    · 3 min read
    Davirain

    欢迎来到Solana共学,这是一个精心设计的教程系列,供任何对Solana感兴趣的人深入学习。无论你是初学者还是有经验的开发者,这些模块都会引导你了解Solana区块链开发的基本内容。

    模块1:Solana基础

    • 区块链基本概念介绍
    • 本地程序开发环境配置
      • 原始Solana合约实现《hello, World》
      • Anchor合约框架实现《hello, World》
      • 使用Solang编译器编译solidity合约实现《hello, World》
    • BackPack钱包使用
    • 客户端开发
    • 钱包和前端
    • 自定义指令
    • 开始你自己的定制项目

    模块2:Solana高级主题

    • SPL token
    • NFTs + 使用Metaplex进行铸造
    • 在用户界面中展示NFTs
    • 创造神奇的网络货币并出售JPEG图片

    更深入的模块:深入了解Solana

    • 模块3:Rust入门,原生Solana开发,安全性,NFT质押
    • 模块4:本地环境,跨程序调用,测试,质押应用开发
    • 模块5:Anchor入门,全栈Anchor应用开发
    • 模块6:发布周,随机性,完善

    特别主题:超越基础

    • Solana程序中的环境变量
    • Solana支付,版本化事务,Rust宏
    • Solana程序安全:签名授权,所有者检查,重新初始化攻击,PDA共享等
    • 使用Solidity编写Solana合约
    • 发行Token2020,压缩NFT
    • 在Solana中使用The Graph,Oracles Pyth SDK
    • TipLink使用,如何在Quicknode和Helius申请RPC endpoint
    • 等等...

    和我们一起,在这全面的指南中探索Solana的每一个方面。从最基本的内容到安全和合约开发的复杂方面,Solana共学为每一位Solana爱好者提供了内容。

    敬请期待,如果有任何问题或需要进一步的协助,请随时与我们联系。欢迎来到Solana共学!

    - - + + \ No newline at end of file diff --git a/blog/tags/gulf/index.html b/blog/tags/gulf/index.html index a2593146d..9d32a632c 100644 --- a/blog/tags/gulf/index.html +++ b/blog/tags/gulf/index.html @@ -9,13 +9,13 @@ - - + +

    One post tagged with "gulf"

    View All Tags

    · 7 min read
    Davirain

    在这篇博文中,我们将探讨 Gulf Stream,这是 Solana 用于高性能对抗网络的内存池管理解决方案。在进一步的博客文章中,我们将列出所有 7 个关键创新。

    内存池解释

    内存池是一组已提交但尚未被网络处理的交易。您现在可以看到比特币和以太坊内存池。

    30 天的比特币内存池(以字节为单位)。

    以交易量衡量的 30 天以太坊内存池

    对于比特币和以太坊来说,未经确认的交易数量通常约为 20K-100K,如上所示。内存池的大小(通常以未确认交易的数量来衡量)取决于区块空间的供需。即使在区块链时代的早期,当内存池上升时,这也会对整个网络产生显着的瓶颈效应。

    那么,Solana 如何做得更好呢?在不增加网络吞吐量的情况下,Solana 验证器可以管理 100,000 的内存池大小。这意味着在网络吞吐量为 50,000 TPS 的情况下,100,000 个交易内存池只需几秒钟即可执行。这就是 Solana 成为世界上性能最高的无需许可区块链的原因。

    令人印象深刻,对吧?但这个简单的分析忽略了很多重要因素……

    以太坊和比特币中的内存池使用八卦协议以点对点方式在随机节点之间传播。网络中的节点定期构建代表本地内存池的布隆过滤器,并向网络上的其他节点请求与该过滤器不匹配的任何交易(以及其他一些交易,例如最低费用)。将单个事务传播到网络的其余部分将至少需要 log(N) 步骤,消耗过滤它所需的带宽、内存和计算资源。

    当基准客户端开始每秒生成 100,000 个事务时,八卦协议就会不堪重负。计算过滤器以及在机器之间应用过滤器同时维护内存中的所有事务的成本变得非常高。领导者(区块生产者)还必须在区块中重新传输相同的交易,这意味着每笔交易至少通过网络传播两次。这既不高效也不实用。

    Introducing Gulf Stream 墨西哥湾流简介

    我们在 Solana 网络上解决这个问题的解决方案是将事务缓存和转发推到网络边缘。我们称之为湾流。由于每个验证者都知道即将到来的领导者的顺序,因此客户端和验证者会提前将交易转发给预期的领导者。这使得验证者可以提前执行交易,减少确认时间,更快地切换领导者,并减少未确认交易池对验证者的内存压力。该解决方案在具有非确定性领导者的网络中是不可能的

    那么它是怎样工作的?客户端(例如钱包)签署引用特定区块哈希的交易。客户端选择一个最近的、已被网络完全确认的区块哈希值。区块大约每 800 毫秒提议一次,并且每增加一个区块就需要指数级增加的超时时间来展开。使用我们的默认超时曲线,在最坏的情况下,完全确认的块哈希值是 32 个块旧的。该交易仅在引用块的子块中有效,并且仅对 X 个块有效。虽然 X 尚未最终确定,但我们预计区块哈希的 TTL(生存时间)约为 32 个区块。假设区块时间为 800 毫秒,相当于 24 秒。

    一旦交易被转发给任何验证者,验证者就会将其转发给即将到来的领导者之一。客户可以订阅来自验证器的交易确认。客户知道区块哈希会在有限的时间内过期,或者交易已被网络确认。这允许客户签署保证执行或失败的交易。一旦网络越过回滚点,使得交易引用的区块哈希过期,客户端就可以保证交易现在无效并且永远不会在链上执行。

    https://podcasts.apple.com/us/podcast/anatoly-yakovenko-ceo-co-founder-solana-what-sharding/id1434060078?i=1000439218245&source=post_page-----d342e72186ad--------------------------------

    这种架构固有的许多积极的副作用。首先,负载下的验证器可以提前执行交易并丢弃任何失败的交易。其次,领导者可以根据转发交易的验证器的权益权重来优先处理交易。这允许网络在大规模拒绝服务期间正常降级。

    到目前为止,很明显,区块链网络的功能只有在其内存池最小的情况下才能发挥作用。虽然交易吞吐量有限的网络承担着尝试改造全新扩展技术以解决不断增长的内存池的崇高努力,但 Solana 自构思以来一直通过历史证明、湾流和海平面等优化来解决第一代的问题区块链网络并实现巨大的交易吞吐量。从一开始,这就是全球范围内的惊人速度,也是为世界各地的企业、经济和人民创建功能强大的去中心化基础设施的根本性发展。

    - - + + \ No newline at end of file diff --git a/blog/tags/index.html b/blog/tags/index.html index 26a7ee384..52ad1866c 100644 --- a/blog/tags/index.html +++ b/blog/tags/index.html @@ -9,13 +9,13 @@ - - + + - - + + \ No newline at end of file diff --git a/blog/tags/phoenix/index.html b/blog/tags/phoenix/index.html index b1ee535f9..64fc0f42c 100644 --- a/blog/tags/phoenix/index.html +++ b/blog/tags/phoenix/index.html @@ -9,13 +9,13 @@ - - + +

    One post tagged with "Phoenix"

    View All Tags

    · One min read
    Davirain

    Phoenix是Solana上的去中心化限价订单簿,支持现货资产市场。

    Why

    可组合的流动性中心是DeFi的公共产品。开发者可以构建其他链上应用,将流动性发布到或从规范的流动性来源中提取流动性。

    - - + + \ No newline at end of file diff --git a/blog/tags/pipeline/index.html b/blog/tags/pipeline/index.html index 383ed8b4b..15ed76bfc 100644 --- a/blog/tags/pipeline/index.html +++ b/blog/tags/pipeline/index.html @@ -9,13 +9,13 @@ - - + +

    One post tagged with "pipeline"

    View All Tags

    · 6 min read
    Davirain

    为了达到亚秒级的确认时间和 Solana 成为世界上第一个网络规模区块链所需的交易能力,仅仅快速达成共识是不够的。该团队必须开发一种方法来快速验证大量交易块,同时在网络上快速复制它们。为了实现这一目标,Solana 网络上的事务验证过程广泛使用了 CPU 设计中常见的一种称为管道的优化。

    当存在需要通过一系列步骤处理的输入数据流并且每个步骤都有不同的硬件负责时,流水线是一个合适的过程。解释这一点的典型比喻是洗衣机和烘干机,它按顺序洗涤/烘干/折叠多批衣物。清洗必须在干燥之前进行,干燥之前必须进行折叠,但这三个操作中的每一个都由单独的单元执行。

    为了最大限度地提高效率,人们创建了一系列阶段的管道。我们将洗衣机称为第一阶段,烘干机称为第二阶段,折叠过程称为第三阶段。为了运行管道,需要在第一批衣物添加到烘干机后立即将第二批衣物添加到洗衣机中。同样,第三个负载在第二个负载放入烘干机并且第一个负载被折叠之后添加到洗衣机。通过这种方式,人们可以同时处理三批衣物。给定无限负载,管道将始终以管道中最慢阶段的速率完成负载。

    “我们需要找到一种方法让所有硬件始终保持忙碌状态。这就是网卡、CPU 核心和所有 GPU 核心。为此,我们借鉴了 CPU 设计的经验”,Solana 创始人兼首席技术官 Greg Fitzgerald 解释道。 “我们在软件中创建了一个四级交易处理器。我们称之为 TPU,我们的交易处理单元。”

    在 Solana 网络上,管道机制——交易处理单元——通过内核级别的数据获取、GPU 级别的签名验证、CPU 级别的存储和内核空间的写入来进行。当 TPU 开始向验证器发送块时,它已经在下一组数据包中获取,验证了它们的签名,并开始记入令牌。

    验证器节点同时运行两个管道进程,一个用于领导者模式,称为 TPU,另一个用于验证器模式,称为 TVU。在这两种情况下,管道化的硬件是相同的:网络输入、GPU 卡、CPU 内核、磁盘写入和网络输出。它对该硬件的作用是不同的。 TPU 的存在是为了创建分类帐条目,而 TVU 的存在是为了验证它们。

    “我们知道签名验证将成为瓶颈,但我们也可以将这种与上下文无关的操作卸载到 GPU,”Fitzgersald 说道。 “即使卸载了这一最昂贵的操作,仍然存在许多额外的瓶颈,例如与网络驱动程序交互以及管理限制并发性的智能合约中的数据依赖性。”

    在这个四级管道中的 GPU 并行化之间,在任何给定时刻,Solana TPU 都可以同时处理 50,000 个事务。 “这一切都可以通过一台现成的计算机来实现,价格不到 5000 美元,”Fitzgerland 解释道。 “不是超级计算机。”

    通过将 GPU 卸载到 Solana 的事务处理单元上,网络可以影响单个节点的效率。实现这一目标一直是 Solana 自成立以来的目标。

    “下一个挑战是以某种方式将块从领导节点发送到所有验证节点,并且以一种不会拥塞网络并导致吞吐量缓慢的方式进行,”Fitzgerald 继续说道。 “为此,我们提出了一种称为 Turbine 的区块传播策略。

    “通过 Turbine,我们将验证器节点构建为多个级别,其中每个级别的大小至少是其上一级的两倍。通过这种结构,这些不同的级别,确认时间最终与树的高度成正比,而不是与树中的节点数量成正比,后者要大得多。每当网络规模扩大一倍时,您都会看到确认时间略有增加,但仅此而已。”

    - - + + \ No newline at end of file diff --git a/blog/tags/poh/index.html b/blog/tags/poh/index.html index 50c8fac0e..685b41a87 100644 --- a/blog/tags/poh/index.html +++ b/blog/tags/poh/index.html @@ -9,13 +9,13 @@ - - + +

    One post tagged with "poh"

    View All Tags

    · 9 min read
    Davirain

    分布式系统中最困难的问题之一是时间一致性。事实上,一些人认为比特币的工作量证明算法最重要的功能是充当系统的去中心化时钟。在 Solana,我们相信历史证明提供了这个解决方案,并且我们已经基于它构建了一个区块链。

    去中心化网络通过可信的集中式计时解决方案解决了这个问题。例如,谷歌的 Spanner 在其数据中心之间使用同步原子钟。谷歌的工程师以非常高的精度同步这些时钟并不断维护它们。

    在区块链等对抗性系统中,这个问题更加困难。网络中的节点不能信任外部时间源或消息中出现的任何时间戳。例如,哈希图通过“中值”时间戳解决了这个问题。网络看到的每条消息都由网络的绝大多数人签名和时间戳。消息的时间戳中位数就是 Hashgraph 所说的“公平”排序。每条消息都必须传播到系统中的绝大多数节点,然后在消息收集到足够的签名后,整个集合需要传播到整个网络。正如您可以想象的那样,这确实很慢。

    如果您可以简单地信任编码到消息中的时间戳怎么办?大量的分布式系统优化将突然可供您使用。例如。

    info

    同步时钟很有趣,因为它们可以用来提高分布式算法的性能。它们使得用本地计算取代通信成为可能。

    — Liskov, B. 分布式系统中同步时钟的实际应用

    在我们的例子中,这意味着高吞吐量、高性能的区块链

    历史证明

    如果您可以证明消息是在事件之前和之后的某个时间发生的,而不是信任时间戳,该怎么办?当您拍摄《纽约时报》封面的照片时,您正在创建一个证据,证明您的照片是在该报纸出版后拍摄的,或者您有某种方式影响《纽约时报》的出版内容。通过历史证明,您可以创建历史记录,证明事件在特定时刻发生。

    历史时间戳证明

    历史证明是一种高频可验证延迟函数。可验证延迟函数需要特定数量的连续步骤来进行评估,但会产生可以有效且公开验证的独特输出。

    我们的具体实现使用顺序原像抗散列,该散列连续地运行在自身上,并将先前的输出用作下一个输入。定期记录计数和当前输出。

    对于 SHA256 哈希函数,如果不使用 2^2⁸ 核心进行强力攻击,则该过程不可能并行化。

    然后我们可以确定每个计数器在生成时已经经过了实时时间,并且每个计数器记录的顺序与实时时的顺序相同。

    时间上限

    将消息记录到历史证明序列中

    通过将数据的散列附加到先前生成的状态,可以将数据插入到序列中。状态、输入数据和计数均已发布。附加输入会导致所有未来的输出发生不可预测的变化。并行化仍然是不可能的,并且只要散列函数是原像和抗碰撞的,就不可能创建一个在未来生成所需散列的输入,或者创建具有相同散列的替代历史记录。我们可以证明任意两个追加操作之间经过的时间。我们可以证明数据是在附加之前的某个时间创建的。就像我们知道《纽约时报》上刊登的事件发生在报纸撰写之前。

    时间下限

    历史证明的时间下限

    历史证明的输入可以引用历史证明本身。反向引用可以作为带有用户签名的签名消息的一部分插入,因此如果没有用户私钥就无法对其进行修改。这就像以《纽约时报》为背景拍照一样。因为此消息包含 0xdeadc0de 哈希值,所以我们知道它是在创建计数 510144806912 之后生成的。

    但由于该消息也被插入回历史证明流中,就好像您以《纽约时报》为背景拍了一张照片,第二天《纽约时报》发布了这张照片。我们知道该照片的内容在特定日期之前和之后存在。

    确认

    虽然记录的序列只能在单个 CPU 内核上生成,但可以并行验证输出。

    并行验证

    每个记录的切片都可以在单独的核心上从头到尾进行验证,所需时间仅为生成时间的 1/(核心数)。因此,具有 4000 个核心的现代 GPU 可以在 0.25 毫秒内验证一秒。

    ASICS 亚瑟士

    是不是每个 CPU 都不同,有些 CPU 比其他 CPU 快得多?您如何真正相信我们的 SHA256 循环生成的“时间”是准确的?

    这个主题值得单独写一篇文章,但长话短说,我们不太关心某些 CPU 是否比其他 CPU 更快,以及 ASIC 是否可以比网络可用的 CPU 更快。最重要的是 ASIC 的速度是有限的。

    我们正在使用 SHA256,并且感谢比特币,在使这种加密哈希函数变得更快方面进行了大量研究。该功能不可能通过使用更大的芯片区域(例如查找表)或在不影响时钟速度的情况下展开它来加速。 Intel 和 AMD 都发布了可以在 1.75 个周期内完成一轮 SHA256 的消费类芯片。

    因此,我们非常确定定制 ASIC 的速度不会快 100 倍,更不用说 1000 倍了,而且很可能会在网络可用速度的 30% 以内。我们可以构建利用这个界限的协议,并且只允许攻击者有非常有限的、容易检测到的、短暂的拒绝服务攻击机会。下一篇文章将详细介绍这一点!

    代码

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

    - - + + \ No newline at end of file diff --git a/blog/tags/runtime/index.html b/blog/tags/runtime/index.html index 45d678fef..b74048fd5 100644 --- a/blog/tags/runtime/index.html +++ b/blog/tags/runtime/index.html @@ -9,13 +9,13 @@ - - + +

    One post tagged with "runtime"

    View All Tags

    · 7 min read
    Davirain

    在这篇博文中,我们将探讨 Solana 的并行智能合约运行时 Sealevel。在开始之前,需要考虑的一件事是 EVM 和 EOS 基于 WASM 的运行时都是单线程的。这意味着一次一个合约会修改区块链状态。我们在 Solana 中构建的是一个运行时,可以使用验证器可用的尽可能多的内核并行处理数万个合约。

    Solana 之所以能够并行处理事务,是因为 Solana 事务描述了事务在执行时将读取或写入的所有状态。这不仅允许非重叠事务并发执行,还允许仅读取相同状态的事务并发执行。

    程序和帐户

    Cloudbreak,我们的帐户数据库,是公钥到帐户的映射。账户维护余额和数据,其中数据是字节向量。帐户有一个“所有者”字段。所有者是管理帐户状态转换的程序的公钥。程序是代码,没有状态。他们依赖分配给他们的账户中的数据向量来进行状态转换。

    1. 程序只能更改其拥有的帐户的数据。

    2. 程序只能借记其拥有的账户。

    3. 任何程序都可以存入任何帐户。

    4. 任何程序都可以读取任何帐户。

    默认情况下,所有帐户一开始均由系统程序拥有。

    1. 系统程序是唯一可以分配帐户所有权的程序。

    2. 系统程序是唯一可以分配零初始化数据的程序。

    3. 帐户所有权的分配在帐户的生命周期内只能发生一次。

    用户定义的程序由加载程序加载。加载程序能够将帐户中的数据标记为可执行。用户执行以下事务来加载自定义程序:

    1. 创建一个新的公钥。

    2. 将硬币转移到钥匙上。

    3. 告诉系统程序分配内存。

    4. 告诉系统程序将帐户分配给加载程序。

    5. 将字节码分块上传到内存中。

    6. 告诉 Loader 程序将内存标记为可执行文件。

    此时,加载器对字节码进行验证,字节码加载到的账户就可以作为可执行程序了。新帐户可以标记为由用户定义的程序拥有。

    这里的关键见解是程序是代码,并且在我们的键值存储中,存在程序的某些键子集,并且只有该程序具有写访问权限。

    交易

    事务指定一个指令向量。每条指令都包含程序、程序指令以及交易想要读写的账户列表。该接口的灵感来自于设备的低级操作系统接口:

    size_t readv(int d, const struct iovec *iov, int iovcnt);

    struct iovec {
    char *iov_base; /* Base address. */
    size_t iov_len; /* Length. */
    };

    readv 或 writev 等接口提前告诉内核用户想要读取或写入的所有内存。这允许操作系统预取、准备设备,并在设备允许的情况下并发执行操作。

    在 Solana 上,每条指令都会提前告诉虚拟机要读取和写入哪些帐户。这就是我们对VM进行优化的根源。

    1. 对数以百万计的待处理交易进行排序。

    2. 并行安排所有非重叠事务。

    更重要的是,我们可以利用 CPU 和 GPU 硬件的设计方式。

    SIMD 指令允许在多个数据流上执行一段代码。这意味着 Sealevel 可以执行额外的优化,这是 Solana 设计所独有的:

    1. 按程序 ID 对所有指令进行排序。

    2. 同时在所有帐户上运行相同的程序。

    要了解为什么这是一个如此强大的优化,请查看 CUDA 开发人员指南

    info

    CUDA 架构是围绕可扩展的多线程流多处理器 (SM) 阵列构建的。当主机 CPU 上的 CUDA 程序调用内核网格时,网格的块将被枚举并分配给具有可用执行能力的多处理器。

    现代 Nvidia GPU 拥有 4000 个 CUDA 核心,但大约有 50 个多处理器。虽然多处理器一次只能执行一条程序指令,但它可以并行执行超过 80 个不同输入的指令。因此,如果 Sealvel 加载的传入事务都调用相同的程序指令(例如 CryptoKitties::BreedCats),Solana 可以在所有可用的 CUDA 核心上同时执行所有事务。

    性能方面没有免费的午餐,因此为了使 SIMD 优化可行,执行的指令应该包含少量分支,并且都应该采用相同的分支。多处理器受到批处理中执行速度最慢的路径的限制。即使考虑到这一点,与单线程运行时相比,通过 Sealevel 进行的并行处理在区块链网络的运行方式方面呈现出基础性的发展,从而实现了极高的吞吐量和可用性。

    - - + + \ No newline at end of file diff --git a/blog/tags/sealevel/index.html b/blog/tags/sealevel/index.html index 8fadcaf91..b40a8964a 100644 --- a/blog/tags/sealevel/index.html +++ b/blog/tags/sealevel/index.html @@ -9,13 +9,13 @@ - - + +

    One post tagged with "sealevel"

    View All Tags

    · 7 min read
    Davirain

    在这篇博文中,我们将探讨 Solana 的并行智能合约运行时 Sealevel。在开始之前,需要考虑的一件事是 EVM 和 EOS 基于 WASM 的运行时都是单线程的。这意味着一次一个合约会修改区块链状态。我们在 Solana 中构建的是一个运行时,可以使用验证器可用的尽可能多的内核并行处理数万个合约。

    Solana 之所以能够并行处理事务,是因为 Solana 事务描述了事务在执行时将读取或写入的所有状态。这不仅允许非重叠事务并发执行,还允许仅读取相同状态的事务并发执行。

    程序和帐户

    Cloudbreak,我们的帐户数据库,是公钥到帐户的映射。账户维护余额和数据,其中数据是字节向量。帐户有一个“所有者”字段。所有者是管理帐户状态转换的程序的公钥。程序是代码,没有状态。他们依赖分配给他们的账户中的数据向量来进行状态转换。

    1. 程序只能更改其拥有的帐户的数据。

    2. 程序只能借记其拥有的账户。

    3. 任何程序都可以存入任何帐户。

    4. 任何程序都可以读取任何帐户。

    默认情况下,所有帐户一开始均由系统程序拥有。

    1. 系统程序是唯一可以分配帐户所有权的程序。

    2. 系统程序是唯一可以分配零初始化数据的程序。

    3. 帐户所有权的分配在帐户的生命周期内只能发生一次。

    用户定义的程序由加载程序加载。加载程序能够将帐户中的数据标记为可执行。用户执行以下事务来加载自定义程序:

    1. 创建一个新的公钥。

    2. 将硬币转移到钥匙上。

    3. 告诉系统程序分配内存。

    4. 告诉系统程序将帐户分配给加载程序。

    5. 将字节码分块上传到内存中。

    6. 告诉 Loader 程序将内存标记为可执行文件。

    此时,加载器对字节码进行验证,字节码加载到的账户就可以作为可执行程序了。新帐户可以标记为由用户定义的程序拥有。

    这里的关键见解是程序是代码,并且在我们的键值存储中,存在程序的某些键子集,并且只有该程序具有写访问权限。

    交易

    事务指定一个指令向量。每条指令都包含程序、程序指令以及交易想要读写的账户列表。该接口的灵感来自于设备的低级操作系统接口:

    size_t readv(int d, const struct iovec *iov, int iovcnt);

    struct iovec {
    char *iov_base; /* Base address. */
    size_t iov_len; /* Length. */
    };

    readv 或 writev 等接口提前告诉内核用户想要读取或写入的所有内存。这允许操作系统预取、准备设备,并在设备允许的情况下并发执行操作。

    在 Solana 上,每条指令都会提前告诉虚拟机要读取和写入哪些帐户。这就是我们对VM进行优化的根源。

    1. 对数以百万计的待处理交易进行排序。

    2. 并行安排所有非重叠事务。

    更重要的是,我们可以利用 CPU 和 GPU 硬件的设计方式。

    SIMD 指令允许在多个数据流上执行一段代码。这意味着 Sealevel 可以执行额外的优化,这是 Solana 设计所独有的:

    1. 按程序 ID 对所有指令进行排序。

    2. 同时在所有帐户上运行相同的程序。

    要了解为什么这是一个如此强大的优化,请查看 CUDA 开发人员指南

    info

    CUDA 架构是围绕可扩展的多线程流多处理器 (SM) 阵列构建的。当主机 CPU 上的 CUDA 程序调用内核网格时,网格的块将被枚举并分配给具有可用执行能力的多处理器。

    现代 Nvidia GPU 拥有 4000 个 CUDA 核心,但大约有 50 个多处理器。虽然多处理器一次只能执行一条程序指令,但它可以并行执行超过 80 个不同输入的指令。因此,如果 Sealvel 加载的传入事务都调用相同的程序指令(例如 CryptoKitties::BreedCats),Solana 可以在所有可用的 CUDA 核心上同时执行所有事务。

    性能方面没有免费的午餐,因此为了使 SIMD 优化可行,执行的指令应该包含少量分支,并且都应该采用相同的分支。多处理器受到批处理中执行速度最慢的路径的限制。即使考虑到这一点,与单线程运行时相比,通过 Sealevel 进行的并行处理在区块链网络的运行方式方面呈现出基础性的发展,从而实现了极高的吞吐量和可用性。

    - - + + \ No newline at end of file diff --git a/blog/tags/solana/index.html b/blog/tags/solana/index.html index 1368d4d0e..c88d66047 100644 --- a/blog/tags/solana/index.html +++ b/blog/tags/solana/index.html @@ -9,8 +9,8 @@ - - + +
    @@ -19,7 +19,7 @@ 通常由特定程序(通常是一个智能合约)关联额外的账户。该账号没有私钥,故除程序本身外,无法完成数据签名,无法完成完整的数据交易。

    • ADA (Account Derived Account)

    由 createWithSeed 方法产生。 有一个账号公钥派生出来的关联账户,数据签名权限属于主账号。也即,需要主账号的签名才能完成完整的数据交易。

    solana中,根据数据签名,决定了数据的真实所有权。即 我的数据我做主

    本文主要分析这两种账号的异同。

    地址生成逻辑介绍如下

    • PDA 地址生成规则
    1. buffer = [seed,programId,"ProgramDerivedAddress"]
    2. 对buffer 取 sha256
    3. 如果在曲线上,那么抛出error, 如果不在,那么直接返回作为 使用地址

    createProgramAddressSync

    • ADA 生成
    1. buffer=[fromPublicKey,seed,programId]
    2. buffer 取 sha256, 直接返回

    createWithSeed

    区别在于,数据的托管使用逻辑.

    • ADA 数据签名权限,在于账户本身。即 我的数据我做主,未经允许(我未签名)不能修改。
    • PDA 数据签名权限在于合约。经过程序签名,可以修改 account 的数据和提取其中的sol。

    ADA 账号使用

    数据操作,有配套的函数对应,内部包含 xxxxWithSeedParams 类型的参数,完成对应的操作。 操作数据,需要 主账户的签名,这一点决定了,账号的真实所有权。

    • SystemProgram.createAccountWithSeed 初始化账号
    • SystemProgram.assign 重新分配owner
    • SystemProgram.allocate 分配空间
    • SystemProgram.transfer 转移SOL

    PDA 账号使用

    • 客户端只用于账户地址推导,不能初始化。初始化过程在合约内部完成。
    • 因其签名权限,必须在合约内部完成。他的操作权限完全属于智能合约。

    ADA 账号使用 example

      const seed = "ada.creator";

    // 初始化ada 账户
    let ada_account = await web3.PublicKey.createWithSeed(
    signer.publicKey,
    seed,
    program
    );
    console.log("ada_account address: ", ada_account.toBase58());

    let ada_info = await connection.getAccountInfo(ada_account);

    // 根据是否存在账号,决定是否初始化
    if (ada_info) {
    console.log(ada_info);
    } else {
    console.log("ada account not found");
    const transaction = new web3.Transaction().add(
    web3.SystemProgram.createAccountWithSeed({
    newAccountPubkey: ada_account,
    fromPubkey: signer.publicKey,
    basePubkey: signer.publicKey,
    programId: program,
    seed,
    lamports: web3.LAMPORTS_PER_SOL,
    space: 20,
    })
    );

    PDA 使用 example

    客户端部分代码逻辑

    const pda_seed = "pda.creator";

    const obj = new Model();

    const [pda, bump_seed] = web3.PublicKey.findProgramAddressSync(
    [signer.publicKey.toBuffer(), new TextEncoder().encode(movie.title)],
    program
    );

    console.log("pda address : ", pda.toBase58());

    const instruction = new web3.TransactionInstruction({
    keys: [
    {
    // 付钱的账户
    pubkey: signer.publicKey,
    isSigner: true,
    isWritable: false,
    },
    {
    // PDA将存储数据
    pubkey: pda,
    isSigner: false,
    isWritable: true,
    },
    {
    // 系统程序将用于创建PDA
    pubkey: web3.SystemProgram.programId,
    isSigner: false,
    isWritable: false,
    },
    ],
    // 传输数据
    data: obj.serialize(),
    programId: program,
    });

    const transaction = new web3.Transaction().add(instruction);

    const signature = await web3.sendAndConfirmTransaction(
    connection,
    transaction,
    [signer]
    );

    console.log(signature);

    合约部分代码逻辑

    // 获取账户迭代器
    let account_info_iter = &mut accounts.iter();

    // 获取账户
    let initializer = next_account_info(account_info_iter)?;
    let pda_account = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    // 构造PDA账户
    let (pda, bump_seed) =
    Pubkey::find_program_address(&[initializer.key.as_ref(), title.as_bytes()], program_id);

    // 和客户端比对
    if pda != *pda_account.key {
    msg!("Invalid seeds for PDA");
    return Err(ProgramError::InvalidArgument);
    }

    // 计算所需的租金
    let rent = Rent::get()?;
    let rent_lamports = rent.minimum_balance(total_len);

    // 创建账户
    invoke_signed(
    &system_instruction::create_account(
    initializer.key,
    pda_account.key,
    rent_lamports,
    total_len
    .try_into()
    .map_err(|_| Error::ConvertUsizeToU64Failed)?,
    program_id,
    ),
    &[
    initializer.clone(),
    pda_account.clone(),
    system_program.clone(),
    ],
    &[&[initializer.key.as_ref(), title.as_bytes(), &[bump_seed]]],
    )?;

    // MovieAccountState 定义的state类型
    let mut account_data =
    try_from_slice_unchecked::<MovieAccountState>(&pda_account.data.borrow()).unwrap();

    account_data.title = title;
    account_data.rating = rating;
    account_data.description = description;
    account_data.is_initialized = true;

    // 写入pda 数据本身
    account_data.serialize(&mut &mut pda_account.data.borrow_mut()[..])?;

    参考资料

    - - + + \ No newline at end of file diff --git a/blog/tags/solana/page/2/index.html b/blog/tags/solana/page/2/index.html index 43cad1b56..1877a5d59 100644 --- a/blog/tags/solana/page/2/index.html +++ b/blog/tags/solana/page/2/index.html @@ -9,13 +9,13 @@ - - + +

    13 posts tagged with "solana"

    View All Tags

    · 3 min read
    YanAemons

    報錯日志

    在使用solana-cli時候,鑑於一些依賴版本限制,會用到cli14.xx(主網版本),而不是16.xx(測試網版本)

    例如,在使用solana-cli版本爲1.14.17, anchor版本爲0.26.0的環境中, anchor init創建一個新項目後運行 anchor build會發生以下錯誤:

    error: package constant_time_eq v0.3.0 cannot be built because it requires rustc 1.66.0 or newer, while the currently active rustc version is 1.62.0-dev

    報錯原因

    使用的solana-cli版本在14.xxx, cli內自帶的rustc版本過老,無法編譯較新的依賴

    解決方案

    1. 升級solana-cli至最新版本

    solana-install update

    2.指定依賴包版本

    需要在Cargo.toml文件下指定以下依賴版本

    getrandom = { version = "0.2.9", features = ["custom"] }  
    solana-program = "=1.14.17"
    winnow="=0.4.1"
    toml_datetime="=0.6.1"
    blake3 = "=1.3.1"

    運行cargo clean後重新運行anchor build即可解決

    監聽程序log監聽到兩次

    在使用program.addEventListener()有可能聽到兩次相同的事件,其中一次的txSign會是“1111111111111111111111111111111111111111111111111111111111111111”, 這是因爲監聽到了模擬時的交易哈系,我們只需要在監聽到該交易哈系時拋棄即可

    program.addEventListener("eventName", (event, slot, signature) => {
    if (signature === '1111111111111111111111111111111111111111111111111111111111111111') return

    // do ur stuff
    })

    然而,有時websocket訂閱也會多次返回實際簽名。如果是這種情況,您可以使用一些緩存解決方案。例如,創建一個具有一定長度限制的集合,在此處添加簽名並檢查該集合中是否存在新簽名:

    const handledSignatures = new Set<string>()
    const maxHandledSignaturesLen = 100

    program.addEventListener("eventName", (event, slot, signature) => {
    if (signature === '1111111111111111111111111111111111111111111111111111111111111111') return
    if (handledSignatures.has(signature)) return

    // do ur stuff

    handledSignatures.add(signature)
    if (handledSignatures.size > maxHandledSignaturesLen) {
    handledSignatures.delete(handledSignatures.values().next().value)
    }
    })

    · 17 min read
    Davirain

    Solana上,状态压缩是一种创建离链数据的“指纹”(或哈希)并将该指纹存储在链上以进行安全验证的方法。有效地利用Solana账本的安全性来安全验证离链数据,以确保其未被篡改。

    这种“压缩”方法使得Solana的程序和dApps能够使用廉价的区块链账本空间来安全存储数据,而不是更昂贵的账户空间。

    这是通过使用一种特殊的二叉树结构,称为并发默克尔树,对每个数据片段(称为 leaf )创建哈希,将它们哈希在一起,并仅将最终哈希存储在链上来实现的。

    什么是状态压缩?

    简单来说,状态压缩使用“树”结构将链外数据以确定性的方式进行加密哈希,计算出一个最终的哈希值,并将其存储在链上。

    这些树是通过这个“确定性”过程创建的:

    • 获取任何数据
    • 创建这些数据的哈希值
    • 将此哈希值存储为树底部的 leaf
    • 每个 leaf 对都会被一起哈希,创建一个 branch
    • 每个 branch 然后一起哈希
    • 不断攀爬树木并将相邻的树枝连接在一起
    • 树顶上一旦到达,就会产生最后的 root hash

    这个 root hash 然后存储在链上,作为每个叶子节点中所有数据的可验证证据。这样任何人都可以通过加密验证树中所有离链数据,而实际上只需在链上存储少量数据。因此,由于这种"状态压缩",大大降低了存储/证明大量数据的成本。

    默克尔树和并发默克尔树

    Solana的状态压缩使用了一种特殊类型的默克尔树,允许对任何给定的树进行多次更改,同时仍然保持树的完整性和有效性。

    这棵特殊的树被称为“并发默克尔树”,有效地在链上保留了树的“更改日志”。允许在一个证明失效之前对同一棵树进行多次快速更改(即在同一个区块中)。

    默克尔树是什么?

    默克尔树,有时也被称为“哈希树”,是一种基于哈希的二叉树结构,其中每个leaf节点都被表示为其内部数据的加密哈希。而每个非叶节点,也被称为“branch节点”,则被表示为其子叶节点哈希的哈希值。

    每个分支也被哈希在一起,沿着树向上爬,直到最后只剩下一个哈希。这个最终的哈希,称为 root hash 或者"根",可以与一个"证明路径"结合使用,来验证存储在叶节点中的任何数据。

    一旦计算出最终的根哈希值(root hash),可以通过重新计算特定叶子(leaf)节点的数据和每个相邻分支的哈希标签(称为“证明路径”)来验证存储在节点中的任何数据。将这个“重新哈希”与根哈希值进行比较,可以验证底层叶子数据的准确性。如果它们匹配,数据就被验证为准确的。如果它们不匹配,叶子数据已被更改。

    只要需要,原始叶子数据可以通过对新的叶子数据进行哈希运算并重新计算根哈希值来进行更改,方法与原始根哈希值的计算方式相同。然后,这个新的根哈希值用于验证任何数据,并且有效地使之前的根哈希值和证明无效。因此,对这些传统的默克尔树的每一次更改都需要按顺序执行。

    info

    当使用默克尔树时,更改叶子数据并计算新的根哈希的过程可能是非常常见的事情!虽然这是树的设计要点之一,但它可能导致最显著的缺点之一:快速变化。

    什么是并发默克尔树?

    在高吞吐量的应用中,比如在Solana运行时中,对于链上传统Merkle树的更改请求可能会相对快速地连续接收到验证者(例如在同一个槽中)。每个叶子数据的更改仍然需要按顺序执行。这导致每个后续的更改请求都会失败,因为根哈希和证明已经被同一槽中之前的更改请求无效化了。

    进入,并发默克尔树。

    并发默克尔树存储了最近更改的安全日志、它们的根哈希以及用于推导根哈希的证明。这个日志缓冲区存储在链上的每个树对应的特定账户中,最大记录数为(也称为 maxBufferSize )。

    当同一时隙内的验证者收到多个叶子数据变更请求时,链上并发 Merkle 树可以将这个“变更日志缓冲区”作为更可接受的证明的真实来源。有效地允许在同一时隙内对同一棵树进行多达 maxBufferSize 次变更。大幅提升吞吐量。

    并发默克尔树的大小调整

    创建这种链上树时,有三个值将决定您的树的大小、创建树的成本以及对树的并发更改数量:

    1. max depth 最大深度
    2. max buffer size 最大缓冲区大小
    3. canopy depth

    max depth

    树的“最大深度”是从任何数据 leaf 到树的 root 所需的最大跳数。

    由于默克尔树是二叉树,每个叶子节点只与另一个叶子节点相连;存在于一个 leaf pair 中。

    因此,树的 maxDepth 被用来确定可以通过简单的计算存储在树中的最大节点数(也称为数据或 leafs

    nodes_count = 2 ^ maxDepth

    由于树的深度必须在创建树时设置,您必须决定您希望树存储多少个数据。然后使用上述简单的计算,您可以确定存储数据的最低 maxDepth

    示例1:铸造100个NFTs

    如果你想创建一个用于存储100个压缩NFT的树,我们至少需要"100个叶子"或"100个节点"。

    // maxDepth=6 -> 64 nodes
    2^6 = 64

    // maxDepth=7 -> 128 nodes
    2^7 = 128

    因此,我们需要一个最大深度为 7 的树,以存储 100 个数据。

    例子2:铸造15000个NFTs

    如果你想创建一个用于存储15000个压缩NFT的树,我们将需要至少"15000个叶子"或"15000个节点"。

    // maxDepth=13 -> 8192 nodes
    2^13 = 8192

    // maxDepth=14 -> 16384 nodes
    2^14 = 16384

    因此,我们需要一个最大深度为 14 的树,以存储 15000 个数据。

    最大深度越高,成本越高

    创建树时, maxDepth 值将是成本的主要驱动因素之一,因为您将在创建树时支付这笔成本。最大树深度越高,您可以存储的数据指纹(也称为哈希)越多,成本就越高。

    max buffer size

    max buffer size” 实际上是树上可以发生的最大变化数量,同时仍然有效的 root hash

    由于根哈希有效地是所有叶子数据的单一哈希,改变任何一个叶子将使得所有后续尝试改变常规树的叶子所需的证明无效。

    但是使用并发树,对于这些证明来说,实际上有一个更新的日志。这个日志缓冲区的大小和设置是通过这个 maxBufferSize 值在树创建时完成的。

    Canopy depth

    Canopy depth”,有时也称为Canopy大小,是指在任何给定的证明路径上缓存/存储在链上的证明节点数量。

    在对 leaf 执行更新操作时,例如转让所有权(例如出售压缩的NFT),必须使用完整的证明路径来验证叶子节点的原始所有权,从而允许进行更新操作。此验证是使用完整的证明路径来正确计算当前的 root hash (或通过链上的“并发缓冲区”缓存的任何 root hash )来执行的。

    树的最大深度越大,执行此验证所需的证明节点就越多。例如,如果您的最大深度是 14 ,则需要使用 14 个总的证明节点进行验证。随着树的增大,完整的证明路径也会变得更长。

    通常情况下,每个这些证明节点都需要在每个树更新事务中包含。由于每个证明节点的值在事务中占用 32 bytes (类似于提供公钥),较大的树很快就会超过最大事务大小限制。

    进入CanopyCanopy可以在链上存储一定数量的验证节点(对于任何给定的验证路径)。这样可以在每个更新交易中包含较少的验证节点,从而保持整体交易大小低于限制。

    例如,深度为 14 的树需要 14 个总的验证节点。而有 10Canopy的情况下,每个更新事务只需要提交 4 个验证节点。

    Canopy深度值越大,成本越高

    canopyDepth 值也是创建树时成本的主要因素,因为您将在树的创建时支付这个成本。canopyDepth越高,链上存储的数据证明节点越多,成本也越高。

    较小的Canopy限制了可组合性

    虽然树的创建成本随着Canopy的高度而增加,但较低的Canopy将需要在每个更新事务中包含更多的证明节点。所需提交的节点越多,事务的大小就越大,因此超过事务大小限制就越容易。

    这也适用于任何其他试图与您的树/叶子进行交互的Solana程序或dApp。如果您的树需要太多的证明节点(因为Canopy深度较低),那么任何其他链上程序可能提供的额外操作都将受到其特定指令大小加上您的证明节点列表大小的限制。这限制了可组合性,并限制了您的特定树的潜在附加效用。

    例如,如果您的树被用于压缩的非同质化代币(NFTs),并且Canopy深度非常低,一个NFT市场可能只能支持简单的NFT转移,而无法支持链上竞标系统。

    创建一棵树的成本

    创建并发 Merkle 树的成本基于树的大小参数: maxDepthmaxBufferSizecanopyDepth 。这些值都用于计算在链上存在树所需的链上存储空间(以字节为单位)。

    一旦计算出所需的空间(以字节为单位),并使用 getMinimumBalanceForRentExemption RPC方法,请求在链上分配这些字节所需的费用(以lamports为单位)。

    在JavaScript中计算树木成本

    @solana/spl-account-compression 包中,开发人员可以使用 getConcurrentMerkleTreeAccountSize 函数来计算给定树大小参数所需的空间。

    然后使用 getMinimumBalanceForRentExemption 函数来获取在链上分配所需空间的最终成本(以lamports计算)。

    然后确定以lamports计算的成本,使得这个大小的账户免除租金,与其他账户创建类似。

    // calculate the space required for the tree
    const requiredSpace = getConcurrentMerkleTreeAccountSize(
    maxDepth,
    maxBufferSize,
    canopyDepth,
    );

    // get the cost (in lamports) to store the tree on-chain
    const storageCost = await connection.getMinimumBalanceForRentExemption(
    requiredSpace,
    );

    示例费用

    以下是几个不同树大小的示例成本,包括每个树可能的叶节点数量:

    例子 #1:16,384个节点,成本为0.222 SOL

    • 最大深度为 14 ,最大缓冲区大小为 64
    • 叶节点的最大数量: 16,384
    • 创建 0.222 SOLCanopy深度大约需要 0 的成本

    例子 #2:16,384个节点,成本为1.134 SOL

    • 最大深度为 14 ,最大缓冲区大小为 64
    • 叶节点的最大数量: 16,384
    • 创建 1.134 SOLCanopy深度大约需要 11 的成本

    示例 #3:1,048,576个节点,成本为1.673 SOL

    • 最大深度为 20 ,最大缓冲区大小为 256
    • 叶节点的最大数量: 1,048,576
    • 创建 1.673 SOLCanopy深度大约需要 10 的成本

    示例#4:1,048,576个节点,成本为15.814 SOL

    • 最大深度为 20 ,最大缓冲区大小为 256
    • 叶节点的最大数量: 1,048,576
    • 创建 15.814 SOLCanopy深度大约需要 15 的成本

    压缩的NFTs

    压缩的NFT是Solana上状态压缩的最受欢迎的应用之一。通过压缩,一个拥有一百万个NFT的收藏品可以以 ~50 SOL 的价格铸造,而不是其未压缩的等价收藏品。

    开发者指南:

    阅读我们的开发者指南,了解如何铸造和转移压缩的NFT

    · 3 min read
    Davirain

    欢迎来到Solana共学,这是一个精心设计的教程系列,供任何对Solana感兴趣的人深入学习。无论你是初学者还是有经验的开发者,这些模块都会引导你了解Solana区块链开发的基本内容。

    模块1:Solana基础

    • 区块链基本概念介绍
    • 本地程序开发环境配置
      • 原始Solana合约实现《hello, World》
      • Anchor合约框架实现《hello, World》
      • 使用Solang编译器编译solidity合约实现《hello, World》
    • BackPack钱包使用
    • 客户端开发
    • 钱包和前端
    • 自定义指令
    • 开始你自己的定制项目

    模块2:Solana高级主题

    • SPL token
    • NFTs + 使用Metaplex进行铸造
    • 在用户界面中展示NFTs
    • 创造神奇的网络货币并出售JPEG图片

    更深入的模块:深入了解Solana

    • 模块3:Rust入门,原生Solana开发,安全性,NFT质押
    • 模块4:本地环境,跨程序调用,测试,质押应用开发
    • 模块5:Anchor入门,全栈Anchor应用开发
    • 模块6:发布周,随机性,完善

    特别主题:超越基础

    • Solana程序中的环境变量
    • Solana支付,版本化事务,Rust宏
    • Solana程序安全:签名授权,所有者检查,重新初始化攻击,PDA共享等
    • 使用Solidity编写Solana合约
    • 发行Token2020,压缩NFT
    • 在Solana中使用The Graph,Oracles Pyth SDK
    • TipLink使用,如何在Quicknode和Helius申请RPC endpoint
    • 等等...

    和我们一起,在这全面的指南中探索Solana的每一个方面。从最基本的内容到安全和合约开发的复杂方面,Solana共学为每一位Solana爱好者提供了内容。

    敬请期待,如果有任何问题或需要进一步的协助,请随时与我们联系。欢迎来到Solana共学!

    - - + + \ No newline at end of file diff --git a/blog/tags/state-compression/index.html b/blog/tags/state-compression/index.html index 84fe77de4..1586c33b1 100644 --- a/blog/tags/state-compression/index.html +++ b/blog/tags/state-compression/index.html @@ -9,13 +9,13 @@ - - + +

    One post tagged with "state-compression"

    View All Tags

    · 17 min read
    Davirain

    Solana上,状态压缩是一种创建离链数据的“指纹”(或哈希)并将该指纹存储在链上以进行安全验证的方法。有效地利用Solana账本的安全性来安全验证离链数据,以确保其未被篡改。

    这种“压缩”方法使得Solana的程序和dApps能够使用廉价的区块链账本空间来安全存储数据,而不是更昂贵的账户空间。

    这是通过使用一种特殊的二叉树结构,称为并发默克尔树,对每个数据片段(称为 leaf )创建哈希,将它们哈希在一起,并仅将最终哈希存储在链上来实现的。

    什么是状态压缩?

    简单来说,状态压缩使用“树”结构将链外数据以确定性的方式进行加密哈希,计算出一个最终的哈希值,并将其存储在链上。

    这些树是通过这个“确定性”过程创建的:

    • 获取任何数据
    • 创建这些数据的哈希值
    • 将此哈希值存储为树底部的 leaf
    • 每个 leaf 对都会被一起哈希,创建一个 branch
    • 每个 branch 然后一起哈希
    • 不断攀爬树木并将相邻的树枝连接在一起
    • 树顶上一旦到达,就会产生最后的 root hash

    这个 root hash 然后存储在链上,作为每个叶子节点中所有数据的可验证证据。这样任何人都可以通过加密验证树中所有离链数据,而实际上只需在链上存储少量数据。因此,由于这种"状态压缩",大大降低了存储/证明大量数据的成本。

    默克尔树和并发默克尔树

    Solana的状态压缩使用了一种特殊类型的默克尔树,允许对任何给定的树进行多次更改,同时仍然保持树的完整性和有效性。

    这棵特殊的树被称为“并发默克尔树”,有效地在链上保留了树的“更改日志”。允许在一个证明失效之前对同一棵树进行多次快速更改(即在同一个区块中)。

    默克尔树是什么?

    默克尔树,有时也被称为“哈希树”,是一种基于哈希的二叉树结构,其中每个leaf节点都被表示为其内部数据的加密哈希。而每个非叶节点,也被称为“branch节点”,则被表示为其子叶节点哈希的哈希值。

    每个分支也被哈希在一起,沿着树向上爬,直到最后只剩下一个哈希。这个最终的哈希,称为 root hash 或者"根",可以与一个"证明路径"结合使用,来验证存储在叶节点中的任何数据。

    一旦计算出最终的根哈希值(root hash),可以通过重新计算特定叶子(leaf)节点的数据和每个相邻分支的哈希标签(称为“证明路径”)来验证存储在节点中的任何数据。将这个“重新哈希”与根哈希值进行比较,可以验证底层叶子数据的准确性。如果它们匹配,数据就被验证为准确的。如果它们不匹配,叶子数据已被更改。

    只要需要,原始叶子数据可以通过对新的叶子数据进行哈希运算并重新计算根哈希值来进行更改,方法与原始根哈希值的计算方式相同。然后,这个新的根哈希值用于验证任何数据,并且有效地使之前的根哈希值和证明无效。因此,对这些传统的默克尔树的每一次更改都需要按顺序执行。

    info

    当使用默克尔树时,更改叶子数据并计算新的根哈希的过程可能是非常常见的事情!虽然这是树的设计要点之一,但它可能导致最显著的缺点之一:快速变化。

    什么是并发默克尔树?

    在高吞吐量的应用中,比如在Solana运行时中,对于链上传统Merkle树的更改请求可能会相对快速地连续接收到验证者(例如在同一个槽中)。每个叶子数据的更改仍然需要按顺序执行。这导致每个后续的更改请求都会失败,因为根哈希和证明已经被同一槽中之前的更改请求无效化了。

    进入,并发默克尔树。

    并发默克尔树存储了最近更改的安全日志、它们的根哈希以及用于推导根哈希的证明。这个日志缓冲区存储在链上的每个树对应的特定账户中,最大记录数为(也称为 maxBufferSize )。

    当同一时隙内的验证者收到多个叶子数据变更请求时,链上并发 Merkle 树可以将这个“变更日志缓冲区”作为更可接受的证明的真实来源。有效地允许在同一时隙内对同一棵树进行多达 maxBufferSize 次变更。大幅提升吞吐量。

    并发默克尔树的大小调整

    创建这种链上树时,有三个值将决定您的树的大小、创建树的成本以及对树的并发更改数量:

    1. max depth 最大深度
    2. max buffer size 最大缓冲区大小
    3. canopy depth

    max depth

    树的“最大深度”是从任何数据 leaf 到树的 root 所需的最大跳数。

    由于默克尔树是二叉树,每个叶子节点只与另一个叶子节点相连;存在于一个 leaf pair 中。

    因此,树的 maxDepth 被用来确定可以通过简单的计算存储在树中的最大节点数(也称为数据或 leafs

    nodes_count = 2 ^ maxDepth

    由于树的深度必须在创建树时设置,您必须决定您希望树存储多少个数据。然后使用上述简单的计算,您可以确定存储数据的最低 maxDepth

    示例1:铸造100个NFTs

    如果你想创建一个用于存储100个压缩NFT的树,我们至少需要"100个叶子"或"100个节点"。

    // maxDepth=6 -> 64 nodes
    2^6 = 64

    // maxDepth=7 -> 128 nodes
    2^7 = 128

    因此,我们需要一个最大深度为 7 的树,以存储 100 个数据。

    例子2:铸造15000个NFTs

    如果你想创建一个用于存储15000个压缩NFT的树,我们将需要至少"15000个叶子"或"15000个节点"。

    // maxDepth=13 -> 8192 nodes
    2^13 = 8192

    // maxDepth=14 -> 16384 nodes
    2^14 = 16384

    因此,我们需要一个最大深度为 14 的树,以存储 15000 个数据。

    最大深度越高,成本越高

    创建树时, maxDepth 值将是成本的主要驱动因素之一,因为您将在创建树时支付这笔成本。最大树深度越高,您可以存储的数据指纹(也称为哈希)越多,成本就越高。

    max buffer size

    max buffer size” 实际上是树上可以发生的最大变化数量,同时仍然有效的 root hash

    由于根哈希有效地是所有叶子数据的单一哈希,改变任何一个叶子将使得所有后续尝试改变常规树的叶子所需的证明无效。

    但是使用并发树,对于这些证明来说,实际上有一个更新的日志。这个日志缓冲区的大小和设置是通过这个 maxBufferSize 值在树创建时完成的。

    Canopy depth

    Canopy depth”,有时也称为Canopy大小,是指在任何给定的证明路径上缓存/存储在链上的证明节点数量。

    在对 leaf 执行更新操作时,例如转让所有权(例如出售压缩的NFT),必须使用完整的证明路径来验证叶子节点的原始所有权,从而允许进行更新操作。此验证是使用完整的证明路径来正确计算当前的 root hash (或通过链上的“并发缓冲区”缓存的任何 root hash )来执行的。

    树的最大深度越大,执行此验证所需的证明节点就越多。例如,如果您的最大深度是 14 ,则需要使用 14 个总的证明节点进行验证。随着树的增大,完整的证明路径也会变得更长。

    通常情况下,每个这些证明节点都需要在每个树更新事务中包含。由于每个证明节点的值在事务中占用 32 bytes (类似于提供公钥),较大的树很快就会超过最大事务大小限制。

    进入CanopyCanopy可以在链上存储一定数量的验证节点(对于任何给定的验证路径)。这样可以在每个更新交易中包含较少的验证节点,从而保持整体交易大小低于限制。

    例如,深度为 14 的树需要 14 个总的验证节点。而有 10Canopy的情况下,每个更新事务只需要提交 4 个验证节点。

    Canopy深度值越大,成本越高

    canopyDepth 值也是创建树时成本的主要因素,因为您将在树的创建时支付这个成本。canopyDepth越高,链上存储的数据证明节点越多,成本也越高。

    较小的Canopy限制了可组合性

    虽然树的创建成本随着Canopy的高度而增加,但较低的Canopy将需要在每个更新事务中包含更多的证明节点。所需提交的节点越多,事务的大小就越大,因此超过事务大小限制就越容易。

    这也适用于任何其他试图与您的树/叶子进行交互的Solana程序或dApp。如果您的树需要太多的证明节点(因为Canopy深度较低),那么任何其他链上程序可能提供的额外操作都将受到其特定指令大小加上您的证明节点列表大小的限制。这限制了可组合性,并限制了您的特定树的潜在附加效用。

    例如,如果您的树被用于压缩的非同质化代币(NFTs),并且Canopy深度非常低,一个NFT市场可能只能支持简单的NFT转移,而无法支持链上竞标系统。

    创建一棵树的成本

    创建并发 Merkle 树的成本基于树的大小参数: maxDepthmaxBufferSizecanopyDepth 。这些值都用于计算在链上存在树所需的链上存储空间(以字节为单位)。

    一旦计算出所需的空间(以字节为单位),并使用 getMinimumBalanceForRentExemption RPC方法,请求在链上分配这些字节所需的费用(以lamports为单位)。

    在JavaScript中计算树木成本

    @solana/spl-account-compression 包中,开发人员可以使用 getConcurrentMerkleTreeAccountSize 函数来计算给定树大小参数所需的空间。

    然后使用 getMinimumBalanceForRentExemption 函数来获取在链上分配所需空间的最终成本(以lamports计算)。

    然后确定以lamports计算的成本,使得这个大小的账户免除租金,与其他账户创建类似。

    // calculate the space required for the tree
    const requiredSpace = getConcurrentMerkleTreeAccountSize(
    maxDepth,
    maxBufferSize,
    canopyDepth,
    );

    // get the cost (in lamports) to store the tree on-chain
    const storageCost = await connection.getMinimumBalanceForRentExemption(
    requiredSpace,
    );

    示例费用

    以下是几个不同树大小的示例成本,包括每个树可能的叶节点数量:

    例子 #1:16,384个节点,成本为0.222 SOL

    • 最大深度为 14 ,最大缓冲区大小为 64
    • 叶节点的最大数量: 16,384
    • 创建 0.222 SOLCanopy深度大约需要 0 的成本

    例子 #2:16,384个节点,成本为1.134 SOL

    • 最大深度为 14 ,最大缓冲区大小为 64
    • 叶节点的最大数量: 16,384
    • 创建 1.134 SOLCanopy深度大约需要 11 的成本

    示例 #3:1,048,576个节点,成本为1.673 SOL

    • 最大深度为 20 ,最大缓冲区大小为 256
    • 叶节点的最大数量: 1,048,576
    • 创建 1.673 SOLCanopy深度大约需要 10 的成本

    示例#4:1,048,576个节点,成本为15.814 SOL

    • 最大深度为 20 ,最大缓冲区大小为 256
    • 叶节点的最大数量: 1,048,576
    • 创建 15.814 SOLCanopy深度大约需要 15 的成本

    压缩的NFTs

    压缩的NFT是Solana上状态压缩的最受欢迎的应用之一。通过压缩,一个拥有一百万个NFT的收藏品可以以 ~50 SOL 的价格铸造,而不是其未压缩的等价收藏品。

    开发者指南:

    阅读我们的开发者指南,了解如何铸造和转移压缩的NFT

    - - + + \ No newline at end of file diff --git a/blog/tags/tower-bft/index.html b/blog/tags/tower-bft/index.html index 3a3607908..0aae6ae35 100644 --- a/blog/tags/tower-bft/index.html +++ b/blog/tags/tower-bft/index.html @@ -9,13 +9,13 @@ - - + +

    One post tagged with "tower-bft"

    View All Tags

    · 9 min read
    Davirain

    在这篇博文中,我们将探讨 Tower BFT,这是 Solana 的 PBFT 自定义实现,它更喜欢活跃性而不是一致性。 Tower BFT 在达成共识之前利用 Solana 的 PoH 作为时钟,以减少消息传递开销和延迟。

    info

    为了提供活力,如果副本无法执行请求,则必须移动到新视图。然而,当至少 2f + 1 个无故障副本处于同一视图中时,最大化时间段非常重要,并确保这段时间呈指数增长,直到执行某些请求的操作

    Solana 实现了 PBFT 的一种衍生,但有一个根本区别。历史证明(PoH)提供了达成共识之前的全球时间来源。我们的 PBFT 实现使用 PoH 作为网络时间时钟,并且副本在 PBFT 中使用的指数增长超时可以在 PoH 本身中计算和强制执行。

    PoH 是一种可验证延迟函数,以顺序哈希函数的形式实现。我们使用 VDF 的松散定义,因为验证需要(计算时间)/(核心数量)。 PoH 工作的基本原理如下:

    1. Sha256 尽可能快地循环,使得每个输出都是下一个输入。
    2. 对循环进行采样,并记录迭代次数和状态。

    记录的样本代表了编码为可验证数据结构的时间流逝。此外,该循环还可用于记录事件。

    1. 引用任何示例的消息都保证是在该示例之后创建的。

    2. 消息可以插入到循环中并与状态一起进行哈希处理。这保证了在下一次插入之前创建了一条消息。

    这种数据结构保证了嵌入事件的时间和顺序,这一核心思想是 Solana 中所有主要技术优化的基础。

    换句话说:想象一下你在一座岛上,一个瓶子漂过,里面有一个拇指驱动器。该驱动器上是 Solana PoH 分类账。仅使用 PoH 账本,您就可以计算网络中所有节点的状态。例如,如果对账本的投票尚未记录在最后 X 个哈希值中,则节点被视为失败。如果在过去的 X 个哈希中,对已签署验证消息的网络的绝大多数进行哈希处理,我们就可以认为账本是有效的。

    1. 检查此数据结构的所有节点将计算完全相同的结果,而不需要任何点对点通信。

    2. PoH 哈希唯一标识账本的该分叉;和

    3. 仅当验证投票消息所投票的 PoH 哈希值存在于账本中时,验证投票消息才有效。

    这就引出了投票和 PBFT。由于账本本身可作为可靠的网络时钟,因此我们可以在账本本身中对 PBFT 超时进行编码。

    1. 投票以 N 个哈希超时开始。

    验证者保证(通过削减)一旦对 PoH 哈希进行投票,验证者将不会投票给任何不是该投票子项的 PoH 哈希,至少 N 个哈希。

    1. 所有前任投票的超时时间加倍

    为了使操作更易于管理,投票被限制在固定的哈希周期内,我们称之为时隙。我们对时隙的目标是代表 400 毫秒左右的哈希数。每 400 毫秒,网络就有一个潜在的回滚点,但随后的每一次投票都会使网络在展开该投票之前必须停滞的实时时间加倍。

    想象一下,每个验证者在过去 12 秒内投票了 32 次。 12 秒前的投票现在有 232 个时隙的超时,即大约 54 年。实际上,这次投票永远不会被网络回滚。而最近的投票有 2 个时隙的超时,即大约 800 毫秒。随着新区块被添加到账本中,旧区块被确认的可能性越来越大,因为旧投票的时隙数量会在每个时隙或每 400 毫秒增加一倍。

    请注意,虽然这听起来像是工作量证明中的概率最终性,但事实并非如此。一旦 2/3 的验证者对某个 PoH 哈希进行了投票,该 PoH 哈希就会被规范化,并且无法回滚。这与工作量证明不同,工作量证明中没有规范化的概念。

    为了防止被网络其他部分锁定,每个验证者确保只有在看到绝大多数网络也在同一账本上投票时才进行投票。每个验证器都会监控祖先投票的超时时间何时超过预定义的阈值(例如从 5 到 10 分钟),并确保网络的绝大多数人已对包含该投票的分叉进行了投票。在实践中,验证者:

    1. 检查是否有绝大多数人对一个将承诺 10 分钟超时的插槽进行了投票

    2. 如果没有,请不要投票

    那么在分区期间网络会发生什么并且超时实际上开始到期呢?

    1. 任何已过期的投票都会被清除

    2. 当且仅当孩子有相同的超时时,祖先的超时加倍

    例如,让我们考虑当前超时的场景:

    64, 32, 16, 8, 4, 2

    如果验证者停止对 17 个插槽进行投票并再次投票,则验证者的超时结果将是:

    64, 32, 2

    还需要连续4次投票,所有祖先的暂停时间才会再次加倍。

    64, 32, 4, 2

    64, 32, 8, 4, 2

    64, 32, 16, 4, 2

    最后第四次投票将使所有超时加倍

    128, 64, 32, 16, 8, 4, 2

    这种方法允许网络连续传输区块,而不会导致账本停滞,直到绝大多数人观察到相同的账本。另一个值得注意的方面是,网络中的每个参与者都可以计算每个其他参与者的超时,而无需任何 P2P 通信。这就是 Tower BFT 异步的原因。

    我们预计会有许多微分叉很快被丢弃。当验证者检测到多个分叉时,诚实的验证者会计算每个分叉的有效权益加权超时并选择最重的一个。仅针对达到 2³² 超时的投票生成验证者奖励。因此,验证者在最重的分叉之上进行投票是兼容激励的,因为具有最大量权益加权超时的分叉将为网络产生最大量的奖励。

    - - + + \ No newline at end of file diff --git a/blog/tags/tubine/index.html b/blog/tags/tubine/index.html index c6f3bf769..9ab15a681 100644 --- a/blog/tags/tubine/index.html +++ b/blog/tags/tubine/index.html @@ -9,13 +9,13 @@ - - + +

    One post tagged with "tubine"

    View All Tags

    · 7 min read
    Davirain

    在这篇文章中,我们将探讨 Turbine,这是 Solana 的区块传播协议(受 BitTorrent 启发),它解决了区块链可扩展性的难题。

    可扩展性的困境

    区块链技术中的可扩展性三难困境都与带宽有关。在当今的大多数区块链网络中,给定每个节点的固定带宽量,增加节点数将增加将所有数据传播到所有节点所需的时间。这是一个大问题。

    然而,有无数的机会来优化数据的传播方式。有许多新颖的数据传播技术,每种技术都针对特定应用进行了优化。例如,BitTorrent 经过优化,可使用 TCP 向大量人员提供大型文件,而我参与的项目 MediaFLO 是一种针对物理层数据传播进行优化的协议,以提高无线网络上的多播效率。

    在此背景下,让我们进入 Solana 的区块传播协议 Turbine,来解释 Solana 网络如何传播数据来解决区块链可扩展性三难困境。

    Turbine 涡轮

    高性能区块链面临的挑战之一是网络如何将大量数据传播到大量节点。例如,让我们考虑一个由 20,000 个验证者组成的网络。领导者需要向所有 20,000 个验证者传输一个 128 MB 的区块(大约 500,000 个交易 @ 250 字节/交易)。简单的实现将要求领导者与每个验证者建立唯一的连接,并传输完整的 128 MB 20,000 次。根本没有足够的带宽来容纳这么多的连接。

    我们针对这个问题的解决方案 Turbine 大量借鉴了 BitTorrent,尽管两者在一些主要技术细节上有所区别。 Turbine 针对流式传输进行了优化,仅使用 UDP 传输数据,并在领导者(区块生产者)流式传输数据时通过网络实现每个数据包的随机路径。领导者将块分成大小最大为 64KB 的数据包。对于 128MB 的块,领导者会生成 2,000 个 64KB 数据包,并将每个数据包传输到不同的验证器。

    反过来,每个验证器将数据包重新传输给一组我们称为邻居的对等点。您可以将网络可视化为邻域树,从而使网络能够增长到远远超过 1,000 个验证者:

    每个邻域负责将其部分数据传输到其下面的每个邻域。

    如果每个邻域由 200 个节点组成,则从根部的单个领导者开始的 3 级网络可以在 2 跳内达到 40,000 个验证者——或者假设每个网络链路平均为 100 毫秒,大约需要 200 毫秒。

    我们使用这项技术面临的挑战是安全性。例如:敌对节点可以选择不重播数据,或者重播不正确的数据。为了处理对抗性节点,领导者生成里德-所罗门擦除码。纠删码允许每个验证器在不接收所有数据包的情况下重建整个块。

    如果领导者将块的 33% 的数据包作为纠删码传输,那么网络可以丢弃任意 33% 的数据包而不会丢失该块。领导者甚至可以根据网络状况动态调整这个数字。这些决定是根据领导者在之前区块中观察到的数据包丢失率来做出的。

    并非所有验证器都是生而平等的。最重要的验证者是那些拥有最多股份的验证者。因此,我们相应地优先考虑传播。权益加权选择算法构建树,使得较高权益的验证者位于更接近领导者的邻域。每个验证器独立地计算同一棵树。虽然纠删码可以修复故障,但敌对节点有可能将自己定位在树中,从而引发高于其组合权益大小的故障,尤其是与拒绝服务攻击相结合时。

    我们该如何应对这种日食攻击呢?我们的扇出算法使用基于数据包数字签名的随机源为每个数据包生成一个权益加权树。由于每个数据包采用不同的路径,并且路径事先未知,因此邻域级 Eclipse 攻击将需要几乎完全控制网络。

    对于一个级别,该技术可以扩展到 200 到 1,000 个节点之间。支持 1 Gbps 的网卡每秒可传输一百万个数据包。如果网络连接允许,单个验证器可以在一秒钟内向 1,000 台机器发送最多 64 KB 的数据包。

    - - + + \ No newline at end of file diff --git a/blog/tower-bft/index.html b/blog/tower-bft/index.html index 76f5349bf..5a05291c6 100644 --- a/blog/tower-bft/index.html +++ b/blog/tower-bft/index.html @@ -9,13 +9,13 @@ - - + +

    Tower BFT Solana High Performance Implementation of PBFT

    · 9 min read
    Davirain

    在这篇博文中,我们将探讨 Tower BFT,这是 Solana 的 PBFT 自定义实现,它更喜欢活跃性而不是一致性。 Tower BFT 在达成共识之前利用 Solana 的 PoH 作为时钟,以减少消息传递开销和延迟。

    info

    为了提供活力,如果副本无法执行请求,则必须移动到新视图。然而,当至少 2f + 1 个无故障副本处于同一视图中时,最大化时间段非常重要,并确保这段时间呈指数增长,直到执行某些请求的操作

    Solana 实现了 PBFT 的一种衍生,但有一个根本区别。历史证明(PoH)提供了达成共识之前的全球时间来源。我们的 PBFT 实现使用 PoH 作为网络时间时钟,并且副本在 PBFT 中使用的指数增长超时可以在 PoH 本身中计算和强制执行。

    PoH 是一种可验证延迟函数,以顺序哈希函数的形式实现。我们使用 VDF 的松散定义,因为验证需要(计算时间)/(核心数量)。 PoH 工作的基本原理如下:

    1. Sha256 尽可能快地循环,使得每个输出都是下一个输入。
    2. 对循环进行采样,并记录迭代次数和状态。

    记录的样本代表了编码为可验证数据结构的时间流逝。此外,该循环还可用于记录事件。

    1. 引用任何示例的消息都保证是在该示例之后创建的。

    2. 消息可以插入到循环中并与状态一起进行哈希处理。这保证了在下一次插入之前创建了一条消息。

    这种数据结构保证了嵌入事件的时间和顺序,这一核心思想是 Solana 中所有主要技术优化的基础。

    换句话说:想象一下你在一座岛上,一个瓶子漂过,里面有一个拇指驱动器。该驱动器上是 Solana PoH 分类账。仅使用 PoH 账本,您就可以计算网络中所有节点的状态。例如,如果对账本的投票尚未记录在最后 X 个哈希值中,则节点被视为失败。如果在过去的 X 个哈希中,对已签署验证消息的网络的绝大多数进行哈希处理,我们就可以认为账本是有效的。

    1. 检查此数据结构的所有节点将计算完全相同的结果,而不需要任何点对点通信。

    2. PoH 哈希唯一标识账本的该分叉;和

    3. 仅当验证投票消息所投票的 PoH 哈希值存在于账本中时,验证投票消息才有效。

    这就引出了投票和 PBFT。由于账本本身可作为可靠的网络时钟,因此我们可以在账本本身中对 PBFT 超时进行编码。

    1. 投票以 N 个哈希超时开始。

    验证者保证(通过削减)一旦对 PoH 哈希进行投票,验证者将不会投票给任何不是该投票子项的 PoH 哈希,至少 N 个哈希。

    1. 所有前任投票的超时时间加倍

    为了使操作更易于管理,投票被限制在固定的哈希周期内,我们称之为时隙。我们对时隙的目标是代表 400 毫秒左右的哈希数。每 400 毫秒,网络就有一个潜在的回滚点,但随后的每一次投票都会使网络在展开该投票之前必须停滞的实时时间加倍。

    想象一下,每个验证者在过去 12 秒内投票了 32 次。 12 秒前的投票现在有 232 个时隙的超时,即大约 54 年。实际上,这次投票永远不会被网络回滚。而最近的投票有 2 个时隙的超时,即大约 800 毫秒。随着新区块被添加到账本中,旧区块被确认的可能性越来越大,因为旧投票的时隙数量会在每个时隙或每 400 毫秒增加一倍。

    请注意,虽然这听起来像是工作量证明中的概率最终性,但事实并非如此。一旦 2/3 的验证者对某个 PoH 哈希进行了投票,该 PoH 哈希就会被规范化,并且无法回滚。这与工作量证明不同,工作量证明中没有规范化的概念。

    为了防止被网络其他部分锁定,每个验证者确保只有在看到绝大多数网络也在同一账本上投票时才进行投票。每个验证器都会监控祖先投票的超时时间何时超过预定义的阈值(例如从 5 到 10 分钟),并确保网络的绝大多数人已对包含该投票的分叉进行了投票。在实践中,验证者:

    1. 检查是否有绝大多数人对一个将承诺 10 分钟超时的插槽进行了投票

    2. 如果没有,请不要投票

    那么在分区期间网络会发生什么并且超时实际上开始到期呢?

    1. 任何已过期的投票都会被清除

    2. 当且仅当孩子有相同的超时时,祖先的超时加倍

    例如,让我们考虑当前超时的场景:

    64, 32, 16, 8, 4, 2

    如果验证者停止对 17 个插槽进行投票并再次投票,则验证者的超时结果将是:

    64, 32, 2

    还需要连续4次投票,所有祖先的暂停时间才会再次加倍。

    64, 32, 4, 2

    64, 32, 8, 4, 2

    64, 32, 16, 4, 2

    最后第四次投票将使所有超时加倍

    128, 64, 32, 16, 8, 4, 2

    这种方法允许网络连续传输区块,而不会导致账本停滞,直到绝大多数人观察到相同的账本。另一个值得注意的方面是,网络中的每个参与者都可以计算每个其他参与者的超时,而无需任何 P2P 通信。这就是 Tower BFT 异步的原因。

    我们预计会有许多微分叉很快被丢弃。当验证者检测到多个分叉时,诚实的验证者会计算每个分叉的有效权益加权超时并选择最重的一个。仅针对达到 2³² 超时的投票生成验证者奖励。因此,验证者在最重的分叉之上进行投票是兼容激励的,因为具有最大量权益加权超时的分叉将为网络产生最大量的奖励。

    - - + + \ No newline at end of file diff --git a/blog/tubine/index.html b/blog/tubine/index.html index 7f78667bb..4da29e6c4 100644 --- a/blog/tubine/index.html +++ b/blog/tubine/index.html @@ -9,13 +9,13 @@ - - + +

    Turbine Solana Block Propagation Protocol Solves the Scalability Trilemma

    · 7 min read
    Davirain

    在这篇文章中,我们将探讨 Turbine,这是 Solana 的区块传播协议(受 BitTorrent 启发),它解决了区块链可扩展性的难题。

    可扩展性的困境

    区块链技术中的可扩展性三难困境都与带宽有关。在当今的大多数区块链网络中,给定每个节点的固定带宽量,增加节点数将增加将所有数据传播到所有节点所需的时间。这是一个大问题。

    然而,有无数的机会来优化数据的传播方式。有许多新颖的数据传播技术,每种技术都针对特定应用进行了优化。例如,BitTorrent 经过优化,可使用 TCP 向大量人员提供大型文件,而我参与的项目 MediaFLO 是一种针对物理层数据传播进行优化的协议,以提高无线网络上的多播效率。

    在此背景下,让我们进入 Solana 的区块传播协议 Turbine,来解释 Solana 网络如何传播数据来解决区块链可扩展性三难困境。

    Turbine 涡轮

    高性能区块链面临的挑战之一是网络如何将大量数据传播到大量节点。例如,让我们考虑一个由 20,000 个验证者组成的网络。领导者需要向所有 20,000 个验证者传输一个 128 MB 的区块(大约 500,000 个交易 @ 250 字节/交易)。简单的实现将要求领导者与每个验证者建立唯一的连接,并传输完整的 128 MB 20,000 次。根本没有足够的带宽来容纳这么多的连接。

    我们针对这个问题的解决方案 Turbine 大量借鉴了 BitTorrent,尽管两者在一些主要技术细节上有所区别。 Turbine 针对流式传输进行了优化,仅使用 UDP 传输数据,并在领导者(区块生产者)流式传输数据时通过网络实现每个数据包的随机路径。领导者将块分成大小最大为 64KB 的数据包。对于 128MB 的块,领导者会生成 2,000 个 64KB 数据包,并将每个数据包传输到不同的验证器。

    反过来,每个验证器将数据包重新传输给一组我们称为邻居的对等点。您可以将网络可视化为邻域树,从而使网络能够增长到远远超过 1,000 个验证者:

    每个邻域负责将其部分数据传输到其下面的每个邻域。

    如果每个邻域由 200 个节点组成,则从根部的单个领导者开始的 3 级网络可以在 2 跳内达到 40,000 个验证者——或者假设每个网络链路平均为 100 毫秒,大约需要 200 毫秒。

    我们使用这项技术面临的挑战是安全性。例如:敌对节点可以选择不重播数据,或者重播不正确的数据。为了处理对抗性节点,领导者生成里德-所罗门擦除码。纠删码允许每个验证器在不接收所有数据包的情况下重建整个块。

    如果领导者将块的 33% 的数据包作为纠删码传输,那么网络可以丢弃任意 33% 的数据包而不会丢失该块。领导者甚至可以根据网络状况动态调整这个数字。这些决定是根据领导者在之前区块中观察到的数据包丢失率来做出的。

    并非所有验证器都是生而平等的。最重要的验证者是那些拥有最多股份的验证者。因此,我们相应地优先考虑传播。权益加权选择算法构建树,使得较高权益的验证者位于更接近领导者的邻域。每个验证器独立地计算同一棵树。虽然纠删码可以修复故障,但敌对节点有可能将自己定位在树中,从而引发高于其组合权益大小的故障,尤其是与拒绝服务攻击相结合时。

    我们该如何应对这种日食攻击呢?我们的扇出算法使用基于数据包数字签名的随机源为每个数据包生成一个权益加权树。由于每个数据包采用不同的路径,并且路径事先未知,因此邻域级 Eclipse 攻击将需要几乎完全控制网络。

    对于一个级别,该技术可以扩展到 200 到 1,000 个节点之间。支持 1 Gbps 的网卡每秒可传输一百万个数据包。如果网络连接允许,单个验证器可以在一秒钟内向 1,000 台机器发送最多 64 KB 的数据包。

    - - + + \ No newline at end of file diff --git a/blog/what-is-Phoenix/index.html b/blog/what-is-Phoenix/index.html index 0d729da54..365c9d261 100644 --- a/blog/what-is-Phoenix/index.html +++ b/blog/what-is-Phoenix/index.html @@ -9,13 +9,13 @@ - - + +

    什么是Phoenix?

    · One min read
    Davirain

    Phoenix是Solana上的去中心化限价订单簿,支持现货资产市场。

    Why

    可组合的流动性中心是DeFi的公共产品。开发者可以构建其他链上应用,将流动性发布到或从规范的流动性来源中提取流动性。

    - - + + \ No newline at end of file diff --git a/blog/what-is-anchor/index.html b/blog/what-is-anchor/index.html index 7a4684b73..f279b7eed 100644 --- a/blog/what-is-anchor/index.html +++ b/blog/what-is-anchor/index.html @@ -9,14 +9,14 @@ - - + +

    什么是Anchor?

    · 11 min read
    Davirain

    今天,开始我们学习从Solana上开发智能合约,这里我打算先从Anchor开始。因为Anchor也是Solana上现在如今用的最多的开发框架,哦,这里主要用的也是Rust语言,对于Anchor还支持的Solidity语法来写合约,暂时我先不考虑。也希望有🧍‍♂️能一起完善。

    那今天就简单的介绍下Anchor上如何从项目的初始化,到后面如何部署合约以及前端如何来调用这个简单的Example合约。

    先来介绍下什么是Anchor吧

    这里我先引用下官方的介绍

    Anchor是一个快速构建安全Solana程序的框架。

    使用Anchor,您可以快速构建程序,因为它会为您编写各种样板代码,例如账户和指令数据的(反)序列化。

    由于Anchor为您处理了某些安全检查,因此您可以更轻松地构建安全的程序。除此之外,它还允许您简洁地定义额外的检查,并将其与业务逻辑分开。

    这两个方面意味着,你不必再花费时间在繁琐的Solana原始程序上,而是可以更多地投入到最重要的事情上,即你的产品。

    简单的点来说,就是Anchor做为一个Solana上的合约开发框架,对于原生使用Rust开发来说的话,anchor 提供了对于一些模版代码,或者说公共代码操作的抽象,使得开发者更加具体的专注与自己的业务逻辑。

    简单的Anchor介绍完了,我们看看如何来初始化一个合约以及部署合约到本地测试网。本地测试网的部署查看这个教程完成✅。

    一个简单的Anchor合约的部署测试

    对于要使用Anchor来开发他需要一些前置的环境配置,例如你需要先安装Rust环境,第二个是安装Solana-cli工具。因为这里Anchor要使用solana cli的 solana-keygen new 命令来生成一个本地册测试账户。最后一个是Yarn。这里是Anchor官方给出的安装教程,按照这个安装即可。

    下面是具体的anchor如何安装

    官方推荐的是avm,一个Anchor的多版本管理器。前面我们已经安装了Rust语言,我们就可以使用cargo来安装这个工具。

    通过执行这个命令,我们就可以安装avm了。

    cargo install --git https://github.com/coral-xyz/anchor avm --locked --force

    按照完之后我们就可以使用avm选择一个具体的版本安装,下面者一个命令我们安装的Anchor版本是最新的Anchor。

    avm install latest
    avm use latest

    验证安装成功的我们可以执行anchor --version命令,我们可以看到有版本号输出,说明我们安装成功了。

    一个anchor项目的结构

    通过执行anchor init new-workspace-name 我们就可以初始化一个solana program。

    下面是通过执行anchor init hello-world的输出。

    ls --tree . --depth 1
    .
    ├──  .git
    ├──  .gitignore
    ├──  .prettierignore
    ├──  Anchor.toml
    ├──  app
    ├──  Cargo.toml
    ├──  migrations
    ├──  node_modules
    ├──  package-lock.json
    ├──  package.json
    ├──  programs
    ├──  target
    ├──  tests
    ├──  tsconfig.json
    └──  yarn-error.log
    • app 文件夹:初始化之后是一个空文件夹,这里可以用来存放自己的前端代码。
    • programs 文件夹:此文件夹包含程序代码。它可以包含多个文件,但最初只包含与 <new-workspace-name> 相同名称的程序。并且这个program中已经包含了一些示例代码,在lib.rs中可以看到。
    • tests 文件夹:包含您的端到端测试的文件夹。它已经包含一个测试 programs/<new-workspace-name> 中示例代码的文件,这里面的测试都是使用typescript写✍️的代码。当执行anchor test的时候会在本地启动一个solana的测试节点,执行里面的测试代码。
    • migrations 文件夹:在这个文件夹中,保存程序的部署和迁移脚本。
    • Anchor.toml 文件:此文件配置了程序的工作区范围设置。
      • 程序在本地网络上的地址( [programs.localnet]
      • 程序可以推送到的注册表 ( [registry] )
      • 一个可以在你的测试中使用的也就是通过solana-keygen new 生成的私钥文件路径 ( [provider] )
    • .anchor 这个文件是只有在执行anchor test之后才生成的文件夹,里面包含了最新的程序日志和用于测试的本地账本。

    这个是在执行anchor test之后的文件内容。

    ls --tree . --depth 1
    .
    ├──  .anchor
    ├──  .git
    ├──  .gitignore
    ├──  .prettierignore
    ├──  Anchor.toml
    ├──  app
    ├──  Cargo.lock
    ├──  Cargo.toml
    ├──  migrations
    ├──  node_modules
    ├──  package-lock.json
    ├──  package.json
    ├──  programs
    ├──  target
    ├──  tests
    ├──  tsconfig.json
    └──  yarn-error.log

    下面这个是执行anchor test之后.anchor里面生成的日志内容。

    好说了这么多,我们看下如何使用anchor打印一个hello world, 目前先只通过anchor test 来观察打印,后面在做介绍如何通过前端调用打印。

    初始化一个 hello world program

    通过执行anchor init hello-world, 会为我们创建一个solana program的样板代码。

    use anchor_lang::prelude::*;

    declare_id!("2HvxNpAdkkWitSQyDy9vMvJDpRsvtrhZ6JNqsXzGRi3i");

    #[program]
    pub mod counter {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    Ok(())
    }
    }

    #[derive(Accounts)]
    pub struct Initialize {}

    上面这段代码就是通过anchor init hello-world 创建出来的代码,文件存放在hello-world/programs/hello-world/src/lib.rs中。

    下面我们就通过简单的修改下这个简单的代码,在里面添加一个打印hello, world!的消息。

    use anchor_lang::prelude::*;

    declare_id!("2HvxNpAdkkWitSQyDy9vMvJDpRsvtrhZ6JNqsXzGRi3i");

    #[program]
    pub mod counter {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    msg!("hello, world!");
    Ok(())
    }
    }

    #[derive(Accounts)]
    pub struct Initialize {}

    这个是添加了msg!这段代码,msg!主要做的事情,类似于在rust中打印内容到标准输出的println!, 因为是solana program,他是链上代码,我们不可能打印到标准输出的,所以这我们就通过使用msg!这个宏记录自己需要打印的东西。

    在Solana中,由于智能合约在执行时是在分布式网络中运行的,无法直接使用传统的标准输出来打印消息。为了在智能合约中输出调试信息或日志,Solana提供了msg!宏。

    msg!宏的使用方式与println!宏类似,你可以在智能合约中使用它来打印消息。这些消息将被记录并作为日志输出到Solana节点的日志文件中。

    需要注意的是,msg!宏只在Solana智能合约中可用,用于在智能合约执行过程中输出消息。它与Rust中的println!宏略有不同,因为它将消息记录到Solana节点的日志文件中,而不是直接输出到控制台。

    观察👀 hello,world!消息

    想要观察是否打印了hello, world!这个消息,我们可以通过运行anchor test。这个会记录📝program在测试执行的内容。

    我们可以看到通过执行anchor test已经将我们打印的hello,world! 记录下来了。

    来看下执行的这个测试脚本吧。这里是执行的程序的initialize执行的调用。我们在这个指令中添加了打印hello, world的代码。

    import * as anchor from "@coral-xyz/anchor";
    import { Program } from "@coral-xyz/anchor";
    import { Hello } from "../target/types/hello";

    describe("hello", () => {
    // Configure the client to use the local cluster.
    anchor.setProvider(anchor.AnchorProvider.env());

    const program = anchor.workspace.Hello as Program<Hello>;

    it("Is initialized!", async () => {
    // Add your test here.
    const tx = await program.methods.initialize().rpc();
    console.log("Your transaction signature", tx);
    });
    });

    anchor.setProvider(anchor.AnchorProvider.env()); 这段代码是通过读取的Anchor.toml中的配置初始化了Anchor的provider。

    [features]
    seeds = false
    skip-lint = false
    [programs.localnet]
    hello = "2HvxNpAdkkWitSQyDy9vMvJDpRsvtrhZ6JNqsXzGRi3i"

    [registry]
    url = "https://api.apr.dev"

    [provider]
    cluster = "Localnet"
    wallet = "/Users/davirain/.config/solana/id.json"

    [scripts]
    test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

    我们可以看到这里的provider是localnet,wallet是自己本地的私钥路径。

    const program = anchor.workspace.Hello as Program<Hello>;

    这一步是我们初始化了一个solana 的program 实例,通过Hello这个IDL文件。

    在测试中,使用it函数定义了一个测试用例,名称为"Is initialized!"。在这个测试用例中,调用了program.methods.initialize().rpc()方法,该方法是调用合约中的initialize方法,并通过RPC方式发送交易。然后,使用console.log打印出交易的签名。

    这段代码的目的是测试hello程序是否能够成功初始化。通过调用initialize方法并打印交易签名,可以验证初始化过程是否成功。

    这就是一个简单的Anchor合约的入门。

    - - + + \ No newline at end of file diff --git a/cookbook-zh/core-concepts/accounts/index.html b/cookbook-zh/core-concepts/accounts/index.html index 1ccd08246..d6beb70cb 100644 --- a/cookbook-zh/core-concepts/accounts/index.html +++ b/cookbook-zh/core-concepts/accounts/index.html @@ -9,16 +9,16 @@ - - + +

    账户

    在Solana中,账户是用来存储状态的。账户是Solana开发中非常重要的构成要素。

    info

    tip 要点

    • 账户是用来存放数据的
    • 每个账户都有一个独一无二的地址
    • 每个账户大小不能超过10MB
    • 程序派生账户(PDA accounts)大小不能超过10KB
    • 程序派生账户(PDA accounts)可以用其对应程序进行签名
    • 账户大小在创建时固定,但可以使用realloc进行调整
    • 账户数据存储需要付租金
    • 默认的账户所有者是"系统程序"

    深入

    账户模型

    在Solana中有三类账户:

    • 数据账户,用来存储数据
    • 程序账户,用来存储可执行程序
    • 原生账户,指Solana上的原生程序,例如"System","Stake",以及"Vote"。

    数据账户又分为两类:

    • 系统所有账户
    • 程序派生账户(PDA)

    每个数据账户都有一个地址(一般情况下是一个公钥)以及一个所有者(程序账户的地址)。 下面详细列出一个账户存储的完整字段列表。

    字段描述
    lamports这个账户拥有的lamport(兰波特)数量
    owner这个账户的所有者程序
    executable这个账户成是否可以处理指令
    data这个账户存储的数据的字节码
    rent_epoch下一个需要付租金的epoch(代)

    关于所有权,有几条重要的规则:

    • 只有账户的所有者才能改变账户中的数据,提取lamport
    • 任何人都可以向数据账户中存入lamport
    • 当账户中的数据被抹除之后,账户的所有者可以指定新的所有者

    程序账户不储存状态。

    例如,假设有一个计数程序,这个程序用来为一个计数器加数,你需要创建两个账户,一个用于存储程序的代码, 另一个用于存储计数器本身。

    为了避免账户被删除,必须付租金。

    租金

    在账户中存储数据需要花费SOL来维持,这部分花费的SOL被称作租金。如果你在一个账户中存入大于两年租金的SOL, -这个账户就可以被豁免付租。租金可以通过关闭账户的方式来取回。lamport会被返还回你的钱包。

    租金在这两个不同的时间点被支取:

    1. 被一个交易引用的时候
    2. epoch更迭时

    收取的租金,一定百分比会被销毁,另一部分会在每个slot(插槽)结束时被分配给投票账户。

    当一个账户没有足够的余额支付租金时,这个账户会被释放,数据会被清除。

    其他资料

    致谢

    这些核心概念来源于Pencilflip. 在Twitter上关注他.

    - - +这个账户就可以被豁免付租。租金可以通过关闭账户的方式来取回。lamport会被返还回你的钱包。

    租金在这两个不同的时间点被支取:

    1. 被一个交易引用的时候
    2. epoch更迭时

    收取的租金,一定百分比会被销毁,另一部分会在每个slot(插槽)结束时被分配给投票账户。

    当一个账户没有足够的余额支付租金时,这个账户会被释放,数据会被清除。

    其他资料

    致谢

    这些核心概念来源于Pencilflip. 在Twitter上关注他.

    + + \ No newline at end of file diff --git a/cookbook-zh/core-concepts/cpi/index.html b/cookbook-zh/core-concepts/cpi/index.html index be9724f6c..95a0dac24 100644 --- a/cookbook-zh/core-concepts/cpi/index.html +++ b/cookbook-zh/core-concepts/cpi/index.html @@ -9,13 +9,13 @@ - - + +
    -

    Cross Program Invocations (CPIs)

    A Cross-Program Invocation (CPI) is a direct call from one program into another, allowing for the composability of Solana programs. Just as any client can call any program using the JSON RPC, any program can call any other program via a CPI. CPIs essentially turn the entire Solana ecosystem into one giant API that is at your disposal as a developer.

    The purpose of this section is to provide a high-level overview CPIs. Please refer to the linked resources below for more detailed explanations, examples, and walkthroughs.

    info

    tip Fact Sheet

    • A Cross-Program Invocation (CPI) is a call from one program to another, targeting a specific instruction on the program being called
    • CPIs allow the calling program to extend its signer privileges to the callee program
    • Programs can execute CPIs using either invoke or invoke_signed within their instructions
    • invoke is used when all required signatures are accessible prior to invocation, without the need for PDAs to act as signers
    • invoke_signed is used when PDAs from the calling program are required as signers in the CPI
    • After a CPI is made to another program, the callee program can make further CPIs to other programs, up to a maximum depth of 4

    Deep Dive

    Cross Program Invocations (CPIs) enable the composability of Solana programs, which allow developers to utilize and build on the instruction of existing programs.

    To execute CPIs, use the invoke or invoke_signed function found in the solana_program crate.

    // Used when there are not signatures for PDAs needed
    pub fn invoke(
    instruction: &Instruction,
    account_infos: &[AccountInfo<'_>]
    ) -> ProgramResult

    // Used when a program must provide a 'signature' for a PDA, hence the signer_seeds parameter
    pub fn invoke_signed(
    instruction: &Instruction,
    account_infos: &[AccountInfo<'_>],
    signers_seeds: &[&[&[u8]]]
    ) -> ProgramResult

    To make a CPI, you must specify and construct an instruction on the program being invoked and supply a list of accounts necessary for that instruction. If a PDA is required as a signer, the signers_seeds must also be provided when using invoke_signed.

    CPI with invoke

    The invoke function is used when making a CPI that does not require any PDAs to act as signers. When making CPIs, the Solana runtime extends the original signature passed into a program to the callee program.

    invoke(
    &some_instruction, // instruction to invoke
    &[account_one.clone(), account_two.clone()], // accounts required by instruction
    )?;

    CPI with invoke_signed

    To make a CPI that requires a PDA as a signer, use the invoke_signed function and provide the necessary seeds to derive the required PDA of the calling program.

    invoke_signed(
    &some_instruction, // instruction to invoke
    &[account_one.clone(), pda.clone()], // accounts required by instruction, where one is a pda required as signer
    &[signers_seeds], // seeds to derive pda
    )?;

    While PDAs have no private keys of their own, they can still act as a signer in an instruction via a CPI. To verify that a PDA belongs to the calling program, the seeds used to generate the PDA required as a signer must be included in as signers_seeds.

    The Solana runtime will internally call create_program_address using the seeds provided and the program_id of the calling program. The resulting PDA is then compared to the addresses supplied in the instruction. If there's a match, the PDA is considered a valid signer.

    CPI Instruction

    Depending on the program you're making the call to, there may be a crate available with helper functions for creating the Instruction. Many individuals and organizations create publicly available crates alongside their programs that expose these sorts of functions to simplify calling their programs.

    The definition of the Instruction type required for a CPI includes:

    • program_id - the public key of the program that executes the instruction
    • accounts - a list of all accounts that may be read or written to during the execution of the instruction
    • data - the instruction data required by the instruction
    pub struct Instruction {
    pub program_id: Pubkey,
    pub accounts: Vec<AccountMeta>,
    pub data: Vec<u8>,
    }

    The AccountMeta struct has the following definition:

    pub struct AccountMeta {
    pub pubkey: Pubkey,
    pub is_signer: bool,
    pub is_writable: bool,
    }

    When creating a CPI, use the following syntax to specify the AccountMeta for each account:

    • AccountMeta::new - indicates writable
    • AccountMeta::new_readonly - indicates not writable
    • (pubkey, true) - indicates account is signer
    • (pubkey, false) - indicates account is not signer

    Here is an example:

    use solana_program::instruction::AccountMeta;

    let account_metas = vec![
    AccountMeta::new(account1_pubkey, true),
    AccountMeta::new(account2_pubkey, false),
    AccountMeta::new_readonly(account3_pubkey, false),
    AccountMeta::new_readonly(account4_pubkey, true),
    ]

    CPI AccountInfo

    To use invoke and invoke_signed, a list of account_infos is also required. Similar to the list of AccountMeta in the instruction, you need to include all the AccountInfo of each account that the program you're calling will read from or write to.

    For reference, the AccountInfo struct has the following definition:

    /// Account information
    #[derive(Clone)]
    pub struct AccountInfo<'a> {
    /// Public key of the account
    pub key: &'a Pubkey,
    /// Was the transaction signed by this account's public key?
    pub is_signer: bool,
    /// Is the account writable?
    pub is_writable: bool,
    /// The lamports in the account. Modifiable by programs.
    pub lamports: Rc<RefCell<&'a mut u64>>,
    /// The data held in this account. Modifiable by programs.
    pub data: Rc<RefCell<&'a mut [u8]>>,
    /// Program that owns this account
    pub owner: &'a 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,
    }

    You can create a copy of the AccountInfo for each required account using the Clone trait, which is implemented for the AccountInfo struct in the solana_program crate.

    let accounts_infos = [
    account_one.clone(),
    account_two.clone(),
    account_three.clone(),
    ];

    While this section has provided a high-level overview of CPIs, more detailed explanations, examples, and walkthroughs can be found in the linked resources below.

    Other Resources

    - - +

    Cross Program Invocations (CPIs)

    A Cross-Program Invocation (CPI) is a direct call from one program into another, allowing for the composability of Solana programs. Just as any client can call any program using the JSON RPC, any program can call any other program via a CPI. CPIs essentially turn the entire Solana ecosystem into one giant API that is at your disposal as a developer.

    The purpose of this section is to provide a high-level overview CPIs. Please refer to the linked resources below for more detailed explanations, examples, and walkthroughs.

    info

    tip Fact Sheet

    • A Cross-Program Invocation (CPI) is a call from one program to another, targeting a specific instruction on the program being called
    • CPIs allow the calling program to extend its signer privileges to the callee program
    • Programs can execute CPIs using either invoke or invoke_signed within their instructions
    • invoke is used when all required signatures are accessible prior to invocation, without the need for PDAs to act as signers
    • invoke_signed is used when PDAs from the calling program are required as signers in the CPI
    • After a CPI is made to another program, the callee program can make further CPIs to other programs, up to a maximum depth of 4

    Deep Dive

    Cross Program Invocations (CPIs) enable the composability of Solana programs, which allow developers to utilize and build on the instruction of existing programs.

    To execute CPIs, use the invoke or invoke_signed function found in the solana_program crate.

    // Used when there are not signatures for PDAs needed
    pub fn invoke(
    instruction: &Instruction,
    account_infos: &[AccountInfo<'_>]
    ) -> ProgramResult

    // Used when a program must provide a 'signature' for a PDA, hence the signer_seeds parameter
    pub fn invoke_signed(
    instruction: &Instruction,
    account_infos: &[AccountInfo<'_>],
    signers_seeds: &[&[&[u8]]]
    ) -> ProgramResult

    To make a CPI, you must specify and construct an instruction on the program being invoked and supply a list of accounts necessary for that instruction. If a PDA is required as a signer, the signers_seeds must also be provided when using invoke_signed.

    CPI with invoke

    The invoke function is used when making a CPI that does not require any PDAs to act as signers. When making CPIs, the Solana runtime extends the original signature passed into a program to the callee program.

    invoke(
    &some_instruction, // instruction to invoke
    &[account_one.clone(), account_two.clone()], // accounts required by instruction
    )?;

    CPI with invoke_signed

    To make a CPI that requires a PDA as a signer, use the invoke_signed function and provide the necessary seeds to derive the required PDA of the calling program.

    invoke_signed(
    &some_instruction, // instruction to invoke
    &[account_one.clone(), pda.clone()], // accounts required by instruction, where one is a pda required as signer
    &[signers_seeds], // seeds to derive pda
    )?;

    While PDAs have no private keys of their own, they can still act as a signer in an instruction via a CPI. To verify that a PDA belongs to the calling program, the seeds used to generate the PDA required as a signer must be included in as signers_seeds.

    The Solana runtime will internally call create_program_address using the seeds provided and the program_id of the calling program. The resulting PDA is then compared to the addresses supplied in the instruction. If there's a match, the PDA is considered a valid signer.

    CPI Instruction

    Depending on the program you're making the call to, there may be a crate available with helper functions for creating the Instruction. Many individuals and organizations create publicly available crates alongside their programs that expose these sorts of functions to simplify calling their programs.

    The definition of the Instruction type required for a CPI includes:

    • program_id - the public key of the program that executes the instruction
    • accounts - a list of all accounts that may be read or written to during the execution of the instruction
    • data - the instruction data required by the instruction
    pub struct Instruction {
    pub program_id: Pubkey,
    pub accounts: Vec<AccountMeta>,
    pub data: Vec<u8>,
    }

    The AccountMeta struct has the following definition:

    pub struct AccountMeta {
    pub pubkey: Pubkey,
    pub is_signer: bool,
    pub is_writable: bool,
    }

    When creating a CPI, use the following syntax to specify the AccountMeta for each account:

    • AccountMeta::new - indicates writable
    • AccountMeta::new_readonly - indicates not writable
    • (pubkey, true) - indicates account is signer
    • (pubkey, false) - indicates account is not signer

    Here is an example:

    use solana_program::instruction::AccountMeta;

    let account_metas = vec![
    AccountMeta::new(account1_pubkey, true),
    AccountMeta::new(account2_pubkey, false),
    AccountMeta::new_readonly(account3_pubkey, false),
    AccountMeta::new_readonly(account4_pubkey, true),
    ]

    CPI AccountInfo

    To use invoke and invoke_signed, a list of account_infos is also required. Similar to the list of AccountMeta in the instruction, you need to include all the AccountInfo of each account that the program you're calling will read from or write to.

    For reference, the AccountInfo struct has the following definition:

    /// Account information
    #[derive(Clone)]
    pub struct AccountInfo<'a> {
    /// Public key of the account
    pub key: &'a Pubkey,
    /// Was the transaction signed by this account's public key?
    pub is_signer: bool,
    /// Is the account writable?
    pub is_writable: bool,
    /// The lamports in the account. Modifiable by programs.
    pub lamports: Rc<RefCell<&'a mut u64>>,
    /// The data held in this account. Modifiable by programs.
    pub data: Rc<RefCell<&'a mut [u8]>>,
    /// Program that owns this account
    pub owner: &'a 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,
    }

    You can create a copy of the AccountInfo for each required account using the Clone trait, which is implemented for the AccountInfo struct in the solana_program crate.

    let accounts_infos = [
    account_one.clone(),
    account_two.clone(),
    account_three.clone(),
    ];

    While this section has provided a high-level overview of CPIs, more detailed explanations, examples, and walkthroughs can be found in the linked resources below.

    Other Resources

    + + \ No newline at end of file diff --git a/cookbook-zh/core-concepts/index.html b/cookbook-zh/core-concepts/index.html index 54a27bf45..b473cc9cd 100644 --- a/cookbook-zh/core-concepts/index.html +++ b/cookbook-zh/core-concepts/index.html @@ -9,13 +9,13 @@ - - + + - - + + + \ No newline at end of file diff --git a/cookbook-zh/core-concepts/pdas/index.html b/cookbook-zh/core-concepts/pdas/index.html index def9c9d39..c9d873936 100644 --- a/cookbook-zh/core-concepts/pdas/index.html +++ b/cookbook-zh/core-concepts/pdas/index.html @@ -9,8 +9,8 @@ - - + +
    @@ -23,8 +23,8 @@ 这个看起来简陋的办法可以让我们每次生成PDA的时候都能够得到唯一确定的结果。

    落在椭圆曲线上的PDA

    与PDA交互

    生成PDA的时候,findProgramAddress会把得到的地址和用来将PDA碰撞出椭圆曲线所用的bump都返回出来。 有了这个bump,程序就可以对任何需要这个PDA地址的指令进行签名。签名时,程序调用invoke_signed函数,传入指令,账户列表,以及用于生成PDA的种子和bump。 除了为指令签名之外,PDA在他自己通过invoke_signed函数被创建时,也需要签名。

    在使用PDA编写程序时,经常会将这个bump存储在这个账户本身的数据当中。 -这种机制可以让开发者轻易的对PDA进行验证,而不用重新在指令参数当中传入这个值。

    Other Resources

    - - +这种机制可以让开发者轻易的对PDA进行验证,而不用重新在指令参数当中传入这个值。

    Other Resources

    + + \ No newline at end of file diff --git a/cookbook-zh/core-concepts/programs/index.html b/cookbook-zh/core-concepts/programs/index.html index 51961aba3..9b5967b20 100644 --- a/cookbook-zh/core-concepts/programs/index.html +++ b/cookbook-zh/core-concepts/programs/index.html @@ -9,8 +9,8 @@ - - + +
    @@ -31,8 +31,8 @@ Solana支持以下的几个环境:

    集群环境RPC连接URL
    Mainnet-betahttps://api.mainnet-beta.solana.com
    Testnethttps://api.testnet.solana.com
    Devnethttps://api.devnet.solana.com
    Localhost默认端口:8899(例如,http://localhost:8899,http://192.168.1.88:8899)

    部署到一个环境之后,客户端就可以通过对应集群的RPC连接与链上程序进行交互。

    部署程序

    开发者可以使用命令行部署程序:

    solana program deploy <PROGRAM_FILEPATH>

    部署程序的时候,程序会被编译为包含BPF字节码的ELF共享对象,并上传到Solana集群上。 和Solana上其他的任何东西一样,程序储存在账户当中。唯一的特殊之处是,这些账户标记为executable(可执行),并且其所有者是"BPF Loader(BPF加载器)"。 这个账户的地址被称为program_id,在后面的一切交易当中,用于指代这个程序。

    Solana支持多种BPF加载器,最新的是Upgradable BPF Loader。 -BPF加载器负责管理程序账户,让客户端可以通过其program_id对程序进行访问。每个程序都只有一个入口点,这里对指令进行处理。这里的参数须包括:

    • program_id: pubkey(公钥)
    • accounts: array(数组)
    • instruction_data: byte array(字节数组)

    当程序被调用时,会在Solana运行库中被执行。

    其他资料

    - - +BPF加载器负责管理程序账户,让客户端可以通过其program_id对程序进行访问。每个程序都只有一个入口点,这里对指令进行处理。这里的参数须包括:

    • program_id: pubkey(公钥)
    • accounts: array(数组)
    • instruction_data: byte array(字节数组)

    当程序被调用时,会在Solana运行库中被执行。

    其他资料

    + + \ No newline at end of file diff --git a/cookbook-zh/core-concepts/transactions/index.html b/cookbook-zh/core-concepts/transactions/index.html index 0e812c68f..e4681d016 100644 --- a/cookbook-zh/core-concepts/transactions/index.html +++ b/cookbook-zh/core-concepts/transactions/index.html @@ -9,8 +9,8 @@ - - + +
    @@ -24,8 +24,8 @@ 块哈希被用于去重,以及移除过期交易。一个块哈希的最大寿命是150个区块,成文时这个时间大约是1分钟19秒。

    费用

    Solana网络收取两种费用:

    • 交易费,用于向网络广播消息(亦即gas费)
    • 租金,用于向区块链上存储数据

    在Solana中,交易费是确定的。并没有费率竞价的概念,用户无法通过增加交易费的方式增加自己的交易被打包进下一个区块的概率。 在成文时,交易费只与交易所需的签名数量相关(参见lamports_per_signature),与交易所使用的资源无关。 这是因为目前所有交易都有一个严格的1232字节的限制。

    每个交易都需要至少有一个writable(可写)的账户,用于为交易签名。这个账户无论交易成功与否都需要为交易成本付费。 -如果付费者没有足够为交易付费的余额,这个交易就会被丢弃。

    成文时,50%的交易费被出块的验证节点收取,剩下的50%被燃烧掉。这样的结构会激励验证节点在leader schedule(领导时间表)规定的属于自己的slot(插槽)中处理尽可能多的交易。

    Other Resources

    - - +如果付费者没有足够为交易付费的余额,这个交易就会被丢弃。

    成文时,50%的交易费被出块的验证节点收取,剩下的50%被燃烧掉。这样的结构会激励验证节点在leader schedule(领导时间表)规定的属于自己的slot(插槽)中处理尽可能多的交易。

    Other Resources

    + + \ No newline at end of file diff --git a/cookbook-zh/getting-started/contributing/index.html b/cookbook-zh/getting-started/contributing/index.html index 5d7500004..076a17b9b 100644 --- a/cookbook-zh/getting-started/contributing/index.html +++ b/cookbook-zh/getting-started/contributing/index.html @@ -9,13 +9,13 @@ - - + +
    -

    贡献

    欢迎任何人对这本食谱进行贡献。在贡献新的代码片段时,请参考项目的风格。

    结构

    目前我们在 /docs 下有"cookbook-zh",所有的内容都在这个文件中。

    参考文献

    参考资料是一个总体主题,其中列出了关于如何在该主题下进行操作的参考资料。一般的结构如下:

    Code Reference Title

    Short Summary

    Code Snippet

    指南

    指南是关于各种主题的长篇信息文档。撰写指南的一般结构如下:

    Brief Summary/TLDR

    Fact Sheet

    Deep Dive

    Other Resources

    建筑

    我们使用 Docusaurus 来构建这个网站。请参考它的文档来了解如何在本地运行它。

    一般是这样的:

    npm install
    npm run build
    npm run start

    Committing

    我们在这个仓库中使用传统的提交方式。

    选择一个任务或者自己创建一个,按照以下步骤进行:

    1. 任务添加一个问题,并将其分配给自己或在问题上进行评论
    2. 制作一份涉及该问题的初稿公关文件。

    做出贡献的一般流程:

    1. 在GitHub上fork这个仓库
    2. 将项目克隆到您自己的机器上
    3. 提交更改到你自己的分支
    4. 将你的工作推回到你的分支
    5. 提交一个Pull请求,以便我们可以审查您的更改
    caution

    注意:在发起拉取请求之前,请确保将最新的更改合并到“上游”!

    您可以在项目看板上找到任务,或者创建一个问题并将其分配给自己。

    快乐烹饪!

    - - +

    贡献

    欢迎任何人对这本食谱进行贡献。在贡献新的代码片段时,请参考项目的风格。

    结构

    目前我们在 /docs 下有"cookbook-zh",所有的内容都在这个文件中。

    参考文献

    参考资料是一个总体主题,其中列出了关于如何在该主题下进行操作的参考资料。一般的结构如下:

    Code Reference Title

    Short Summary

    Code Snippet

    指南

    指南是关于各种主题的长篇信息文档。撰写指南的一般结构如下:

    Brief Summary/TLDR

    Fact Sheet

    Deep Dive

    Other Resources

    建筑

    我们使用 Docusaurus 来构建这个网站。请参考它的文档来了解如何在本地运行它。

    一般是这样的:

    npm install
    npm run build
    npm run start

    Committing

    我们在这个仓库中使用传统的提交方式。

    选择一个任务或者自己创建一个,按照以下步骤进行:

    1. 任务添加一个问题,并将其分配给自己或在问题上进行评论
    2. 制作一份涉及该问题的初稿公关文件。

    做出贡献的一般流程:

    1. 在GitHub上fork这个仓库
    2. 将项目克隆到您自己的机器上
    3. 提交更改到你自己的分支
    4. 将你的工作推回到你的分支
    5. 提交一个Pull请求,以便我们可以审查您的更改
    caution

    注意:在发起拉取请求之前,请确保将最新的更改合并到“上游”!

    您可以在项目看板上找到任务,或者创建一个问题并将其分配给自己。

    快乐烹饪!

    + + \ No newline at end of file diff --git a/cookbook-zh/getting-started/index.html b/cookbook-zh/getting-started/index.html index 2cc5849f4..fc19f9d39 100644 --- a/cookbook-zh/getting-started/index.html +++ b/cookbook-zh/getting-started/index.html @@ -9,13 +9,13 @@ - - + + - - + + + \ No newline at end of file diff --git a/cookbook-zh/getting-started/installation/index.html b/cookbook-zh/getting-started/installation/index.html index 24fb2f3df..67c3c9dfc 100644 --- a/cookbook-zh/getting-started/installation/index.html +++ b/cookbook-zh/getting-started/installation/index.html @@ -9,8 +9,8 @@ - - + +
    @@ -28,8 +28,8 @@ https://github.com/solana-labs/solana/releases/latest, 下载 solana-release-x86_64-pc-windows-msvc.tar.bz2并解压。

    打开命令提示符并切换目录至刚才解压的位置,运行以下命令:

    cd solana-release/
    set PATH=%cd%/bin;%PATH%

    从源码编译

    如果你不能使用预编译的二进制文件,或者希望自己从源码进行编译,可以访问 https://github.com/solana-labs/solana/releases/latest, -下载Source Code压缩包。解压代码,用以下命令编译二进制文件:

    ./scripts/cargo-install-all.sh .
    export PATH=$PWD/bin:$PATH

    然后运行以下命令,可以和预编译二进制文件获得一样的结果:

    solana-install init
    - - +下载Source Code压缩包。解压代码,用以下命令编译二进制文件:

    ./scripts/cargo-install-all.sh .
    export PATH=$PWD/bin:$PATH

    然后运行以下命令,可以和预编译二进制文件获得一样的结果:

    solana-install init
    + + \ No newline at end of file diff --git a/cookbook-zh/guides/account-maps/index.html b/cookbook-zh/guides/account-maps/index.html index 5c623e3da..afb9c3174 100644 --- a/cookbook-zh/guides/account-maps/index.html +++ b/cookbook-zh/guides/account-maps/index.html @@ -9,13 +9,13 @@ - - + +
    -

    账户映射

    在编程中,我们经常使用映射(Map)这种数据结构,将一个键与某种值关联起来。键和值可以是任意类型的数据,键用作标识要保存的特定值的标识符。通过键,我们可以高效地插入、检索和更新这些值。

    正如我们所了解的,Solana的账户模型要求程序数据和相关状态数据存储在不同的账户中。这些账户都有与之关联的地址,这本身就有映射的作用!在这里了解更多关于Solana账户模型的信息。

    因此,将值存储在单独的账户中,以其地址作为检索值所需的键是有意义的。但这也带来了一些问题,比如:

    *上述地址很可能不是理想的键,你可能难以记住并检索所需的值。

    *上述地址是不同Keypair的公钥,每个公钥(或地址)都有与之关联的私钥。如果需要,这个私钥将用于对不同的指令进行签名,这意味着我们需要在某个地方存储私钥,这绝对不是推荐的做法!

    这给许多Solana开发者带来了一个问题,即如何在他们的程序中实现类似Map的逻辑。让我们看看几种解决这个问题的方法。

    派生PDA

    PDA的全称是“程序派生地址” - Program Derived Address,简而言之,它们是从一组种子和程序ID(或地址)派生出来的地址。

    PDAs的独特之处在于,这些地址不与任何私钥相关联。这是因为这些地址不位于ED25519曲线上。因此,只有派生此地址的程序可以使用提供的密钥和种子对指令进行签名。在这里了解更多信息。

    现在我们对PDAs有了一个概念,让我们使用它们来映射一些账户!我们以一个博客程序作为示例,演示如何实现这一点。

    在这个博客程序中,我们希望每个User都拥有一个Blog。这个博客可以有任意数量的Posts。这意味着我们将每个用户映射到一个博客,每个帖子映射到某个博客。

    简而言之,用户和他/她的博客之间是1:1的映射,而博客和其帖子之间是1:N的映射。

    对于1:1的映射,我们希望一个博客的地址仅从其用户派生,这样我们可以通过其权限(或用户)来检索博客。因此,博客的种子将包括其权限的密钥,可能还有一个前缀博客,作为类型标识符。

    对于1:N的映射,我们希望每个帖子的地址不仅从它所关联的博客派生,还从另一个标识符派生,以区分博客中的多个帖子。在下面的示例中,每个帖子的地址是从博客的密钥、一个用于标识每个帖子的slug和一个前缀帖子派生出来的,作为类型标识符。

    代码如下所示:

    #[derive(Accounts)]
    #[instruction(blog_account_bump: u8)]
    pub struct InitializeBlog<'info> {
    #[account(
    init,
    seeds = [
    b"blog".as_ref(),
    authority.key().as_ref()
    ],
    bump = blog_account_bump,
    payer = authority,
    space = Blog::LEN
    )]
    pub blog_account: Account<'info, Blog>,

    #[account(mut)]
    pub authority: Signer<'info>,

    pub system_program: Program<'info, System>
    }

    #[derive(Accounts)]
    #[instruction(post_account_bump: u8, post: Post)]
    pub struct CreatePost<'info> {
    #[account(mut, has_one = authority)]
    pub blog_account: Account<'info, Blog>,

    #[account(
    init,
    seeds = [
    b"post".as_ref(),
    blog_account.key().as_ref(),
    post.slug.as_ref(),
    ],
    bump = post_account_bump,
    payer = authority,
    space = Post::LEN
    )]
    pub post_account: Account<'info, Post>,

    #[account(mut)]
    pub authority: Signer<'info>,

    pub system_program: Program<'info, System>
    }

    在客户端,你可以使用PublicKey.findProgramAddress()来获取所需的BlogPost账户地址,然后将其传递给connection.getAccountInfo()来获取账户数据。下面是一个示例:

    async () => {
    const connection = new Connection("http://localhost:8899", "confirmed");

    const [blogAccount] = await PublicKey.findProgramAddress(
    [Buffer.from("blog"), user.publicKey.toBuffer()],
    MY_PROGRAM_ID
    );

    const [postAccount] = await PublicKey.findProgramAddress(
    [Buffer.from("post"), Buffer.from("slug-1"), user.publicKey.toBuffer()],
    MY_PROGRAM_ID
    );

    const blogAccountInfo = await connection.getAccountInfo(blogAccount);
    const blogAccountState = BLOG_ACCOUNT_DATA_LAYOUT.decode(
    blogAccountInfo.data
    );
    console.log("Blog account state: ", blogAccountState);

    const postAccountInfo = await connection.getAccountInfo(postAccount);
    const postAccountState = POST_ACCOUNT_DATA_LAYOUT.decode(
    postAccountInfo.data
    );
    console.log("Post account state: ", postAccountState);
    };

    单个映射账户

    另一种实现映射的方法是在单个账户中显式存储一个BTreeMap数据结构。这个账户的地址本身可以是一个PDA,或者是生成的Keypair的公钥。

    这种账户映射的方法并不理想,原因如下:

    *首先,你需要初始化存储BTreeMap的账户,然后才能向其中插入必要的键值对。然后,你还需要将这个账户的地址存储在某个地方,以便每次更新时进行更新。

    *账户存在内存限制,每个账户的最大大小为10 Mb,这限制了BTreeMap存储大量键值对的能力。

    因此,在考虑你的用例后,可以按照以下方式实现这种方法:

    fn process_init_map(accounts: &[AccountInfo], program_id: &Pubkey) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();

    let authority_account = next_account_info(account_info_iter)?;
    let map_account = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    if !authority_account.is_signer {
    return Err(ProgramError::MissingRequiredSignature)
    }

    let (map_pda, map_bump) = Pubkey::find_program_address(
    &[b"map".as_ref()],
    program_id
    );

    if map_pda != *map_account.key || !map_account.is_writable || !map_account.data_is_empty() {
    return Err(BlogError::InvalidMapAccount.into())
    }

    let rent = Rent::get()?;
    let rent_lamports = rent.minimum_balance(MapAccount::LEN);

    let create_map_ix = &system_instruction::create_account(
    authority_account.key,
    map_account.key,
    rent_lamports,
    MapAccount::LEN.try_into().unwrap(),
    program_id
    );

    msg!("Creating MapAccount account");
    invoke_signed(
    create_map_ix,
    &[
    authority_account.clone(),
    map_account.clone(),
    system_program.clone()
    ],
    &[&[
    b"map".as_ref(),
    &[map_bump]
    ]]
    )?;

    msg!("Deserializing MapAccount account");
    let mut map_state = try_from_slice_unchecked::<MapAccount>(&map_account.data.borrow()).unwrap();
    let empty_map: BTreeMap<Pubkey, Pubkey> = BTreeMap::new();

    map_state.is_initialized = 1;
    map_state.map = empty_map;

    msg!("Serializing MapAccount account");
    map_state.serialize(&mut &mut map_account.data.borrow_mut()[..])?;

    Ok(())
    }

    fn process_insert_entry(accounts: &[AccountInfo], program_id: &Pubkey) -> ProgramResult {

    let account_info_iter = &mut accounts.iter();

    let a_account = next_account_info(account_info_iter)?;
    let b_account = next_account_info(account_info_iter)?;
    let map_account = next_account_info(account_info_iter)?;

    if !a_account.is_signer {
    return Err(ProgramError::MissingRequiredSignature)
    }

    if map_account.data.borrow()[0] == 0 || *map_account.owner != *program_id {
    return Err(BlogError::InvalidMapAccount.into())
    }

    msg!("Deserializing MapAccount account");
    let mut map_state = try_from_slice_unchecked::<MapAccount>(&map_account.data.borrow())?;

    if map_state.map.contains_key(a_account.key) {
    return Err(BlogError::AccountAlreadyHasEntry.into())
    }

    map_state.map.insert(*a_account.key, *b_account.key);

    msg!("Serializing MapAccount account");
    map_state.serialize(&mut &mut map_account.data.borrow_mut()[..])?;

    Ok(())
    }

    上述程序的客户端测试代码可能如下所示:

    const insertABIx = new TransactionInstruction({
    programId: MY_PROGRAM_ID,
    keys: [
    {
    pubkey: userA.publicKey,
    isSigner: true,
    isWritable: true,
    },
    {
    pubkey: userB.publicKey,
    isSigner: false,
    isWritable: false,
    },
    {
    pubkey: mapKey,
    isSigner: false,
    isWritable: true,
    },
    ],
    data: Buffer.from(Uint8Array.of(1)),
    });

    const insertBCIx = new TransactionInstruction({
    programId: MY_PROGRAM_ID,
    keys: [
    {
    pubkey: userB.publicKey,
    isSigner: true,
    isWritable: true,
    },
    {
    pubkey: userC.publicKey,
    isSigner: false,
    isWritable: false,
    },
    {
    pubkey: mapKey,
    isSigner: false,
    isWritable: true,
    },
    ],
    data: Buffer.from(Uint8Array.of(1)),
    });

    const insertCAIx = new TransactionInstruction({
    programId: MY_PROGRAM_ID,
    keys: [
    {
    pubkey: userC.publicKey,
    isSigner: true,
    isWritable: true,
    },
    {
    pubkey: userA.publicKey,
    isSigner: false,
    isWritable: false,
    },
    {
    pubkey: mapKey,
    isSigner: false,
    isWritable: true,
    },
    ],
    data: Buffer.from(Uint8Array.of(1)),
    });

    const tx = new Transaction();
    tx.add(initMapIx);
    tx.add(insertABIx);
    tx.add(insertBCIx);
    tx.add(insertCAIx);
    - - +

    账户映射

    在编程中,我们经常使用映射(Map)这种数据结构,将一个键与某种值关联起来。键和值可以是任意类型的数据,键用作标识要保存的特定值的标识符。通过键,我们可以高效地插入、检索和更新这些值。

    正如我们所了解的,Solana的账户模型要求程序数据和相关状态数据存储在不同的账户中。这些账户都有与之关联的地址,这本身就有映射的作用!在这里了解更多关于Solana账户模型的信息。

    因此,将值存储在单独的账户中,以其地址作为检索值所需的键是有意义的。但这也带来了一些问题,比如:

    *上述地址很可能不是理想的键,你可能难以记住并检索所需的值。

    *上述地址是不同Keypair的公钥,每个公钥(或地址)都有与之关联的私钥。如果需要,这个私钥将用于对不同的指令进行签名,这意味着我们需要在某个地方存储私钥,这绝对不是推荐的做法!

    这给许多Solana开发者带来了一个问题,即如何在他们的程序中实现类似Map的逻辑。让我们看看几种解决这个问题的方法。

    派生PDA

    PDA的全称是“程序派生地址” - Program Derived Address,简而言之,它们是从一组种子和程序ID(或地址)派生出来的地址。

    PDAs的独特之处在于,这些地址不与任何私钥相关联。这是因为这些地址不位于ED25519曲线上。因此,只有派生此地址的程序可以使用提供的密钥和种子对指令进行签名。在这里了解更多信息。

    现在我们对PDAs有了一个概念,让我们使用它们来映射一些账户!我们以一个博客程序作为示例,演示如何实现这一点。

    在这个博客程序中,我们希望每个User都拥有一个Blog。这个博客可以有任意数量的Posts。这意味着我们将每个用户映射到一个博客,每个帖子映射到某个博客。

    简而言之,用户和他/她的博客之间是1:1的映射,而博客和其帖子之间是1:N的映射。

    对于1:1的映射,我们希望一个博客的地址仅从其用户派生,这样我们可以通过其权限(或用户)来检索博客。因此,博客的种子将包括其权限的密钥,可能还有一个前缀博客,作为类型标识符。

    对于1:N的映射,我们希望每个帖子的地址不仅从它所关联的博客派生,还从另一个标识符派生,以区分博客中的多个帖子。在下面的示例中,每个帖子的地址是从博客的密钥、一个用于标识每个帖子的slug和一个前缀帖子派生出来的,作为类型标识符。

    代码如下所示:

    #[derive(Accounts)]
    #[instruction(blog_account_bump: u8)]
    pub struct InitializeBlog<'info> {
    #[account(
    init,
    seeds = [
    b"blog".as_ref(),
    authority.key().as_ref()
    ],
    bump = blog_account_bump,
    payer = authority,
    space = Blog::LEN
    )]
    pub blog_account: Account<'info, Blog>,

    #[account(mut)]
    pub authority: Signer<'info>,

    pub system_program: Program<'info, System>
    }

    #[derive(Accounts)]
    #[instruction(post_account_bump: u8, post: Post)]
    pub struct CreatePost<'info> {
    #[account(mut, has_one = authority)]
    pub blog_account: Account<'info, Blog>,

    #[account(
    init,
    seeds = [
    b"post".as_ref(),
    blog_account.key().as_ref(),
    post.slug.as_ref(),
    ],
    bump = post_account_bump,
    payer = authority,
    space = Post::LEN
    )]
    pub post_account: Account<'info, Post>,

    #[account(mut)]
    pub authority: Signer<'info>,

    pub system_program: Program<'info, System>
    }

    在客户端,你可以使用PublicKey.findProgramAddress()来获取所需的BlogPost账户地址,然后将其传递给connection.getAccountInfo()来获取账户数据。下面是一个示例:

    async () => {
    const connection = new Connection("http://localhost:8899", "confirmed");

    const [blogAccount] = await PublicKey.findProgramAddress(
    [Buffer.from("blog"), user.publicKey.toBuffer()],
    MY_PROGRAM_ID
    );

    const [postAccount] = await PublicKey.findProgramAddress(
    [Buffer.from("post"), Buffer.from("slug-1"), user.publicKey.toBuffer()],
    MY_PROGRAM_ID
    );

    const blogAccountInfo = await connection.getAccountInfo(blogAccount);
    const blogAccountState = BLOG_ACCOUNT_DATA_LAYOUT.decode(
    blogAccountInfo.data
    );
    console.log("Blog account state: ", blogAccountState);

    const postAccountInfo = await connection.getAccountInfo(postAccount);
    const postAccountState = POST_ACCOUNT_DATA_LAYOUT.decode(
    postAccountInfo.data
    );
    console.log("Post account state: ", postAccountState);
    };

    单个映射账户

    另一种实现映射的方法是在单个账户中显式存储一个BTreeMap数据结构。这个账户的地址本身可以是一个PDA,或者是生成的Keypair的公钥。

    这种账户映射的方法并不理想,原因如下:

    *首先,你需要初始化存储BTreeMap的账户,然后才能向其中插入必要的键值对。然后,你还需要将这个账户的地址存储在某个地方,以便每次更新时进行更新。

    *账户存在内存限制,每个账户的最大大小为10 Mb,这限制了BTreeMap存储大量键值对的能力。

    因此,在考虑你的用例后,可以按照以下方式实现这种方法:

    fn process_init_map(accounts: &[AccountInfo], program_id: &Pubkey) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();

    let authority_account = next_account_info(account_info_iter)?;
    let map_account = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    if !authority_account.is_signer {
    return Err(ProgramError::MissingRequiredSignature)
    }

    let (map_pda, map_bump) = Pubkey::find_program_address(
    &[b"map".as_ref()],
    program_id
    );

    if map_pda != *map_account.key || !map_account.is_writable || !map_account.data_is_empty() {
    return Err(BlogError::InvalidMapAccount.into())
    }

    let rent = Rent::get()?;
    let rent_lamports = rent.minimum_balance(MapAccount::LEN);

    let create_map_ix = &system_instruction::create_account(
    authority_account.key,
    map_account.key,
    rent_lamports,
    MapAccount::LEN.try_into().unwrap(),
    program_id
    );

    msg!("Creating MapAccount account");
    invoke_signed(
    create_map_ix,
    &[
    authority_account.clone(),
    map_account.clone(),
    system_program.clone()
    ],
    &[&[
    b"map".as_ref(),
    &[map_bump]
    ]]
    )?;

    msg!("Deserializing MapAccount account");
    let mut map_state = try_from_slice_unchecked::<MapAccount>(&map_account.data.borrow()).unwrap();
    let empty_map: BTreeMap<Pubkey, Pubkey> = BTreeMap::new();

    map_state.is_initialized = 1;
    map_state.map = empty_map;

    msg!("Serializing MapAccount account");
    map_state.serialize(&mut &mut map_account.data.borrow_mut()[..])?;

    Ok(())
    }

    fn process_insert_entry(accounts: &[AccountInfo], program_id: &Pubkey) -> ProgramResult {

    let account_info_iter = &mut accounts.iter();

    let a_account = next_account_info(account_info_iter)?;
    let b_account = next_account_info(account_info_iter)?;
    let map_account = next_account_info(account_info_iter)?;

    if !a_account.is_signer {
    return Err(ProgramError::MissingRequiredSignature)
    }

    if map_account.data.borrow()[0] == 0 || *map_account.owner != *program_id {
    return Err(BlogError::InvalidMapAccount.into())
    }

    msg!("Deserializing MapAccount account");
    let mut map_state = try_from_slice_unchecked::<MapAccount>(&map_account.data.borrow())?;

    if map_state.map.contains_key(a_account.key) {
    return Err(BlogError::AccountAlreadyHasEntry.into())
    }

    map_state.map.insert(*a_account.key, *b_account.key);

    msg!("Serializing MapAccount account");
    map_state.serialize(&mut &mut map_account.data.borrow_mut()[..])?;

    Ok(())
    }

    上述程序的客户端测试代码可能如下所示:

    const insertABIx = new TransactionInstruction({
    programId: MY_PROGRAM_ID,
    keys: [
    {
    pubkey: userA.publicKey,
    isSigner: true,
    isWritable: true,
    },
    {
    pubkey: userB.publicKey,
    isSigner: false,
    isWritable: false,
    },
    {
    pubkey: mapKey,
    isSigner: false,
    isWritable: true,
    },
    ],
    data: Buffer.from(Uint8Array.of(1)),
    });

    const insertBCIx = new TransactionInstruction({
    programId: MY_PROGRAM_ID,
    keys: [
    {
    pubkey: userB.publicKey,
    isSigner: true,
    isWritable: true,
    },
    {
    pubkey: userC.publicKey,
    isSigner: false,
    isWritable: false,
    },
    {
    pubkey: mapKey,
    isSigner: false,
    isWritable: true,
    },
    ],
    data: Buffer.from(Uint8Array.of(1)),
    });

    const insertCAIx = new TransactionInstruction({
    programId: MY_PROGRAM_ID,
    keys: [
    {
    pubkey: userC.publicKey,
    isSigner: true,
    isWritable: true,
    },
    {
    pubkey: userA.publicKey,
    isSigner: false,
    isWritable: false,
    },
    {
    pubkey: mapKey,
    isSigner: false,
    isWritable: true,
    },
    ],
    data: Buffer.from(Uint8Array.of(1)),
    });

    const tx = new Transaction();
    tx.add(initMapIx);
    tx.add(insertABIx);
    tx.add(insertBCIx);
    tx.add(insertCAIx);
    + + \ No newline at end of file diff --git a/cookbook-zh/guides/data-migration/index.html b/cookbook-zh/guides/data-migration/index.html index b1bc5d9d8..bf69222e8 100644 --- a/cookbook-zh/guides/data-migration/index.html +++ b/cookbook-zh/guides/data-migration/index.html @@ -9,14 +9,14 @@ - - + +

    迁移程序的数据账户

    你如何迁移一个程序的数据账户?

    当你创建一个程序时,与该程序关联的每个数据账户都将具有特定的数据结构。如果你需要升级一个程序派生账户,那么你将得到一堆具有旧结构的剩余程序派生账户。

    通过账户版本控制,您可以将旧账户升级到新的结构。

    info

    tip 注意 -这只是在程序拥有的账户(POA)中迁移数据的众多方法之一。

    场景

    为了对账户数据进行版本控制和迁移,我们将为每个账户提供一个ID。该ID允许我们在将其传递给程序时识别账户的版本,从而正确处理账户。

    假设有以下账户状态和程序:

    #[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
    pub struct AccountContentCurrent {
    pub somevalue: u64,
    }

    #[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
    pub struct ProgramAccountState {
    is_initialized: bool,
    data_version: u8,
    account_data: AccountContentCurrent,
    }

    在我们账户的第一个版本中,我们执行以下操作:

    IDAction
    1Include a 'data version' field in your data. It can be a simple incrementing ordinal (e.g. u8) or something more sophisticated
    2Allocating enough space for data growth
    3Initializing a number of constants to be used across program versions
    4Add an update account function under fn conversion_logic for future upgrades

    假设我们现在希望升级程序的账户,包括一个新的必需字段:somestring字段。

    如果我们之前没有为账户分配额外的空间,我们将无法升级该账户,而被卡住。

    升级账户

    在我们的新程序中,我们希望为内容状态添加一个新属性。下面的变化展示了我们如何利用初始的程序结构,并在现在使用时进行修改。

    1. 添加账户转换逻辑

    /// Current state (DATA_VERSION 1). If version changes occur, this
    /// should be copied to another (see AccountContentOld below)
    /// We've added a new field: 'somestring'
    #[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
    pub struct AccountContentCurrent {
    pub somevalue: u64,
    pub somestring: String,
    }

    /// Old content state (DATA_VERSION 0).
    #[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
    pub struct AccountContentOld {
    pub somevalue: u64,
    }

    /// Maintains account data
    #[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
    pub struct ProgramAccountState {
    is_initialized: bool,
    data_version: u8,
    account_data: AccountContentCurrent,
    }
    Line(s)Note
    6We've added Solana's solana_program::borsh::try_from_slice_unchecked to simplify reading subsets of data from the larger data block
    13-26Here we've preserved the old content structure, AccountContentOld line 24, before extending the AccountContentCurrent starting in line 17.
    60We bump the DATA_VERSION constant
    71We now have a 'previous' version and we want to know it's size
    86The Coup de grâce is adding the plumbing to upgrade the previous content state to the new (current) content state

    然后,我们更新指令,添加一个新的指令来更新somestring,并更新处理器来处理新的指令。请注意,"升级"数据结构是通过pack/unpack封装起来的。

    //! instruction Contains the main VersionProgramInstruction enum

    use {
    crate::error::DataVersionError,
    borsh::{BorshDeserialize, BorshSerialize},
    solana_program::{borsh::try_from_slice_unchecked, msg, program_error::ProgramError},
    };

    #[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)]
    /// All custom program instructions
    pub enum VersionProgramInstruction {
    InitializeAccount,
    SetU64Value(u64),
    SetString(String), // Added with data version change
    FailInstruction,
    }

    impl VersionProgramInstruction {
    /// Unpack inbound buffer to associated Instruction
    /// The expected format for input is a Borsh serialized vector
    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
    let payload = try_from_slice_unchecked::<VersionProgramInstruction>(input).unwrap();
    // let payload = VersionProgramInstruction::try_from_slice(input).unwrap();
    match payload {
    VersionProgramInstruction::InitializeAccount => Ok(payload),
    VersionProgramInstruction::SetU64Value(_) => Ok(payload),
    VersionProgramInstruction::SetString(_) => Ok(payload), // Added with data version change
    _ => Err(DataVersionError::InvalidInstruction.into()),
    }
    }
    }

    资料

    - - +这只是在程序拥有的账户(POA)中迁移数据的众多方法之一。

    场景

    为了对账户数据进行版本控制和迁移,我们将为每个账户提供一个ID。该ID允许我们在将其传递给程序时识别账户的版本,从而正确处理账户。

    假设有以下账户状态和程序:

    #[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
    pub struct AccountContentCurrent {
    pub somevalue: u64,
    }

    #[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
    pub struct ProgramAccountState {
    is_initialized: bool,
    data_version: u8,
    account_data: AccountContentCurrent,
    }

    在我们账户的第一个版本中,我们执行以下操作:

    IDAction
    1Include a 'data version' field in your data. It can be a simple incrementing ordinal (e.g. u8) or something more sophisticated
    2Allocating enough space for data growth
    3Initializing a number of constants to be used across program versions
    4Add an update account function under fn conversion_logic for future upgrades

    假设我们现在希望升级程序的账户,包括一个新的必需字段:somestring字段。

    如果我们之前没有为账户分配额外的空间,我们将无法升级该账户,而被卡住。

    升级账户

    在我们的新程序中,我们希望为内容状态添加一个新属性。下面的变化展示了我们如何利用初始的程序结构,并在现在使用时进行修改。

    1. 添加账户转换逻辑

    /// Current state (DATA_VERSION 1). If version changes occur, this
    /// should be copied to another (see AccountContentOld below)
    /// We've added a new field: 'somestring'
    #[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
    pub struct AccountContentCurrent {
    pub somevalue: u64,
    pub somestring: String,
    }

    /// Old content state (DATA_VERSION 0).
    #[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
    pub struct AccountContentOld {
    pub somevalue: u64,
    }

    /// Maintains account data
    #[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
    pub struct ProgramAccountState {
    is_initialized: bool,
    data_version: u8,
    account_data: AccountContentCurrent,
    }
    Line(s)Note
    6We've added Solana's solana_program::borsh::try_from_slice_unchecked to simplify reading subsets of data from the larger data block
    13-26Here we've preserved the old content structure, AccountContentOld line 24, before extending the AccountContentCurrent starting in line 17.
    60We bump the DATA_VERSION constant
    71We now have a 'previous' version and we want to know it's size
    86The Coup de grâce is adding the plumbing to upgrade the previous content state to the new (current) content state

    然后,我们更新指令,添加一个新的指令来更新somestring,并更新处理器来处理新的指令。请注意,"升级"数据结构是通过pack/unpack封装起来的。

    //! instruction Contains the main VersionProgramInstruction enum

    use {
    crate::error::DataVersionError,
    borsh::{BorshDeserialize, BorshSerialize},
    solana_program::{borsh::try_from_slice_unchecked, msg, program_error::ProgramError},
    };

    #[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)]
    /// All custom program instructions
    pub enum VersionProgramInstruction {
    InitializeAccount,
    SetU64Value(u64),
    SetString(String), // Added with data version change
    FailInstruction,
    }

    impl VersionProgramInstruction {
    /// Unpack inbound buffer to associated Instruction
    /// The expected format for input is a Borsh serialized vector
    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
    let payload = try_from_slice_unchecked::<VersionProgramInstruction>(input).unwrap();
    // let payload = VersionProgramInstruction::try_from_slice(input).unwrap();
    match payload {
    VersionProgramInstruction::InitializeAccount => Ok(payload),
    VersionProgramInstruction::SetU64Value(_) => Ok(payload),
    VersionProgramInstruction::SetString(_) => Ok(payload), // Added with data version change
    _ => Err(DataVersionError::InvalidInstruction.into()),
    }
    }
    }

    资料

    + + \ No newline at end of file diff --git a/cookbook-zh/guides/debugging-solana-programs/index.html b/cookbook-zh/guides/debugging-solana-programs/index.html index 93d21d27f..2ab54ba8c 100644 --- a/cookbook-zh/guides/debugging-solana-programs/index.html +++ b/cookbook-zh/guides/debugging-solana-programs/index.html @@ -9,16 +9,16 @@ - - + +

    调试 Solana 程序

    有许多选项和支持工具可用于测试和调试Solana程序。

    info

    tip 事实表

    • solana-program-test 包可以使用基本的本地运行时,在其中可以交互式地测试和调试程序(例如在 vscode 中)。
    • solana-validator 包可以使用solana-test-validator实现进行更可靠的测试,该测试发生在本地验证器节点上。你可以从编辑器中运行,但是程序中的断点将被忽略。
    • CLI工具solana-test-validator 可以从命令行运行和加载你的程序,并处理来自命令行 Rust 应用程序或使用 web3 的 JavaScript/TypeScript 应用程序的事务执行。
    • 对于上述所有情况,建议在开始时大量使用msg!宏进行输出,然后在测试和确保行为稳定后将其移除。请记住,msg! 会消耗计算单位,如果达到计算单位的预算限制,最终可能导致程序失败。

    按照以下步骤使用 solana-program-bpf-template。将其克隆到你的计算机上:

    git clone git@github.com:mvines/solana-bpf-program-template.git
    cd solana-bpf-program-template
    code .

    在编辑器中进行运行时测试和调试

    打开文件 src/lib.rs

    你会看到该程序非常简单,基本上只是记录程序入口函数process_instruction接收到的内容。

    1.转到 #[cfg(test)] 部分,并点击Run Tests。这将构建程序,然后执行 async fn test_transaction() 测试。你将在 vscode 终端中看到简化的日志消息。

    running 1 test
    "bpf_program_template" program loaded as native code
    Program 4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM invoke [1]
    Program log: process_instruction: 4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM: 1 accounts, data=[1, 2, 3]
    Program 4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM success
    test test::test_transaction ... ok
    test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 33.41s

    2.在程序的第11行(msg!行)上设置一个断点。 3. 返回测试模块,点击Debug,几秒钟后调试器会在断点处停下,现在你可以检查数据、逐步执行函数等等。

    这些测试也可以通过命令行运行:cargo testcargo test-bpf。当然,任何断点都会被忽略。

    多酷啊!

    info

    tip 请注意 你并没有使用验证节点,因此默认的程序、区块哈希等在验证节点中的行为可能与你的运行结果不同。这就是Solana 团队为我们提供本地验证节点测试的原因!

    在编辑器中进行本地验证节点测试

    tests/integration.rs 文件中,定义了使用程序加载本地验证节点进行集成测试。

    默认情况下,模板仓库的集成测试只能通过命令行使用 cargo test-bpf 运行。以下步骤将使你能够在编辑器中运行测试,并显示程序的验证节点日志和 msg! 输出:

    1. 在仓库目录中运行 cargo build-bpf 来构建示例程序
    2. 在编辑器中打开 tests/integration.rs 文件
    3. 将第 1 行注释掉 -> // #![cfg(feature = "test-bpf")]
    4. 在第 19 行将其修改为:.add_program("target/deploy/bpf_program_template", program_id)
    5. 在第 22 行插入以下内容solana_logger::setup_with_default("solana_runtime::message=debug");
    6. 点击在 test_validator_transaction() 函数上方的 Run Test

    这将加载验证节点,然后允许您构建一个交易(按照 Rust 的方式),并使用RpcClient提交给节点。

    程序的输出也将打印在编辑器的终端中。例如(简化):

    running 1 test
    Waiting for fees to stabilize 1...
    Waiting for fees to stabilize 2...
    Program 4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM invoke [1]
    Program log: process_instruction: 4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM: 1 accounts, data=[1, 2, 3]
    Program 4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM consumed 13027 of 200000 compute units
    Program 4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM success

    test test_validator_transaction ... ok
    test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.40s

    在这里进行调试将允许你调试测试主体中使用的函数和方法,但不会在你的程序中设置断点。

    非常出色,不是吗?

    从客户端应用程序进行本地验证节点测试

    最后,你可以从命令行启动一个本地验证节点,并使用solana-test-validator加载你的程序和任何账户。

    在这种方法中,你需要一个客户端应用程序,可以使用Rust的 RcpClient,也可以使用 -JavaScript or Typescript clients的客户端。

    有关更多详细信息和选项,请参阅solana-test-validator --help。对于这个示例程序,以下是基本设置:

    1. 在存储库文件夹中打开一个终端
    2. 运行solana config set -ul命令,将配置设置为指向本地
    3. 运行solana-test-validator --bpf-program target/deploy/bpf_program_template-keypair.json target/deploy/bpf_program_template.so
    4. 打开另一个终端并运行solana logs以启动日志流
    5. 然后,你可以运行客户端程序,并在您启动日志流的终端中观察程序输出

    那可真是太棒了!

    资料

    solana-program-bpf-template

    RcpClient

    JavaScript/Typescript Library

    - - +JavaScript or Typescript clients的客户端。

    有关更多详细信息和选项,请参阅solana-test-validator --help。对于这个示例程序,以下是基本设置:

    1. 在存储库文件夹中打开一个终端
    2. 运行solana config set -ul命令,将配置设置为指向本地
    3. 运行solana-test-validator --bpf-program target/deploy/bpf_program_template-keypair.json target/deploy/bpf_program_template.so
    4. 打开另一个终端并运行solana logs以启动日志流
    5. 然后,你可以运行客户端程序,并在您启动日志流的终端中观察程序输出

    那可真是太棒了!

    资料

    solana-program-bpf-template

    RcpClient

    JavaScript/Typescript Library

    + + \ No newline at end of file diff --git a/cookbook-zh/guides/feature-parity-testing/index.html b/cookbook-zh/guides/feature-parity-testing/index.html index 318344e64..6e5f11d70 100644 --- a/cookbook-zh/guides/feature-parity-testing/index.html +++ b/cookbook-zh/guides/feature-parity-testing/index.html @@ -9,13 +9,13 @@ - - + +
    -

    功能相等测试

    当测试程序时,确保它在各个集群中以相同的方式运行对于确保质量和产生预期结果非常重要。

    综述

    info

    tip 事实表

    • 功能是为 Solana 验证节点引入的能力,需要激活才能使用。
    • 某个集群(例如测试网)中可能激活了某些特性,而另一个集群(例如主网测试网)则未激活。
    • 然而,在本地运行默认的solana-test-validator时,你的 Solana 版本中的所有可用功能都会自动激活。结果是,在本地测试时,特性和测试结果可能与在不同集群中部署和运行时不同!

    场景

    假设你有一个包含三(3)条指令的交易,每个指令大约消耗 100,000 计算单元(Compute Units,CU)。在运行 Solana 1.8.x 版本时,你会观察到指令的计算单元消耗类似于:

    InstructionStarting CUExecutionRemaining CU
    1200_000-100_000100_000
    2200_000-100_000100_000
    3200_000-100_000100_000

    在 Solana 1.9.2 中引入了一个名为“transaction wide compute cap”的功能,其中默认情况下,一个交易具有 200,000 计算单元(CU)的预算,封装的指令从该交易预算中消耗。运行上述相同的交易将会有非常不同的行为:

    InstructionStarting CUExecutionRemaining CU
    1200_000-100_000100_000
    2100_000-100_0000
    30FAIL!!!FAIL!!!

    天哪!如果你不知道这一点,你可能会感到沮丧,因为你的指令行为没有任何变化会导致这种情况。在开发网络上它正常工作,但在本地却失败了?!?

    你可以增加整体交易预算,比如将其增加到 300,000 计算单元(CU),来保持你的理智,但这也展示了为什么以功能相等的方式进行测试是避免任何混淆的积极方式。

    功能状态

    使用solana feature status命令可以很容易地检查特定集群启用了哪些功能。

    solana feature status -ud   // Displays by feature status for devnet
    solana feature status -ut // Displays for testnet
    solana feature status -um // Displays for mainnet-beta
    solana feature status -ul // Displays for local, requires running solana-test-validator

    或者,你可以使用类似的工具,像 scfsd,观察所有集群上的功能状态。该工具会显示如下的部分屏幕内容,并且不需要solana-test-validator运行:

    功能相等测试

    正如前面提到的,solana-test-validator 会自动激活所有功能。所以回答问题“如何在本地测试环境中与 devnet、testnet 或者 mainnet-beta 保持一致?”的解决方案是:Solana 1.9.6 添加了 PR 来允许禁用功能:

    solana-test-validator --deactivate-feature <FEATURE_PUBKEY> ...

    简单演示

    假设你有一个简单的程序,在其入口点中记录接收到的数据。你正在测试一个包含两(2)个指令的事务,用于执行你的程序。

    所有功能已激活

    1. 你在一个终端中启动测试验证节点:
    solana config set -ul
    solana-test-validator -l ./ledger --bpf-program ADDRESS target/deploy/PROGNAME.so --reset`
    1. 在另一个终端中启动日志流处理器:
    solana logs
    1. 然后运行你的事务。你会在日志终端中看到类似的输出(为了清晰起见进行了编辑):
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc invoke [1]
    Program log: process_instruction: PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc: 0 accounts, data=[0]
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc consumed 12843 of 200000 compute units
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc success
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc invoke [1]
    Program log: process_instruction: PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc: 0 accounts, data=[1]
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc consumed 12843 of 187157 compute units
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc success[

    因为我们的功能“事务整体计算容量”默认情况下是自动激活的,我们观察到每个指令从起始事务预算的 200,000 CU 中消耗 CU。

    选择性功能已停用

    1. 在这次运行中,我们希望使 CU 预算的行为与 devnet 中运行的行为保持一致。使用 Feature Status 中描述的工具,我们可以找到transaction wide compute cap的公钥,并在测试验证器启动时使用 --deactivate-feature 参数。
    solana-test-validator -l ./ledger --deactivate-feature 5ekBxc8itEnPv4NzGJtr8BVVQLNMQuLMNQQj7pHoLNZ9 --bpf-program target/deploy/PROGNAME.so --reset`
    1. 现在我们可以在日志中看到我们的指令现在拥有自己的 200,000 CU 预算(为了清晰起见进行了编辑),这目前是所有上游集群的状态。
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc invoke [1]
    Program log: process_instruction: PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc: 0 accounts, data=[0]
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc consumed 12843 of 200000 compute units
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc success
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc invoke [1]
    Program log: process_instruction: PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc: 0 accounts, data=[0]
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc consumed 12843 of 200000 compute units
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc success

    全面相等性测试

    你可以通过识别尚未激活的每个功能,并在调用solana-test-validator时添加--deactivate-feature <FEATURE_PUBKEY>来与特定集群完全保持一致。

    solana-test-validator --deactivate-feature PUBKEY_1 --deactivate-feature PUBKEY_2 ...

    或者,scfsd 提供了一个命令开关,用于输出集群的完整停用功能集,可以直接用于solana-test-validator的启动参数:

    solana-test-validator -l ./.ledger $(scfsd -c devnet -k -t)

    如果你在验证器运行时打开另一个终端,并运行solana feature status命令,你会看到一些在 devnet 中停用的功能也被停用了。

    以编程方式进行全面相等性测试

    对于那些在测试代码中控制运行测试验证器的人来说,可以使用TestValidatorGenesis来修改测试验证器的激活/停用功能。在 Solana 1.9.6 中,验证器构建器添加了一个函数来支持这个功能。

    在您的程序文件夹的根目录下,创建一个名为tests的新文件夹,并添加一个parity_test.rs文件。以下是每个测试使用的基本函数(模板函数):

    /// Setup the test validator passing features
    /// you want to deactivate before running transactions
    pub fn setup_validator(
    invalidate_features: Vec<Pubkey>,
    ) -> Result<(TestValidator, Keypair), Box<dyn error::Error>> {
    // Extend environment variable to include our program location
    std::env::set_var("BPF_OUT_DIR", PROG_PATH);
    // Instantiate the test validator
    let mut test_validator = TestValidatorGenesis::default();
    // Once instantiated, TestValidatorGenesis configuration functions follow
    // a builder pattern enabling chaining of settings function calls
    let (test_validator, kp) = test_validator
    // Set the ledger path and name
    // maps to `solana-test-validator --ledger <DIR>`
    .ledger_path(LEDGER_PATH)
    // Load our program. Ignored if reusing ledger
    // maps to `solana-test-validator --bpf-program <ADDRESS_OR_PATH BPF_PROGRAM.SO>`
    .add_program(PROG_NAME, PROG_KEY)
    // Identify features to deactivate. Ignored if reusing ledger
    // maps to `solana-test-validator --deactivate-feature <FEATURE_PUBKEY>`
    .deactivate_features(&invalidate_features)
    // Start the test validator
    .start();
    Ok((test_validator, kp))
    }

    /// Convenience function to remove existing ledger before TestValidatorGenesis setup
    /// maps to `solana-test-validator ... --reset`
    pub fn clean_ledger_setup_validator(
    invalidate_features: Vec<Pubkey>,
    ) -> Result<(TestValidator, Keypair), Box<dyn error::Error>> {
    if PathBuf::from_str(LEDGER_PATH).unwrap().exists() {
    std::fs::remove_dir_all(LEDGER_PATH).unwrap();
    }
    setup_validator(invalidate_features)
    }

    /// Submits a transaction with programs instruction
    /// Boiler plate
    fn submit_transaction(
    rpc_client: &RpcClient,
    wallet_signer: &dyn Signer,
    instructions: Vec<Instruction>,
    ) -> Result<Signature, Box<dyn std::error::Error>> {
    let mut transaction =
    Transaction::new_unsigned(Message::new(&instructions, Some(&wallet_signer.pubkey())));
    let recent_blockhash = rpc_client
    .get_latest_blockhash()
    .map_err(|err| format!("error: unable to get recent blockhash: {}", err))?;
    transaction
    .try_sign(&vec![wallet_signer], recent_blockhash)
    .map_err(|err| format!("error: failed to sign transaction: {}", err))?;
    let signature = rpc_client
    .send_and_confirm_transaction(&transaction)
    .map_err(|err| format!("error: send transaction: {}", err))?;
    Ok(signature)
    }

    现在我们可以在mod test {...}的主体中添加测试函数,来展示默认验证器的设置(所有功能都启用),然后禁用事务广域计算限制,就像之前在命令行中运行solana-test-validator的示例一样。

    #[test]
    fn test_base_pass() {
    // Run with all features activated (default for TestValidatorGenesis)
    let inv_feat = vec![];
    // Start validator with clean (new) ledger
    let (test_validator, main_payer) = clean_ledger_setup_validator(inv_feat).unwrap();
    // Get the RpcClient
    let connection = test_validator.get_rpc_client();
    // Capture our programs log statements
    solana_logger::setup_with_default("solana_runtime::message=debug");

    // This example doesn't require sending any accounts to program
    let accounts = &[];
    // Build instruction array and submit transaction
    let txn = submit_transaction(
    &connection,
    &main_payer,
    // Add two (2) instructions to transaction to demonstrate
    // that each instruction CU draws down from default Transaction CU (200_000)
    // Replace with instructions that make sense for your program
    [
    Instruction::new_with_borsh(PROG_KEY, &0u8, accounts.to_vec()),
    Instruction::new_with_borsh(PROG_KEY, &1u8, accounts.to_vec()),
    ]
    .to_vec(),
    );
    assert!(txn.is_ok());
    }

    另外,scfs engine gadget可以生成一个包含某个集群的所有已停用功能的完整向量。以下示例演示了如何使用该 engine 来获取 devnet 的所有已停用功能列表。

    devent parity

    #[test]
    fn test_devnet_parity_pass() {
    // Use gadget-scfs to get all deactivated features from devnet
    // must have `gadgets-scfs = "0.2.0" in Cargo.toml to use
    // Here we setup for a run that samples features only
    // from devnet
    let mut my_matrix = ScfsMatrix::new(Some(ScfsCriteria {
    clusters: Some(vec![SCFS_DEVNET.to_string()]),
    ..Default::default()
    }))
    .unwrap();
    // Run the sampler matrix
    assert!(my_matrix.run().is_ok());
    // Get all deactivated features
    let deactivated = my_matrix
    .get_features(Some(&ScfsMatrix::any_inactive))
    .unwrap();
    // Confirm we have them
    assert_ne!(deactivated.len(), 0);
    // Setup test validator and logging while deactivating all
    // features that are deactivated in devnet
    let (test_validator, main_payer) = clean_ledger_setup_validator(deactivated).unwrap();
    let connection = test_validator.get_rpc_client();
    solana_logger::setup_with_default("solana_runtime::message=debug");

    let accounts = &[];
    let txn = submit_transaction(
    &connection,
    &main_payer,
    [
    // Add two (2) instructions to transaction
    // Replace with instructions that make sense for your program
    Instruction::new_with_borsh(PROG_KEY, &0u8, accounts.to_vec()),
    Instruction::new_with_borsh(PROG_KEY, &1u8, accounts.to_vec()),
    ]
    .to_vec(),
    );
    assert!(txn.is_ok());
    }

    愉快的测试!

    资料

    - - +

    功能相等测试

    当测试程序时,确保它在各个集群中以相同的方式运行对于确保质量和产生预期结果非常重要。

    综述

    info

    tip 事实表

    • 功能是为 Solana 验证节点引入的能力,需要激活才能使用。
    • 某个集群(例如测试网)中可能激活了某些特性,而另一个集群(例如主网测试网)则未激活。
    • 然而,在本地运行默认的solana-test-validator时,你的 Solana 版本中的所有可用功能都会自动激活。结果是,在本地测试时,特性和测试结果可能与在不同集群中部署和运行时不同!

    场景

    假设你有一个包含三(3)条指令的交易,每个指令大约消耗 100,000 计算单元(Compute Units,CU)。在运行 Solana 1.8.x 版本时,你会观察到指令的计算单元消耗类似于:

    InstructionStarting CUExecutionRemaining CU
    1200_000-100_000100_000
    2200_000-100_000100_000
    3200_000-100_000100_000

    在 Solana 1.9.2 中引入了一个名为“transaction wide compute cap”的功能,其中默认情况下,一个交易具有 200,000 计算单元(CU)的预算,封装的指令从该交易预算中消耗。运行上述相同的交易将会有非常不同的行为:

    InstructionStarting CUExecutionRemaining CU
    1200_000-100_000100_000
    2100_000-100_0000
    30FAIL!!!FAIL!!!

    天哪!如果你不知道这一点,你可能会感到沮丧,因为你的指令行为没有任何变化会导致这种情况。在开发网络上它正常工作,但在本地却失败了?!?

    你可以增加整体交易预算,比如将其增加到 300,000 计算单元(CU),来保持你的理智,但这也展示了为什么以功能相等的方式进行测试是避免任何混淆的积极方式。

    功能状态

    使用solana feature status命令可以很容易地检查特定集群启用了哪些功能。

    solana feature status -ud   // Displays by feature status for devnet
    solana feature status -ut // Displays for testnet
    solana feature status -um // Displays for mainnet-beta
    solana feature status -ul // Displays for local, requires running solana-test-validator

    或者,你可以使用类似的工具,像 scfsd,观察所有集群上的功能状态。该工具会显示如下的部分屏幕内容,并且不需要solana-test-validator运行:

    功能相等测试

    正如前面提到的,solana-test-validator 会自动激活所有功能。所以回答问题“如何在本地测试环境中与 devnet、testnet 或者 mainnet-beta 保持一致?”的解决方案是:Solana 1.9.6 添加了 PR 来允许禁用功能:

    solana-test-validator --deactivate-feature <FEATURE_PUBKEY> ...

    简单演示

    假设你有一个简单的程序,在其入口点中记录接收到的数据。你正在测试一个包含两(2)个指令的事务,用于执行你的程序。

    所有功能已激活

    1. 你在一个终端中启动测试验证节点:
    solana config set -ul
    solana-test-validator -l ./ledger --bpf-program ADDRESS target/deploy/PROGNAME.so --reset`
    1. 在另一个终端中启动日志流处理器:
    solana logs
    1. 然后运行你的事务。你会在日志终端中看到类似的输出(为了清晰起见进行了编辑):
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc invoke [1]
    Program log: process_instruction: PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc: 0 accounts, data=[0]
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc consumed 12843 of 200000 compute units
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc success
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc invoke [1]
    Program log: process_instruction: PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc: 0 accounts, data=[1]
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc consumed 12843 of 187157 compute units
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc success[

    因为我们的功能“事务整体计算容量”默认情况下是自动激活的,我们观察到每个指令从起始事务预算的 200,000 CU 中消耗 CU。

    选择性功能已停用

    1. 在这次运行中,我们希望使 CU 预算的行为与 devnet 中运行的行为保持一致。使用 Feature Status 中描述的工具,我们可以找到transaction wide compute cap的公钥,并在测试验证器启动时使用 --deactivate-feature 参数。
    solana-test-validator -l ./ledger --deactivate-feature 5ekBxc8itEnPv4NzGJtr8BVVQLNMQuLMNQQj7pHoLNZ9 --bpf-program target/deploy/PROGNAME.so --reset`
    1. 现在我们可以在日志中看到我们的指令现在拥有自己的 200,000 CU 预算(为了清晰起见进行了编辑),这目前是所有上游集群的状态。
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc invoke [1]
    Program log: process_instruction: PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc: 0 accounts, data=[0]
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc consumed 12843 of 200000 compute units
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc success
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc invoke [1]
    Program log: process_instruction: PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc: 0 accounts, data=[0]
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc consumed 12843 of 200000 compute units
    Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc success

    全面相等性测试

    你可以通过识别尚未激活的每个功能,并在调用solana-test-validator时添加--deactivate-feature <FEATURE_PUBKEY>来与特定集群完全保持一致。

    solana-test-validator --deactivate-feature PUBKEY_1 --deactivate-feature PUBKEY_2 ...

    或者,scfsd 提供了一个命令开关,用于输出集群的完整停用功能集,可以直接用于solana-test-validator的启动参数:

    solana-test-validator -l ./.ledger $(scfsd -c devnet -k -t)

    如果你在验证器运行时打开另一个终端,并运行solana feature status命令,你会看到一些在 devnet 中停用的功能也被停用了。

    以编程方式进行全面相等性测试

    对于那些在测试代码中控制运行测试验证器的人来说,可以使用TestValidatorGenesis来修改测试验证器的激活/停用功能。在 Solana 1.9.6 中,验证器构建器添加了一个函数来支持这个功能。

    在您的程序文件夹的根目录下,创建一个名为tests的新文件夹,并添加一个parity_test.rs文件。以下是每个测试使用的基本函数(模板函数):

    /// Setup the test validator passing features
    /// you want to deactivate before running transactions
    pub fn setup_validator(
    invalidate_features: Vec<Pubkey>,
    ) -> Result<(TestValidator, Keypair), Box<dyn error::Error>> {
    // Extend environment variable to include our program location
    std::env::set_var("BPF_OUT_DIR", PROG_PATH);
    // Instantiate the test validator
    let mut test_validator = TestValidatorGenesis::default();
    // Once instantiated, TestValidatorGenesis configuration functions follow
    // a builder pattern enabling chaining of settings function calls
    let (test_validator, kp) = test_validator
    // Set the ledger path and name
    // maps to `solana-test-validator --ledger <DIR>`
    .ledger_path(LEDGER_PATH)
    // Load our program. Ignored if reusing ledger
    // maps to `solana-test-validator --bpf-program <ADDRESS_OR_PATH BPF_PROGRAM.SO>`
    .add_program(PROG_NAME, PROG_KEY)
    // Identify features to deactivate. Ignored if reusing ledger
    // maps to `solana-test-validator --deactivate-feature <FEATURE_PUBKEY>`
    .deactivate_features(&invalidate_features)
    // Start the test validator
    .start();
    Ok((test_validator, kp))
    }

    /// Convenience function to remove existing ledger before TestValidatorGenesis setup
    /// maps to `solana-test-validator ... --reset`
    pub fn clean_ledger_setup_validator(
    invalidate_features: Vec<Pubkey>,
    ) -> Result<(TestValidator, Keypair), Box<dyn error::Error>> {
    if PathBuf::from_str(LEDGER_PATH).unwrap().exists() {
    std::fs::remove_dir_all(LEDGER_PATH).unwrap();
    }
    setup_validator(invalidate_features)
    }

    /// Submits a transaction with programs instruction
    /// Boiler plate
    fn submit_transaction(
    rpc_client: &RpcClient,
    wallet_signer: &dyn Signer,
    instructions: Vec<Instruction>,
    ) -> Result<Signature, Box<dyn std::error::Error>> {
    let mut transaction =
    Transaction::new_unsigned(Message::new(&instructions, Some(&wallet_signer.pubkey())));
    let recent_blockhash = rpc_client
    .get_latest_blockhash()
    .map_err(|err| format!("error: unable to get recent blockhash: {}", err))?;
    transaction
    .try_sign(&vec![wallet_signer], recent_blockhash)
    .map_err(|err| format!("error: failed to sign transaction: {}", err))?;
    let signature = rpc_client
    .send_and_confirm_transaction(&transaction)
    .map_err(|err| format!("error: send transaction: {}", err))?;
    Ok(signature)
    }

    现在我们可以在mod test {...}的主体中添加测试函数,来展示默认验证器的设置(所有功能都启用),然后禁用事务广域计算限制,就像之前在命令行中运行solana-test-validator的示例一样。

    #[test]
    fn test_base_pass() {
    // Run with all features activated (default for TestValidatorGenesis)
    let inv_feat = vec![];
    // Start validator with clean (new) ledger
    let (test_validator, main_payer) = clean_ledger_setup_validator(inv_feat).unwrap();
    // Get the RpcClient
    let connection = test_validator.get_rpc_client();
    // Capture our programs log statements
    solana_logger::setup_with_default("solana_runtime::message=debug");

    // This example doesn't require sending any accounts to program
    let accounts = &[];
    // Build instruction array and submit transaction
    let txn = submit_transaction(
    &connection,
    &main_payer,
    // Add two (2) instructions to transaction to demonstrate
    // that each instruction CU draws down from default Transaction CU (200_000)
    // Replace with instructions that make sense for your program
    [
    Instruction::new_with_borsh(PROG_KEY, &0u8, accounts.to_vec()),
    Instruction::new_with_borsh(PROG_KEY, &1u8, accounts.to_vec()),
    ]
    .to_vec(),
    );
    assert!(txn.is_ok());
    }

    另外,scfs engine gadget可以生成一个包含某个集群的所有已停用功能的完整向量。以下示例演示了如何使用该 engine 来获取 devnet 的所有已停用功能列表。

    devent parity

    #[test]
    fn test_devnet_parity_pass() {
    // Use gadget-scfs to get all deactivated features from devnet
    // must have `gadgets-scfs = "0.2.0" in Cargo.toml to use
    // Here we setup for a run that samples features only
    // from devnet
    let mut my_matrix = ScfsMatrix::new(Some(ScfsCriteria {
    clusters: Some(vec![SCFS_DEVNET.to_string()]),
    ..Default::default()
    }))
    .unwrap();
    // Run the sampler matrix
    assert!(my_matrix.run().is_ok());
    // Get all deactivated features
    let deactivated = my_matrix
    .get_features(Some(&ScfsMatrix::any_inactive))
    .unwrap();
    // Confirm we have them
    assert_ne!(deactivated.len(), 0);
    // Setup test validator and logging while deactivating all
    // features that are deactivated in devnet
    let (test_validator, main_payer) = clean_ledger_setup_validator(deactivated).unwrap();
    let connection = test_validator.get_rpc_client();
    solana_logger::setup_with_default("solana_runtime::message=debug");

    let accounts = &[];
    let txn = submit_transaction(
    &connection,
    &main_payer,
    [
    // Add two (2) instructions to transaction
    // Replace with instructions that make sense for your program
    Instruction::new_with_borsh(PROG_KEY, &0u8, accounts.to_vec()),
    Instruction::new_with_borsh(PROG_KEY, &1u8, accounts.to_vec()),
    ]
    .to_vec(),
    );
    assert!(txn.is_ok());
    }

    愉快的测试!

    资料

    + + \ No newline at end of file diff --git a/cookbook-zh/guides/get-program-accounts/index.html b/cookbook-zh/guides/get-program-accounts/index.html index afaabb7a6..250a5dd09 100644 --- a/cookbook-zh/guides/get-program-accounts/index.html +++ b/cookbook-zh/guides/get-program-accounts/index.html @@ -9,13 +9,13 @@ - - + +
    -

    获取程序帐户

    一个返回程序所拥有的账户的RPC方法。目前不支持分页。请求getProgramAccounts应该包括dataSlice和/或filters参数,以提高响应时间并返回只有预期结果的内容。

    info

    tip 参数

    • programId: string - 要查询的程序的公钥,以base58编码的字符串形式提供。
    • (可选) configOrCommitment: object - 包含以下可选字段的配置参数:
      • (可选) commitment: string - 状态承诺/State commitment
      • (可选) encoding: string - 账户数据的编码方式,可以是: base58, base64, 或 jsonParsed. 请注意 web3js 用户应改用 getParsedProgramAccounts
      • (可选) dataSlice: object - 根据以下内容限制返回的账户数据:
        • offset: number - 开始返回账户数据的字节数
        • length: number - 要返回的账户数据的字节数
      • (可选) filters: array - 使用以下过滤器对象对结果进行过滤:
        • memcmp: object - 将一系列字节与账户数据匹配:
        • offset: number - 开始比较的账户数据字节偏移量
        • bytes: string - 要匹配的数据,以base58编码的字符串形式,限制为129个字节
        • dataSize: number - 将账户数据的长度与提供的数据大小进行比较
      • (可选) withContext: boolean - 将结果包装在一个 RpcResponse JSON object
    响应

    默认情况下,getProgramAccounts将返回一个具有以下结构的 JSON 对象数组:

    • pubkey: string - 账户公钥,以 base58 编码的字符串形式
    • account: object - 一个包含以下子字段的 JSON 对象:
      • lamports: number, 分配给账户的 lamports 数量
      • owner: string, 账户所分配的程序的 base58 编码的公钥
      • data: string | object - 与账户关联的数据,根据提供的编码参数,可以是编码的二进制数据或 JSON 格式 parameter
      • executable: boolean, 指示账户是否包含着程序
      • rentEpoch: number, 该账户下次需要支付租金的纪元(epoch)

    深入

    getProgramAccounts 是一个多功能的RPC方法,用于返回由程序拥有的所有账户。我们可以利用getProgramAccounts进行许多有用的查询,例如查找:

    • 特定钱包的所有代币账户
    • 特定代币发行的所有代币账户(即所有SRM持有人)
    • 特定程序的所有自定义账户(即所有Mango用户)

    尽管getProgramAccounts非常有用,但由于目前的限制,它经常被误解。许多由getProgramAccounts支持的查询需要RPC节点扫描大量数据。这些扫描需要大量的内存和资源。因此,调用过于频繁或范围过大可能导致连接超时。此外,在撰写本文时,getProgramAccounts端点不支持分页。如果查询结果太大,响应将被截断。

    为了解决当前的限制,getProgramAccounts提供了一些有用的参数,包括dataSlicefilters选项的memcmpdataSize。通过提供这些参数的组合,我们可以将查询范围缩小到可管理和可预测的大小。

    getProgramAccounts的一个常见示例涉及与SPL-Token Program 程序交互。仅使用基本调用请求由Token程序拥有的所有账户将涉及大量的数据。然而,通过提供参数,我们可以高效地请求我们要使用的数据。

    filters

    getProgramAccounts一起使用的最常见参数是filters数组。该数组接受两种类型的过滤器,即dataSizememcmp。在使用这些过滤器之前,我们应该熟悉我们请求的数据的布局和序列化方式。

    dataSize

    在Token程序的情况下,我们可以看到代币账户的长度为165个字节。 具体而言,一个代币账户有八个不同的字段,每个字段需要一定数量的字节。我们可以使用下面的示例图来可视化这些数据的布局。

    Account Size

    如果我们想找到由我们的钱包地址拥有的所有代币账户,我们可以在filters数组中添加{ dataSize: 165 }来将查询范围缩小为仅限长度为165个字节的账户。然而,仅此还不够。我们还需要添加一个过滤器来查找由我们的地址拥有的账户。我们可以使用memcmp过滤器实现这一点。

    memcmp

    memcmp过滤器,也叫"内存比较"过滤器,允许我们比较存储在账户上的任何字段的数据。具体而言,我们可以查询仅与特定位置上的特定一组字节匹配的账户。memcmp需要两个参数:

    • offset: 开始比较数据的位置。这个位置以字节为单位,表示为一个整数。
    • bytes: 数据应该与账户的数据匹配。这表示为一个base58编码的字符串,应该限制在129个字节以下。

    需要注意的是,memcmp只会返回与提供的bytes完全匹配的结果。目前,它不支持与提供的bytes相比小于或大于的比较。

    继续使用我们的Token程序示例,我们可以修改查询,只返回由我们的钱包地址拥有的代币账户。观察代币账户时,我们可以看到存储在代币账户上的前两个字段都是公钥,而且每个公钥的长度为32个字节。鉴于owner是第二个字段,我们应该从offset为32字节的位置开始进行memcmp。从这里开始,我们将寻找owner字段与我们的钱包地址匹配的账户。

    Account Size

    我们可以通过以下实例来调用此查询:

    use solana_client::{
    rpc_client::RpcClient,
    rpc_filter::{RpcFilterType, Memcmp, MemcmpEncodedBytes, MemcmpEncoding},
    rpc_config::{RpcProgramAccountsConfig, RpcAccountInfoConfig},
    };
    use solana_sdk::{commitment_config::CommitmentConfig, program_pack::Pack};
    use spl_token::{state::{Mint, Account}};
    use solana_account_decoder::{UiAccountEncoding};

    fn main() {
    const MY_WALLET_ADDRESS: &str = "FriELggez2Dy3phZeHHAdpcoEXkKQVkv6tx3zDtCVP8T";

    let rpc_url = String::from("http://api.devnet.solana.com");
    let connection = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());

    let filters = Some(vec![
    RpcFilterType::Memcmp(Memcmp {
    offset: 32,
    bytes: MemcmpEncodedBytes::Base58(MY_WALLET_ADDRESS.to_string()),
    encoding: Some(MemcmpEncoding::Binary),
    }),
    RpcFilterType::DataSize(165),
    ]);

    let accounts = connection.get_program_accounts_with_config(
    &spl_token::ID,
    RpcProgramAccountsConfig {
    filters,
    account_config: RpcAccountInfoConfig {
    encoding: Some(UiAccountEncoding::Base64),
    commitment: Some(connection.commitment()),
    ..RpcAccountInfoConfig::default()
    },
    ..RpcProgramAccountsConfig::default()
    },
    ).unwrap();

    println!("Found {:?} token account(s) for wallet {MY_WALLET_ADDRESS}: ", accounts.len());

    for (i, account) in accounts.iter().enumerate() {
    println!("-- Token Account Address {:?}: {:?} --", i, account.0);

    let mint_token_account = Account::unpack_from_slice(account.1.data.as_slice()).unwrap();
    println!("Mint: {:?}", mint_token_account.mint);

    let mint_account_data = connection.get_account_data(&mint_token_account.mint).unwrap();
    let mint = Mint::unpack_from_slice(mint_account_data.as_slice()).unwrap();
    println!("Amount: {:?}", mint_token_account.amount as f64 /10usize.pow(mint.decimals as u32) as f64);
    }
    }

    /*
    // Output

    Found 2 token account(s) for wallet FriELggez2Dy3phZeHHAdpcoEXkKQVkv6tx3zDtCVP8T:
    -- Token Account Address 0: H12yCcKLHFJFfohkeKiN8v3zgaLnUMwRcnJTyB4igAsy --
    Mint: CKKDsBT6KiT4GDKs3e39Ue9tDkhuGUKM3cC2a7pmV9YK
    Amount: 1.0
    -- Token Account Address 1: Et3bNDxe2wP1yE5ao6mMvUByQUHg8nZTndpJNvfKLdCb --
    Mint: BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf
    Amount: 3.0
    */

    dataSlice

    除了上面提到的两个过滤器参数以外,getProgramAccounts的第三个最常见参数是dataSlice。与filters参数不同,dataSlice不会减少查询返回的账户数量。dataSlice将限制的是每个账户的数据量。

    memcmp类似,dataSlice接受两个参数:

    • offset: 开始返回账户数据的位置(以字节为单位)
    • length: 应该返回的字节数

    在处理大型数据集但实际上不关心账户数据本身时,dataSlice特别有用。例如,如果我们想找到特定代币发行的代币账户数量(即代币持有者数量),就可以使用dataSlice

    use solana_client::{
    rpc_client::RpcClient,
    rpc_filter::{RpcFilterType, Memcmp, MemcmpEncodedBytes, MemcmpEncoding},
    rpc_config::{RpcProgramAccountsConfig, RpcAccountInfoConfig},
    };
    use solana_sdk::{commitment_config::CommitmentConfig};
    use solana_account_decoder::{UiAccountEncoding, UiDataSliceConfig};

    pub fn main() {
    const MY_TOKEN_MINT_ADDRESS: &str = "BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf";

    let rpc_url = String::from("http://api.devnet.solana.com");
    let connection = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());

    let filters = Some(vec![
    RpcFilterType::Memcmp(Memcmp {
    offset: 0, // number of bytes
    bytes: MemcmpEncodedBytes::Base58(MY_TOKEN_MINT_ADDRESS.to_string()),
    encoding: Some(MemcmpEncoding::Binary),
    }),
    RpcFilterType::DataSize(165), // number of bytes
    ]);

    let accounts = connection.get_program_accounts_with_config(
    &spl_token::ID,
    RpcProgramAccountsConfig {
    filters,
    account_config: RpcAccountInfoConfig {
    data_slice: Some(UiDataSliceConfig {
    offset: 0, // number of bytes
    length: 0, // number of bytes
    }),
    encoding: Some(UiAccountEncoding::Base64),
    commitment: Some(connection.commitment()),
    ..RpcAccountInfoConfig::default()
    },
    ..RpcProgramAccountsConfig::default()
    },
    ).unwrap();

    println!("Found {:?} token account(s) for mint {MY_TOKEN_MINT_ADDRESS}: ", accounts.len());
    println!("{:#?}", accounts);
    }

    /*
    // Output (notice the empty <Buffer > at acccount.data)

    Found 3 token account(s) for mint BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf:
    [
    (
    tofD3NzLfZ5pWG91JcnbfsAbfMcFF2SRRp3ChnjeTcL,
    Account {
    lamports: 2039280,
    data.len: 0,
    owner: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,
    executable: false,
    rent_epoch: 319,
    },
    ),
    (
    CMSC2GeWDsTPjfnhzCZHEqGRjKseBhrWaC2zNcfQQuGS,
    Account {
    lamports: 2039280,
    data.len: 0,
    owner: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,
    executable: false,
    rent_epoch: 318,
    },
    ),
    (
    Et3bNDxe2wP1yE5ao6mMvUByQUHg8nZTndpJNvfKLdCb,
    Account {
    lamports: 2039280,
    data.len: 0,
    owner: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,
    executable: false,
    rent_epoch: 318,
    },
    ),
    ]
    */

    通过组合这三个参数(dataSlicedataSizememcmp),我们可以限制查询的范围,并高效地返回我们想要的数据。

    其他资料

    - - +

    获取程序帐户

    一个返回程序所拥有的账户的RPC方法。目前不支持分页。请求getProgramAccounts应该包括dataSlice和/或filters参数,以提高响应时间并返回只有预期结果的内容。

    info

    tip 参数

    • programId: string - 要查询的程序的公钥,以base58编码的字符串形式提供。
    • (可选) configOrCommitment: object - 包含以下可选字段的配置参数:
      • (可选) commitment: string - 状态承诺/State commitment
      • (可选) encoding: string - 账户数据的编码方式,可以是: base58, base64, 或 jsonParsed. 请注意 web3js 用户应改用 getParsedProgramAccounts
      • (可选) dataSlice: object - 根据以下内容限制返回的账户数据:
        • offset: number - 开始返回账户数据的字节数
        • length: number - 要返回的账户数据的字节数
      • (可选) filters: array - 使用以下过滤器对象对结果进行过滤:
        • memcmp: object - 将一系列字节与账户数据匹配:
        • offset: number - 开始比较的账户数据字节偏移量
        • bytes: string - 要匹配的数据,以base58编码的字符串形式,限制为129个字节
        • dataSize: number - 将账户数据的长度与提供的数据大小进行比较
      • (可选) withContext: boolean - 将结果包装在一个 RpcResponse JSON object
    响应

    默认情况下,getProgramAccounts将返回一个具有以下结构的 JSON 对象数组:

    • pubkey: string - 账户公钥,以 base58 编码的字符串形式
    • account: object - 一个包含以下子字段的 JSON 对象:
      • lamports: number, 分配给账户的 lamports 数量
      • owner: string, 账户所分配的程序的 base58 编码的公钥
      • data: string | object - 与账户关联的数据,根据提供的编码参数,可以是编码的二进制数据或 JSON 格式 parameter
      • executable: boolean, 指示账户是否包含着程序
      • rentEpoch: number, 该账户下次需要支付租金的纪元(epoch)

    深入

    getProgramAccounts 是一个多功能的RPC方法,用于返回由程序拥有的所有账户。我们可以利用getProgramAccounts进行许多有用的查询,例如查找:

    • 特定钱包的所有代币账户
    • 特定代币发行的所有代币账户(即所有SRM持有人)
    • 特定程序的所有自定义账户(即所有Mango用户)

    尽管getProgramAccounts非常有用,但由于目前的限制,它经常被误解。许多由getProgramAccounts支持的查询需要RPC节点扫描大量数据。这些扫描需要大量的内存和资源。因此,调用过于频繁或范围过大可能导致连接超时。此外,在撰写本文时,getProgramAccounts端点不支持分页。如果查询结果太大,响应将被截断。

    为了解决当前的限制,getProgramAccounts提供了一些有用的参数,包括dataSlicefilters选项的memcmpdataSize。通过提供这些参数的组合,我们可以将查询范围缩小到可管理和可预测的大小。

    getProgramAccounts的一个常见示例涉及与SPL-Token Program 程序交互。仅使用基本调用请求由Token程序拥有的所有账户将涉及大量的数据。然而,通过提供参数,我们可以高效地请求我们要使用的数据。

    filters

    getProgramAccounts一起使用的最常见参数是filters数组。该数组接受两种类型的过滤器,即dataSizememcmp。在使用这些过滤器之前,我们应该熟悉我们请求的数据的布局和序列化方式。

    dataSize

    在Token程序的情况下,我们可以看到代币账户的长度为165个字节。 具体而言,一个代币账户有八个不同的字段,每个字段需要一定数量的字节。我们可以使用下面的示例图来可视化这些数据的布局。

    Account Size

    如果我们想找到由我们的钱包地址拥有的所有代币账户,我们可以在filters数组中添加{ dataSize: 165 }来将查询范围缩小为仅限长度为165个字节的账户。然而,仅此还不够。我们还需要添加一个过滤器来查找由我们的地址拥有的账户。我们可以使用memcmp过滤器实现这一点。

    memcmp

    memcmp过滤器,也叫"内存比较"过滤器,允许我们比较存储在账户上的任何字段的数据。具体而言,我们可以查询仅与特定位置上的特定一组字节匹配的账户。memcmp需要两个参数:

    • offset: 开始比较数据的位置。这个位置以字节为单位,表示为一个整数。
    • bytes: 数据应该与账户的数据匹配。这表示为一个base58编码的字符串,应该限制在129个字节以下。

    需要注意的是,memcmp只会返回与提供的bytes完全匹配的结果。目前,它不支持与提供的bytes相比小于或大于的比较。

    继续使用我们的Token程序示例,我们可以修改查询,只返回由我们的钱包地址拥有的代币账户。观察代币账户时,我们可以看到存储在代币账户上的前两个字段都是公钥,而且每个公钥的长度为32个字节。鉴于owner是第二个字段,我们应该从offset为32字节的位置开始进行memcmp。从这里开始,我们将寻找owner字段与我们的钱包地址匹配的账户。

    Account Size

    我们可以通过以下实例来调用此查询:

    use solana_client::{
    rpc_client::RpcClient,
    rpc_filter::{RpcFilterType, Memcmp, MemcmpEncodedBytes, MemcmpEncoding},
    rpc_config::{RpcProgramAccountsConfig, RpcAccountInfoConfig},
    };
    use solana_sdk::{commitment_config::CommitmentConfig, program_pack::Pack};
    use spl_token::{state::{Mint, Account}};
    use solana_account_decoder::{UiAccountEncoding};

    fn main() {
    const MY_WALLET_ADDRESS: &str = "FriELggez2Dy3phZeHHAdpcoEXkKQVkv6tx3zDtCVP8T";

    let rpc_url = String::from("http://api.devnet.solana.com");
    let connection = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());

    let filters = Some(vec![
    RpcFilterType::Memcmp(Memcmp {
    offset: 32,
    bytes: MemcmpEncodedBytes::Base58(MY_WALLET_ADDRESS.to_string()),
    encoding: Some(MemcmpEncoding::Binary),
    }),
    RpcFilterType::DataSize(165),
    ]);

    let accounts = connection.get_program_accounts_with_config(
    &spl_token::ID,
    RpcProgramAccountsConfig {
    filters,
    account_config: RpcAccountInfoConfig {
    encoding: Some(UiAccountEncoding::Base64),
    commitment: Some(connection.commitment()),
    ..RpcAccountInfoConfig::default()
    },
    ..RpcProgramAccountsConfig::default()
    },
    ).unwrap();

    println!("Found {:?} token account(s) for wallet {MY_WALLET_ADDRESS}: ", accounts.len());

    for (i, account) in accounts.iter().enumerate() {
    println!("-- Token Account Address {:?}: {:?} --", i, account.0);

    let mint_token_account = Account::unpack_from_slice(account.1.data.as_slice()).unwrap();
    println!("Mint: {:?}", mint_token_account.mint);

    let mint_account_data = connection.get_account_data(&mint_token_account.mint).unwrap();
    let mint = Mint::unpack_from_slice(mint_account_data.as_slice()).unwrap();
    println!("Amount: {:?}", mint_token_account.amount as f64 /10usize.pow(mint.decimals as u32) as f64);
    }
    }

    /*
    // Output

    Found 2 token account(s) for wallet FriELggez2Dy3phZeHHAdpcoEXkKQVkv6tx3zDtCVP8T:
    -- Token Account Address 0: H12yCcKLHFJFfohkeKiN8v3zgaLnUMwRcnJTyB4igAsy --
    Mint: CKKDsBT6KiT4GDKs3e39Ue9tDkhuGUKM3cC2a7pmV9YK
    Amount: 1.0
    -- Token Account Address 1: Et3bNDxe2wP1yE5ao6mMvUByQUHg8nZTndpJNvfKLdCb --
    Mint: BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf
    Amount: 3.0
    */

    dataSlice

    除了上面提到的两个过滤器参数以外,getProgramAccounts的第三个最常见参数是dataSlice。与filters参数不同,dataSlice不会减少查询返回的账户数量。dataSlice将限制的是每个账户的数据量。

    memcmp类似,dataSlice接受两个参数:

    • offset: 开始返回账户数据的位置(以字节为单位)
    • length: 应该返回的字节数

    在处理大型数据集但实际上不关心账户数据本身时,dataSlice特别有用。例如,如果我们想找到特定代币发行的代币账户数量(即代币持有者数量),就可以使用dataSlice

    use solana_client::{
    rpc_client::RpcClient,
    rpc_filter::{RpcFilterType, Memcmp, MemcmpEncodedBytes, MemcmpEncoding},
    rpc_config::{RpcProgramAccountsConfig, RpcAccountInfoConfig},
    };
    use solana_sdk::{commitment_config::CommitmentConfig};
    use solana_account_decoder::{UiAccountEncoding, UiDataSliceConfig};

    pub fn main() {
    const MY_TOKEN_MINT_ADDRESS: &str = "BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf";

    let rpc_url = String::from("http://api.devnet.solana.com");
    let connection = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());

    let filters = Some(vec![
    RpcFilterType::Memcmp(Memcmp {
    offset: 0, // number of bytes
    bytes: MemcmpEncodedBytes::Base58(MY_TOKEN_MINT_ADDRESS.to_string()),
    encoding: Some(MemcmpEncoding::Binary),
    }),
    RpcFilterType::DataSize(165), // number of bytes
    ]);

    let accounts = connection.get_program_accounts_with_config(
    &spl_token::ID,
    RpcProgramAccountsConfig {
    filters,
    account_config: RpcAccountInfoConfig {
    data_slice: Some(UiDataSliceConfig {
    offset: 0, // number of bytes
    length: 0, // number of bytes
    }),
    encoding: Some(UiAccountEncoding::Base64),
    commitment: Some(connection.commitment()),
    ..RpcAccountInfoConfig::default()
    },
    ..RpcProgramAccountsConfig::default()
    },
    ).unwrap();

    println!("Found {:?} token account(s) for mint {MY_TOKEN_MINT_ADDRESS}: ", accounts.len());
    println!("{:#?}", accounts);
    }

    /*
    // Output (notice the empty <Buffer > at acccount.data)

    Found 3 token account(s) for mint BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf:
    [
    (
    tofD3NzLfZ5pWG91JcnbfsAbfMcFF2SRRp3ChnjeTcL,
    Account {
    lamports: 2039280,
    data.len: 0,
    owner: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,
    executable: false,
    rent_epoch: 319,
    },
    ),
    (
    CMSC2GeWDsTPjfnhzCZHEqGRjKseBhrWaC2zNcfQQuGS,
    Account {
    lamports: 2039280,
    data.len: 0,
    owner: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,
    executable: false,
    rent_epoch: 318,
    },
    ),
    (
    Et3bNDxe2wP1yE5ao6mMvUByQUHg8nZTndpJNvfKLdCb,
    Account {
    lamports: 2039280,
    data.len: 0,
    owner: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,
    executable: false,
    rent_epoch: 318,
    },
    ),
    ]
    */

    通过组合这三个参数(dataSlicedataSizememcmp),我们可以限制查询的范围,并高效地返回我们想要的数据。

    其他资料

    + + \ No newline at end of file diff --git a/cookbook-zh/guides/index.html b/cookbook-zh/guides/index.html index d1b331460..651e5764d 100644 --- a/cookbook-zh/guides/index.html +++ b/cookbook-zh/guides/index.html @@ -9,13 +9,13 @@ - - + + - - + + + \ No newline at end of file diff --git a/cookbook-zh/guides/retrying-transactions/index.html b/cookbook-zh/guides/retrying-transactions/index.html index 14cac51e2..c0934ace3 100644 --- a/cookbook-zh/guides/retrying-transactions/index.html +++ b/cookbook-zh/guides/retrying-transactions/index.html @@ -9,13 +9,13 @@ - - + +
    -

    重试交易

    在某些情况下,一个看似有效的交易可能在输入区块之前会被丢弃。这种情况最常发生在网络拥堵期间,当一个RPC节点无法将交易重新广播给区块链的领导节点时。对于最终用户来说,他们的交易可能会完全消失。虽然RPC节点配备了通用的重新广播算法,但应用程序开发人员也可以开发自己的自定义重新广播逻辑。

    info

    tip 事实表

    • RPC节点将尝试使用通用算法重新广播交易
    • 应用程序开发人员可以实现自定义的重新广播逻辑
    • 开发人员应该利用sendTransaction JSON-RPC方法中的maxRetries参数
    • 开发人员应该启用预检查,以便在提交交易之前引发错误
    • 在重新签署任何交易之前,非常重要的是确保初始交易的块哈希已过期

    交易的旅程

    客户端如何提交交易

    在Solana中,没有内存池(mempool)的概念。无论是通过编程还是由最终用户发起,所有的交易都会被高效地路由到领导节点,以便将它们处理成区块。有两种主要的方式可以将交易发送给领导节点:

    1. 通过RPC服务器和sendTransaction JSON-RPC 方法进行代理发送
    2. 通过TPU客户 端直接发送给领导节点

    绝大多数最终用户将通过RPC服务器提交交易。当客户端提交交易时,接收的RPC节点会尝试将交易广播给当前和下一个领导节点。在交易被领导节点处理之前,除了客户端和中继的RPC节点知道的内容外,没有关于交易的记录。在TPU客户端的情况下,重新广播和领导节点的转发完全由客户端软件处理。

    Transaction Journey

    RPC节点如何广播交易

    当RPC节点通过sendTransaction接收到一个交易后,它会将交易转换为UDP 数据包,然后将其转发给相关的领导。UDP允许验证节点之间快速通信,但不提供关于交易传递的任何保证。

    因为Solana的领导节点调度在每个纪元 (大约2天)之前就已知,所以RPC节点会直接将其交易广播给当前和下一个领导节点。这与其他流言协议(如以太坊)随机广播和广泛传播整个网络的交易的方式形成对比。默认情况下,RPC节点会每两秒尝试将交易转发给领导节点,直到交易被确认或交易的块哈希过期(在本文撰写时为150个区块或约1分钟19秒)。如果待重新广播的队列大小超过10,000 transactions 个交易,则新提交的交易将被丢弃。RPC运营商可以调整命令行参数 以更改此重试逻辑的默认行为。

    当RPC节点广播一个交易时,它会尝试将交易转发给领导节点的交易处理单元(TPU)。TPU将交易处理分为五个不同的阶段:

    TPU Overview

    Image Courtesy of Jito Labs

    在这五个阶段中,Fetch阶段负责接收交易。在Fetch阶段中,验证节点会根据三个端口对传入的交易进行分类:

    • tpu 处理常规交易,例如代币转账、NFT铸造和程序指令。
    • tpu_vote 专门处理投票交易。
    • tpu_forwards 将未处理的数据包转发给下一个领导节点,如果当前领导无法处理所有交易。

    如需了解更多关于TPU的信息,请参考Jito Labs出色的文章.

    交易如何被丢弃

    在交易的整个过程中,有几种情况下交易可能意外从网络中丢失。

    在交易被处理之前

    如果网络丢弃一个交易,通常是在交易被领导处理之前发生。UDP 数据包丢失 是可能发生这种情况的最简单原因。在网络负载高峰期,验证节点可能会被大量需要处理的交易压倒。虽然验证节点可以通过 tpu_forwards,端口转发多余的交易,但转发. 的数据量是有限的。此外,每个转发仅限于验证节点之间的单一跳跃。也就是说,通过tpu_forwards端口接收的交易不会被转发给其他验证节点。

    还有两个较少为人知的原因,可能导致交易在被处理之前被丢弃。第一种情况涉及通过RPC池提交的交易。偶尔,RPC池的一部分可能会领先于其他部分。当池中的节点需要共同工作时,这可能会导致问题。在这个例子中,交易的recentBlockhash 从池中的先进部分(后端A)查询。当交易提交到滞后的池中(后端B)时,节点将无法识别先进的块哈希并丢弃交易。如果开发人员在sendTransaction中启用了preflight checks, 可以在提交交易时检测到此问题。

    Dropped via RPC Pool

    网络分叉也可能暂时的导致交易丢失。如果验证在银行阶段重新播放其块的速度较慢,可能会创建一个少数派分叉。当客户端构建一个交易时,交易可能引用仅存在于少数派分叉上的recentBlockhash。在提交交易后,集群可能在交易被处理之前切换到其他分叉。在这种情况下,由于找不到块哈希,交易被丢弃。

    Dropped due to Minority Fork (Before Processed)

    在交易被处理后,但尚未最终确认之前

    如果一个交易引用了来自少数派分叉的recentBlockhash,该交易有可能还会进行处理。在这种情况下,交易将由少数派分叉上的领导节点进行处理。当这个领导试图与不认可少数派分叉的大多数验证节点达成共识时,它将无法与它们分享已处理的交易。在这种情况下,交易在最终确定之前将被丢弃。

    Dropped due to Minority Fork (After Processed)

    处理被丢弃的交易

    虽然RPC节点会尝试重新广播交易,但它们使用的算法是通用的,往往不适合特定应用的需求。为了应对网络拥堵的时候,应用程序开发人员应该自定义自己的重新广播逻辑。

    深入了解sendTransaction

    在提交交易方面,sendTransaction RPC方法是开发者可用的主要工具。sendTransaction仅负责将交易从客户端传递到RPC节点。如果节点接收到交易,sendTransaction将返回用于跟踪交易的交易ID。成功的响应并不表示该交易将由集群处理或最终确定。

    info

    Tips

    请求参数

    • transaction: string - 完全签名的交易,以编码字符串形式表示
    • (可选) configuration object: object
    • skipPreflight: boolean - 如果为 true,则跳过预检事务检查(默认为 false)
    • (可选) preflightCommitment: string - 用于针对银行插槽进行预检模拟的承诺 级别(默认为"finalized")
    • (可选) encoding: string - 用于交易数据的编码方式。可以选择 "base58"(较慢)或 "base64"(默认为 "base58")
    • (可选) maxRetries: usize - RPC节点重试将交易发送给领导者的最大次数。如果未提供此参数,RPC节点将重试交易,直到交易最终确定或块哈希过期为止

    响应

    • transaction id: string - 第一个嵌入在交易中的交易签名,以base-58编码的字符串形式表示。可以使用该交易ID与 getSignatureStatuses 一起使用,以轮询获取状态更新。

    自定义重播逻辑

    为了开发自己的重新广播逻辑,开发者应该利用sendTransactionmaxRetries参数。如果提供了maxRetries,它将覆盖RPC节点的默认重试逻辑,允许开发人员在合理范围内 手动控制重试过程。

    手动重试交易的常见模式涉及临时存储来自getLatestBlockhashlastValidBlockHeight。一旦存储了该值,应用程序可以轮询集群的blockheight, 并在适当的时间间隔内手动重试交易。在网络拥堵的时期,将maxRetries设置为0并通过自定义算法手动重新广播是有优势的。一些应用程序可能采用指数退避, 而其他应用程序(如Mango )选择在恒定间隔内持续重新提交 交易,直到发生超时。

    while (blockheight < lastValidBlockHeight) {
    connection.sendRawTransaction(rawTransaction, {
    skipPreflight: true,
    });
    await sleep(500);
    blockheight = await connection.getBlockHeight();
    }

    当通过getLatestBlockhash进行轮询时,应用程序应该指定其预期的承诺 级别。通过将承诺级别设置为confirmed(已投票)或finalized(在confirmed之后约30个块),应用程序可以避免从少数派分叉轮询块哈希。

    如果应用程序可以访问负载均衡器后面的RPC节点,还可以选择将其工作负载分配给特定节点。为数据密集型请求提供服务的RPC节点(例如getProgramAccounts)可能会滞后,并且可能不适合转发交易。对于处理时间敏感交易的应用程序,最好拥有专用节点仅处理sendTransaction操作。

    跳过预检的后果

    默认情况下,sendTransaction将在提交交易之前执行三个预检查。具体而言,sendTransaction将会:

    • 验证所有签名是否有效
    • 检查引用的块哈希是否在最近的150个块内
    • 针对预检查的preFlightCommitment,模拟交易与银行槽位之间的交互

    如果其中任何一个预检查失败,sendTransaction将在提交交易之前引发错误。预检查常常能够防止交易丢失,并使客户端能够优雅地处理错误。为了确保这些常见错误得到考虑,建议开发人员将skipPreflight设置为false。

    何时重新签署交易

    尽管尽力进行重新广播,但有时客户端可能需要重新签署交易。在重新签署任何交易之前,非常重要的是确保初始交易的块哈希已经过期。如果初始块哈希仍然有效,那么两个交易都有可能被网络接受。对于最终用户来说,这将看起来好像他们无意中发送了相同的交易两次。

    在Solana中,一旦所引用的块哈希早于从getLatestBlockhash接收到的lastValidBlockHeight,可以安全地丢弃已丢弃的交易。开发者应该通过查询 getEpochInfo 并将其与响应中的blockHeight进行比较来跟踪lastValidBlockHeight。一旦一个块哈希无效,客户端可以使用新查询的块哈希重新签署。

    致谢

    非常感谢 Trent Nelson、Jacob Creech, White Tiger、Le Yafo、Buffalu, 和 Jito Labs 的审查和反馈。

    - - +

    重试交易

    在某些情况下,一个看似有效的交易可能在输入区块之前会被丢弃。这种情况最常发生在网络拥堵期间,当一个RPC节点无法将交易重新广播给区块链的领导节点时。对于最终用户来说,他们的交易可能会完全消失。虽然RPC节点配备了通用的重新广播算法,但应用程序开发人员也可以开发自己的自定义重新广播逻辑。

    info

    tip 事实表

    • RPC节点将尝试使用通用算法重新广播交易
    • 应用程序开发人员可以实现自定义的重新广播逻辑
    • 开发人员应该利用sendTransaction JSON-RPC方法中的maxRetries参数
    • 开发人员应该启用预检查,以便在提交交易之前引发错误
    • 在重新签署任何交易之前,非常重要的是确保初始交易的块哈希已过期

    交易的旅程

    客户端如何提交交易

    在Solana中,没有内存池(mempool)的概念。无论是通过编程还是由最终用户发起,所有的交易都会被高效地路由到领导节点,以便将它们处理成区块。有两种主要的方式可以将交易发送给领导节点:

    1. 通过RPC服务器和sendTransaction JSON-RPC 方法进行代理发送
    2. 通过TPU客户 端直接发送给领导节点

    绝大多数最终用户将通过RPC服务器提交交易。当客户端提交交易时,接收的RPC节点会尝试将交易广播给当前和下一个领导节点。在交易被领导节点处理之前,除了客户端和中继的RPC节点知道的内容外,没有关于交易的记录。在TPU客户端的情况下,重新广播和领导节点的转发完全由客户端软件处理。

    Transaction Journey

    RPC节点如何广播交易

    当RPC节点通过sendTransaction接收到一个交易后,它会将交易转换为UDP 数据包,然后将其转发给相关的领导。UDP允许验证节点之间快速通信,但不提供关于交易传递的任何保证。

    因为Solana的领导节点调度在每个纪元 (大约2天)之前就已知,所以RPC节点会直接将其交易广播给当前和下一个领导节点。这与其他流言协议(如以太坊)随机广播和广泛传播整个网络的交易的方式形成对比。默认情况下,RPC节点会每两秒尝试将交易转发给领导节点,直到交易被确认或交易的块哈希过期(在本文撰写时为150个区块或约1分钟19秒)。如果待重新广播的队列大小超过10,000 transactions 个交易,则新提交的交易将被丢弃。RPC运营商可以调整命令行参数 以更改此重试逻辑的默认行为。

    当RPC节点广播一个交易时,它会尝试将交易转发给领导节点的交易处理单元(TPU)。TPU将交易处理分为五个不同的阶段:

    TPU Overview

    Image Courtesy of Jito Labs

    在这五个阶段中,Fetch阶段负责接收交易。在Fetch阶段中,验证节点会根据三个端口对传入的交易进行分类:

    • tpu 处理常规交易,例如代币转账、NFT铸造和程序指令。
    • tpu_vote 专门处理投票交易。
    • tpu_forwards 将未处理的数据包转发给下一个领导节点,如果当前领导无法处理所有交易。

    如需了解更多关于TPU的信息,请参考Jito Labs出色的文章.

    交易如何被丢弃

    在交易的整个过程中,有几种情况下交易可能意外从网络中丢失。

    在交易被处理之前

    如果网络丢弃一个交易,通常是在交易被领导处理之前发生。UDP 数据包丢失 是可能发生这种情况的最简单原因。在网络负载高峰期,验证节点可能会被大量需要处理的交易压倒。虽然验证节点可以通过 tpu_forwards,端口转发多余的交易,但转发. 的数据量是有限的。此外,每个转发仅限于验证节点之间的单一跳跃。也就是说,通过tpu_forwards端口接收的交易不会被转发给其他验证节点。

    还有两个较少为人知的原因,可能导致交易在被处理之前被丢弃。第一种情况涉及通过RPC池提交的交易。偶尔,RPC池的一部分可能会领先于其他部分。当池中的节点需要共同工作时,这可能会导致问题。在这个例子中,交易的recentBlockhash 从池中的先进部分(后端A)查询。当交易提交到滞后的池中(后端B)时,节点将无法识别先进的块哈希并丢弃交易。如果开发人员在sendTransaction中启用了preflight checks, 可以在提交交易时检测到此问题。

    Dropped via RPC Pool

    网络分叉也可能暂时的导致交易丢失。如果验证在银行阶段重新播放其块的速度较慢,可能会创建一个少数派分叉。当客户端构建一个交易时,交易可能引用仅存在于少数派分叉上的recentBlockhash。在提交交易后,集群可能在交易被处理之前切换到其他分叉。在这种情况下,由于找不到块哈希,交易被丢弃。

    Dropped due to Minority Fork (Before Processed)

    在交易被处理后,但尚未最终确认之前

    如果一个交易引用了来自少数派分叉的recentBlockhash,该交易有可能还会进行处理。在这种情况下,交易将由少数派分叉上的领导节点进行处理。当这个领导试图与不认可少数派分叉的大多数验证节点达成共识时,它将无法与它们分享已处理的交易。在这种情况下,交易在最终确定之前将被丢弃。

    Dropped due to Minority Fork (After Processed)

    处理被丢弃的交易

    虽然RPC节点会尝试重新广播交易,但它们使用的算法是通用的,往往不适合特定应用的需求。为了应对网络拥堵的时候,应用程序开发人员应该自定义自己的重新广播逻辑。

    深入了解sendTransaction

    在提交交易方面,sendTransaction RPC方法是开发者可用的主要工具。sendTransaction仅负责将交易从客户端传递到RPC节点。如果节点接收到交易,sendTransaction将返回用于跟踪交易的交易ID。成功的响应并不表示该交易将由集群处理或最终确定。

    info

    Tips

    请求参数

    • transaction: string - 完全签名的交易,以编码字符串形式表示
    • (可选) configuration object: object
    • skipPreflight: boolean - 如果为 true,则跳过预检事务检查(默认为 false)
    • (可选) preflightCommitment: string - 用于针对银行插槽进行预检模拟的承诺 级别(默认为"finalized")
    • (可选) encoding: string - 用于交易数据的编码方式。可以选择 "base58"(较慢)或 "base64"(默认为 "base58")
    • (可选) maxRetries: usize - RPC节点重试将交易发送给领导者的最大次数。如果未提供此参数,RPC节点将重试交易,直到交易最终确定或块哈希过期为止

    响应

    • transaction id: string - 第一个嵌入在交易中的交易签名,以base-58编码的字符串形式表示。可以使用该交易ID与 getSignatureStatuses 一起使用,以轮询获取状态更新。

    自定义重播逻辑

    为了开发自己的重新广播逻辑,开发者应该利用sendTransactionmaxRetries参数。如果提供了maxRetries,它将覆盖RPC节点的默认重试逻辑,允许开发人员在合理范围内 手动控制重试过程。

    手动重试交易的常见模式涉及临时存储来自getLatestBlockhashlastValidBlockHeight。一旦存储了该值,应用程序可以轮询集群的blockheight, 并在适当的时间间隔内手动重试交易。在网络拥堵的时期,将maxRetries设置为0并通过自定义算法手动重新广播是有优势的。一些应用程序可能采用指数退避, 而其他应用程序(如Mango )选择在恒定间隔内持续重新提交 交易,直到发生超时。

    while (blockheight < lastValidBlockHeight) {
    connection.sendRawTransaction(rawTransaction, {
    skipPreflight: true,
    });
    await sleep(500);
    blockheight = await connection.getBlockHeight();
    }

    当通过getLatestBlockhash进行轮询时,应用程序应该指定其预期的承诺 级别。通过将承诺级别设置为confirmed(已投票)或finalized(在confirmed之后约30个块),应用程序可以避免从少数派分叉轮询块哈希。

    如果应用程序可以访问负载均衡器后面的RPC节点,还可以选择将其工作负载分配给特定节点。为数据密集型请求提供服务的RPC节点(例如getProgramAccounts)可能会滞后,并且可能不适合转发交易。对于处理时间敏感交易的应用程序,最好拥有专用节点仅处理sendTransaction操作。

    跳过预检的后果

    默认情况下,sendTransaction将在提交交易之前执行三个预检查。具体而言,sendTransaction将会:

    • 验证所有签名是否有效
    • 检查引用的块哈希是否在最近的150个块内
    • 针对预检查的preFlightCommitment,模拟交易与银行槽位之间的交互

    如果其中任何一个预检查失败,sendTransaction将在提交交易之前引发错误。预检查常常能够防止交易丢失,并使客户端能够优雅地处理错误。为了确保这些常见错误得到考虑,建议开发人员将skipPreflight设置为false。

    何时重新签署交易

    尽管尽力进行重新广播,但有时客户端可能需要重新签署交易。在重新签署任何交易之前,非常重要的是确保初始交易的块哈希已经过期。如果初始块哈希仍然有效,那么两个交易都有可能被网络接受。对于最终用户来说,这将看起来好像他们无意中发送了相同的交易两次。

    在Solana中,一旦所引用的块哈希早于从getLatestBlockhash接收到的lastValidBlockHeight,可以安全地丢弃已丢弃的交易。开发者应该通过查询 getEpochInfo 并将其与响应中的blockHeight进行比较来跟踪lastValidBlockHeight。一旦一个块哈希无效,客户端可以使用新查询的块哈希重新签署。

    致谢

    非常感谢 Trent Nelson、Jacob Creech, White Tiger、Le Yafo、Buffalu, 和 Jito Labs 的审查和反馈。

    + + \ No newline at end of file diff --git a/cookbook-zh/guides/serialization/index.html b/cookbook-zh/guides/serialization/index.html index 39043acf7..138538061 100644 --- a/cookbook-zh/guides/serialization/index.html +++ b/cookbook-zh/guides/serialization/index.html @@ -9,13 +9,13 @@ - - + +
    -

    序列数据

    当我们谈论序列化时,我们指的是数据的序列化和反序列化。

    序列化在Solana程序和程序账户的生命周期中的几个点上起着作用:

    1. 将指令数据序列化到客户端上
    2. 在程序中反序列化指令数据
    3. 将账户数据序列化到程序中
    4. 在客户端上反序列化账户数据

    重要的是,上述操作都应该采用相同的序列化方法。下面的示例演示了使用Borsh进行序列化。

    本文档的其余部分中的示例摘录自Solana CLI 程序模板

    设置Borsh序列化

    为了使用Borsh进行序列化,需要在Rust程序、Rust客户端、节点和/或Python客户端中设置Borsh库。

    [package]
    name = "solana-cli-template-program-bpf"
    version = "0.1.0"
    edition = "2018"
    license = "WTFPL"

    # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

    [features]
    no-entrypoint = []

    [dependencies]
    borsh = "0.9.0"
    lazy_static = "1.4.0"
    num-derive = "0.3"
    num_enum = "0.5.1"
    num-integer = "0.1.44"
    num-traits = "0.2"
    sol-template-shared = {path = "../shared"}
    solana-program = "1.8.2"
    thiserror = "1.0"

    [dev-dependencies]
    solana-program-test = "1.8.2"
    solana-sdk = "1.8.2"

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

    如何序列化客户端上的指令数据

    如果你要将出站指令数据序列化并发送给程序,它必须与程序反序列化入站指令数据的方式保持一致。

    在此模板中,指令数据块是一个包含序列化数组的数据块,例如:

    Instruction (Variant index)Serialized KeySerialized Value
    Initialize (0)not applicable for instructionnot applicable for instruction
    Mint (1)"foo""bar"
    Transfer (2)"foo"not applicable for instruction
    Burn (2)"foo"not applicable for instruction

    在下面的示例中,我们假设程序拥有的账户已经初始化完成。

    // Include borsh functionality

    import { serialize, deserialize, deserializeUnchecked } from "borsh";
    import { Buffer } from "buffer";

    // Get Solana
    import {
    Keypair,
    Connection,
    PublicKey,
    Transaction,
    TransactionInstruction,
    sendAndConfirmTransaction,
    } from "@solana/web3.js";

    // Flexible class that takes properties and imbues them
    // to the object instance
    class Assignable {
    constructor(properties) {
    Object.keys(properties).map((key) => {
    return (this[key] = properties[key]);
    });
    }
    }

    // Our instruction payload vocabulary
    class Payload extends Assignable {}

    // Borsh needs a schema describing the payload
    const payloadSchema = new Map([
    [
    Payload,
    {
    kind: "struct",
    fields: [
    ["id", "u8"],
    ["key", "string"],
    ["value", "string"],
    ],
    },
    ],
    ]);

    // Instruction variant indexes
    enum InstructionVariant {
    InitializeAccount = 0,
    MintKeypair,
    TransferKeypair,
    BurnKeypair,
    }

    /**
    * Mint a key value pair to account
    * @param {Connection} connection - Solana RPC connection
    * @param {PublicKey} progId - Sample Program public key
    * @param {PublicKey} account - Target program owned account for Mint
    * @param {Keypair} wallet - Wallet for signing and payment
    * @param {string} mintKey - The key being minted key
    * @param {string} mintValue - The value being minted
    * @return {Promise<Keypair>} - Keypair
    */

    export async function mintKV(
    connection: Connection,
    progId: PublicKey,
    account: PublicKey,
    wallet: Keypair,
    mintKey: string,
    mintValue: string
    ): Promise<string> {
    // Construct the payload
    const mint = new Payload({
    id: InstructionVariant.MintKeypair,
    key: mintKey, // 'ts key'
    value: mintValue, // 'ts first value'
    });

    // Serialize the payload
    const mintSerBuf = Buffer.from(serialize(payloadSchema, mint));
    // console.log(mintSerBuf)
    // => <Buffer 01 06 00 00 00 74 73 20 6b 65 79 0e 00 00 00 74 73 20 66 69 72 73 74 20 76 61 6c 75 65>
    // let mintPayloadCopy = deserialize(schema, Payload, mintSerBuf)
    // console.log(mintPayloadCopy)
    // => Payload { id: 1, key: 'ts key', value: 'ts first value' }

    // Create Solana Instruction
    const instruction = new TransactionInstruction({
    data: mintSerBuf,
    keys: [
    { pubkey: account, isSigner: false, isWritable: true },
    { pubkey: wallet.publicKey, isSigner: false, isWritable: false },
    ],
    programId: progId,
    });

    // Send Solana Transaction
    const transactionSignature = await sendAndConfirmTransaction(
    connection,
    new Transaction().add(instruction),
    [wallet],
    {
    commitment: "singleGossip",
    preflightCommitment: "singleGossip",
    }
    );
    console.log("Signature = ", transactionSignature);
    return transactionSignature;
    }

    如何在程序中反序列化指令数据

    Deserialize Instruction Data

    //! instruction Contains the main ProgramInstruction enum

    use {
    crate::error::SampleError, borsh::BorshDeserialize, solana_program::program_error::ProgramError,
    };

    #[derive(Debug, PartialEq)]
    /// All custom program instructions
    pub enum ProgramInstruction {
    InitializeAccount,
    MintToAccount { key: String, value: String },
    TransferBetweenAccounts { key: String },
    BurnFromAccount { key: String },
    MintToAccountWithFee { key: String, value: String },
    TransferBetweenAccountsWithFee { key: String },
    BurnFromAccountWithFee { key: String },
    }

    /// Generic Payload Deserialization
    #[derive(BorshDeserialize, Debug)]
    struct Payload {
    variant: u8,
    arg1: String,
    arg2: String,
    }

    impl ProgramInstruction {
    /// Unpack inbound buffer to associated Instruction
    /// The expected format for input is a Borsh serialized vector
    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
    let payload = Payload::try_from_slice(input).unwrap();
    match payload.variant {
    0 => Ok(ProgramInstruction::InitializeAccount),
    1 => Ok(Self::MintToAccount {
    key: payload.arg1,
    value: payload.arg2,
    }),
    2 => Ok(Self::TransferBetweenAccounts { key: payload.arg1 }),
    3 => Ok(Self::BurnFromAccount { key: payload.arg1 }),
    4 => Ok(Self::MintToAccountWithFee {
    key: payload.arg1,
    value: payload.arg2,
    }),
    5 => Ok(Self::TransferBetweenAccountsWithFee { key: payload.arg1 }),
    6 => Ok(Self::BurnFromAccountWithFee { key: payload.arg1 }),
    _ => Err(SampleError::DeserializationFailure.into()),
    }
    }
    }

    如何在程序中序列化账户数据

    Account Data Serialization

    程序账户数据块(来自示例仓库)的布局如下:

    Byte 0Bytes 1-4Remaining Byte up to 1019
    Initialized flaglength of serialized BTreeMapBTreeMap (where key value pairs are stored)

    Pack

    关于 Pack trait

    可以更容易地隐藏账户数据序列化/反序列化的细节,使你的核心程序指令处理代码更简洁。因此,不需要将所有的序列化/反序列化逻辑放在程序处理代码中,而是将这些细节封装在以下三个函数中:

    1. unpack_unchecked - 允许你对账户进行反序列化,而无需检查它是否已被初始化。当实际处理初始化函数(变体索引为0)时,这非常有用。
    2. unpack - 调用你的Pack实现的unpack_from_slice函数,并检查账户是否已被初始化。
    3. pack - 调用您的Pack实现的pack_into_slice函数。

    下面是我们示例程序的Pack trait实现。随后是使用Borsh进行账户数据处理的示例。

    //! @brief account_state manages account data

    use crate::error::SampleError;
    use sol_template_shared::ACCOUNT_STATE_SPACE;
    use solana_program::{
    entrypoint::ProgramResult,
    program_error::ProgramError,
    program_pack::{IsInitialized, Pack, Sealed},
    };
    use std::collections::BTreeMap;

    /// Maintains global accumulator
    #[derive(Debug, Default, PartialEq)]
    pub struct ProgramAccountState {
    is_initialized: bool,
    btree_storage: BTreeMap<String, String>,
    }

    impl ProgramAccountState {
    /// Returns indicator if this account has been initialized
    pub fn set_initialized(&mut self) {
    self.is_initialized = true;
    }
    /// Adds a new key/value pair to the account
    pub fn add(&mut self, key: String, value: String) -> ProgramResult {
    match self.btree_storage.contains_key(&key) {
    true => Err(SampleError::KeyAlreadyExists.into()),
    false => {
    self.btree_storage.insert(key, value);
    Ok(())
    }
    }
    }
    /// Removes a key from account and returns the keys value
    pub fn remove(&mut self, key: &str) -> Result<String, SampleError> {
    match self.btree_storage.contains_key(key) {
    true => Ok(self.btree_storage.remove(key).unwrap()),
    false => Err(SampleError::KeyNotFoundInAccount),
    }
    }
    }

    impl Sealed for ProgramAccountState {}

    // Pack expects the implementation to satisfy whether the
    // account is initialzed.
    impl IsInitialized for ProgramAccountState {
    fn is_initialized(&self) -> bool {
    self.is_initialized
    }
    }

    impl Pack for ProgramAccountState {
    const LEN: usize = ACCOUNT_STATE_SPACE;

    /// Store 'state' of account to its data area
    fn pack_into_slice(&self, dst: &mut [u8]) {
    sol_template_shared::pack_into_slice(self.is_initialized, &self.btree_storage, dst);
    }

    /// Retrieve 'state' of account from account data area
    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
    match sol_template_shared::unpack_from_slice(src) {
    Ok((is_initialized, btree_map)) => Ok(ProgramAccountState {
    is_initialized,
    btree_storage: btree_map,
    }),
    Err(_) => Err(ProgramError::InvalidAccountData),
    }
    }
    }

    序列化/反序列化

    为了完成底层的序列化和反序列化:

    1. sol_template_shared::pack_into_slice - 进行序列化的地方
    2. sol_template_shared::unpack_from_slice - 进行反序列化的地方

    请关注 在下面的示例中,我们在BTREE_LENGTH的数据布局中的BTREE_STORAGE之前有一个u32(4字节)的分区。这是因为在反序列化过程中,borsh会检查您正在反序列化的切片的长度是否与它实际读取的数据量一致,然后才进行对象的重组。下面演示的方法首先读取BTREE_LENGTH,以获取要从BTREE_STORAGE指针中slice的大小。

    use {
    arrayref::*,
    borsh::{BorshDeserialize, BorshSerialize},
    solana_program::program_memory::sol_memcpy,
    std::{collections::BTreeMap, error::Error},
    };

    /// Initialization flag size for account state
    pub const INITIALIZED_BYTES: usize = 1;
    /// Storage for the serialized size of the BTreeMap control
    pub const BTREE_LENGTH: usize = 4;
    /// Storage for the serialized BTreeMap container
    pub const BTREE_STORAGE: usize = 1019;
    /// Sum of all account state lengths
    pub const ACCOUNT_STATE_SPACE: usize = INITIALIZED_BYTES + BTREE_LENGTH + BTREE_STORAGE;

    /// Packs the initialized flag and data content into destination slice
    #[allow(clippy::ptr_offset_with_cast)]
    pub fn pack_into_slice(
    is_initialized: bool,
    btree_storage: &BTreeMap<String, String>,
    dst: &mut [u8],
    ) {
    let dst = array_mut_ref![dst, 0, ACCOUNT_STATE_SPACE];
    // Setup pointers to key areas of account state data
    let (is_initialized_dst, data_len_dst, data_dst) =
    mut_array_refs![dst, INITIALIZED_BYTES, BTREE_LENGTH, BTREE_STORAGE];
    // Set the initialized flag
    is_initialized_dst[0] = is_initialized as u8;
    // Store the core data length and serialized content
    let keyval_store_data = btree_storage.try_to_vec().unwrap();
    let data_len = keyval_store_data.len();
    if data_len < BTREE_STORAGE {
    data_len_dst[..].copy_from_slice(&(data_len as u32).to_le_bytes());
    sol_memcpy(data_dst, &keyval_store_data, data_len);
    } else {
    panic!();
    }
    }

    /// Unpacks the data from slice and return the initialized flag and data content
    #[allow(clippy::ptr_offset_with_cast)]
    pub fn unpack_from_slice(src: &[u8]) -> Result<(bool, BTreeMap<String, String>), Box<dyn Error>> {
    let src = array_ref![src, 0, ACCOUNT_STATE_SPACE];
    // Setup pointers to key areas of account state data
    let (is_initialized_src, data_len_src, data_src) =
    array_refs![src, INITIALIZED_BYTES, BTREE_LENGTH, BTREE_STORAGE];

    let is_initialized = match is_initialized_src {
    [0] => false,
    [1] => true,
    _ => {
    return Err(Box::<dyn Error>::from(format!(
    "unrecognized initialization flag \"{:?}\". in account",
    is_initialized_src
    )))
    }
    };
    // Get current size of content in data area
    let data_len = u32::from_le_bytes(*data_len_src) as usize;
    // If emptry, create a default
    if data_len == 0 {
    Ok((is_initialized, BTreeMap::<String, String>::new()))
    } else {
    let data_dser = BTreeMap::<String, String>::try_from_slice(&data_src[0..data_len]).unwrap();
    Ok((is_initialized, data_dser))
    }
    }

    用法

    以下将所有内容整合在一起,并演示了程序与ProgramAccountState的交互,其中ProgramAccountState封装了初始化标志以及底层的BTreeMap用于存储键值对。

    首先,当我们想要初始化一个全新的账户时:

    /// Initialize a new program account, which is the first in AccountInfo array
    fn initialize_account(accounts: &[AccountInfo]) -> ProgramResult {
    msg!("Initialize account");
    let account_info_iter = &mut accounts.iter();
    let program_account = next_account_info(account_info_iter)?;
    let mut account_data = program_account.data.borrow_mut();
    // Here we use unpack_unchecked as we have yet to initialize
    // Had we tried to use unpack it would fail because, well, chicken and egg
    let mut account_state = ProgramAccountState::unpack_unchecked(&account_data)?;
    // We double check that we haven't already initialized this accounts data
    // more than once. If we are good, we set the initialized flag
    if account_state.is_initialized() {
    return Err(SampleError::AlreadyInitializedState.into());
    } else {
    account_state.set_initialized();
    }
    // Finally, we store back to the accounts space
    ProgramAccountState::pack(account_state, &mut account_data).unwrap();
    Ok(())
    }

    现在,我们可以执行其他指令,下面的示例演示了从客户端发送指令来创建一个新的键值对:

    /// Mint a key/pair to the programs account, which is the first in accounts
    fn mint_keypair_to_account(accounts: &[AccountInfo], key: String, value: String) -> ProgramResult {
    msg!("Mint to account");
    let account_info_iter = &mut accounts.iter();
    let program_account = next_account_info(account_info_iter)?;
    let mut account_data = program_account.data.borrow_mut();
    // Unpacking an uninitialized account state will fail
    let mut account_state = ProgramAccountState::unpack(&account_data)?;
    // Add the key value pair to the underlying BTreeMap
    account_state.add(key, value)?;
    // Finally, serialize back to the accounts data
    ProgramAccountState::pack(account_state, &mut account_data)?;
    Ok(())
    }

    如何在客户端中反序列化账户数据

    客户端可以调用Solana来获取程序所拥有的账户,其中序列化的数据块是返回结果的一部分。进行反序列化需要了解数据块的布局。

    账户数据的布局在这里已经被描述了。

    use {
    arrayref::*,
    borsh::{BorshDeserialize, BorshSerialize},
    std::{collections::BTreeMap, error::Error},
    };

    #[allow(clippy::ptr_offset_with_cast)]
    pub fn unpack_from_slice(src: &[u8]) -> Result<(bool, BTreeMap<String, String>), Box<dyn Error>> {
    let src = array_ref![src, 0, ACCOUNT_STATE_SPACE];
    // Setup pointers to key areas of account state data
    let (is_initialized_src, data_len_src, data_src) =
    array_refs![src, INITIALIZED_BYTES, BTREE_LENGTH, BTREE_STORAGE];

    let is_initialized = match is_initialized_src {
    [0] => false,
    [1] => true,
    _ => {
    return Err(Box::<dyn Error>::from(format!(
    "unrecognized initialization flag \"{:?}\". in account",
    is_initialized_src
    )))
    }
    };
    // Get current size of content in data area
    let data_len = u32::from_le_bytes(*data_len_src) as usize;
    // If emptry, create a default
    if data_len == 0 {
    Ok((is_initialized, BTreeMap::<String, String>::new()))
    } else {
    let data_dser = BTreeMap::<String, String>::try_from_slice(&data_src[0..data_len]).unwrap();
    Ok((is_initialized, data_dser))
    }
    }

    Solana TS/JS 常用映射

    Borsh Specification中包含了大多数基本和复合数据类型的映射关系。

    在TS/JS和Python中,关键是创建一个具有适当定义的Borsh模式,以便序列化和反序列化可以生成或遍历相应的输入。

    首先,我们将演示在Typescript中对基本类型(数字、字符串)和复合类型(固定大小数组、Map)进行序列化,然后在Python中进行序列化,最后在Rust中进行等效的反序列化操作:

    fn main() {}

    #[cfg(test)]
    mod tests {
    use borsh::{BorshDeserialize, BorshSerialize};
    use std::collections::BTreeMap;

    #[test]
    fn primitives() {
    let prim = [
    255u8, 255, 255, 255, 255, 255, 255, 5, 0, 0, 0, 104, 101, 108, 108, 111, 5, 0, 0, 0,
    119, 111, 114, 108, 100, 1, 2, 3, 4, 5, 2, 0, 0, 0, 8, 0, 0, 0, 99, 111, 111, 107, 98,
    111, 111, 107, 6, 0, 0, 0, 114, 101, 99, 105, 112, 101, 6, 0, 0, 0, 114, 101, 99, 105,
    112, 101, 10, 0, 0, 0, 105, 110, 103, 114, 101, 100, 105, 101, 110, 116,
    ];
    #[derive(BorshDeserialize, BorshSerialize, Debug)]
    struct Primitive(
    u8,
    u16,
    u32,
    String,
    String,
    [u8; 5],
    BTreeMap<String, String>,
    );
    let x = Primitive::try_from_slice(&prim).unwrap();
    println!("{:?}", x);
    }
    }

    高级构造

    我们在之前的示例中展示了如何创建简单的负载(Payloads)。有时,Solana会使用某些特殊类型。本节将演示如何正确映射TS/JS和Rust之间的类型,以处理这些情况。

    COption

    #!/usr/bin/env node

    import { serialize, deserialize, deserializeUnchecked } from "borsh";
    import { Buffer } from "buffer";
    import { PublicKey, Struct } from "@solana/web3.js";

    /**
    * COption is meant to mirror the
    * `solana_program::options::COption`
    *
    * This type stores a u32 flag (0 | 1) indicating
    * the presence or not of a underlying PublicKey
    *
    * Similar to a Rust Option
    * @extends {Struct} Solana JS Struct class
    * @implements {encode}
    */
    class COption extends Struct {
    constructor(properties) {
    super(properties);
    }

    /**
    * Creates a COption from a PublicKey
    * @param {PublicKey?} akey
    * @returns {COption} COption
    */
    static fromPublicKey(akey?: PublicKey): COption {
    if (akey == undefined) {
    return new COption({
    noneOrSome: 0,
    pubKeyBuffer: new Uint8Array(32),
    });
    } else {
    return new COption({
    noneOrSome: 1,
    pubKeyBuffer: akey.toBytes(),
    });
    }
    }
    /**
    * @returns {Buffer} Serialized COption (this)
    */
    encode(): Buffer {
    return Buffer.from(serialize(COPTIONSCHEMA, this));
    }
    /**
    * Safe deserializes a borsh serialized buffer to a COption
    * @param {Buffer} data - Buffer containing borsh serialized data
    * @returns {COption} COption object
    */
    static decode(data): COption {
    return deserialize(COPTIONSCHEMA, this, data);
    }

    /**
    * Unsafe deserializes a borsh serialized buffer to a COption
    * @param {Buffer} data - Buffer containing borsh serialized data
    * @returns {COption} COption object
    */
    static decodeUnchecked(data): COption {
    return deserializeUnchecked(COPTIONSCHEMA, this, data);
    }
    }

    /**
    * Defines the layout of the COption object
    * for serializing/deserializing
    * @type {Map}
    */
    const COPTIONSCHEMA = new Map([
    [
    COption,
    {
    kind: "struct",
    fields: [
    ["noneOrSome", "u32"],
    ["pubKeyBuffer", [32]],
    ],
    },
    ],
    ]);

    /**
    * Entry point for script *
    */
    async function entry(indata?: PublicKey) {
    // If we get a PublicKey
    if (indata) {
    // Construct COption instance
    const coption = COption.fromPublicKey(indata);
    console.log("Testing COption with " + indata.toBase58());
    // Serialize it
    let copt_ser = coption.encode();
    console.log("copt_ser ", copt_ser);
    // Deserialize it
    const tdone = COption.decode(copt_ser);
    console.log(tdone);
    // Validate contains PublicKey
    if (tdone["noneOrSome"] == 1) {
    console.log("pubkey: " + new PublicKey(tdone["pubKeyBuffer"]).toBase58());
    }
    /*
    Output:
    Testing COption with A94wMjV54C8f8wn7zL8TxNCdNiGoq7XSN7vWGrtd4vwU
    copt_ser Buffer(36) [1, 0, 0, 0, 135, 202, 71, 214, 68, 105, 98, 176, 211, 130, 105, 2, 55, 187, 86, 186, 109, 176, 80, 208, 77, 100, 221, 101, 20, 203, 149, 166, 96, 171, 119, 35, buffer: ArrayBuffer(8192), byteLength: 36, byteOffset: 1064, length: 36]
    COption {noneOrSome: 1, pubKeyBuffer: Uint8Array(32)}
    pubkey: A94wMjV54C8f8wn7zL8TxNCdNiGoq7XSN7vWGrtd4vwU
    */
    } else {
    console.log("Testing COption with null");
    // Construct COption instance
    const coption = COption.fromPublicKey();
    // Serialize it
    const copt_ser = coption.encode();
    console.log(copt_ser);
    // Deserialize it
    const tdone1 = COption.decode(copt_ser);
    console.log(tdone1);
    // Validate does NOT contains PublicKey
    if (tdone1["noneOrSome"] == 1) {
    throw Error("Expected no public key");
    } else console.log("pubkey: null");
    /*
    Output:
    Testing COption with null
    Buffer(36)[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, buffer: ArrayBuffer(8192), byteLength: 36, byteOffset: 2272, length: 36]
    COption { noneOrSome: 0, pubKeyBuffer: Uint8Array(32) }
    pubkey: null
    */
    }
    }

    // Test with PublicKey
    entry(new PublicKey("A94wMjV54C8f8wn7zL8TxNCdNiGoq7XSN7vWGrtd4vwU"));
    console.log("");
    // Test without PublicKey
    entry();

    资料

    - - +

    序列数据

    当我们谈论序列化时,我们指的是数据的序列化和反序列化。

    序列化在Solana程序和程序账户的生命周期中的几个点上起着作用:

    1. 将指令数据序列化到客户端上
    2. 在程序中反序列化指令数据
    3. 将账户数据序列化到程序中
    4. 在客户端上反序列化账户数据

    重要的是,上述操作都应该采用相同的序列化方法。下面的示例演示了使用Borsh进行序列化。

    本文档的其余部分中的示例摘录自Solana CLI 程序模板

    设置Borsh序列化

    为了使用Borsh进行序列化,需要在Rust程序、Rust客户端、节点和/或Python客户端中设置Borsh库。

    [package]
    name = "solana-cli-template-program-bpf"
    version = "0.1.0"
    edition = "2018"
    license = "WTFPL"

    # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

    [features]
    no-entrypoint = []

    [dependencies]
    borsh = "0.9.0"
    lazy_static = "1.4.0"
    num-derive = "0.3"
    num_enum = "0.5.1"
    num-integer = "0.1.44"
    num-traits = "0.2"
    sol-template-shared = {path = "../shared"}
    solana-program = "1.8.2"
    thiserror = "1.0"

    [dev-dependencies]
    solana-program-test = "1.8.2"
    solana-sdk = "1.8.2"

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

    如何序列化客户端上的指令数据

    如果你要将出站指令数据序列化并发送给程序,它必须与程序反序列化入站指令数据的方式保持一致。

    在此模板中,指令数据块是一个包含序列化数组的数据块,例如:

    Instruction (Variant index)Serialized KeySerialized Value
    Initialize (0)not applicable for instructionnot applicable for instruction
    Mint (1)"foo""bar"
    Transfer (2)"foo"not applicable for instruction
    Burn (2)"foo"not applicable for instruction

    在下面的示例中,我们假设程序拥有的账户已经初始化完成。

    // Include borsh functionality

    import { serialize, deserialize, deserializeUnchecked } from "borsh";
    import { Buffer } from "buffer";

    // Get Solana
    import {
    Keypair,
    Connection,
    PublicKey,
    Transaction,
    TransactionInstruction,
    sendAndConfirmTransaction,
    } from "@solana/web3.js";

    // Flexible class that takes properties and imbues them
    // to the object instance
    class Assignable {
    constructor(properties) {
    Object.keys(properties).map((key) => {
    return (this[key] = properties[key]);
    });
    }
    }

    // Our instruction payload vocabulary
    class Payload extends Assignable {}

    // Borsh needs a schema describing the payload
    const payloadSchema = new Map([
    [
    Payload,
    {
    kind: "struct",
    fields: [
    ["id", "u8"],
    ["key", "string"],
    ["value", "string"],
    ],
    },
    ],
    ]);

    // Instruction variant indexes
    enum InstructionVariant {
    InitializeAccount = 0,
    MintKeypair,
    TransferKeypair,
    BurnKeypair,
    }

    /**
    * Mint a key value pair to account
    * @param {Connection} connection - Solana RPC connection
    * @param {PublicKey} progId - Sample Program public key
    * @param {PublicKey} account - Target program owned account for Mint
    * @param {Keypair} wallet - Wallet for signing and payment
    * @param {string} mintKey - The key being minted key
    * @param {string} mintValue - The value being minted
    * @return {Promise<Keypair>} - Keypair
    */

    export async function mintKV(
    connection: Connection,
    progId: PublicKey,
    account: PublicKey,
    wallet: Keypair,
    mintKey: string,
    mintValue: string
    ): Promise<string> {
    // Construct the payload
    const mint = new Payload({
    id: InstructionVariant.MintKeypair,
    key: mintKey, // 'ts key'
    value: mintValue, // 'ts first value'
    });

    // Serialize the payload
    const mintSerBuf = Buffer.from(serialize(payloadSchema, mint));
    // console.log(mintSerBuf)
    // => <Buffer 01 06 00 00 00 74 73 20 6b 65 79 0e 00 00 00 74 73 20 66 69 72 73 74 20 76 61 6c 75 65>
    // let mintPayloadCopy = deserialize(schema, Payload, mintSerBuf)
    // console.log(mintPayloadCopy)
    // => Payload { id: 1, key: 'ts key', value: 'ts first value' }

    // Create Solana Instruction
    const instruction = new TransactionInstruction({
    data: mintSerBuf,
    keys: [
    { pubkey: account, isSigner: false, isWritable: true },
    { pubkey: wallet.publicKey, isSigner: false, isWritable: false },
    ],
    programId: progId,
    });

    // Send Solana Transaction
    const transactionSignature = await sendAndConfirmTransaction(
    connection,
    new Transaction().add(instruction),
    [wallet],
    {
    commitment: "singleGossip",
    preflightCommitment: "singleGossip",
    }
    );
    console.log("Signature = ", transactionSignature);
    return transactionSignature;
    }

    如何在程序中反序列化指令数据

    Deserialize Instruction Data

    //! instruction Contains the main ProgramInstruction enum

    use {
    crate::error::SampleError, borsh::BorshDeserialize, solana_program::program_error::ProgramError,
    };

    #[derive(Debug, PartialEq)]
    /// All custom program instructions
    pub enum ProgramInstruction {
    InitializeAccount,
    MintToAccount { key: String, value: String },
    TransferBetweenAccounts { key: String },
    BurnFromAccount { key: String },
    MintToAccountWithFee { key: String, value: String },
    TransferBetweenAccountsWithFee { key: String },
    BurnFromAccountWithFee { key: String },
    }

    /// Generic Payload Deserialization
    #[derive(BorshDeserialize, Debug)]
    struct Payload {
    variant: u8,
    arg1: String,
    arg2: String,
    }

    impl ProgramInstruction {
    /// Unpack inbound buffer to associated Instruction
    /// The expected format for input is a Borsh serialized vector
    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
    let payload = Payload::try_from_slice(input).unwrap();
    match payload.variant {
    0 => Ok(ProgramInstruction::InitializeAccount),
    1 => Ok(Self::MintToAccount {
    key: payload.arg1,
    value: payload.arg2,
    }),
    2 => Ok(Self::TransferBetweenAccounts { key: payload.arg1 }),
    3 => Ok(Self::BurnFromAccount { key: payload.arg1 }),
    4 => Ok(Self::MintToAccountWithFee {
    key: payload.arg1,
    value: payload.arg2,
    }),
    5 => Ok(Self::TransferBetweenAccountsWithFee { key: payload.arg1 }),
    6 => Ok(Self::BurnFromAccountWithFee { key: payload.arg1 }),
    _ => Err(SampleError::DeserializationFailure.into()),
    }
    }
    }

    如何在程序中序列化账户数据

    Account Data Serialization

    程序账户数据块(来自示例仓库)的布局如下:

    Byte 0Bytes 1-4Remaining Byte up to 1019
    Initialized flaglength of serialized BTreeMapBTreeMap (where key value pairs are stored)

    Pack

    关于 Pack trait

    可以更容易地隐藏账户数据序列化/反序列化的细节,使你的核心程序指令处理代码更简洁。因此,不需要将所有的序列化/反序列化逻辑放在程序处理代码中,而是将这些细节封装在以下三个函数中:

    1. unpack_unchecked - 允许你对账户进行反序列化,而无需检查它是否已被初始化。当实际处理初始化函数(变体索引为0)时,这非常有用。
    2. unpack - 调用你的Pack实现的unpack_from_slice函数,并检查账户是否已被初始化。
    3. pack - 调用您的Pack实现的pack_into_slice函数。

    下面是我们示例程序的Pack trait实现。随后是使用Borsh进行账户数据处理的示例。

    //! @brief account_state manages account data

    use crate::error::SampleError;
    use sol_template_shared::ACCOUNT_STATE_SPACE;
    use solana_program::{
    entrypoint::ProgramResult,
    program_error::ProgramError,
    program_pack::{IsInitialized, Pack, Sealed},
    };
    use std::collections::BTreeMap;

    /// Maintains global accumulator
    #[derive(Debug, Default, PartialEq)]
    pub struct ProgramAccountState {
    is_initialized: bool,
    btree_storage: BTreeMap<String, String>,
    }

    impl ProgramAccountState {
    /// Returns indicator if this account has been initialized
    pub fn set_initialized(&mut self) {
    self.is_initialized = true;
    }
    /// Adds a new key/value pair to the account
    pub fn add(&mut self, key: String, value: String) -> ProgramResult {
    match self.btree_storage.contains_key(&key) {
    true => Err(SampleError::KeyAlreadyExists.into()),
    false => {
    self.btree_storage.insert(key, value);
    Ok(())
    }
    }
    }
    /// Removes a key from account and returns the keys value
    pub fn remove(&mut self, key: &str) -> Result<String, SampleError> {
    match self.btree_storage.contains_key(key) {
    true => Ok(self.btree_storage.remove(key).unwrap()),
    false => Err(SampleError::KeyNotFoundInAccount),
    }
    }
    }

    impl Sealed for ProgramAccountState {}

    // Pack expects the implementation to satisfy whether the
    // account is initialzed.
    impl IsInitialized for ProgramAccountState {
    fn is_initialized(&self) -> bool {
    self.is_initialized
    }
    }

    impl Pack for ProgramAccountState {
    const LEN: usize = ACCOUNT_STATE_SPACE;

    /// Store 'state' of account to its data area
    fn pack_into_slice(&self, dst: &mut [u8]) {
    sol_template_shared::pack_into_slice(self.is_initialized, &self.btree_storage, dst);
    }

    /// Retrieve 'state' of account from account data area
    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
    match sol_template_shared::unpack_from_slice(src) {
    Ok((is_initialized, btree_map)) => Ok(ProgramAccountState {
    is_initialized,
    btree_storage: btree_map,
    }),
    Err(_) => Err(ProgramError::InvalidAccountData),
    }
    }
    }

    序列化/反序列化

    为了完成底层的序列化和反序列化:

    1. sol_template_shared::pack_into_slice - 进行序列化的地方
    2. sol_template_shared::unpack_from_slice - 进行反序列化的地方

    请关注 在下面的示例中,我们在BTREE_LENGTH的数据布局中的BTREE_STORAGE之前有一个u32(4字节)的分区。这是因为在反序列化过程中,borsh会检查您正在反序列化的切片的长度是否与它实际读取的数据量一致,然后才进行对象的重组。下面演示的方法首先读取BTREE_LENGTH,以获取要从BTREE_STORAGE指针中slice的大小。

    use {
    arrayref::*,
    borsh::{BorshDeserialize, BorshSerialize},
    solana_program::program_memory::sol_memcpy,
    std::{collections::BTreeMap, error::Error},
    };

    /// Initialization flag size for account state
    pub const INITIALIZED_BYTES: usize = 1;
    /// Storage for the serialized size of the BTreeMap control
    pub const BTREE_LENGTH: usize = 4;
    /// Storage for the serialized BTreeMap container
    pub const BTREE_STORAGE: usize = 1019;
    /// Sum of all account state lengths
    pub const ACCOUNT_STATE_SPACE: usize = INITIALIZED_BYTES + BTREE_LENGTH + BTREE_STORAGE;

    /// Packs the initialized flag and data content into destination slice
    #[allow(clippy::ptr_offset_with_cast)]
    pub fn pack_into_slice(
    is_initialized: bool,
    btree_storage: &BTreeMap<String, String>,
    dst: &mut [u8],
    ) {
    let dst = array_mut_ref![dst, 0, ACCOUNT_STATE_SPACE];
    // Setup pointers to key areas of account state data
    let (is_initialized_dst, data_len_dst, data_dst) =
    mut_array_refs![dst, INITIALIZED_BYTES, BTREE_LENGTH, BTREE_STORAGE];
    // Set the initialized flag
    is_initialized_dst[0] = is_initialized as u8;
    // Store the core data length and serialized content
    let keyval_store_data = btree_storage.try_to_vec().unwrap();
    let data_len = keyval_store_data.len();
    if data_len < BTREE_STORAGE {
    data_len_dst[..].copy_from_slice(&(data_len as u32).to_le_bytes());
    sol_memcpy(data_dst, &keyval_store_data, data_len);
    } else {
    panic!();
    }
    }

    /// Unpacks the data from slice and return the initialized flag and data content
    #[allow(clippy::ptr_offset_with_cast)]
    pub fn unpack_from_slice(src: &[u8]) -> Result<(bool, BTreeMap<String, String>), Box<dyn Error>> {
    let src = array_ref![src, 0, ACCOUNT_STATE_SPACE];
    // Setup pointers to key areas of account state data
    let (is_initialized_src, data_len_src, data_src) =
    array_refs![src, INITIALIZED_BYTES, BTREE_LENGTH, BTREE_STORAGE];

    let is_initialized = match is_initialized_src {
    [0] => false,
    [1] => true,
    _ => {
    return Err(Box::<dyn Error>::from(format!(
    "unrecognized initialization flag \"{:?}\". in account",
    is_initialized_src
    )))
    }
    };
    // Get current size of content in data area
    let data_len = u32::from_le_bytes(*data_len_src) as usize;
    // If emptry, create a default
    if data_len == 0 {
    Ok((is_initialized, BTreeMap::<String, String>::new()))
    } else {
    let data_dser = BTreeMap::<String, String>::try_from_slice(&data_src[0..data_len]).unwrap();
    Ok((is_initialized, data_dser))
    }
    }

    用法

    以下将所有内容整合在一起,并演示了程序与ProgramAccountState的交互,其中ProgramAccountState封装了初始化标志以及底层的BTreeMap用于存储键值对。

    首先,当我们想要初始化一个全新的账户时:

    /// Initialize a new program account, which is the first in AccountInfo array
    fn initialize_account(accounts: &[AccountInfo]) -> ProgramResult {
    msg!("Initialize account");
    let account_info_iter = &mut accounts.iter();
    let program_account = next_account_info(account_info_iter)?;
    let mut account_data = program_account.data.borrow_mut();
    // Here we use unpack_unchecked as we have yet to initialize
    // Had we tried to use unpack it would fail because, well, chicken and egg
    let mut account_state = ProgramAccountState::unpack_unchecked(&account_data)?;
    // We double check that we haven't already initialized this accounts data
    // more than once. If we are good, we set the initialized flag
    if account_state.is_initialized() {
    return Err(SampleError::AlreadyInitializedState.into());
    } else {
    account_state.set_initialized();
    }
    // Finally, we store back to the accounts space
    ProgramAccountState::pack(account_state, &mut account_data).unwrap();
    Ok(())
    }

    现在,我们可以执行其他指令,下面的示例演示了从客户端发送指令来创建一个新的键值对:

    /// Mint a key/pair to the programs account, which is the first in accounts
    fn mint_keypair_to_account(accounts: &[AccountInfo], key: String, value: String) -> ProgramResult {
    msg!("Mint to account");
    let account_info_iter = &mut accounts.iter();
    let program_account = next_account_info(account_info_iter)?;
    let mut account_data = program_account.data.borrow_mut();
    // Unpacking an uninitialized account state will fail
    let mut account_state = ProgramAccountState::unpack(&account_data)?;
    // Add the key value pair to the underlying BTreeMap
    account_state.add(key, value)?;
    // Finally, serialize back to the accounts data
    ProgramAccountState::pack(account_state, &mut account_data)?;
    Ok(())
    }

    如何在客户端中反序列化账户数据

    客户端可以调用Solana来获取程序所拥有的账户,其中序列化的数据块是返回结果的一部分。进行反序列化需要了解数据块的布局。

    账户数据的布局在这里已经被描述了。

    use {
    arrayref::*,
    borsh::{BorshDeserialize, BorshSerialize},
    std::{collections::BTreeMap, error::Error},
    };

    #[allow(clippy::ptr_offset_with_cast)]
    pub fn unpack_from_slice(src: &[u8]) -> Result<(bool, BTreeMap<String, String>), Box<dyn Error>> {
    let src = array_ref![src, 0, ACCOUNT_STATE_SPACE];
    // Setup pointers to key areas of account state data
    let (is_initialized_src, data_len_src, data_src) =
    array_refs![src, INITIALIZED_BYTES, BTREE_LENGTH, BTREE_STORAGE];

    let is_initialized = match is_initialized_src {
    [0] => false,
    [1] => true,
    _ => {
    return Err(Box::<dyn Error>::from(format!(
    "unrecognized initialization flag \"{:?}\". in account",
    is_initialized_src
    )))
    }
    };
    // Get current size of content in data area
    let data_len = u32::from_le_bytes(*data_len_src) as usize;
    // If emptry, create a default
    if data_len == 0 {
    Ok((is_initialized, BTreeMap::<String, String>::new()))
    } else {
    let data_dser = BTreeMap::<String, String>::try_from_slice(&data_src[0..data_len]).unwrap();
    Ok((is_initialized, data_dser))
    }
    }

    Solana TS/JS 常用映射

    Borsh Specification中包含了大多数基本和复合数据类型的映射关系。

    在TS/JS和Python中,关键是创建一个具有适当定义的Borsh模式,以便序列化和反序列化可以生成或遍历相应的输入。

    首先,我们将演示在Typescript中对基本类型(数字、字符串)和复合类型(固定大小数组、Map)进行序列化,然后在Python中进行序列化,最后在Rust中进行等效的反序列化操作:

    fn main() {}

    #[cfg(test)]
    mod tests {
    use borsh::{BorshDeserialize, BorshSerialize};
    use std::collections::BTreeMap;

    #[test]
    fn primitives() {
    let prim = [
    255u8, 255, 255, 255, 255, 255, 255, 5, 0, 0, 0, 104, 101, 108, 108, 111, 5, 0, 0, 0,
    119, 111, 114, 108, 100, 1, 2, 3, 4, 5, 2, 0, 0, 0, 8, 0, 0, 0, 99, 111, 111, 107, 98,
    111, 111, 107, 6, 0, 0, 0, 114, 101, 99, 105, 112, 101, 6, 0, 0, 0, 114, 101, 99, 105,
    112, 101, 10, 0, 0, 0, 105, 110, 103, 114, 101, 100, 105, 101, 110, 116,
    ];
    #[derive(BorshDeserialize, BorshSerialize, Debug)]
    struct Primitive(
    u8,
    u16,
    u32,
    String,
    String,
    [u8; 5],
    BTreeMap<String, String>,
    );
    let x = Primitive::try_from_slice(&prim).unwrap();
    println!("{:?}", x);
    }
    }

    高级构造

    我们在之前的示例中展示了如何创建简单的负载(Payloads)。有时,Solana会使用某些特殊类型。本节将演示如何正确映射TS/JS和Rust之间的类型,以处理这些情况。

    COption

    #!/usr/bin/env node

    import { serialize, deserialize, deserializeUnchecked } from "borsh";
    import { Buffer } from "buffer";
    import { PublicKey, Struct } from "@solana/web3.js";

    /**
    * COption is meant to mirror the
    * `solana_program::options::COption`
    *
    * This type stores a u32 flag (0 | 1) indicating
    * the presence or not of a underlying PublicKey
    *
    * Similar to a Rust Option
    * @extends {Struct} Solana JS Struct class
    * @implements {encode}
    */
    class COption extends Struct {
    constructor(properties) {
    super(properties);
    }

    /**
    * Creates a COption from a PublicKey
    * @param {PublicKey?} akey
    * @returns {COption} COption
    */
    static fromPublicKey(akey?: PublicKey): COption {
    if (akey == undefined) {
    return new COption({
    noneOrSome: 0,
    pubKeyBuffer: new Uint8Array(32),
    });
    } else {
    return new COption({
    noneOrSome: 1,
    pubKeyBuffer: akey.toBytes(),
    });
    }
    }
    /**
    * @returns {Buffer} Serialized COption (this)
    */
    encode(): Buffer {
    return Buffer.from(serialize(COPTIONSCHEMA, this));
    }
    /**
    * Safe deserializes a borsh serialized buffer to a COption
    * @param {Buffer} data - Buffer containing borsh serialized data
    * @returns {COption} COption object
    */
    static decode(data): COption {
    return deserialize(COPTIONSCHEMA, this, data);
    }

    /**
    * Unsafe deserializes a borsh serialized buffer to a COption
    * @param {Buffer} data - Buffer containing borsh serialized data
    * @returns {COption} COption object
    */
    static decodeUnchecked(data): COption {
    return deserializeUnchecked(COPTIONSCHEMA, this, data);
    }
    }

    /**
    * Defines the layout of the COption object
    * for serializing/deserializing
    * @type {Map}
    */
    const COPTIONSCHEMA = new Map([
    [
    COption,
    {
    kind: "struct",
    fields: [
    ["noneOrSome", "u32"],
    ["pubKeyBuffer", [32]],
    ],
    },
    ],
    ]);

    /**
    * Entry point for script *
    */
    async function entry(indata?: PublicKey) {
    // If we get a PublicKey
    if (indata) {
    // Construct COption instance
    const coption = COption.fromPublicKey(indata);
    console.log("Testing COption with " + indata.toBase58());
    // Serialize it
    let copt_ser = coption.encode();
    console.log("copt_ser ", copt_ser);
    // Deserialize it
    const tdone = COption.decode(copt_ser);
    console.log(tdone);
    // Validate contains PublicKey
    if (tdone["noneOrSome"] == 1) {
    console.log("pubkey: " + new PublicKey(tdone["pubKeyBuffer"]).toBase58());
    }
    /*
    Output:
    Testing COption with A94wMjV54C8f8wn7zL8TxNCdNiGoq7XSN7vWGrtd4vwU
    copt_ser Buffer(36) [1, 0, 0, 0, 135, 202, 71, 214, 68, 105, 98, 176, 211, 130, 105, 2, 55, 187, 86, 186, 109, 176, 80, 208, 77, 100, 221, 101, 20, 203, 149, 166, 96, 171, 119, 35, buffer: ArrayBuffer(8192), byteLength: 36, byteOffset: 1064, length: 36]
    COption {noneOrSome: 1, pubKeyBuffer: Uint8Array(32)}
    pubkey: A94wMjV54C8f8wn7zL8TxNCdNiGoq7XSN7vWGrtd4vwU
    */
    } else {
    console.log("Testing COption with null");
    // Construct COption instance
    const coption = COption.fromPublicKey();
    // Serialize it
    const copt_ser = coption.encode();
    console.log(copt_ser);
    // Deserialize it
    const tdone1 = COption.decode(copt_ser);
    console.log(tdone1);
    // Validate does NOT contains PublicKey
    if (tdone1["noneOrSome"] == 1) {
    throw Error("Expected no public key");
    } else console.log("pubkey: null");
    /*
    Output:
    Testing COption with null
    Buffer(36)[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, buffer: ArrayBuffer(8192), byteLength: 36, byteOffset: 2272, length: 36]
    COption { noneOrSome: 0, pubKeyBuffer: Uint8Array(32) }
    pubkey: null
    */
    }
    }

    // Test with PublicKey
    entry(new PublicKey("A94wMjV54C8f8wn7zL8TxNCdNiGoq7XSN7vWGrtd4vwU"));
    console.log("");
    // Test without PublicKey
    entry();

    资料

    + + \ No newline at end of file diff --git a/cookbook-zh/guides/versioned-transactions/index.html b/cookbook-zh/guides/versioned-transactions/index.html index 765c95c34..ef4856eb5 100644 --- a/cookbook-zh/guides/versioned-transactions/index.html +++ b/cookbook-zh/guides/versioned-transactions/index.html @@ -9,14 +9,14 @@ - - + +

    版本化交易 (Versioned Transactions)

    Solana最近发布了版本化交易。提议的更改如下:

    1. 引入一个新的程序,用于管理链上地址查找表。

    2. 添加一种新的交易格式,可以利用链上地址查找表。

    info

    tip 事实表 --传统交易存在一个主要问题:最大允许的大小为1232字节,因此原子交易中可以容纳的账户数量为35个地址。

    • 地址查找表(LUTs):一旦账户存储在该表中,可以使用1字节的u8索引,在交易消息中引用该表的地址。
    • 可以使用solana/web3.jscreateLookupTable()构建一个新的查找表,并确定其地址。
    • 一旦创建了LUT,可以进行扩展,即可以将账户追加到表中。
    • 版本化交易:需要修改传统交易的结构以整合LUTs。
    • 在引入版本化之前,交易在其头部的第一个字节中保留了一个未使用的最高位,可以用来显式声明交易的版本。

    我们将更详细地讨论上述引入的更改以及它们对开发人员的意义。然而,为了更好地理解这些更改,我们首先需要了解常规(或传统)交易的结构。

    传统交易(Legacy Transactions)

    Solana网络使用最大事务单元(MTU)大小为1280字节,遵循IPv6 MTU 的大小约束,以确保速度和可靠性。这样留下了1232字节的数据空间,用于存储序列化的交易等数据。

    一个交易由以下组成:

    1. 一个紧凑数组的签名,其中每个签名是一个64字节的ed25519签名。
    2. 一个(传统的)消息。

    Transaction Format

    info

    tip Compact-Array format

    A compact array is an array serialised to have the following components:

    1. An array length in a multi-byte encoding called Compact-u16
    2. Followed by each array item

    Compact array format

    传统消息

    传统消息包含以下组件:

    1. 一个头部(header)。
    2. 一个紧凑数组的账户地址,每个账户地址占用32字节。
    3. 一个最近的区块哈希(recent blockhash):
      • 一个32字节的SHA-256哈希,用于指示上次观察到的账本状态。如果一个区块哈希太旧,验证节点将拒绝它。
    4. 一个紧凑数组的指令

    Legacy Message

    头部

    消息头部是3字节长,包含3个u8整数:

    1. 所需签名数量:Solana运行时会将此数字与交易中紧凑数组签名的长度进行验证。
    2. 需要签名的只读账户地址数量。
    3. 不需要签名的只读账户地址数量。

    Message Header

    紧凑账户地址数组

    这个紧凑数组以紧凑的u16编码的账户地址数量开始,然后是:

    1. 需要签名的账户地址:首先列出请求读取和写入访问权限的地址,然后是请求只读访问权限的地址。
    2. 不需要签名的账户地址:与上述相同,首先列出请求读取和写入访问权限的地址,然后是请求只读访问权限的地址。

    Compact array of account addresses

    紧凑指令数组

    就像账户地址数组一样,这个紧凑指令数组以紧凑的u16编码的指令数量开始,然后是一个指令数组。数组中的每个指令具有以下组件:

    1. 程序ID:用于标识将处理该指令的链上程序。它表示为消息中账户地址紧凑数组的地址的u8索引。
    2. 账户地址索引的紧凑数组:指向紧凑账户地址数组中需要签名的一部分账户地址的u8索引。
    3. 不透明的u8数据的紧凑数组:一个通用的字节数组,与前面提到的程序ID相关。该数据数组指定了程序应执行的任何操作以及账户可能不包含的任何附加信息。

    Compact array of Instructions

    传统交易的问题

    上述交易模型存在的问题是什么?

    交易的最大大小以及因此能够在单个原子交易中容纳的账户数量。

    如前所述,交易的最大允许大小为1232字节。一个账户地址的大小为32字节。因此,考虑到一些用于头部、签名和其他元数据的空间,一个交易最多只能存储35个账户。

    Issue with legacy transactions

    这是一个问题,因为有几种情况下,开发人员需要在单个交易中包含数百个无需签名的账户。但是,传统交易模型目前无法实现这一点。目前使用的解决方案是在链上临时存储状态,并在稍后的交易中使用。但是,当多个程序需要组合在单个交易中时,这种解决方法就不适用了。每个程序都需要多个账户作为输入,因此我们陷入了与之前相同的问题。

    这就是引入 地址查找表(Address Lookup Tables,LUT)的原因。

    地址查找表(Address Lookeup Tables)

    地址查找表的理念是在链上使用表格(数组)的数据结构存储账户地址。一旦账户存储在该表中,可以在交易消息中引用该表的地址。为了指向表中的单个账户,需要使用一个字节的u8索引。

    LUTs

    这样做可以节省空间,因为地址不再需要存储在交易消息中。它们只需要以数组形式的表格中的索引进行引用。这使得有可能引用256个账户,因为账户使用u8索引进行引用。

    当初始化地址查找表或向表中添加新地址时,需要使地址查找表免除租金。地址可以通过链上缓冲区或直接通过Extension指令将其追加到表格中。此外,地址查找表还可以存储相关的元数据,后面是一个紧凑数组的账户。下面是一个典型地址查找表的结构:

    LUT Format

    地址查找表的一个重要缺点是,在交易处理过程中,由于地址查找需要额外的开销,通常会导致交易的成本较高。

    版本化交易: TransactionV0

    传统交易的结构需要修改以包含地址表查找。这些更改不应破坏Solana上的交易处理,也不应对被调用的程序的格式产生任何更改。

    为了确保上述情况,重要的是明确指出交易类型:legacy(传统)或versioned(版本化)。我们如何在交易中包含这些信息呢?

    在引入版本化之前,交易在其消息头部的num_required_signatures字段的第一个字节中留下了一个未使用的上位比特。现在,我们可以使用这个比特位来明确声明我们的交易版本。

    pub enum VersionedMessage {
    Legacy(Message),
    V0(v0::Message),
    }

    如果设置了第一个比特位,那么第一个字节中的剩余比特将用于编码版本号。Solana从“版本0”开始,这是开始使用地址查找表的版本。

    如果未设置第一个比特位,那么该交易将被视为“传统交易”,并且第一个字节的剩余部分将被视为编码传统消息的第一个字节。

    MessageV0

    新的MessageV0的结构基本上是相同的,只是有两个小但重要的变化:

    1. 消息头部:与传统版本相同,没有变化。
    2. 紧凑账户密钥数组:与传统版本相同,没有变化。我们将指向该数组元素的索引数组表示为索引数组A(您很快将看到为什么我们这样表示)。
    3. 最近的区块哈希:与传统版本相同,没有变化。
    4. 紧凑指令数组:与传统版本不同,发生了变化。
    5. 地址表查找的紧凑数组:在版本0中引入。

    Message v0

    在查看指令数组中的变化之前,我们首先讨论地址表查找的紧凑数组的结构。

    地址表查找的紧凑数组

    这个结构将地址查找表(LUT)引入到版本化交易中,从而使得在单个交易中加载更多的只读和可写账户成为可能。

    紧凑数组以紧凑的u16编码表示地址表查找的数量,后跟一个地址表查找的数组。每个查找的结构如下:

    1. 账户密钥:地址查找表的账户密钥。
    2. 可写索引:用于加载可写账户地址的紧凑索引数组。我们将此数组表示为索引数组B
    3. 只读索引:用于加载只读账户地址的紧凑索引数组。我们将此数组表示为索引数组C

    Compact array of LUTs

    现在让我们看看指令紧凑数组中做了哪些改变。

    紧凑指令数组

    如前所述,传统指令的紧凑数组存储了各个传统指令,而这些指令又分别存储了以下内容:

    1. 程序ID索引
    2. 账户地址索引的紧凑数组
    3. 不透明的8位数据的紧凑数组

    新指令中的变化不在于指令本身的结构,而是在于用于获取第1和第2项索引的数组。在传统交易中,使用了索引数组A的子集,而在版本化交易中,则使用了以下组合数组的子集:

    1. 索引数组A:存储在消息中的紧凑账户数组。
    2. 索引数组B:地址表查找中的可写索引。
    3. 索引数组C:地址表查找中的只读索引。

    New Compact array of Instructions

    RPC变更

    事务响应将需要一个新的版本字段:maxSupportedTransactionVersion,以向客户端指示需要遵循的事务结构以进行反序列化。

    以下方法需要进行更新以避免错误:

    • getTransaction
    • getBlock

    请求中需要添加以下参数:

    maxSupportedTransactionVersion: 0

    如果请求中没有显式添加maxSupportedTransactionVersion,事务版本将回退到legacy。任何包含版本化事务的区块,在存在传统事务的情况下将返回客户端错误。

    你可以通过向RPC端点发送JSON格式的请求来设置如下:

    curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d \
    '{"jsonrpc": "2.0", "id":1, "method": "getBlock", "params": [430, {
    "encoding":"json",
    "maxSupportedTransactionVersion":0,
    "transactionDetails":"full",
    "rewards":false
    }]}'

    你还可以使用 @solana/web3.js 库执行相同操作。

    // connect to the `devnet` cluster and get the current `slot`
    const connection = new web3.Connection(web3.clusterApiUrl("devnet"));
    const slot = await connection.getSlot();

    // get the latest block (allowing for v0 transactions)
    const block = await connection.getBlock(slot, {
    maxSupportedTransactionVersion: 0,
    });

    // get a specific transaction (allowing for v0 transactions)
    const getTx = await connection.getTransaction(
    "3jpoANiFeVGisWRY5UP648xRXs3iQasCHABPWRWnoEjeA93nc79WrnGgpgazjq4K9m8g2NJoyKoWBV1Kx5VmtwHQ",
    {
    maxSupportedTransactionVersion: 0,
    },
    );

    其他资料

    参考资料

    - - +-传统交易存在一个主要问题:最大允许的大小为1232字节,因此原子交易中可以容纳的账户数量为35个地址。

    • 地址查找表(LUTs):一旦账户存储在该表中,可以使用1字节的u8索引,在交易消息中引用该表的地址。
    • 可以使用solana/web3.jscreateLookupTable()构建一个新的查找表,并确定其地址。
    • 一旦创建了LUT,可以进行扩展,即可以将账户追加到表中。
    • 版本化交易:需要修改传统交易的结构以整合LUTs。
    • 在引入版本化之前,交易在其头部的第一个字节中保留了一个未使用的最高位,可以用来显式声明交易的版本。

    我们将更详细地讨论上述引入的更改以及它们对开发人员的意义。然而,为了更好地理解这些更改,我们首先需要了解常规(或传统)交易的结构。

    传统交易(Legacy Transactions)

    Solana网络使用最大事务单元(MTU)大小为1280字节,遵循IPv6 MTU 的大小约束,以确保速度和可靠性。这样留下了1232字节的数据空间,用于存储序列化的交易等数据。

    一个交易由以下组成:

    1. 一个紧凑数组的签名,其中每个签名是一个64字节的ed25519签名。
    2. 一个(传统的)消息。

    Transaction Format

    info

    tip Compact-Array format

    A compact array is an array serialised to have the following components:

    1. An array length in a multi-byte encoding called Compact-u16
    2. Followed by each array item

    Compact array format

    传统消息

    传统消息包含以下组件:

    1. 一个头部(header)。
    2. 一个紧凑数组的账户地址,每个账户地址占用32字节。
    3. 一个最近的区块哈希(recent blockhash):
      • 一个32字节的SHA-256哈希,用于指示上次观察到的账本状态。如果一个区块哈希太旧,验证节点将拒绝它。
    4. 一个紧凑数组的指令

    Legacy Message

    头部

    消息头部是3字节长,包含3个u8整数:

    1. 所需签名数量:Solana运行时会将此数字与交易中紧凑数组签名的长度进行验证。
    2. 需要签名的只读账户地址数量。
    3. 不需要签名的只读账户地址数量。

    Message Header

    紧凑账户地址数组

    这个紧凑数组以紧凑的u16编码的账户地址数量开始,然后是:

    1. 需要签名的账户地址:首先列出请求读取和写入访问权限的地址,然后是请求只读访问权限的地址。
    2. 不需要签名的账户地址:与上述相同,首先列出请求读取和写入访问权限的地址,然后是请求只读访问权限的地址。

    Compact array of account addresses

    紧凑指令数组

    就像账户地址数组一样,这个紧凑指令数组以紧凑的u16编码的指令数量开始,然后是一个指令数组。数组中的每个指令具有以下组件:

    1. 程序ID:用于标识将处理该指令的链上程序。它表示为消息中账户地址紧凑数组的地址的u8索引。
    2. 账户地址索引的紧凑数组:指向紧凑账户地址数组中需要签名的一部分账户地址的u8索引。
    3. 不透明的u8数据的紧凑数组:一个通用的字节数组,与前面提到的程序ID相关。该数据数组指定了程序应执行的任何操作以及账户可能不包含的任何附加信息。

    Compact array of Instructions

    传统交易的问题

    上述交易模型存在的问题是什么?

    交易的最大大小以及因此能够在单个原子交易中容纳的账户数量。

    如前所述,交易的最大允许大小为1232字节。一个账户地址的大小为32字节。因此,考虑到一些用于头部、签名和其他元数据的空间,一个交易最多只能存储35个账户。

    Issue with legacy transactions

    这是一个问题,因为有几种情况下,开发人员需要在单个交易中包含数百个无需签名的账户。但是,传统交易模型目前无法实现这一点。目前使用的解决方案是在链上临时存储状态,并在稍后的交易中使用。但是,当多个程序需要组合在单个交易中时,这种解决方法就不适用了。每个程序都需要多个账户作为输入,因此我们陷入了与之前相同的问题。

    这就是引入 地址查找表(Address Lookup Tables,LUT)的原因。

    地址查找表(Address Lookeup Tables)

    地址查找表的理念是在链上使用表格(数组)的数据结构存储账户地址。一旦账户存储在该表中,可以在交易消息中引用该表的地址。为了指向表中的单个账户,需要使用一个字节的u8索引。

    LUTs

    这样做可以节省空间,因为地址不再需要存储在交易消息中。它们只需要以数组形式的表格中的索引进行引用。这使得有可能引用256个账户,因为账户使用u8索引进行引用。

    当初始化地址查找表或向表中添加新地址时,需要使地址查找表免除租金。地址可以通过链上缓冲区或直接通过Extension指令将其追加到表格中。此外,地址查找表还可以存储相关的元数据,后面是一个紧凑数组的账户。下面是一个典型地址查找表的结构:

    LUT Format

    地址查找表的一个重要缺点是,在交易处理过程中,由于地址查找需要额外的开销,通常会导致交易的成本较高。

    版本化交易: TransactionV0

    传统交易的结构需要修改以包含地址表查找。这些更改不应破坏Solana上的交易处理,也不应对被调用的程序的格式产生任何更改。

    为了确保上述情况,重要的是明确指出交易类型:legacy(传统)或versioned(版本化)。我们如何在交易中包含这些信息呢?

    在引入版本化之前,交易在其消息头部的num_required_signatures字段的第一个字节中留下了一个未使用的上位比特。现在,我们可以使用这个比特位来明确声明我们的交易版本。

    pub enum VersionedMessage {
    Legacy(Message),
    V0(v0::Message),
    }

    如果设置了第一个比特位,那么第一个字节中的剩余比特将用于编码版本号。Solana从“版本0”开始,这是开始使用地址查找表的版本。

    如果未设置第一个比特位,那么该交易将被视为“传统交易”,并且第一个字节的剩余部分将被视为编码传统消息的第一个字节。

    MessageV0

    新的MessageV0的结构基本上是相同的,只是有两个小但重要的变化:

    1. 消息头部:与传统版本相同,没有变化。
    2. 紧凑账户密钥数组:与传统版本相同,没有变化。我们将指向该数组元素的索引数组表示为索引数组A(您很快将看到为什么我们这样表示)。
    3. 最近的区块哈希:与传统版本相同,没有变化。
    4. 紧凑指令数组:与传统版本不同,发生了变化。
    5. 地址表查找的紧凑数组:在版本0中引入。

    Message v0

    在查看指令数组中的变化之前,我们首先讨论地址表查找的紧凑数组的结构。

    地址表查找的紧凑数组

    这个结构将地址查找表(LUT)引入到版本化交易中,从而使得在单个交易中加载更多的只读和可写账户成为可能。

    紧凑数组以紧凑的u16编码表示地址表查找的数量,后跟一个地址表查找的数组。每个查找的结构如下:

    1. 账户密钥:地址查找表的账户密钥。
    2. 可写索引:用于加载可写账户地址的紧凑索引数组。我们将此数组表示为索引数组B
    3. 只读索引:用于加载只读账户地址的紧凑索引数组。我们将此数组表示为索引数组C

    Compact array of LUTs

    现在让我们看看指令紧凑数组中做了哪些改变。

    紧凑指令数组

    如前所述,传统指令的紧凑数组存储了各个传统指令,而这些指令又分别存储了以下内容:

    1. 程序ID索引
    2. 账户地址索引的紧凑数组
    3. 不透明的8位数据的紧凑数组

    新指令中的变化不在于指令本身的结构,而是在于用于获取第1和第2项索引的数组。在传统交易中,使用了索引数组A的子集,而在版本化交易中,则使用了以下组合数组的子集:

    1. 索引数组A:存储在消息中的紧凑账户数组。
    2. 索引数组B:地址表查找中的可写索引。
    3. 索引数组C:地址表查找中的只读索引。

    New Compact array of Instructions

    RPC变更

    事务响应将需要一个新的版本字段:maxSupportedTransactionVersion,以向客户端指示需要遵循的事务结构以进行反序列化。

    以下方法需要进行更新以避免错误:

    • getTransaction
    • getBlock

    请求中需要添加以下参数:

    maxSupportedTransactionVersion: 0

    如果请求中没有显式添加maxSupportedTransactionVersion,事务版本将回退到legacy。任何包含版本化事务的区块,在存在传统事务的情况下将返回客户端错误。

    你可以通过向RPC端点发送JSON格式的请求来设置如下:

    curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d \
    '{"jsonrpc": "2.0", "id":1, "method": "getBlock", "params": [430, {
    "encoding":"json",
    "maxSupportedTransactionVersion":0,
    "transactionDetails":"full",
    "rewards":false
    }]}'

    你还可以使用 @solana/web3.js 库执行相同操作。

    // connect to the `devnet` cluster and get the current `slot`
    const connection = new web3.Connection(web3.clusterApiUrl("devnet"));
    const slot = await connection.getSlot();

    // get the latest block (allowing for v0 transactions)
    const block = await connection.getBlock(slot, {
    maxSupportedTransactionVersion: 0,
    });

    // get a specific transaction (allowing for v0 transactions)
    const getTx = await connection.getTransaction(
    "3jpoANiFeVGisWRY5UP648xRXs3iQasCHABPWRWnoEjeA93nc79WrnGgpgazjq4K9m8g2NJoyKoWBV1Kx5VmtwHQ",
    {
    maxSupportedTransactionVersion: 0,
    },
    );

    其他资料

    参考资料

    + + \ No newline at end of file diff --git a/cookbook-zh/index.html b/cookbook-zh/index.html index 7360ab664..f76a12308 100644 --- a/cookbook-zh/index.html +++ b/cookbook-zh/index.html @@ -9,16 +9,16 @@ - - + +

    Solana秘籍

    info

    《Solana 秘籍》中文取自:Solana Cookbook仓库里面已经有的中文版本。后期会根据官方的更新及时更新中文版本,由All in One Solana 社区维护。

    《Solana秘籍》力图为你提供在Solana公链上编写去中心化应用所需的核心概念、 参考。 每个概念和参考都会聚焦于Solana开发中的某个具体方面,同时提供额外的技术细节以及用例。

    贡献代码

    Solana秘籍的设计力图让新的Solana开发者也能容易的贡献代码。 即使你还不太了解如何做项目,贡献代码也是一个很好的学习过程!

    这里 -可以查看所有待解决的issue。

    info

    注意这里是中文版本的issue,如果你想查看英文版本的issue,请点击这里

    如果你觉得还有其他哪些建议,可以新建一个issue.

    如何阅读Solana秘籍

    Solana秘籍分为不同的章节。每个章节都有不同的目标。

    章节描述
    核心概念Solana的基础元素。开发者最好能了解这些内容。
    指南关于开发中所能用到的工具的简要介绍。
    参考常用的代码片段参考
    - - +可以查看所有待解决的issue。

    info

    注意这里是中文版本的issue,如果你想查看英文版本的issue,请点击这里

    如果你觉得还有其他哪些建议,可以新建一个issue.

    如何阅读Solana秘籍

    Solana秘籍分为不同的章节。每个章节都有不同的目标。

    章节描述
    核心概念Solana的基础元素。开发者最好能了解这些内容。
    指南关于开发中所能用到的工具的简要介绍。
    参考常用的代码片段参考
    + + \ No newline at end of file diff --git a/cookbook-zh/references/accounts/index.html b/cookbook-zh/references/accounts/index.html index 61d06c070..83c98f097 100644 --- a/cookbook-zh/references/accounts/index.html +++ b/cookbook-zh/references/accounts/index.html @@ -9,15 +9,15 @@ - - + +

    账户

    如何创建系统账户

    创建一个由系统程序 拥有的账户。Solana运行时将授予账户的所有者对其数据的写入权限或转移Lamports的访问权限。在创建账户时,我们需要预先分配一定大小的存储空间(space)和足够的Lamports来支付租金。 租金(Rent) 是在Solana上保持账户活跃所需支付的费用。

    const createAccountParams = {
    fromPubkey: fromPubkey.publicKey,
    newAccountPubkey: newAccountPubkey.publicKey,
    lamports: rentExemptionAmount,
    space,
    programId: SystemProgram.programId,
    };

    const createAccountTransaction = new Transaction().add(
    SystemProgram.createAccount(createAccountParams)
    );

    await sendAndConfirmTransaction(connection, createAccountTransaction, [
    fromPubkey,
    newAccountPubkey,
    ]);

    如何计算账户费用

    在Solana上保持账户活跃会产生一项存储费用,称为 租金/rent。通过存入至少两年租金的金额,你可以使账户完全免除租金收取。对于费用的计算,你需要考虑你打算在账户中存储的数据量。

    import { Connection, clusterApiUrl } from "@solana/web3.js";

    (async () => {
    const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

    // length of data in the account to calculate rent for
    const dataLength = 1500;
    const rentExemptionAmount =
    await connection.getMinimumBalanceForRentExemption(dataLength);
    console.log({
    rentExemptionAmount,
    });
    })();

    如何使用种子创建账户

    你可以使用 createAccountWithSeed 方法来管理您的账户,而无需创建大量不同的密钥对。

    生成

    PublicKey.createWithSeed(basePubkey, seed, programId);

    创建

    const tx = new Transaction().add(
    SystemProgram.createAccountWithSeed({
    fromPubkey: feePayer.publicKey, // funder
    newAccountPubkey: derived,
    basePubkey: basePubkey,
    seed: seed,
    lamports: 1e8, // 0.1 SOL
    space: 0,
    programId: owner,
    })
    );

    console.log(
    `txhash: ${await sendAndConfirmTransaction(connection, tx, [feePayer, base])}`
    );

    转账

    const tx = new Transaction().add(
    SystemProgram.transfer({
    fromPubkey: derived,
    basePubkey: basePubkey,
    toPubkey: Keypair.generate().publicKey, // create a random receiver
    lamports: 0.01 * LAMPORTS_PER_SOL,
    seed: seed,
    programId: programId,
    })
    );
    console.log(
    `txhash: ${await sendAndConfirmTransaction(connection, tx, [feePayer, base])}`
    );
    info

    Only an account owned by system program can transfer via system program.

    贴士 只有由系统程序拥有的账户才能通过系统程序进行转账。

    如何创建PDA

    程序派生地址/Program derived address(PDA) 与普通地址相比具有以下区别:

    1. 不在ed25519曲线上
    2. 使用程序进行签名,而不是使用私钥
    info

    注意: PDA账户只能在程序上创建,地址可以在客户端创建。

    贴士 -尽管PDA是由程序ID派生的,但这并不意味着PDA归属于相同的程序。(举个例子,你可以将PDA初始化为代币账户,这是一个由代币程序拥有的账户)

    生成一个PDA

    findProgramAddress会在你的种子末尾添加一个额外的字节。它从255递减到0,并返回第一个不在ed25519曲线上的公钥。如果您传入相同的程序ID和种子,您将始终获得相同的结果。

    import { PublicKey } from "@solana/web3.js";

    (async () => {
    const programId = new PublicKey(
    "G1DCNUQTSGHehwdLCAmRyAG8hf51eCHrLNUqkgGKYASj"
    );

    let [pda, bump] = await PublicKey.findProgramAddress(
    [Buffer.from("test")],
    programId
    );
    console.log(`bump: ${bump}, pubkey: ${pda.toBase58()}`);
    // you will find the result is different from `createProgramAddress`.
    // It is expected because the real seed we used to calculate is ["test" + bump]
    })();

    创建一个PDA

    以下是一个创建由程序拥有的PDA账户的示例程序,以及一个使用客户端调用该程序的示例。

    下面是一个示例,展示了使用system_instruction::create_account创建一个具有预分配数据大小为space、预支付rent_lamports数量的lamports的PDA账户的单条指令。该指令使用PDA进行签名,并使用invoke_signed进行调用,与前面讨论的类似。

    invoke_signed(
    &system_instruction::create_account(
    &payer_account_info.key,
    &pda_account_info.key,
    rent_lamports,
    space.into(),
    program_id
    ),
    &[
    payer_account_info.clone(),
    pda_account_info.clone()
    ],
    &[&[&payer_account_info.key.as_ref(), &[bump]]]
    )?;

    如何使用PDA签名

    PDAs只能在程序内部进行签名。以下是使用PDA进行签名的程序示例,并使用客户端调用该程序的示例。

    以下示例展示了一个单个指令,用于从由种子escrow派生的 PDA 转账 SOL 到指定的账户。使用 invoke_signed 函数来使用 PDA 签名。

    invoke_signed(
    &system_instruction::transfer(
    &pda_account_info.key,
    &to_account_info.key,
    100_000_000, // 0.1 SOL
    ),
    &[
    pda_account_info.clone(),
    to_account_info.clone(),
    system_program_account_info.clone(),
    ],
    &[&[b"escrow", &[bump_seed]]],
    )?;

    如何获取程序账户

    返回所有由程序拥有的账户。请参考 指南部分 ,获取有关getProgramAccounts及其配置的更多信息。

    import { clusterApiUrl, Connection, PublicKey } from "@solana/web3.js";

    (async () => {
    const MY_PROGRAM_ID = new PublicKey(
    "6a2GdmttJdanBkoHt7f4Kon4hfadx4UTUgJeRkCaiL3U"
    );
    const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

    const accounts = await connection.getProgramAccounts(MY_PROGRAM_ID);

    console.log(`Accounts for program ${MY_PROGRAM_ID}: `);
    console.log(accounts);

    /*
    // Output

    Accounts for program 6a2GdmttJdanBkoHt7f4Kon4hfadx4UTUgJeRkCaiL3U:
    [
    {
    account: {
    data: <Buffer 60 06 66 ca 2c 1d c7 85 04 00 00 00 00 00 00 00 05 00 00 00 00 00 00 00 fc>,
    executable: false,
    lamports: 1064880,
    owner: [PublicKey],
    rentEpoch: 228
    },
    pubkey: PublicKey {
    _bn: <BN: 82fc5b91154dc5c840cb464ba6a89212d0fd789367c0a1488fb1941d78f9727a>
    }
    },
    {
    account: {
    data: <Buffer 60 06 66 ca 2c 1d c7 85 03 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 fd>,
    executable: false,
    lamports: 1064880,
    owner: [PublicKey],
    rentEpoch: 229
    },
    pubkey: PublicKey {
    _bn: <BN: 404dc1fe368cf194f20cf3c681a071c61893ced98f65cda12ba5a147e984e669>
    }
    }
    ]
    */
    })();

    如何关闭账户

    你可以通过移除账户中的所有 SOL(以擦除所有存储数据的方式)来关闭一个账户。(你可以参考rent来了解更多信息。)

    let dest_starting_lamports = dest_account_info.lamports();
    **dest_account_info.lamports.borrow_mut() = dest_starting_lamports
    .checked_add(source_account_info.lamports())
    .unwrap();
    **source_account_info.lamports.borrow_mut() = 0;

    let mut source_data = source_account_info.data.borrow_mut();
    source_data.fill(0);

    如何获取账户余额

    console.log(`${(await connection.getBalance(wallet)) / LAMPORTS_PER_SOL} SOL`);
    info

    如果你想获取代币余额,你需要知道代币账户的地址。如果像了解更多信息,请参考Token References。

    - - +尽管PDA是由程序ID派生的,但这并不意味着PDA归属于相同的程序。(举个例子,你可以将PDA初始化为代币账户,这是一个由代币程序拥有的账户)

    生成一个PDA

    findProgramAddress会在你的种子末尾添加一个额外的字节。它从255递减到0,并返回第一个不在ed25519曲线上的公钥。如果您传入相同的程序ID和种子,您将始终获得相同的结果。

    import { PublicKey } from "@solana/web3.js";

    (async () => {
    const programId = new PublicKey(
    "G1DCNUQTSGHehwdLCAmRyAG8hf51eCHrLNUqkgGKYASj"
    );

    let [pda, bump] = await PublicKey.findProgramAddress(
    [Buffer.from("test")],
    programId
    );
    console.log(`bump: ${bump}, pubkey: ${pda.toBase58()}`);
    // you will find the result is different from `createProgramAddress`.
    // It is expected because the real seed we used to calculate is ["test" + bump]
    })();

    创建一个PDA

    以下是一个创建由程序拥有的PDA账户的示例程序,以及一个使用客户端调用该程序的示例。

    下面是一个示例,展示了使用system_instruction::create_account创建一个具有预分配数据大小为space、预支付rent_lamports数量的lamports的PDA账户的单条指令。该指令使用PDA进行签名,并使用invoke_signed进行调用,与前面讨论的类似。

    invoke_signed(
    &system_instruction::create_account(
    &payer_account_info.key,
    &pda_account_info.key,
    rent_lamports,
    space.into(),
    program_id
    ),
    &[
    payer_account_info.clone(),
    pda_account_info.clone()
    ],
    &[&[&payer_account_info.key.as_ref(), &[bump]]]
    )?;

    如何使用PDA签名

    PDAs只能在程序内部进行签名。以下是使用PDA进行签名的程序示例,并使用客户端调用该程序的示例。

    以下示例展示了一个单个指令,用于从由种子escrow派生的 PDA 转账 SOL 到指定的账户。使用 invoke_signed 函数来使用 PDA 签名。

    invoke_signed(
    &system_instruction::transfer(
    &pda_account_info.key,
    &to_account_info.key,
    100_000_000, // 0.1 SOL
    ),
    &[
    pda_account_info.clone(),
    to_account_info.clone(),
    system_program_account_info.clone(),
    ],
    &[&[b"escrow", &[bump_seed]]],
    )?;

    如何获取程序账户

    返回所有由程序拥有的账户。请参考 指南部分 ,获取有关getProgramAccounts及其配置的更多信息。

    import { clusterApiUrl, Connection, PublicKey } from "@solana/web3.js";

    (async () => {
    const MY_PROGRAM_ID = new PublicKey(
    "6a2GdmttJdanBkoHt7f4Kon4hfadx4UTUgJeRkCaiL3U"
    );
    const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

    const accounts = await connection.getProgramAccounts(MY_PROGRAM_ID);

    console.log(`Accounts for program ${MY_PROGRAM_ID}: `);
    console.log(accounts);

    /*
    // Output

    Accounts for program 6a2GdmttJdanBkoHt7f4Kon4hfadx4UTUgJeRkCaiL3U:
    [
    {
    account: {
    data: <Buffer 60 06 66 ca 2c 1d c7 85 04 00 00 00 00 00 00 00 05 00 00 00 00 00 00 00 fc>,
    executable: false,
    lamports: 1064880,
    owner: [PublicKey],
    rentEpoch: 228
    },
    pubkey: PublicKey {
    _bn: <BN: 82fc5b91154dc5c840cb464ba6a89212d0fd789367c0a1488fb1941d78f9727a>
    }
    },
    {
    account: {
    data: <Buffer 60 06 66 ca 2c 1d c7 85 03 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 fd>,
    executable: false,
    lamports: 1064880,
    owner: [PublicKey],
    rentEpoch: 229
    },
    pubkey: PublicKey {
    _bn: <BN: 404dc1fe368cf194f20cf3c681a071c61893ced98f65cda12ba5a147e984e669>
    }
    }
    ]
    */
    })();

    如何关闭账户

    你可以通过移除账户中的所有 SOL(以擦除所有存储数据的方式)来关闭一个账户。(你可以参考rent来了解更多信息。)

    let dest_starting_lamports = dest_account_info.lamports();
    **dest_account_info.lamports.borrow_mut() = dest_starting_lamports
    .checked_add(source_account_info.lamports())
    .unwrap();
    **source_account_info.lamports.borrow_mut() = 0;

    let mut source_data = source_account_info.data.borrow_mut();
    source_data.fill(0);

    如何获取账户余额

    console.log(`${(await connection.getBalance(wallet)) / LAMPORTS_PER_SOL} SOL`);
    info

    如果你想获取代币余额,你需要知道代币账户的地址。如果像了解更多信息,请参考Token References。

    + + \ No newline at end of file diff --git a/cookbook-zh/references/basic-transactions/index.html b/cookbook-zh/references/basic-transactions/index.html index 0c1b124d5..26d8a8d8d 100644 --- a/cookbook-zh/references/basic-transactions/index.html +++ b/cookbook-zh/references/basic-transactions/index.html @@ -9,14 +9,14 @@ - - + +

    发送交易

    如何发送SOL

    要发送SOL,你需要与SystemProgram 交互。

    const transferTransaction = new Transaction().add(
    SystemProgram.transfer({
    fromPubkey: fromKeypair.publicKey,
    toPubkey: toKeypair.publicKey,
    lamports: lamportsToSend,
    })
    );

    await sendAndConfirmTransaction(connection, transferTransaction, [fromKeypair]);

    如何发送SPL-代币

    使用 Token Program 来转移SPL代币。为了发送SPL代币,你需要知道它的SPL代币账户地址。你可以使用以下示例来获取地址并发送代币。

    // Add token transfer instructions to transaction
    const transaction = new web3.Transaction().add(
    splToken.Token.createTransferInstruction(
    splToken.TOKEN_PROGRAM_ID,
    fromTokenAccount.address,
    toTokenAccount.address,
    fromWallet.publicKey,
    [],
    1
    )
    );

    // Sign transaction, broadcast, and confirm
    await web3.sendAndConfirmTransaction(connection, transaction, [fromWallet]);

    如何计算交易成本

    交易所需的签名数量用于计算交易成本。只要你不是创建账户,这将是最终的交易成本。如果想了解创建账户的成本,请参考 计算租金豁免

    下面的两个示例展示了目前可用于计算估计交易成本的两种方法。

    第一个示例使用了Transaction类上的新方法getEstimatedFee,而第二个示例使用了Connection类上的getFeeForMessage来替代getFeeCalculatorForBlockhash

    getEstimatedFee

    const recentBlockhash = await connection.getLatestBlockhash();

    const transaction = new Transaction({
    recentBlockhash: recentBlockhash.blockhash,
    }).add(
    SystemProgram.transfer({
    fromPubkey: payer.publicKey,
    toPubkey: payee.publicKey,
    lamports: 10,
    })
    );

    const fees = await transaction.getEstimatedFee(connection);
    console.log(`Estimated SOL transfer cost: ${fees} lamports`);
    // Estimated SOL transfer cost: 5000 lamports

    getFeeForMessage

    const message = new Message(messageParams);

    const fees = await connection.getFeeForMessage(message);
    console.log(`Estimated SOL transfer cost: ${fees.value} lamports`);
    // Estimated SOL transfer cost: 5000 lamports

    如何向交易添加备注

    任何交易都可以利用 备注程序 (memo program). -添加消息。目前,备注程序的programID必须手动添加为MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr

    const transferTransaction = new Transaction().add(
    SystemProgram.transfer({
    fromPubkey: fromKeypair.publicKey,
    toPubkey: toKeypair.publicKey,
    lamports: lamportsToSend,
    })
    );

    await transferTransaction.add(
    new TransactionInstruction({
    keys: [{ pubkey: fromKeypair.publicKey, isSigner: true, isWritable: true }],
    data: Buffer.from("Data to send in transaction", "utf-8"),
    programId: new PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"),
    })
    );

    await sendAndConfirmTransaction(connection, transferTransaction, [fromKeypair]);

    如何更改交易的计算预算、费用和优先级

    交易(TX)的优先级是通过支付优先级费用(Prioritization Fee)来实现的,此外还需要支付基本费用(Base Fee)。默认情况下,计算预算是200,000个计算单元(Compute Units,CU)与指令数的乘积,最大为1.4M CU。基本费用是5,000个Lamport。一个微型Lamport等于0.000001个Lamport。

    要更改单个交易的总计算预算或优先级费用,可以通过添加ComputeBudgetProgram的指令来实现。

    使用ComputeBudgetProgram.setComputeUnitPrice({ microLamports: number })可以在基本费用(5,000个Lamport)之上添加优先级费用。microLamports参数提供的值将与计算预算的CU数相乘,以确定优先级费用(以Lamport为单位)。例如,如果您的计算预算为1M CU,然后添加1个microLamport/CU,优先级费用将为1个Lamport(1M * 0.000001)。总费用将为5001个Lamport。

    使用ComputeBudgetProgram.setComputeUnitLimit({ units: number })来设置新的计算预算。提供的值将替换默认值。交易应该请求执行所需的最小数量的CU,以最大化吞吐量或最小化费用。

    const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({
    units: 1000000
    });

    const addPriorityFee = ComputeBudgetProgram.setComputeUnitPrice({
    microLamports: 1
    });

    const transaction = new Transaction()
    .add(modifyComputeUnits)
    .add(addPriorityFee)
    .add(
    SystemProgram.transfer({
    fromPubkey: payer.publicKey,
    toPubkey: toAccount,
    lamports: 10000000,
    })
    );

    程序日志示例 ( Explorer ):

    // cli
    [ 1] Program ComputeBudget111111111111111111111111111111 invoke [1]
    [ 2] Program ComputeBudget111111111111111111111111111111 success
    [ 3]
    [ 4] Program ComputeBudget111111111111111111111111111111 invoke [1]
    [ 5] Program ComputeBudget111111111111111111111111111111 success
    - - +添加消息。目前,备注程序的programID必须手动添加为MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr

    const transferTransaction = new Transaction().add(
    SystemProgram.transfer({
    fromPubkey: fromKeypair.publicKey,
    toPubkey: toKeypair.publicKey,
    lamports: lamportsToSend,
    })
    );

    await transferTransaction.add(
    new TransactionInstruction({
    keys: [{ pubkey: fromKeypair.publicKey, isSigner: true, isWritable: true }],
    data: Buffer.from("Data to send in transaction", "utf-8"),
    programId: new PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"),
    })
    );

    await sendAndConfirmTransaction(connection, transferTransaction, [fromKeypair]);

    如何更改交易的计算预算、费用和优先级

    交易(TX)的优先级是通过支付优先级费用(Prioritization Fee)来实现的,此外还需要支付基本费用(Base Fee)。默认情况下,计算预算是200,000个计算单元(Compute Units,CU)与指令数的乘积,最大为1.4M CU。基本费用是5,000个Lamport。一个微型Lamport等于0.000001个Lamport。

    要更改单个交易的总计算预算或优先级费用,可以通过添加ComputeBudgetProgram的指令来实现。

    使用ComputeBudgetProgram.setComputeUnitPrice({ microLamports: number })可以在基本费用(5,000个Lamport)之上添加优先级费用。microLamports参数提供的值将与计算预算的CU数相乘,以确定优先级费用(以Lamport为单位)。例如,如果您的计算预算为1M CU,然后添加1个microLamport/CU,优先级费用将为1个Lamport(1M * 0.000001)。总费用将为5001个Lamport。

    使用ComputeBudgetProgram.setComputeUnitLimit({ units: number })来设置新的计算预算。提供的值将替换默认值。交易应该请求执行所需的最小数量的CU,以最大化吞吐量或最小化费用。

    const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({
    units: 1000000
    });

    const addPriorityFee = ComputeBudgetProgram.setComputeUnitPrice({
    microLamports: 1
    });

    const transaction = new Transaction()
    .add(modifyComputeUnits)
    .add(addPriorityFee)
    .add(
    SystemProgram.transfer({
    fromPubkey: payer.publicKey,
    toPubkey: toAccount,
    lamports: 10000000,
    })
    );

    程序日志示例 ( Explorer ):

    // cli
    [ 1] Program ComputeBudget111111111111111111111111111111 invoke [1]
    [ 2] Program ComputeBudget111111111111111111111111111111 success
    [ 3]
    [ 4] Program ComputeBudget111111111111111111111111111111 invoke [1]
    [ 5] Program ComputeBudget111111111111111111111111111111 success
    + + \ No newline at end of file diff --git a/cookbook-zh/references/gaming/auto-approve/index.html b/cookbook-zh/references/gaming/auto-approve/index.html index 5bc40b5b8..da93d8acc 100644 --- a/cookbook-zh/references/gaming/auto-approve/index.html +++ b/cookbook-zh/references/gaming/auto-approve/index.html @@ -9,16 +9,16 @@ - - + +

    auto-approve

    WIP - This is a work in progress

    How to auto approve transaction for fast game play and great ux

    To have a fluid game play for on-chain games it is beneficial to have an auto approve wallet.

    1. Solflare wallet offers auto-approve functionality with burner wallets, but this limits your players to only one wallet.

    Burner Auto Approve Wallets

    1. Another way to do it is to create a key pair in your game and let the player transfer some sol to that wallet and then use it to pay for transaction fees. Only problem with this is that you need to handle the security for this wallet and the players would need to have access to their seed phrase.

    Example Source Code
    Example Game

    1. You can pay the fees yourself, by creating and signing the transactions in the backend and interact with it via an API. For that you send parameters to your backend and sign the transaction there and send a confirmation to the client as soon as it is done.

    2. There is a protocol called @gumisfunn and they released a feature called session keys. Session Keys are ephemeral keys with fine-grained program/instruction scoping for tiered access in your @solana programs. They allow users to interact with apps under particular parameters like duration, max tokens, amount of posts or any other function specific to an app. -Link

    - - +Link

    + + \ No newline at end of file diff --git a/cookbook-zh/references/gaming/distribution/index.html b/cookbook-zh/references/gaming/distribution/index.html index 0b9fc0b39..2d3a66d03 100644 --- a/cookbook-zh/references/gaming/distribution/index.html +++ b/cookbook-zh/references/gaming/distribution/index.html @@ -9,13 +9,13 @@ - - + +
    -

    Distribution

    Distribution of your game depends highly on the platform you are using. With Solana, there are game SDKs you can build for IOS, Android, Web and Native Windows or Mac. Using the Unity SDK you could even connect Nintendo Switch or XBox to Solana theoretically. Many game companies are pivoting to a mobile first approach because there are so many people with mobile phones in the world. Mobile comes with its own complications though, so you should pick what fits best to your game.

    Solana has a distinct edge over other blockchain platforms due to its offering of a crypto-native mobile phone, named Saga, that comes equipped with an innovative dApps store. This store enables the distribution of crypto games without the limitations imposed by conventional app stores such as Google or Apple.

    Publishing Platforms

    Platforms where you can host your games

    PlatformDescription
    FractalA game publishing platform that supports Solana and Ethereum. They also have their own wallet and account handling and there is an SDK for high scores and tournaments.
    ElixirPlatform for web3 games that also offers a PC launcher
    Self HostingJust host your game yourself. For example using Vercel which can be easily setup so that a new version get deployed as soon as you push to your repository. Other options are github pages or Google Firebase
    Solana mobile DApp StoreThe Solana alternative to Google Play and the Apple App Store. A crypto first variant of a dApp store, which is open source free for everyone to use.
    Apple App StoreThe Apple app store has a high reach and is trusted by its customers. The entrance barrier for crypto games is high though. The rules are very strict for everything that tries to circumvent the fees that Apple takes for in app purchases. A soon as an NFT provides benefits for the player for example Apple requires you for example to have them purchased via their in app purchase system.
    Google Play StoreGoogle is much more crypto friendly and games with NFTs and wallet deep links for example have had a track record of being approved for the official play store.
    xNFT BackpackBackpack is a Solana wallet which allows you to release apps as xNFTs. They appear in the users wallet as soon as they purchase them as applications. The Unity SDK has a xNFT export and any other web app can be published as xNFT as well.
    - - +

    Distribution

    Distribution of your game depends highly on the platform you are using. With Solana, there are game SDKs you can build for IOS, Android, Web and Native Windows or Mac. Using the Unity SDK you could even connect Nintendo Switch or XBox to Solana theoretically. Many game companies are pivoting to a mobile first approach because there are so many people with mobile phones in the world. Mobile comes with its own complications though, so you should pick what fits best to your game.

    Solana has a distinct edge over other blockchain platforms due to its offering of a crypto-native mobile phone, named Saga, that comes equipped with an innovative dApps store. This store enables the distribution of crypto games without the limitations imposed by conventional app stores such as Google or Apple.

    Publishing Platforms

    Platforms where you can host your games

    PlatformDescription
    FractalA game publishing platform that supports Solana and Ethereum. They also have their own wallet and account handling and there is an SDK for high scores and tournaments.
    ElixirPlatform for web3 games that also offers a PC launcher
    Self HostingJust host your game yourself. For example using Vercel which can be easily setup so that a new version get deployed as soon as you push to your repository. Other options are github pages or Google Firebase
    Solana mobile DApp StoreThe Solana alternative to Google Play and the Apple App Store. A crypto first variant of a dApp store, which is open source free for everyone to use.
    Apple App StoreThe Apple app store has a high reach and is trusted by its customers. The entrance barrier for crypto games is high though. The rules are very strict for everything that tries to circumvent the fees that Apple takes for in app purchases. A soon as an NFT provides benefits for the player for example Apple requires you for example to have them purchased via their in app purchase system.
    Google Play StoreGoogle is much more crypto friendly and games with NFTs and wallet deep links for example have had a track record of being approved for the official play store.
    xNFT BackpackBackpack is a Solana wallet which allows you to release apps as xNFTs. They appear in the users wallet as soon as they purchase them as applications. The Unity SDK has a xNFT export and any other web app can be published as xNFT as well.
    + + \ No newline at end of file diff --git a/cookbook-zh/references/gaming/energy-system/index.html b/cookbook-zh/references/gaming/energy-system/index.html index dd2bb1384..34ca482cc 100644 --- a/cookbook-zh/references/gaming/energy-system/index.html +++ b/cookbook-zh/references/gaming/energy-system/index.html @@ -9,8 +9,8 @@ - - + +
    @@ -23,8 +23,8 @@ The is a common technique in game development.


    const TIME_TO_REFILL_ENERGY: i64 = 60;
    const MAX_ENERGY: u64 = 10;

    pub fn update_energy(ctx: &mut ChopTree) -> Result<()> {
    let mut time_passed: i64 = &Clock::get()?.unix_timestamp - &ctx.player.last_login;
    let mut time_spent: i64 = 0;
    while time_passed > TIME_TO_REFILL_ENERGY {
    ctx.player.energy = ctx.player.energy + 1;
    time_passed -= TIME_TO_REFILL_ENERGY;
    time_spent += TIME_TO_REFILL_ENERGY;
    if ctx.player.energy == MAX_ENERGY {
    break;
    }
    }

    if ctx.player.energy >= MAX_ENERGY {
    ctx.player.last_login = Clock::get()?.unix_timestamp;
    } else {
    ctx.player.last_login += time_spent;
    }

    Ok(())
    }

    Js client

    Here is a complete example based on the Solana dapp scaffold with a react client: Source

    Create connection

    In the Anchor.ts file we create a connection:

    export const connection = new Connection(
    "https://api.devnet.solana.com",
    "confirmed"
    );

    Notice that the confirmation parameter is set to 'confirmed'. This means that we wait until the transactions are confirmed instead of finalized. This means that we wait until the super majority of the network said that the transaction is valid. This takes around 400ms and there was never a confirmed transaction which did not get finalized. So for games this is the perfect confirmation flag.

    Initialize player data

    First thing we do is find the program address for the player account using the seed string playerand the player's public key. Then we call initPlayer to create the account.

    const [pda] = PublicKey.findProgramAddressSync(
    [Buffer.from("player", "utf8"), publicKey.toBuffer()],
    new PublicKey(LUMBERJACK_PROGRAM_ID)
    );

    const transaction = program.methods
    .initPlayer()
    .accounts({
    player: pda,
    signer: publicKey,
    systemProgram: SystemProgram.programId,
    })
    .transaction();

    const tx = await transaction;
    const txSig = await sendTransaction(tx, connection, {
    skipPreflight: true,
    });

    await connection.confirmTransaction(txSig, "confirmed");

    Subscribe to account updates

    Here you can see how to get account data in the js client and how to subscribe to an account. connection.onAccountChange creates a socket connection to the RPC node which will push any changes that happen to the account to the client. This is faster than fetching new account data after every change. -We can then use the program.coder to decode the account data into the TS types and directly use it in the game.

    useEffect(() => {
    if (!publicKey) {return;}
    const [pda] = PublicKey.findProgramAddressSync(
    [Buffer.from("player", "utf8"), publicKey.toBuffer()],
    new PublicKey(LUMBERJACK_PROGRAM_ID)
    );
    try {
    program.account.playerData.fetch(pda).then((data) => {
    setGameState(data);
    });
    } catch (e) {
    window.alert("No player data found, please init!");
    }

    connection.onAccountChange(pda, (account) => {
    setGameState(program.coder.accounts.decode("playerData", account.data));
    });

    }, [publicKey]);

    Calculate energy and show count down

    In the javascript client we can then perform the same logic as in the program to precalculate how much energy the player would have at this point in time and show a countdown timer for the player so that he knows when the next energy will be available:

    useEffect(() => {
    const interval = setInterval(async () => {
    if (gameState == null || gameState.lastLogin == undefined || gameState.energy >= 10) {return;}
    const lastLoginTime = gameState.lastLogin * 1000;
    let timePassed = ((Date.now() - lastLoginTime) / 1000);
    while (timePassed > TIME_TO_REFILL_ENERGY && gameState.energy < MAX_ENERGY) {
    gameState.energy = (parseInt(gameState.energy) + 1);
    gameState.lastLogin = parseInt(gameState.lastLogin) + TIME_TO_REFILL_ENERGY;
    timePassed -= TIME_TO_REFILL_ENERGY;
    }
    setTimePassed(timePassed);
    let nextEnergyIn = Math.floor(TIME_TO_REFILL_ENERGY - timePassed);
    if (nextEnergyIn < TIME_TO_REFILL_ENERGY && nextEnergyIn > 0) {
    setEnergyNextIn(nextEnergyIn);
    } else {
    setEnergyNextIn(0);
    }

    }, 1000);

    return () => clearInterval(interval);
    }, [gameState, timePassed]);

    ...

    {(gameState && <div className="flex flex-col items-center">
    {("Wood: " + gameState.wood + " Energy: " + gameState.energy + " Next energy in: " + nextEnergyIn )}
    </div>)}

    With this you can now build any energy based game and even if someone builds a bot for the game the most he can do is play optimally, which maybe even easier to achieve when playing normally depending on the logic of your game.

    This game becomes even better when combined with the Token example and you actually drop some spl token to the players.

    - - +We can then use the program.coder to decode the account data into the TS types and directly use it in the game.

    useEffect(() => {
    if (!publicKey) {return;}
    const [pda] = PublicKey.findProgramAddressSync(
    [Buffer.from("player", "utf8"), publicKey.toBuffer()],
    new PublicKey(LUMBERJACK_PROGRAM_ID)
    );
    try {
    program.account.playerData.fetch(pda).then((data) => {
    setGameState(data);
    });
    } catch (e) {
    window.alert("No player data found, please init!");
    }

    connection.onAccountChange(pda, (account) => {
    setGameState(program.coder.accounts.decode("playerData", account.data));
    });

    }, [publicKey]);

    Calculate energy and show count down

    In the javascript client we can then perform the same logic as in the program to precalculate how much energy the player would have at this point in time and show a countdown timer for the player so that he knows when the next energy will be available:

    useEffect(() => {
    const interval = setInterval(async () => {
    if (gameState == null || gameState.lastLogin == undefined || gameState.energy >= 10) {return;}
    const lastLoginTime = gameState.lastLogin * 1000;
    let timePassed = ((Date.now() - lastLoginTime) / 1000);
    while (timePassed > TIME_TO_REFILL_ENERGY && gameState.energy < MAX_ENERGY) {
    gameState.energy = (parseInt(gameState.energy) + 1);
    gameState.lastLogin = parseInt(gameState.lastLogin) + TIME_TO_REFILL_ENERGY;
    timePassed -= TIME_TO_REFILL_ENERGY;
    }
    setTimePassed(timePassed);
    let nextEnergyIn = Math.floor(TIME_TO_REFILL_ENERGY - timePassed);
    if (nextEnergyIn < TIME_TO_REFILL_ENERGY && nextEnergyIn > 0) {
    setEnergyNextIn(nextEnergyIn);
    } else {
    setEnergyNextIn(0);
    }

    }, 1000);

    return () => clearInterval(interval);
    }, [gameState, timePassed]);

    ...

    {(gameState && <div className="flex flex-col items-center">
    {("Wood: " + gameState.wood + " Energy: " + gameState.energy + " Next energy in: " + nextEnergyIn )}
    </div>)}

    With this you can now build any energy based game and even if someone builds a bot for the game the most he can do is play optimally, which maybe even easier to achieve when playing normally depending on the logic of your game.

    This game becomes even better when combined with the Token example and you actually drop some spl token to the players.

    + + \ No newline at end of file diff --git a/cookbook-zh/references/gaming/game-examples/index.html b/cookbook-zh/references/gaming/game-examples/index.html index 652fdc85f..26cbcf6db 100644 --- a/cookbook-zh/references/gaming/game-examples/index.html +++ b/cookbook-zh/references/gaming/game-examples/index.html @@ -9,8 +9,8 @@ - - + +
    @@ -19,8 +19,8 @@ A simple multiplayer game written in Anchor

    Tutorial

    Source

    On Chain Chess

    Chess Complete on chain playable chess game written in Anchor. Send someone a link to start a game. Looking for contributors.

    Live Version

    Source

    Multiplayer Game using voting system

    Pokemon voting system A game where collectively people vote on moves in a game boy game. Every move is recorded and each move can be minted as an NFTs.

    Live Version

    Source

    Entity component system example

    Kyoudai Clash is an on chain realtime -Using the jump crypto Arc framework which is an on chain entity component system for Solana.

    xNFT Version

    Source

    Adventure killing monsters and gaining xp

    Lumia online was a hackthon submission and is a nice reference for a little adventure game.

    xNFT Version

    Source

    Real-time pvp on chain game

    SolHunter

    Real-time Solana Battle Royal Game. Using Anchor program, UnitySDK, WebSocket account subscription. Players can spawn their characters represented as one of their NFTs on a grid and move around. If a player hits another player or chest he collect its Sol. The grid is implemented as a two dimensional array where every tile saves the players wallet key and the NFT public key.

    Example

    Source

    - - +Using the jump crypto Arc framework which is an on chain entity component system for Solana.

    xNFT Version

    Source

    Adventure killing monsters and gaining xp

    Lumia online was a hackthon submission and is a nice reference for a little adventure game.

    xNFT Version

    Source

    Real-time pvp on chain game

    SolHunter

    Real-time Solana Battle Royal Game. Using Anchor program, UnitySDK, WebSocket account subscription. Players can spawn their characters represented as one of their NFTs on a grid and move around. If a player hits another player or chest he collect its Sol. The grid is implemented as a two dimensional array where every tile saves the players wallet key and the NFT public key.

    Example

    Source

    + + \ No newline at end of file diff --git a/cookbook-zh/references/gaming/game-sdks/index.html b/cookbook-zh/references/gaming/game-sdks/index.html index ae8a99368..577da7b20 100644 --- a/cookbook-zh/references/gaming/game-sdks/index.html +++ b/cookbook-zh/references/gaming/game-sdks/index.html @@ -9,8 +9,8 @@ - - + +
    @@ -23,8 +23,8 @@ The fastest way to set it up is:

    npx create-solana-dapp your-app

    This will generate a great starting application with wallet-adapter support. A benefit of using Next.js is that you can use the same code in the backend and in the frontend, speeding up development.

    Web3Js
    Solana Cookbook

    Python

    Python is an easy to learn programming language which is often used in AI programming. There is a framework called Seahorse which lets you build smart contracts in Python.

    Anchor Playground Example
    -Source and Docs

    Native C#

    The original port of Web3js to C#. It comes with a bunch of functionality like transactions, RPC functions and anchor client code generation.

    Source and Docs

    - - +Source and Docs

    Native C#

    The original port of Web3js to C#. It comes with a bunch of functionality like transactions, RPC functions and anchor client code generation.

    Source and Docs

    + + \ No newline at end of file diff --git a/cookbook-zh/references/gaming/hello-world/index.html b/cookbook-zh/references/gaming/hello-world/index.html index 730a26394..ddab1bc5a 100644 --- a/cookbook-zh/references/gaming/hello-world/index.html +++ b/cookbook-zh/references/gaming/hello-world/index.html @@ -9,14 +9,14 @@ - - + +

    Building an on-chain game on Solana

    Getting started with your first Solana game

    Video Walkthrough:

    YouTube video player

    Live Version. (use devnet in the embedded version)

    Tiny Adventure

    Tiny Adventure is a beginner-friendly Solana program created using the Anchor framework. The goal of this program is to show you how to create a simple game that allows players to track their position and move left or right.

    The Tiny Adventure Program consists of only 3 instructions:

    • initialize - This instruction sets up an on-chain account to store the player's position
    • move_left - This instruction lets the player move their position to the left
    • move_right - This instruction lets the player move their position to the right

    In the upcoming sections, we'll walk through the process of building this game step by step. -You can find the complete source code, available to deploy from your browser, in this Solana Playground example.

    If need to familiarize yourself with the Anchor framework, feel free to check out the Anchor module of the Solana Course to get started.

    Getting Started

    To start building the Tiny Adventure game, follow these steps:

    Visit Solana Playground and create a new Anchor project. If you're new to Solana Playground, you'll also need to create a Playground Wallet. Here is an example of how to use Solana Playground:

    solpg.gif

    After creating a new project, replace the default starter code with the code below:

    use anchor_lang::prelude::*;

    declare_id!("11111111111111111111111111111111");

    #[program]
    mod tiny_adventure {
    use super::*;
    }

    fn print_player(player_position: u8) {
    if player_position == 0 {
    msg!("A Journey Begins!");
    msg!("o.......");
    } else if player_position == 1 {
    msg!("..o.....");
    } else if player_position == 2 {
    msg!("....o...");
    } else if player_position == 3 {
    msg!("........\\o/");
    msg!("You have reached the end! Super!");
    }
    }

    In this game, the player starts at position 0 and can move left or right. To show the player's progress throughout the game, we'll use message logs to display their journey.

    Defining the Game Data Account

    The first step in building the game is to define a structure for the on-chain account that will store the player's position.

    The GameDataAccount struct contains a single field, player_position, which stores the player's current position as an unsigned 8-bit integer.

    use anchor_lang::prelude::*;

    declare_id!("11111111111111111111111111111111");

    #[program]
    mod tiny_adventure {
    use super::*;

    }

    ...

    // Define the Game Data Account structure
    #[account]
    pub struct GameDataAccount {
    player_position: u8,
    }

    Initialize Instruction

    After defining the program account, let’s implement the initialize instruction. This instruction initializes the GameDataAccount if it doesn't already exist, sets the player_position to 0, and print some message logs.

    The initialize instruction requires 3 accounts:

    • new_game_data_account - the GameDataAccount we are initializing
    • signer - the player paying for the initialization of the GameDataAccount
    • system_program - a required account when creating a new account
    #[program]
    pub mod tiny_adventure {
    use super::*;

    // Instruction to initialize GameDataAccount and set position to 0
    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    ctx.accounts.new_game_data_account.player_position = 0;
    msg!("A Journey Begins!");
    msg!("o.......");
    Ok(())
    }
    }

    // Specify the accounts required by the initialize instruction
    #[derive(Accounts)]
    pub struct Initialize<'info> {
    #[account(
    init_if_needed,
    seeds = [b"level1"],
    bump,
    payer = signer,
    space = 8 + 1
    )]
    pub new_game_data_account: Account<'info, GameDataAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    ...

    In this example, a Program Derived Address (PDA) is used for the GameDataAccount address. This enables us to deterministically locate the address later on. It is important to note that the PDA in this example is generated with a single fixed value as the seed (level1), limiting our program to creating only one GameDataAccount. The init_if_needed constraint then ensures that the GameDataAccount is initialized only if it doesn't already exist.

    It is worth noting that the current implementation does not have any restrictions on who can modify the GameDataAccount. This effectively transforms the game into a multiplayer experience where everyone can control the player's movement.

    Alternatively, you can use the signer's address as an extra seed in the initialize instruction, which would enable each player to create their own GameDataAccount.

    Move Left Instruction

    Now that we can initialize a GameDataAccount account, let’s implement the move_left instruction. This lets a player update their player_position. In this example, moving left simply means decrementing the player_position by 1. We'll also set the minimum position to 0.

    The only account needed for this instruction is the GameDataAccount.

    #[program]
    pub mod tiny_adventure {
    use super::*;
    ...

    // Instruction to move left
    pub fn move_left(ctx: Context<MoveLeft>) -> Result<()> {
    let game_data_account = &mut ctx.accounts.game_data_account;
    if game_data_account.player_position == 0 {
    msg!("You are back at the start.");
    } else {
    game_data_account.player_position -= 1;
    print_player(game_data_account.player_position);
    }
    Ok(())
    }
    }

    // Specify the account required by the move_left instruction
    #[derive(Accounts)]
    pub struct MoveLeft<'info> {
    #[account(mut)]
    pub game_data_account: Account<'info, GameDataAccount>,
    }

    ...

    Move Right Instruction

    Lastly, let’s implement the move_right instruction. Similarly, moving right will simply mean incrementing the player_position by 1. We’ll also limit the maximum position to 3.

    Just like before, the only account needed for this instruction is the GameDataAccount.

    #[program]
    pub mod tiny_adventure {
    use super::*;
    ...

    // Instruction to move right
    pub fn move_right(ctx: Context<MoveRight>) -> Result<()> {
    let game_data_account = &mut ctx.accounts.game_data_account;
    if game_data_account.player_position == 3 {
    msg!("You have reached the end! Super!");
    } else {
    game_data_account.player_position = game_data_account.player_position + 1;
    print_player(game_data_account.player_position);
    }
    Ok(())
    }
    }

    // Specify the account required by the move_right instruction
    #[derive(Accounts)]
    pub struct MoveRight<'info> {
    #[account(mut)]
    pub game_data_account: Account<'info, GameDataAccount>,
    }

    ...

    Build and Deploy

    We've now completed the Tiny Adventure program! Your final program should resemble the following:

    use anchor_lang::prelude::*;

    // This is your program's public key and it will update
    // automatically when you build the project.
    declare_id!("BouPBVWkdVHbxsdzqeMwkjqd5X67RX5nwMEwxn8MDpor");

    #[program]
    mod tiny_adventure {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    ctx.accounts.new_game_data_account.player_position = 0;
    msg!("A Journey Begins!");
    msg!("o.......");
    Ok(())
    }

    pub fn move_left(ctx: Context<MoveLeft>) -> Result<()> {
    let game_data_account = &mut ctx.accounts.game_data_account;
    if game_data_account.player_position == 0 {
    msg!("You are back at the start.");
    } else {
    game_data_account.player_position -= 1;
    print_player(game_data_account.player_position);
    }
    Ok(())
    }

    pub fn move_right(ctx: Context<MoveRight>) -> Result<()> {
    let game_data_account = &mut ctx.accounts.game_data_account;
    if game_data_account.player_position == 3 {
    msg!("You have reached the end! Super!");
    } else {
    game_data_account.player_position = game_data_account.player_position + 1;
    print_player(game_data_account.player_position);
    }
    Ok(())
    }
    }

    fn print_player(player_position: u8) {
    if player_position == 0 {
    msg!("A Journey Begins!");
    msg!("o.......");
    } else if player_position == 1 {
    msg!("..o.....");
    } else if player_position == 2 {
    msg!("....o...");
    } else if player_position == 3 {
    msg!("........\\o/");
    msg!("You have reached the end! Super!");
    }
    }

    #[derive(Accounts)]
    pub struct Initialize<'info> {
    #[account(
    init_if_needed,
    seeds = [b"level1"],
    bump,
    payer = signer,
    space = 8 + 1
    )]
    pub new_game_data_account: Account<'info, GameDataAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    #[derive(Accounts)]
    pub struct MoveLeft<'info> {
    #[account(mut)]
    pub game_data_account: Account<'info, GameDataAccount>,
    }

    #[derive(Accounts)]
    pub struct MoveRight<'info> {
    #[account(mut)]
    pub game_data_account: Account<'info, GameDataAccount>,
    }

    #[account]
    pub struct GameDataAccount {
    player_position: u8,
    }

    With the program completed, it's time to build and deploy it on Solana Playground!

    If this is your first time using Solana Playground, create a Playground Wallet first and ensure that you're connected to a Devnet endpoint. Then, run solana airdrop 2 until you have 6 SOL. Once you have enough SOL, build and deploy the program.

    Get Started with the Client

    This next section will guide you through a simple client-side implementation for interacting with the game. We'll break down the code and provide detailed explanations for each step. In Solana Playground, navigate to the client.ts file and add the code snippets from the following sections.

    First, let’s derive the PDA for the GameDataAccount. A PDA is a unique address in the format of a public key, derived using the program's ID and additional seeds. Feel free to check out the PDA lessons of the Solana Course for more details.

    // The PDA adress everyone will be able to control the character if the interact with your program
    const [globalLevel1GameDataAccount, bump] =
    await anchor.web3.PublicKey.findProgramAddress(
    [Buffer.from("level1", "utf8")],
    pg.program.programId
    );

    Next, let’s try to fetch the game data account using the PDA from the previous step. If the account doesn't exist, we'll create it by invoking the initialize instruction from our program.

    let txHash;
    let gameDateAccount;
    try {
    gameDateAccount = await pg.program.account.gameDataAccount.fetch(
    globalLevel1GameDataAccount
    );
    } catch {
    // Check if the account is already initialized, other wise initialize it
    txHash = await pg.program.methods
    .initialize()
    .accounts({
    newGameDataAccount: globalLevel1GameDataAccount,
    signer: pg.wallet.publicKey,
    systemProgram: web3.SystemProgram.programId,
    })
    .signers([pg.wallet.keypair])
    .rpc();

    console.log(`Use 'solana confirm -v ${txHash}' to see the logs`);
    await pg.connection.confirmTransaction(txHash);
    console.log("A journey begins...");
    console.log("o........");
    }

    Now we are ready to interact with the game by moving left or right. This is done by invoking the moveLeft or moveRight instructions from the program and submitting a transaction to the Solana network. You can repeat this step as many times as you'd like.

    // Here you can play around now, move left and right
    txHash = await pg.program.methods
    //.moveLeft()
    .moveRight()
    .accounts({
    gameDataAccount: globalLevel1GameDataAccount,
    })
    .signers([pg.wallet.keypair])
    .rpc();
    console.log(`Use 'solana confirm -v ${txHash}' to see the logs`);
    await pg.connection.confirmTransaction(txHash);

    gameDateAccount = await pg.program.account.gameDataAccount.fetch(
    globalLevel1GameDataAccount
    );

    console.log("Player position is:", gameDateAccount.playerPosition.toString());

    Lastly, let’s use a switch statement to log the character's position based on the playerPosition value stored in the gameDateAccount. We’ll use this as a visual representation of the character's movement in the game.

    switch (gameDateAccount.playerPosition) {
    case 0:
    console.log("A journey begins...");
    console.log("o........");
    break;
    case 1:
    console.log("....o....");
    break;
    case 2:
    console.log("......o..");
    break;
    case 3:
    console.log(".........\\o/");
    break;
    }

    Finally, run the client by clicking the “Run” button in Solana Playground. The output should be similar to the following:

    Running client...
    client.ts:
    My address: 8ujtDmwpkQ4Bp4GU4zUWmzf65sc21utdcxFAELESca22
    My balance: 4.649749614 SOL
    Use 'solana confirm -v 4MRXEWfGqvmro1KsKb94Zz8qTZsPa9x99oMFbLBz2WicLnr8vdYYsQwT5u3pK5Vt1i9BDrVH5qqTXwtif6sCRJCy' to see the logs
    Player position is: 1
    ....o....

    Congratulations! You have successfully built, deployed, and invoked the Tiny Adventure game from the client. To further illustrate the possibilities, check out this demo that demonstrates how to interact with the Tiny Adventure program through a Next.js frontend.

    Where to Go from Here

    With the basic game complete, unleash your creativity and practice building independently by implementing your own ideas to enrich the game experience. Here are a few suggestions:

    1. Modify the in-game texts to create an intriguing story. Invite a friend to play through your custom narrative and observe the on-chain transactions as they unfold!
    2. Add a chest that rewards players with Sol Rewards or let the player collect coins Interact with tokens as they progress through the game.
    3. Create a grid that allows the player to move up, down, left, and right, and introduce multiple players for a more dynamic experience.

    In the next installment, Tiny Adventure Two, we'll learn how to store SOL in the program and distribute it to players as rewards.

    - - +You can find the complete source code, available to deploy from your browser, in this Solana Playground example.

    If need to familiarize yourself with the Anchor framework, feel free to check out the Anchor module of the Solana Course to get started.

    Getting Started

    To start building the Tiny Adventure game, follow these steps:

    Visit Solana Playground and create a new Anchor project. If you're new to Solana Playground, you'll also need to create a Playground Wallet. Here is an example of how to use Solana Playground:

    solpg.gif

    After creating a new project, replace the default starter code with the code below:

    use anchor_lang::prelude::*;

    declare_id!("11111111111111111111111111111111");

    #[program]
    mod tiny_adventure {
    use super::*;
    }

    fn print_player(player_position: u8) {
    if player_position == 0 {
    msg!("A Journey Begins!");
    msg!("o.......");
    } else if player_position == 1 {
    msg!("..o.....");
    } else if player_position == 2 {
    msg!("....o...");
    } else if player_position == 3 {
    msg!("........\\o/");
    msg!("You have reached the end! Super!");
    }
    }

    In this game, the player starts at position 0 and can move left or right. To show the player's progress throughout the game, we'll use message logs to display their journey.

    Defining the Game Data Account

    The first step in building the game is to define a structure for the on-chain account that will store the player's position.

    The GameDataAccount struct contains a single field, player_position, which stores the player's current position as an unsigned 8-bit integer.

    use anchor_lang::prelude::*;

    declare_id!("11111111111111111111111111111111");

    #[program]
    mod tiny_adventure {
    use super::*;

    }

    ...

    // Define the Game Data Account structure
    #[account]
    pub struct GameDataAccount {
    player_position: u8,
    }

    Initialize Instruction

    After defining the program account, let’s implement the initialize instruction. This instruction initializes the GameDataAccount if it doesn't already exist, sets the player_position to 0, and print some message logs.

    The initialize instruction requires 3 accounts:

    • new_game_data_account - the GameDataAccount we are initializing
    • signer - the player paying for the initialization of the GameDataAccount
    • system_program - a required account when creating a new account
    #[program]
    pub mod tiny_adventure {
    use super::*;

    // Instruction to initialize GameDataAccount and set position to 0
    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    ctx.accounts.new_game_data_account.player_position = 0;
    msg!("A Journey Begins!");
    msg!("o.......");
    Ok(())
    }
    }

    // Specify the accounts required by the initialize instruction
    #[derive(Accounts)]
    pub struct Initialize<'info> {
    #[account(
    init_if_needed,
    seeds = [b"level1"],
    bump,
    payer = signer,
    space = 8 + 1
    )]
    pub new_game_data_account: Account<'info, GameDataAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    ...

    In this example, a Program Derived Address (PDA) is used for the GameDataAccount address. This enables us to deterministically locate the address later on. It is important to note that the PDA in this example is generated with a single fixed value as the seed (level1), limiting our program to creating only one GameDataAccount. The init_if_needed constraint then ensures that the GameDataAccount is initialized only if it doesn't already exist.

    It is worth noting that the current implementation does not have any restrictions on who can modify the GameDataAccount. This effectively transforms the game into a multiplayer experience where everyone can control the player's movement.

    Alternatively, you can use the signer's address as an extra seed in the initialize instruction, which would enable each player to create their own GameDataAccount.

    Move Left Instruction

    Now that we can initialize a GameDataAccount account, let’s implement the move_left instruction. This lets a player update their player_position. In this example, moving left simply means decrementing the player_position by 1. We'll also set the minimum position to 0.

    The only account needed for this instruction is the GameDataAccount.

    #[program]
    pub mod tiny_adventure {
    use super::*;
    ...

    // Instruction to move left
    pub fn move_left(ctx: Context<MoveLeft>) -> Result<()> {
    let game_data_account = &mut ctx.accounts.game_data_account;
    if game_data_account.player_position == 0 {
    msg!("You are back at the start.");
    } else {
    game_data_account.player_position -= 1;
    print_player(game_data_account.player_position);
    }
    Ok(())
    }
    }

    // Specify the account required by the move_left instruction
    #[derive(Accounts)]
    pub struct MoveLeft<'info> {
    #[account(mut)]
    pub game_data_account: Account<'info, GameDataAccount>,
    }

    ...

    Move Right Instruction

    Lastly, let’s implement the move_right instruction. Similarly, moving right will simply mean incrementing the player_position by 1. We’ll also limit the maximum position to 3.

    Just like before, the only account needed for this instruction is the GameDataAccount.

    #[program]
    pub mod tiny_adventure {
    use super::*;
    ...

    // Instruction to move right
    pub fn move_right(ctx: Context<MoveRight>) -> Result<()> {
    let game_data_account = &mut ctx.accounts.game_data_account;
    if game_data_account.player_position == 3 {
    msg!("You have reached the end! Super!");
    } else {
    game_data_account.player_position = game_data_account.player_position + 1;
    print_player(game_data_account.player_position);
    }
    Ok(())
    }
    }

    // Specify the account required by the move_right instruction
    #[derive(Accounts)]
    pub struct MoveRight<'info> {
    #[account(mut)]
    pub game_data_account: Account<'info, GameDataAccount>,
    }

    ...

    Build and Deploy

    We've now completed the Tiny Adventure program! Your final program should resemble the following:

    use anchor_lang::prelude::*;

    // This is your program's public key and it will update
    // automatically when you build the project.
    declare_id!("BouPBVWkdVHbxsdzqeMwkjqd5X67RX5nwMEwxn8MDpor");

    #[program]
    mod tiny_adventure {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    ctx.accounts.new_game_data_account.player_position = 0;
    msg!("A Journey Begins!");
    msg!("o.......");
    Ok(())
    }

    pub fn move_left(ctx: Context<MoveLeft>) -> Result<()> {
    let game_data_account = &mut ctx.accounts.game_data_account;
    if game_data_account.player_position == 0 {
    msg!("You are back at the start.");
    } else {
    game_data_account.player_position -= 1;
    print_player(game_data_account.player_position);
    }
    Ok(())
    }

    pub fn move_right(ctx: Context<MoveRight>) -> Result<()> {
    let game_data_account = &mut ctx.accounts.game_data_account;
    if game_data_account.player_position == 3 {
    msg!("You have reached the end! Super!");
    } else {
    game_data_account.player_position = game_data_account.player_position + 1;
    print_player(game_data_account.player_position);
    }
    Ok(())
    }
    }

    fn print_player(player_position: u8) {
    if player_position == 0 {
    msg!("A Journey Begins!");
    msg!("o.......");
    } else if player_position == 1 {
    msg!("..o.....");
    } else if player_position == 2 {
    msg!("....o...");
    } else if player_position == 3 {
    msg!("........\\o/");
    msg!("You have reached the end! Super!");
    }
    }

    #[derive(Accounts)]
    pub struct Initialize<'info> {
    #[account(
    init_if_needed,
    seeds = [b"level1"],
    bump,
    payer = signer,
    space = 8 + 1
    )]
    pub new_game_data_account: Account<'info, GameDataAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    #[derive(Accounts)]
    pub struct MoveLeft<'info> {
    #[account(mut)]
    pub game_data_account: Account<'info, GameDataAccount>,
    }

    #[derive(Accounts)]
    pub struct MoveRight<'info> {
    #[account(mut)]
    pub game_data_account: Account<'info, GameDataAccount>,
    }

    #[account]
    pub struct GameDataAccount {
    player_position: u8,
    }

    With the program completed, it's time to build and deploy it on Solana Playground!

    If this is your first time using Solana Playground, create a Playground Wallet first and ensure that you're connected to a Devnet endpoint. Then, run solana airdrop 2 until you have 6 SOL. Once you have enough SOL, build and deploy the program.

    Get Started with the Client

    This next section will guide you through a simple client-side implementation for interacting with the game. We'll break down the code and provide detailed explanations for each step. In Solana Playground, navigate to the client.ts file and add the code snippets from the following sections.

    First, let’s derive the PDA for the GameDataAccount. A PDA is a unique address in the format of a public key, derived using the program's ID and additional seeds. Feel free to check out the PDA lessons of the Solana Course for more details.

    // The PDA adress everyone will be able to control the character if the interact with your program
    const [globalLevel1GameDataAccount, bump] =
    await anchor.web3.PublicKey.findProgramAddress(
    [Buffer.from("level1", "utf8")],
    pg.program.programId
    );

    Next, let’s try to fetch the game data account using the PDA from the previous step. If the account doesn't exist, we'll create it by invoking the initialize instruction from our program.

    let txHash;
    let gameDateAccount;
    try {
    gameDateAccount = await pg.program.account.gameDataAccount.fetch(
    globalLevel1GameDataAccount
    );
    } catch {
    // Check if the account is already initialized, other wise initialize it
    txHash = await pg.program.methods
    .initialize()
    .accounts({
    newGameDataAccount: globalLevel1GameDataAccount,
    signer: pg.wallet.publicKey,
    systemProgram: web3.SystemProgram.programId,
    })
    .signers([pg.wallet.keypair])
    .rpc();

    console.log(`Use 'solana confirm -v ${txHash}' to see the logs`);
    await pg.connection.confirmTransaction(txHash);
    console.log("A journey begins...");
    console.log("o........");
    }

    Now we are ready to interact with the game by moving left or right. This is done by invoking the moveLeft or moveRight instructions from the program and submitting a transaction to the Solana network. You can repeat this step as many times as you'd like.

    // Here you can play around now, move left and right
    txHash = await pg.program.methods
    //.moveLeft()
    .moveRight()
    .accounts({
    gameDataAccount: globalLevel1GameDataAccount,
    })
    .signers([pg.wallet.keypair])
    .rpc();
    console.log(`Use 'solana confirm -v ${txHash}' to see the logs`);
    await pg.connection.confirmTransaction(txHash);

    gameDateAccount = await pg.program.account.gameDataAccount.fetch(
    globalLevel1GameDataAccount
    );

    console.log("Player position is:", gameDateAccount.playerPosition.toString());

    Lastly, let’s use a switch statement to log the character's position based on the playerPosition value stored in the gameDateAccount. We’ll use this as a visual representation of the character's movement in the game.

    switch (gameDateAccount.playerPosition) {
    case 0:
    console.log("A journey begins...");
    console.log("o........");
    break;
    case 1:
    console.log("....o....");
    break;
    case 2:
    console.log("......o..");
    break;
    case 3:
    console.log(".........\\o/");
    break;
    }

    Finally, run the client by clicking the “Run” button in Solana Playground. The output should be similar to the following:

    Running client...
    client.ts:
    My address: 8ujtDmwpkQ4Bp4GU4zUWmzf65sc21utdcxFAELESca22
    My balance: 4.649749614 SOL
    Use 'solana confirm -v 4MRXEWfGqvmro1KsKb94Zz8qTZsPa9x99oMFbLBz2WicLnr8vdYYsQwT5u3pK5Vt1i9BDrVH5qqTXwtif6sCRJCy' to see the logs
    Player position is: 1
    ....o....

    Congratulations! You have successfully built, deployed, and invoked the Tiny Adventure game from the client. To further illustrate the possibilities, check out this demo that demonstrates how to interact with the Tiny Adventure program through a Next.js frontend.

    Where to Go from Here

    With the basic game complete, unleash your creativity and practice building independently by implementing your own ideas to enrich the game experience. Here are a few suggestions:

    1. Modify the in-game texts to create an intriguing story. Invite a friend to play through your custom narrative and observe the on-chain transactions as they unfold!
    2. Add a chest that rewards players with Sol Rewards or let the player collect coins Interact with tokens as they progress through the game.
    3. Create a grid that allows the player to move up, down, left, and right, and introduce multiple players for a more dynamic experience.

    In the next installment, Tiny Adventure Two, we'll learn how to store SOL in the program and distribute it to players as rewards.

    + + \ No newline at end of file diff --git a/cookbook-zh/references/gaming/index.html b/cookbook-zh/references/gaming/index.html index f59d8efdd..12f34eeb4 100644 --- a/cookbook-zh/references/gaming/index.html +++ b/cookbook-zh/references/gaming/index.html @@ -9,13 +9,13 @@ - - + + - - + + + \ No newline at end of file diff --git a/cookbook-zh/references/gaming/interact-with-tokens/index.html b/cookbook-zh/references/gaming/interact-with-tokens/index.html index 0ca4b6614..12943fb83 100644 --- a/cookbook-zh/references/gaming/interact-with-tokens/index.html +++ b/cookbook-zh/references/gaming/interact-with-tokens/index.html @@ -9,13 +9,13 @@ - - + +
    -

    Using tokens in games on Solana

    Tokens on Solana can serve various purposes, such as in-game rewards, incentives, or other applications. For example, you can create tokens and distribute them to players when they complete specific in-game actions.

    Create, Mint, and Burn Tokens with Anchor

    In this tutorial, we will build a game using Anchor to introduce the basics of interacting with the Token Program on Solana. The game will be structured around four main actions: creating a new token mint, initializing player accounts, rewarding players for defeating enemies, and allowing players to heal by burning tokens.

    The program consists of 4 instructions:

    • create_mint - This instruction creates a new token mint with a Program Derived Address (PDA) as the mint authority and creates the metadata account for the mint. We will add a constraint that allows only an "admin" to invoke this instruction
    • init_player - This instruction initializes a new player account with a starting health of 100
    • kill_enemy - This instruction deducts 10 health points from the player account upon “defeating an enemy” and mints 1 token as a reward for the player
    • heal - This instruction allows a player to burn 1 token to restore their health back to 100

    For a high-level overview of the relationship among user wallets, token mints, token accounts, and token metadata accounts, consider exploring this portion of the Metaplex documentation.

    Getting Started

    To start building the program, follow these steps:

    Visit the Solana Playground and create a new Anchor project. If you're new to Solana Playground, you'll also need to create a Playground Wallet.

    After creating a new project, replace the default starter code with the code below:

    use anchor_lang::prelude::*;
    use anchor_spl::{
    associated_token::AssociatedToken,
    metadata::{create_metadata_accounts_v3, CreateMetadataAccountsV3, Metadata},
    token::{burn, mint_to, Burn, Mint, MintTo, Token, TokenAccount},
    };
    use mpl_token_metadata::{pda::find_metadata_account, state::DataV2};
    use solana_program::{pubkey, pubkey::Pubkey};

    declare_id!("11111111111111111111111111111111");

    #[program]
    pub mod anchor_token {
    use super::*;
    }

    Here we are simply bringing into scope the crates and corresponding modules we will be using for this program. We’ll be using the anchor_spl and mpl_token_metadata crates to help us interact with the Token program and the Token Metadata program.

    Create Mint instruction

    First, let’s implement an instruction to create a new token mint and its metadata account. The on-chain token metadata, including the name, symbol, and URI, will be provided as parameters to the instruction.

    Additionally, we'll only allow an "admin" to invoke this instruction by defining an ADMIN_PUBKEY constant and using it as a constraint. Be sure to replace the ADMIN_PUBKEY with your Solana Playground wallet public key.

    The create_mint instruction requires the following accounts:

    • admin - the ADMIN_PUBKEY that signs the transaction and pays for the initialization of the accounts
    • reward_token_mint - the new token mint we are initializing, using a PDA as both the mint account’s address and its mint authority
    • metadata_account - the metadata account we are initializing for the token mint
    • token_program - required for interacting with instructions on the Token program
    • token_metadata_program - required account for interacting with instructions on the Token Metadata program
    • system_program- a required account when creating a new account
    • rent - Sysvar Rent, a required account when creating the metadata account
    // Only this public key can call this instruction
    const ADMIN_PUBKEY: Pubkey = pubkey!("REPLACE_WITH_YOUR_WALLET_PUBKEY");

    #[program]
    pub mod anchor_token {
    use super::*;

    // Create new token mint with PDA as mint authority
    pub fn create_mint(
    ctx: Context<CreateMint>,
    uri: String,
    name: String,
    symbol: String,
    ) -> Result<()> {
    // PDA seeds and bump to "sign" for CPI
    let seeds = b"reward";
    let bump = *ctx.bumps.get("reward_token_mint").unwrap();
    let signer: &[&[&[u8]]] = &[&[seeds, &[bump]]];

    // On-chain token metadata for the mint
    let data_v2 = DataV2 {
    name: name,
    symbol: symbol,
    uri: uri,
    seller_fee_basis_points: 0,
    creators: None,
    collection: None,
    uses: None,
    };

    // CPI Context
    let cpi_ctx = CpiContext::new_with_signer(
    ctx.accounts.token_metadata_program.to_account_info(),
    CreateMetadataAccountsV3 {
    metadata: ctx.accounts.metadata_account.to_account_info(), // the metadata account being created
    mint: ctx.accounts.reward_token_mint.to_account_info(), // the mint account of the metadata account
    mint_authority: ctx.accounts.reward_token_mint.to_account_info(), // the mint authority of the mint account
    update_authority: ctx.accounts.reward_token_mint.to_account_info(), // the update authority of the metadata account
    payer: ctx.accounts.admin.to_account_info(), // the payer for creating the metadata account
    system_program: ctx.accounts.system_program.to_account_info(), // the system program account
    rent: ctx.accounts.rent.to_account_info(), // the rent sysvar account
    },
    signer,
    );

    create_metadata_accounts_v3(
    cpi_ctx, // cpi context
    data_v2, // token metadata
    true, // is_mutable
    true, // update_authority_is_signer
    None, // collection details
    )?;

    Ok(())
    }
    }

    #[derive(Accounts)]
    pub struct CreateMint<'info> {
    #[account(
    mut,
    address = ADMIN_PUBKEY
    )]
    pub admin: Signer<'info>,

    // The PDA is both the address of the mint account and the mint authority
    #[account(
    init,
    seeds = [b"reward"],
    bump,
    payer = admin,
    mint::decimals = 9,
    mint::authority = reward_token_mint,

    )]
    pub reward_token_mint: Account<'info, Mint>,

    ///CHECK: Using "address" constraint to validate metadata account address
    #[account(
    mut,
    address=find_metadata_account(&reward_token_mint.key()).0
    )]
    pub metadata_account: UncheckedAccount<'info>,

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

    The create_mint instruction creates a new token mint, using a Program Derived Address (PDA) as both the address of the token mint and its mint authority. The instruction takes a URI (off-chain metadata), name, and symbol as parameters.

    This instruction then creates a metadata account for the token mint through a Cross-Program Invocation (CPI) calling the create_metadata_accounts_v3 instruction from the Token Metadata program.

    The PDA is used to "sign" the CPI since it is the mint authority, which is a required signer when creating the metadata account for a mint. The instruction data (URI, name, symbol) is included in the DataV2 struct to specify the new token mint's metadata.

    We also verify that the address of the admin account signing the transaction matches the value of the ADMIN_PUBKEY constant to ensure only the intended wallet can invoke this instruction.

    const ADMIN_PUBKEY: Pubkey = pubkey!("REPLACE_WITH_YOUR_WALLET_PUBKEY");

    Init Player Instruction

    Next, let's implement the init_player instruction which creates a new player account with an initial health of 100. The constant MAX_HEALTH is set to 100 to represent the starting health.

    The init_player instruction requires the following accounts:

    • player_data - the new player account we are initializing, which will store the player's health
    • player - the user who signs the transaction and pays for the initialization of the account
    • system_program - a required account when creating a new account
    // Player max health
    const MAX_HEALTH: u8 = 100;

    #[program]
    pub mod anchor_token {
    use super::*;
    ...

    // Create new player account
    pub fn init_player(ctx: Context<InitPlayer>) -> Result<()> {
    ctx.accounts.player_data.health = MAX_HEALTH;
    Ok(())
    }
    }
    ...

    #[derive(Accounts)]
    pub struct InitPlayer<'info> {
    #[account(
    init,
    payer = player,
    space = 8 + 8,
    seeds = [b"player".as_ref(), player.key().as_ref()],
    bump,
    )]
    pub player_data: Account<'info, PlayerData>,
    #[account(mut)]
    pub player: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    #[account]
    pub struct PlayerData {
    pub health: u8,
    }

    The player_data account is initialized using a Program Derived Address (PDA) with the player public key as one of the seeds. This ensures that each player_data account is unique and associated with the player, allowing every player to create their own player_data account.

    Kill Enemy Instruction

    Next, let's implement the kill_enemy instruction which reduces the player's health by 10 and mints 1 token to the player's token account as a reward.

    The kill_enemy instruction requires the following accounts:

    • player - the player receiving the token
    • player_data - the player data account storing the player’s current health
    • player_token_account - the player's associated token account where tokens will be minted
    • reward_token_mint - the token mint account, specifying the type of token that will be minted
    • token_program - required for interacting with instructions on the token program
    • associated_token_program - required when working with associated token accounts
    • system_program - a required account when creating a new account
    #[program]
    pub mod anchor_token {
    use super::*;
    ...

    // Mint token to player token account
    pub fn kill_enemy(ctx: Context<KillEnemy>) -> Result<()> {
    // Check if player has enough health
    if ctx.accounts.player_data.health == 0 {
    return err!(ErrorCode::NotEnoughHealth);
    }
    // Subtract 10 health from player
    ctx.accounts.player_data.health = ctx.accounts.player_data.health.checked_sub(10).unwrap();

    // PDA seeds and bump to "sign" for CPI
    let seeds = b"reward";
    let bump = *ctx.bumps.get("reward_token_mint").unwrap();
    let signer: &[&[&[u8]]] = &[&[seeds, &[bump]]];

    // CPI Context
    let cpi_ctx = CpiContext::new_with_signer(
    ctx.accounts.token_program.to_account_info(),
    MintTo {
    mint: ctx.accounts.reward_token_mint.to_account_info(),
    to: ctx.accounts.player_token_account.to_account_info(),
    authority: ctx.accounts.reward_token_mint.to_account_info(),
    },
    signer,
    );

    // Mint 1 token, accounting for decimals of mint
    let amount = (1u64)
    .checked_mul(10u64.pow(ctx.accounts.reward_token_mint.decimals as u32))
    .unwrap();

    mint_to(cpi_ctx, amount)?;
    Ok(())
    }
    }
    ...

    #[derive(Accounts)]
    pub struct KillEnemy<'info> {
    #[account(mut)]
    pub player: Signer<'info>,

    #[account(
    mut,
    seeds = [b"player".as_ref(), player.key().as_ref()],
    bump,
    )]
    pub player_data: Account<'info, PlayerData>,

    // Initialize player token account if it doesn't exist
    #[account(
    init_if_needed,
    payer = player,
    associated_token::mint = reward_token_mint,
    associated_token::authority = player
    )]
    pub player_token_account: Account<'info, TokenAccount>,

    #[account(
    mut,
    seeds = [b"reward"],
    bump,
    )]
    pub reward_token_mint: Account<'info, Mint>,

    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
    }

    #[error_code]
    pub enum ErrorCode {
    #[msg("Not enough health")]
    NotEnoughHealth,
    }

    The player's health is reduced by 10 to represent the “battle with the enemy”. We’ll also check the player's current health and return a custom Anchor error if the player has 0 health.

    The instruction then uses a cross-program invocation (CPI) to call the mint_to instruction from the Token program and mints 1 token of the reward_token_mint to the player_token_account as a reward for killing the enemy.

    Since the mint authority for the token mint is a Program Derived Address (PDA), we can mint tokens directly by calling this instruction without additional signers. The program can "sign" on behalf of the PDA, allowing token minting without explicitly requiring extra signers.

    Heal Instruction

    Next, let's implement the heal instruction which allows a player to burn 1 token and restore their health to its maximum value.

    The heal instruction requires the following accounts:

    • player - the player executing the healing action
    • player_data - the player data account storing the player’s current health
    • player_token_account - the player's associated token account where the tokens will be burned
    • reward_token_mint - the token mint account, specifying the type of token that will be burned
    • token_program - required for interacting with instructions on the token program
    • associated_token_program - required when working with associated token accounts
    #[program]
    pub mod anchor_token {
    use super::*;
    ...

    // Burn token to health player
    pub fn heal(ctx: Context<Heal>) -> Result<()> {
    ctx.accounts.player_data.health = MAX_HEALTH;

    // CPI Context
    let cpi_ctx = CpiContext::new(
    ctx.accounts.token_program.to_account_info(),
    Burn {
    mint: ctx.accounts.reward_token_mint.to_account_info(),
    from: ctx.accounts.player_token_account.to_account_info(),
    authority: ctx.accounts.player.to_account_info(),
    },
    );

    // Burn 1 token, accounting for decimals of mint
    let amount = (1u64)
    .checked_mul(10u64.pow(ctx.accounts.reward_token_mint.decimals as u32))
    .unwrap();

    burn(cpi_ctx, amount)?;
    Ok(())
    }
    }
    ...

    #[derive(Accounts)]
    pub struct Heal<'info> {
    #[account(mut)]
    pub player: Signer<'info>,

    #[account(
    mut,
    seeds = [b"player".as_ref(), player.key().as_ref()],
    bump,
    )]
    pub player_data: Account<'info, PlayerData>,

    #[account(
    mut,
    associated_token::mint = reward_token_mint,
    associated_token::authority = player
    )]
    pub player_token_account: Account<'info, TokenAccount>,

    #[account(
    mut,
    seeds = [b"reward"],
    bump,
    )]
    pub reward_token_mint: Account<'info, Mint>,

    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    }

    The player's health is restored to its maximum value using the heal instruction. The instruction then uses a cross-program invocation (CPI) to call the burn instruction from the Token program, which burns 1 token from the player_token_account to heal the player.

    Build and Deploy

    Great job! You've now completed the program! Go ahead and build and deploy it using the Solana Playground. Your final program should look like this:

    use anchor_lang::prelude::*;
    use anchor_spl::{
    associated_token::AssociatedToken,
    metadata::{create_metadata_accounts_v3, CreateMetadataAccountsV3, Metadata},
    token::{burn, mint_to, Burn, Mint, MintTo, Token, TokenAccount},
    };
    use mpl_token_metadata::{pda::find_metadata_account, state::DataV2};
    use solana_program::{pubkey, pubkey::Pubkey};

    declare_id!("CCLnXJAJYFjCHLCugpBCEQKrpiSApiRM4UxkBUHJRrv4");

    const ADMIN_PUBKEY: Pubkey = pubkey!("REPLACE_WITH_YOUR_WALLET_PUBKEY");
    const MAX_HEALTH: u8 = 100;

    #[program]
    pub mod anchor_token {
    use super::*;

    // Create new token mint with PDA as mint authority
    pub fn create_mint(
    ctx: Context<CreateMint>,
    uri: String,
    name: String,
    symbol: String,
    ) -> Result<()> {
    // PDA seeds and bump to "sign" for CPI
    let seeds = b"reward";
    let bump = *ctx.bumps.get("reward_token_mint").unwrap();
    let signer: &[&[&[u8]]] = &[&[seeds, &[bump]]];

    // On-chain token metadata for the mint
    let data_v2 = DataV2 {
    name: name,
    symbol: symbol,
    uri: uri,
    seller_fee_basis_points: 0,
    creators: None,
    collection: None,
    uses: None,
    };

    // CPI Context
    let cpi_ctx = CpiContext::new_with_signer(
    ctx.accounts.token_metadata_program.to_account_info(),
    CreateMetadataAccountsV3 {
    metadata: ctx.accounts.metadata_account.to_account_info(), // the metadata account being created
    mint: ctx.accounts.reward_token_mint.to_account_info(), // the mint account of the metadata account
    mint_authority: ctx.accounts.reward_token_mint.to_account_info(), // the mint authority of the mint account
    update_authority: ctx.accounts.reward_token_mint.to_account_info(), // the update authority of the metadata account
    payer: ctx.accounts.admin.to_account_info(), // the payer for creating the metadata account
    system_program: ctx.accounts.system_program.to_account_info(), // the system program account
    rent: ctx.accounts.rent.to_account_info(), // the rent sysvar account
    },
    signer,
    );

    create_metadata_accounts_v3(
    cpi_ctx, // cpi context
    data_v2, // token metadata
    true, // is_mutable
    true, // update_authority_is_signer
    None, // collection details
    )?;

    Ok(())
    }

    // Create new player account
    pub fn init_player(ctx: Context<InitPlayer>) -> Result<()> {
    ctx.accounts.player_data.health = MAX_HEALTH;
    Ok(())
    }

    // Mint tokens to player token account
    pub fn kill_enemy(ctx: Context<KillEnemy>) -> Result<()> {
    // Check if player has enough health
    if ctx.accounts.player_data.health == 0 {
    return err!(ErrorCode::NotEnoughHealth);
    }
    // Subtract 10 health from player
    ctx.accounts.player_data.health = ctx.accounts.player_data.health.checked_sub(10).unwrap();

    // PDA seeds and bump to "sign" for CPI
    let seeds = b"reward";
    let bump = *ctx.bumps.get("reward_token_mint").unwrap();
    let signer: &[&[&[u8]]] = &[&[seeds, &[bump]]];

    // CPI Context
    let cpi_ctx = CpiContext::new_with_signer(
    ctx.accounts.token_program.to_account_info(),
    MintTo {
    mint: ctx.accounts.reward_token_mint.to_account_info(),
    to: ctx.accounts.player_token_account.to_account_info(),
    authority: ctx.accounts.reward_token_mint.to_account_info(),
    },
    signer,
    );

    // Mint 1 token, accounting for decimals of mint
    let amount = (1u64)
    .checked_mul(10u64.pow(ctx.accounts.reward_token_mint.decimals as u32))
    .unwrap();

    mint_to(cpi_ctx, amount)?;
    Ok(())
    }

    // Burn Token to health player
    pub fn heal(ctx: Context<Heal>) -> Result<()> {
    ctx.accounts.player_data.health = MAX_HEALTH;

    // CPI Context
    let cpi_ctx = CpiContext::new(
    ctx.accounts.token_program.to_account_info(),
    Burn {
    mint: ctx.accounts.reward_token_mint.to_account_info(),
    from: ctx.accounts.player_token_account.to_account_info(),
    authority: ctx.accounts.player.to_account_info(),
    },
    );

    // Burn 1 token, accounting for decimals of mint
    let amount = (1u64)
    .checked_mul(10u64.pow(ctx.accounts.reward_token_mint.decimals as u32))
    .unwrap();

    burn(cpi_ctx, amount)?;
    Ok(())
    }
    }

    #[derive(Accounts)]
    pub struct CreateMint<'info> {
    #[account(
    mut,
    address = ADMIN_PUBKEY
    )]
    pub admin: Signer<'info>,

    // The PDA is both the address of the mint account and the mint authority
    #[account(
    init,
    seeds = [b"reward"],
    bump,
    payer = admin,
    mint::decimals = 9,
    mint::authority = reward_token_mint,

    )]
    pub reward_token_mint: Account<'info, Mint>,

    ///CHECK: Using "address" constraint to validate metadata account address
    #[account(
    mut,
    address=find_metadata_account(&reward_token_mint.key()).0
    )]
    pub metadata_account: UncheckedAccount<'info>,

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

    #[derive(Accounts)]
    pub struct InitPlayer<'info> {
    #[account(
    init,
    payer = player,
    space = 8 + 8,
    seeds = [b"player".as_ref(), player.key().as_ref()],
    bump,
    )]
    pub player_data: Account<'info, PlayerData>,
    #[account(mut)]
    pub player: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    #[derive(Accounts)]
    pub struct KillEnemy<'info> {
    #[account(mut)]
    pub player: Signer<'info>,

    #[account(
    mut,
    seeds = [b"player".as_ref(), player.key().as_ref()],
    bump,
    )]
    pub player_data: Account<'info, PlayerData>,

    // Initialize player token account if it doesn't exist
    #[account(
    init_if_needed,
    payer = player,
    associated_token::mint = reward_token_mint,
    associated_token::authority = player
    )]
    pub player_token_account: Account<'info, TokenAccount>,

    #[account(
    mut,
    seeds = [b"reward"],
    bump,
    )]
    pub reward_token_mint: Account<'info, Mint>,

    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
    }

    #[derive(Accounts)]
    pub struct Heal<'info> {
    #[account(mut)]
    pub player: Signer<'info>,

    #[account(
    mut,
    seeds = [b"player".as_ref(), player.key().as_ref()],
    bump,
    )]
    pub player_data: Account<'info, PlayerData>,

    #[account(
    mut,
    associated_token::mint = reward_token_mint,
    associated_token::authority = player
    )]
    pub player_token_account: Account<'info, TokenAccount>,

    #[account(
    mut,
    seeds = [b"reward"],
    bump,
    )]
    pub reward_token_mint: Account<'info, Mint>,

    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    }

    #[account]
    pub struct PlayerData {
    pub health: u8,
    }

    #[error_code]
    pub enum ErrorCode {
    #[msg("Not enough health")]
    NotEnoughHealth,
    }

    Get Started with the Client

    In this section, we'll walk you through a simple client-side implementation for interacting with the program. To get started, navigate to the client.ts file in Solana Playground, remove the placeholder code, and add the code snippets from the following sections.

    Start by adding the following code for the setup.

    import { Metaplex } from "@metaplex-foundation/js";
    import { getMint, getAssociatedTokenAddressSync } from "@solana/spl-token";

    // metaplex token metadata program ID
    const TOKEN_METADATA_PROGRAM_ID = new web3.PublicKey(
    "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
    );

    // metaplex setup
    const metaplex = Metaplex.make(pg.connection);

    // token metadata
    const metadata = {
    uri: "https://raw.githubusercontent.com/solana-developers/program-examples/new-examples/tokens/tokens/.assets/spl-token.json",
    name: "Solana Gold",
    symbol: "GOLDSOL",
    };

    // reward token mint PDA
    const [rewardTokenMintPda] = anchor.web3.PublicKey.findProgramAddressSync(
    [Buffer.from("reward")],
    pg.PROGRAM_ID
    );

    // player data account PDA
    const [playerPDA] = anchor.web3.PublicKey.findProgramAddressSync(
    [Buffer.from("player"), pg.wallet.publicKey.toBuffer()],
    pg.PROGRAM_ID
    );

    // reward token mint metadata account address
    const rewardTokenMintMetadataPDA = await metaplex
    .nfts()
    .pdas()
    .metadata({ mint: rewardTokenMintPda });

    // player token account address
    const playerTokenAccount = getAssociatedTokenAddressSync(
    rewardTokenMintPda,
    pg.wallet.publicKey
    );

    Next, add the following two helper functions. These functions will be used to confirm transactions and fetch account data.

    async function logTransaction(txHash) {
    const { blockhash, lastValidBlockHeight } =
    await pg.connection.getLatestBlockhash();

    await pg.connection.confirmTransaction({
    blockhash,
    lastValidBlockHeight,
    signature: txHash,
    });

    console.log(`Use 'solana confirm -v ${txHash}' to see the logs`);
    }

    async function fetchAccountData() {
    const [playerBalance, playerData] = await Promise.all([
    pg.connection.getTokenAccountBalance(playerTokenAccount),
    pg.program.account.playerData.fetch(playerPDA),
    ]);

    console.log("Player Token Balance: ", playerBalance.value.uiAmount);
    console.log("Player Health: ", playerData.health);
    }

    Next, invoke the createMint instruction to create a new token mint if it does not already exist.

    let txHash;

    try {
    const mintData = await getMint(pg.connection, rewardTokenMintPda);
    console.log("Mint Already Exists");
    } catch {
    txHash = await pg.program.methods
    .createMint(metadata.uri, metadata.name, metadata.symbol)
    .accounts({
    rewardTokenMint: rewardTokenMintPda,
    metadataAccount: rewardTokenMintMetadataPDA,
    tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
    })
    .rpc();
    await logTransaction(txHash);
    }
    console.log("Token Mint: ", rewardTokenMintPda.toString());

    Next, call the initPlayer instruction to create a new player account if one does not already exist.

    try {
    const playerData = await pg.program.account.playerData.fetch(playerPDA);
    console.log("Player Already Exists");
    console.log("Player Health: ", playerData.health);
    } catch {
    txHash = await pg.program.methods
    .initPlayer()
    .accounts({
    playerData: playerPDA,
    player: pg.wallet.publicKey,
    })
    .rpc();
    await logTransaction(txHash);
    console.log("Player Account Created");
    }

    Next, invoke the killEnemy instruction.

    txHash = await pg.program.methods
    .killEnemy()
    .accounts({
    playerData: playerPDA,
    playerTokenAccount: playerTokenAccount,
    rewardTokenMint: rewardTokenMintPda,
    })
    .rpc();
    await logTransaction(txHash);
    console.log("Enemy Defeated");
    await fetchAccountData();

    Next, invoke the heal instruction.

    txHash = await pg.program.methods
    .heal()
    .accounts({
    playerData: playerPDA,
    playerTokenAccount: playerTokenAccount,
    rewardTokenMint: rewardTokenMintPda,
    })
    .rpc();
    await logTransaction(txHash);
    console.log("Player Healed");
    await fetchAccountData();

    Finally, run the client by clicking the “Run” button in Solana Playground. You can copy the Token Mint address printed to the console and verify on Solana Explorer that the token now has metadata. The output should be similar to the following:

    Running client...
    client.ts:
    Use 'solana confirm -v 3AWnpt2Wy6jQckue4QeKsgDNKhKkhpewPmRtxvJpzxGgvK9XK9KEpTiUzAQ5vSC6CUoUjc6xWZCtrihVrFy8sACC' to see the logs
    Token Mint: 3eS7hdyeVX5g8JGhn3Z7qFXJaewoJ8hzgvubovQsPm4S
    Use 'solana confirm -v 63jbBr5U4LG75TiiHfz65q7yKJfHDhGP2ocCiDat5M2k4cWtUMAx9sHvxhnEguLDKXMbDUQKUt1nhvyQkXoDhxst' to see the logs
    Player Account Created
    Use 'solana confirm -v 2ziK41WLoxfEHvtUgc5c1SyKCAr5FvAS54ARBJrjqh9GDwzYqu7qWCwHJCgMZyFEVovYK5nUZhDRHPTMrTjq1Mm6' to see the logs
    Enemy Defeated
    Player Token Balance: 1
    Player Health: 90
    Use 'solana confirm -v 2QoAH22Q3xXz9t2TYRycQMqpEmauaRvmUfZ7ZNKUEoUyHWqpjW972VD3eZyeJrXsviaiCC3g6TE54oKmKbFQf2Q7' to see the logs
    Player Healed
    Player Token Balance: 0
    Player Health: 100
    - - +

    Using tokens in games on Solana

    Tokens on Solana can serve various purposes, such as in-game rewards, incentives, or other applications. For example, you can create tokens and distribute them to players when they complete specific in-game actions.

    Create, Mint, and Burn Tokens with Anchor

    In this tutorial, we will build a game using Anchor to introduce the basics of interacting with the Token Program on Solana. The game will be structured around four main actions: creating a new token mint, initializing player accounts, rewarding players for defeating enemies, and allowing players to heal by burning tokens.

    The program consists of 4 instructions:

    • create_mint - This instruction creates a new token mint with a Program Derived Address (PDA) as the mint authority and creates the metadata account for the mint. We will add a constraint that allows only an "admin" to invoke this instruction
    • init_player - This instruction initializes a new player account with a starting health of 100
    • kill_enemy - This instruction deducts 10 health points from the player account upon “defeating an enemy” and mints 1 token as a reward for the player
    • heal - This instruction allows a player to burn 1 token to restore their health back to 100

    For a high-level overview of the relationship among user wallets, token mints, token accounts, and token metadata accounts, consider exploring this portion of the Metaplex documentation.

    Getting Started

    To start building the program, follow these steps:

    Visit the Solana Playground and create a new Anchor project. If you're new to Solana Playground, you'll also need to create a Playground Wallet.

    After creating a new project, replace the default starter code with the code below:

    use anchor_lang::prelude::*;
    use anchor_spl::{
    associated_token::AssociatedToken,
    metadata::{create_metadata_accounts_v3, CreateMetadataAccountsV3, Metadata},
    token::{burn, mint_to, Burn, Mint, MintTo, Token, TokenAccount},
    };
    use mpl_token_metadata::{pda::find_metadata_account, state::DataV2};
    use solana_program::{pubkey, pubkey::Pubkey};

    declare_id!("11111111111111111111111111111111");

    #[program]
    pub mod anchor_token {
    use super::*;
    }

    Here we are simply bringing into scope the crates and corresponding modules we will be using for this program. We’ll be using the anchor_spl and mpl_token_metadata crates to help us interact with the Token program and the Token Metadata program.

    Create Mint instruction

    First, let’s implement an instruction to create a new token mint and its metadata account. The on-chain token metadata, including the name, symbol, and URI, will be provided as parameters to the instruction.

    Additionally, we'll only allow an "admin" to invoke this instruction by defining an ADMIN_PUBKEY constant and using it as a constraint. Be sure to replace the ADMIN_PUBKEY with your Solana Playground wallet public key.

    The create_mint instruction requires the following accounts:

    • admin - the ADMIN_PUBKEY that signs the transaction and pays for the initialization of the accounts
    • reward_token_mint - the new token mint we are initializing, using a PDA as both the mint account’s address and its mint authority
    • metadata_account - the metadata account we are initializing for the token mint
    • token_program - required for interacting with instructions on the Token program
    • token_metadata_program - required account for interacting with instructions on the Token Metadata program
    • system_program- a required account when creating a new account
    • rent - Sysvar Rent, a required account when creating the metadata account
    // Only this public key can call this instruction
    const ADMIN_PUBKEY: Pubkey = pubkey!("REPLACE_WITH_YOUR_WALLET_PUBKEY");

    #[program]
    pub mod anchor_token {
    use super::*;

    // Create new token mint with PDA as mint authority
    pub fn create_mint(
    ctx: Context<CreateMint>,
    uri: String,
    name: String,
    symbol: String,
    ) -> Result<()> {
    // PDA seeds and bump to "sign" for CPI
    let seeds = b"reward";
    let bump = *ctx.bumps.get("reward_token_mint").unwrap();
    let signer: &[&[&[u8]]] = &[&[seeds, &[bump]]];

    // On-chain token metadata for the mint
    let data_v2 = DataV2 {
    name: name,
    symbol: symbol,
    uri: uri,
    seller_fee_basis_points: 0,
    creators: None,
    collection: None,
    uses: None,
    };

    // CPI Context
    let cpi_ctx = CpiContext::new_with_signer(
    ctx.accounts.token_metadata_program.to_account_info(),
    CreateMetadataAccountsV3 {
    metadata: ctx.accounts.metadata_account.to_account_info(), // the metadata account being created
    mint: ctx.accounts.reward_token_mint.to_account_info(), // the mint account of the metadata account
    mint_authority: ctx.accounts.reward_token_mint.to_account_info(), // the mint authority of the mint account
    update_authority: ctx.accounts.reward_token_mint.to_account_info(), // the update authority of the metadata account
    payer: ctx.accounts.admin.to_account_info(), // the payer for creating the metadata account
    system_program: ctx.accounts.system_program.to_account_info(), // the system program account
    rent: ctx.accounts.rent.to_account_info(), // the rent sysvar account
    },
    signer,
    );

    create_metadata_accounts_v3(
    cpi_ctx, // cpi context
    data_v2, // token metadata
    true, // is_mutable
    true, // update_authority_is_signer
    None, // collection details
    )?;

    Ok(())
    }
    }

    #[derive(Accounts)]
    pub struct CreateMint<'info> {
    #[account(
    mut,
    address = ADMIN_PUBKEY
    )]
    pub admin: Signer<'info>,

    // The PDA is both the address of the mint account and the mint authority
    #[account(
    init,
    seeds = [b"reward"],
    bump,
    payer = admin,
    mint::decimals = 9,
    mint::authority = reward_token_mint,

    )]
    pub reward_token_mint: Account<'info, Mint>,

    ///CHECK: Using "address" constraint to validate metadata account address
    #[account(
    mut,
    address=find_metadata_account(&reward_token_mint.key()).0
    )]
    pub metadata_account: UncheckedAccount<'info>,

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

    The create_mint instruction creates a new token mint, using a Program Derived Address (PDA) as both the address of the token mint and its mint authority. The instruction takes a URI (off-chain metadata), name, and symbol as parameters.

    This instruction then creates a metadata account for the token mint through a Cross-Program Invocation (CPI) calling the create_metadata_accounts_v3 instruction from the Token Metadata program.

    The PDA is used to "sign" the CPI since it is the mint authority, which is a required signer when creating the metadata account for a mint. The instruction data (URI, name, symbol) is included in the DataV2 struct to specify the new token mint's metadata.

    We also verify that the address of the admin account signing the transaction matches the value of the ADMIN_PUBKEY constant to ensure only the intended wallet can invoke this instruction.

    const ADMIN_PUBKEY: Pubkey = pubkey!("REPLACE_WITH_YOUR_WALLET_PUBKEY");

    Init Player Instruction

    Next, let's implement the init_player instruction which creates a new player account with an initial health of 100. The constant MAX_HEALTH is set to 100 to represent the starting health.

    The init_player instruction requires the following accounts:

    • player_data - the new player account we are initializing, which will store the player's health
    • player - the user who signs the transaction and pays for the initialization of the account
    • system_program - a required account when creating a new account
    // Player max health
    const MAX_HEALTH: u8 = 100;

    #[program]
    pub mod anchor_token {
    use super::*;
    ...

    // Create new player account
    pub fn init_player(ctx: Context<InitPlayer>) -> Result<()> {
    ctx.accounts.player_data.health = MAX_HEALTH;
    Ok(())
    }
    }
    ...

    #[derive(Accounts)]
    pub struct InitPlayer<'info> {
    #[account(
    init,
    payer = player,
    space = 8 + 8,
    seeds = [b"player".as_ref(), player.key().as_ref()],
    bump,
    )]
    pub player_data: Account<'info, PlayerData>,
    #[account(mut)]
    pub player: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    #[account]
    pub struct PlayerData {
    pub health: u8,
    }

    The player_data account is initialized using a Program Derived Address (PDA) with the player public key as one of the seeds. This ensures that each player_data account is unique and associated with the player, allowing every player to create their own player_data account.

    Kill Enemy Instruction

    Next, let's implement the kill_enemy instruction which reduces the player's health by 10 and mints 1 token to the player's token account as a reward.

    The kill_enemy instruction requires the following accounts:

    • player - the player receiving the token
    • player_data - the player data account storing the player’s current health
    • player_token_account - the player's associated token account where tokens will be minted
    • reward_token_mint - the token mint account, specifying the type of token that will be minted
    • token_program - required for interacting with instructions on the token program
    • associated_token_program - required when working with associated token accounts
    • system_program - a required account when creating a new account
    #[program]
    pub mod anchor_token {
    use super::*;
    ...

    // Mint token to player token account
    pub fn kill_enemy(ctx: Context<KillEnemy>) -> Result<()> {
    // Check if player has enough health
    if ctx.accounts.player_data.health == 0 {
    return err!(ErrorCode::NotEnoughHealth);
    }
    // Subtract 10 health from player
    ctx.accounts.player_data.health = ctx.accounts.player_data.health.checked_sub(10).unwrap();

    // PDA seeds and bump to "sign" for CPI
    let seeds = b"reward";
    let bump = *ctx.bumps.get("reward_token_mint").unwrap();
    let signer: &[&[&[u8]]] = &[&[seeds, &[bump]]];

    // CPI Context
    let cpi_ctx = CpiContext::new_with_signer(
    ctx.accounts.token_program.to_account_info(),
    MintTo {
    mint: ctx.accounts.reward_token_mint.to_account_info(),
    to: ctx.accounts.player_token_account.to_account_info(),
    authority: ctx.accounts.reward_token_mint.to_account_info(),
    },
    signer,
    );

    // Mint 1 token, accounting for decimals of mint
    let amount = (1u64)
    .checked_mul(10u64.pow(ctx.accounts.reward_token_mint.decimals as u32))
    .unwrap();

    mint_to(cpi_ctx, amount)?;
    Ok(())
    }
    }
    ...

    #[derive(Accounts)]
    pub struct KillEnemy<'info> {
    #[account(mut)]
    pub player: Signer<'info>,

    #[account(
    mut,
    seeds = [b"player".as_ref(), player.key().as_ref()],
    bump,
    )]
    pub player_data: Account<'info, PlayerData>,

    // Initialize player token account if it doesn't exist
    #[account(
    init_if_needed,
    payer = player,
    associated_token::mint = reward_token_mint,
    associated_token::authority = player
    )]
    pub player_token_account: Account<'info, TokenAccount>,

    #[account(
    mut,
    seeds = [b"reward"],
    bump,
    )]
    pub reward_token_mint: Account<'info, Mint>,

    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
    }

    #[error_code]
    pub enum ErrorCode {
    #[msg("Not enough health")]
    NotEnoughHealth,
    }

    The player's health is reduced by 10 to represent the “battle with the enemy”. We’ll also check the player's current health and return a custom Anchor error if the player has 0 health.

    The instruction then uses a cross-program invocation (CPI) to call the mint_to instruction from the Token program and mints 1 token of the reward_token_mint to the player_token_account as a reward for killing the enemy.

    Since the mint authority for the token mint is a Program Derived Address (PDA), we can mint tokens directly by calling this instruction without additional signers. The program can "sign" on behalf of the PDA, allowing token minting without explicitly requiring extra signers.

    Heal Instruction

    Next, let's implement the heal instruction which allows a player to burn 1 token and restore their health to its maximum value.

    The heal instruction requires the following accounts:

    • player - the player executing the healing action
    • player_data - the player data account storing the player’s current health
    • player_token_account - the player's associated token account where the tokens will be burned
    • reward_token_mint - the token mint account, specifying the type of token that will be burned
    • token_program - required for interacting with instructions on the token program
    • associated_token_program - required when working with associated token accounts
    #[program]
    pub mod anchor_token {
    use super::*;
    ...

    // Burn token to health player
    pub fn heal(ctx: Context<Heal>) -> Result<()> {
    ctx.accounts.player_data.health = MAX_HEALTH;

    // CPI Context
    let cpi_ctx = CpiContext::new(
    ctx.accounts.token_program.to_account_info(),
    Burn {
    mint: ctx.accounts.reward_token_mint.to_account_info(),
    from: ctx.accounts.player_token_account.to_account_info(),
    authority: ctx.accounts.player.to_account_info(),
    },
    );

    // Burn 1 token, accounting for decimals of mint
    let amount = (1u64)
    .checked_mul(10u64.pow(ctx.accounts.reward_token_mint.decimals as u32))
    .unwrap();

    burn(cpi_ctx, amount)?;
    Ok(())
    }
    }
    ...

    #[derive(Accounts)]
    pub struct Heal<'info> {
    #[account(mut)]
    pub player: Signer<'info>,

    #[account(
    mut,
    seeds = [b"player".as_ref(), player.key().as_ref()],
    bump,
    )]
    pub player_data: Account<'info, PlayerData>,

    #[account(
    mut,
    associated_token::mint = reward_token_mint,
    associated_token::authority = player
    )]
    pub player_token_account: Account<'info, TokenAccount>,

    #[account(
    mut,
    seeds = [b"reward"],
    bump,
    )]
    pub reward_token_mint: Account<'info, Mint>,

    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    }

    The player's health is restored to its maximum value using the heal instruction. The instruction then uses a cross-program invocation (CPI) to call the burn instruction from the Token program, which burns 1 token from the player_token_account to heal the player.

    Build and Deploy

    Great job! You've now completed the program! Go ahead and build and deploy it using the Solana Playground. Your final program should look like this:

    use anchor_lang::prelude::*;
    use anchor_spl::{
    associated_token::AssociatedToken,
    metadata::{create_metadata_accounts_v3, CreateMetadataAccountsV3, Metadata},
    token::{burn, mint_to, Burn, Mint, MintTo, Token, TokenAccount},
    };
    use mpl_token_metadata::{pda::find_metadata_account, state::DataV2};
    use solana_program::{pubkey, pubkey::Pubkey};

    declare_id!("CCLnXJAJYFjCHLCugpBCEQKrpiSApiRM4UxkBUHJRrv4");

    const ADMIN_PUBKEY: Pubkey = pubkey!("REPLACE_WITH_YOUR_WALLET_PUBKEY");
    const MAX_HEALTH: u8 = 100;

    #[program]
    pub mod anchor_token {
    use super::*;

    // Create new token mint with PDA as mint authority
    pub fn create_mint(
    ctx: Context<CreateMint>,
    uri: String,
    name: String,
    symbol: String,
    ) -> Result<()> {
    // PDA seeds and bump to "sign" for CPI
    let seeds = b"reward";
    let bump = *ctx.bumps.get("reward_token_mint").unwrap();
    let signer: &[&[&[u8]]] = &[&[seeds, &[bump]]];

    // On-chain token metadata for the mint
    let data_v2 = DataV2 {
    name: name,
    symbol: symbol,
    uri: uri,
    seller_fee_basis_points: 0,
    creators: None,
    collection: None,
    uses: None,
    };

    // CPI Context
    let cpi_ctx = CpiContext::new_with_signer(
    ctx.accounts.token_metadata_program.to_account_info(),
    CreateMetadataAccountsV3 {
    metadata: ctx.accounts.metadata_account.to_account_info(), // the metadata account being created
    mint: ctx.accounts.reward_token_mint.to_account_info(), // the mint account of the metadata account
    mint_authority: ctx.accounts.reward_token_mint.to_account_info(), // the mint authority of the mint account
    update_authority: ctx.accounts.reward_token_mint.to_account_info(), // the update authority of the metadata account
    payer: ctx.accounts.admin.to_account_info(), // the payer for creating the metadata account
    system_program: ctx.accounts.system_program.to_account_info(), // the system program account
    rent: ctx.accounts.rent.to_account_info(), // the rent sysvar account
    },
    signer,
    );

    create_metadata_accounts_v3(
    cpi_ctx, // cpi context
    data_v2, // token metadata
    true, // is_mutable
    true, // update_authority_is_signer
    None, // collection details
    )?;

    Ok(())
    }

    // Create new player account
    pub fn init_player(ctx: Context<InitPlayer>) -> Result<()> {
    ctx.accounts.player_data.health = MAX_HEALTH;
    Ok(())
    }

    // Mint tokens to player token account
    pub fn kill_enemy(ctx: Context<KillEnemy>) -> Result<()> {
    // Check if player has enough health
    if ctx.accounts.player_data.health == 0 {
    return err!(ErrorCode::NotEnoughHealth);
    }
    // Subtract 10 health from player
    ctx.accounts.player_data.health = ctx.accounts.player_data.health.checked_sub(10).unwrap();

    // PDA seeds and bump to "sign" for CPI
    let seeds = b"reward";
    let bump = *ctx.bumps.get("reward_token_mint").unwrap();
    let signer: &[&[&[u8]]] = &[&[seeds, &[bump]]];

    // CPI Context
    let cpi_ctx = CpiContext::new_with_signer(
    ctx.accounts.token_program.to_account_info(),
    MintTo {
    mint: ctx.accounts.reward_token_mint.to_account_info(),
    to: ctx.accounts.player_token_account.to_account_info(),
    authority: ctx.accounts.reward_token_mint.to_account_info(),
    },
    signer,
    );

    // Mint 1 token, accounting for decimals of mint
    let amount = (1u64)
    .checked_mul(10u64.pow(ctx.accounts.reward_token_mint.decimals as u32))
    .unwrap();

    mint_to(cpi_ctx, amount)?;
    Ok(())
    }

    // Burn Token to health player
    pub fn heal(ctx: Context<Heal>) -> Result<()> {
    ctx.accounts.player_data.health = MAX_HEALTH;

    // CPI Context
    let cpi_ctx = CpiContext::new(
    ctx.accounts.token_program.to_account_info(),
    Burn {
    mint: ctx.accounts.reward_token_mint.to_account_info(),
    from: ctx.accounts.player_token_account.to_account_info(),
    authority: ctx.accounts.player.to_account_info(),
    },
    );

    // Burn 1 token, accounting for decimals of mint
    let amount = (1u64)
    .checked_mul(10u64.pow(ctx.accounts.reward_token_mint.decimals as u32))
    .unwrap();

    burn(cpi_ctx, amount)?;
    Ok(())
    }
    }

    #[derive(Accounts)]
    pub struct CreateMint<'info> {
    #[account(
    mut,
    address = ADMIN_PUBKEY
    )]
    pub admin: Signer<'info>,

    // The PDA is both the address of the mint account and the mint authority
    #[account(
    init,
    seeds = [b"reward"],
    bump,
    payer = admin,
    mint::decimals = 9,
    mint::authority = reward_token_mint,

    )]
    pub reward_token_mint: Account<'info, Mint>,

    ///CHECK: Using "address" constraint to validate metadata account address
    #[account(
    mut,
    address=find_metadata_account(&reward_token_mint.key()).0
    )]
    pub metadata_account: UncheckedAccount<'info>,

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

    #[derive(Accounts)]
    pub struct InitPlayer<'info> {
    #[account(
    init,
    payer = player,
    space = 8 + 8,
    seeds = [b"player".as_ref(), player.key().as_ref()],
    bump,
    )]
    pub player_data: Account<'info, PlayerData>,
    #[account(mut)]
    pub player: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    #[derive(Accounts)]
    pub struct KillEnemy<'info> {
    #[account(mut)]
    pub player: Signer<'info>,

    #[account(
    mut,
    seeds = [b"player".as_ref(), player.key().as_ref()],
    bump,
    )]
    pub player_data: Account<'info, PlayerData>,

    // Initialize player token account if it doesn't exist
    #[account(
    init_if_needed,
    payer = player,
    associated_token::mint = reward_token_mint,
    associated_token::authority = player
    )]
    pub player_token_account: Account<'info, TokenAccount>,

    #[account(
    mut,
    seeds = [b"reward"],
    bump,
    )]
    pub reward_token_mint: Account<'info, Mint>,

    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
    }

    #[derive(Accounts)]
    pub struct Heal<'info> {
    #[account(mut)]
    pub player: Signer<'info>,

    #[account(
    mut,
    seeds = [b"player".as_ref(), player.key().as_ref()],
    bump,
    )]
    pub player_data: Account<'info, PlayerData>,

    #[account(
    mut,
    associated_token::mint = reward_token_mint,
    associated_token::authority = player
    )]
    pub player_token_account: Account<'info, TokenAccount>,

    #[account(
    mut,
    seeds = [b"reward"],
    bump,
    )]
    pub reward_token_mint: Account<'info, Mint>,

    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    }

    #[account]
    pub struct PlayerData {
    pub health: u8,
    }

    #[error_code]
    pub enum ErrorCode {
    #[msg("Not enough health")]
    NotEnoughHealth,
    }

    Get Started with the Client

    In this section, we'll walk you through a simple client-side implementation for interacting with the program. To get started, navigate to the client.ts file in Solana Playground, remove the placeholder code, and add the code snippets from the following sections.

    Start by adding the following code for the setup.

    import { Metaplex } from "@metaplex-foundation/js";
    import { getMint, getAssociatedTokenAddressSync } from "@solana/spl-token";

    // metaplex token metadata program ID
    const TOKEN_METADATA_PROGRAM_ID = new web3.PublicKey(
    "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
    );

    // metaplex setup
    const metaplex = Metaplex.make(pg.connection);

    // token metadata
    const metadata = {
    uri: "https://raw.githubusercontent.com/solana-developers/program-examples/new-examples/tokens/tokens/.assets/spl-token.json",
    name: "Solana Gold",
    symbol: "GOLDSOL",
    };

    // reward token mint PDA
    const [rewardTokenMintPda] = anchor.web3.PublicKey.findProgramAddressSync(
    [Buffer.from("reward")],
    pg.PROGRAM_ID
    );

    // player data account PDA
    const [playerPDA] = anchor.web3.PublicKey.findProgramAddressSync(
    [Buffer.from("player"), pg.wallet.publicKey.toBuffer()],
    pg.PROGRAM_ID
    );

    // reward token mint metadata account address
    const rewardTokenMintMetadataPDA = await metaplex
    .nfts()
    .pdas()
    .metadata({ mint: rewardTokenMintPda });

    // player token account address
    const playerTokenAccount = getAssociatedTokenAddressSync(
    rewardTokenMintPda,
    pg.wallet.publicKey
    );

    Next, add the following two helper functions. These functions will be used to confirm transactions and fetch account data.

    async function logTransaction(txHash) {
    const { blockhash, lastValidBlockHeight } =
    await pg.connection.getLatestBlockhash();

    await pg.connection.confirmTransaction({
    blockhash,
    lastValidBlockHeight,
    signature: txHash,
    });

    console.log(`Use 'solana confirm -v ${txHash}' to see the logs`);
    }

    async function fetchAccountData() {
    const [playerBalance, playerData] = await Promise.all([
    pg.connection.getTokenAccountBalance(playerTokenAccount),
    pg.program.account.playerData.fetch(playerPDA),
    ]);

    console.log("Player Token Balance: ", playerBalance.value.uiAmount);
    console.log("Player Health: ", playerData.health);
    }

    Next, invoke the createMint instruction to create a new token mint if it does not already exist.

    let txHash;

    try {
    const mintData = await getMint(pg.connection, rewardTokenMintPda);
    console.log("Mint Already Exists");
    } catch {
    txHash = await pg.program.methods
    .createMint(metadata.uri, metadata.name, metadata.symbol)
    .accounts({
    rewardTokenMint: rewardTokenMintPda,
    metadataAccount: rewardTokenMintMetadataPDA,
    tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
    })
    .rpc();
    await logTransaction(txHash);
    }
    console.log("Token Mint: ", rewardTokenMintPda.toString());

    Next, call the initPlayer instruction to create a new player account if one does not already exist.

    try {
    const playerData = await pg.program.account.playerData.fetch(playerPDA);
    console.log("Player Already Exists");
    console.log("Player Health: ", playerData.health);
    } catch {
    txHash = await pg.program.methods
    .initPlayer()
    .accounts({
    playerData: playerPDA,
    player: pg.wallet.publicKey,
    })
    .rpc();
    await logTransaction(txHash);
    console.log("Player Account Created");
    }

    Next, invoke the killEnemy instruction.

    txHash = await pg.program.methods
    .killEnemy()
    .accounts({
    playerData: playerPDA,
    playerTokenAccount: playerTokenAccount,
    rewardTokenMint: rewardTokenMintPda,
    })
    .rpc();
    await logTransaction(txHash);
    console.log("Enemy Defeated");
    await fetchAccountData();

    Next, invoke the heal instruction.

    txHash = await pg.program.methods
    .heal()
    .accounts({
    playerData: playerPDA,
    playerTokenAccount: playerTokenAccount,
    rewardTokenMint: rewardTokenMintPda,
    })
    .rpc();
    await logTransaction(txHash);
    console.log("Player Healed");
    await fetchAccountData();

    Finally, run the client by clicking the “Run” button in Solana Playground. You can copy the Token Mint address printed to the console and verify on Solana Explorer that the token now has metadata. The output should be similar to the following:

    Running client...
    client.ts:
    Use 'solana confirm -v 3AWnpt2Wy6jQckue4QeKsgDNKhKkhpewPmRtxvJpzxGgvK9XK9KEpTiUzAQ5vSC6CUoUjc6xWZCtrihVrFy8sACC' to see the logs
    Token Mint: 3eS7hdyeVX5g8JGhn3Z7qFXJaewoJ8hzgvubovQsPm4S
    Use 'solana confirm -v 63jbBr5U4LG75TiiHfz65q7yKJfHDhGP2ocCiDat5M2k4cWtUMAx9sHvxhnEguLDKXMbDUQKUt1nhvyQkXoDhxst' to see the logs
    Player Account Created
    Use 'solana confirm -v 2ziK41WLoxfEHvtUgc5c1SyKCAr5FvAS54ARBJrjqh9GDwzYqu7qWCwHJCgMZyFEVovYK5nUZhDRHPTMrTjq1Mm6' to see the logs
    Enemy Defeated
    Player Token Balance: 1
    Player Health: 90
    Use 'solana confirm -v 2QoAH22Q3xXz9t2TYRycQMqpEmauaRvmUfZ7ZNKUEoUyHWqpjW972VD3eZyeJrXsviaiCC3g6TE54oKmKbFQf2Q7' to see the logs
    Player Healed
    Player Token Balance: 0
    Player Health: 100
    + + \ No newline at end of file diff --git a/cookbook-zh/references/gaming/intro/index.html b/cookbook-zh/references/gaming/intro/index.html index 5c2b64f8e..1aeb2a6d6 100644 --- a/cookbook-zh/references/gaming/intro/index.html +++ b/cookbook-zh/references/gaming/intro/index.html @@ -9,13 +9,13 @@ - - + +
    -

    Intro into gaming on Solana

    The gaming space in the Solana ecosystem is expanding rapidly. Integrating with Solana can provide numerous benefits for games, such as enabling players to own and trade their assets via NFTs in games, building a real in-game economy, creating composable game programs, and allowing players to compete for valuable assets.

    Solana is purpose-built for games, with its 400ms block time and lightning-fast confirmations making it a real-time database that's free for all. It's perfect for genres like strategy games, city builders, turn-based games, and more.

    However, not everything needs to be put on the blockchain. Smaller integrations using NFTs that represent game items, for example, can be easily done. Transaction fees are extremely cheap, and there are many tools and SDKs available to start building today. You can build your game in Javascript and Canvas, Flutter, or use one of the Solana Game SDKs for the two biggest game engines - UnitySDK, UnrealSDK, and more Game SDKs.

    There are several ways to integrate Solana into your game:

    1. Give players digital collectibles for in-game items or use them as characters. Check out Nfts in games
    2. Use tokens for in-app purchases or micro-payments in the game. See use tokens
    3. Use the player's wallet to authenticate them in the game. Sign message
    4. Run tournaments and pay out crypto rewards to your players.
    5. Develop the game entirely on-chain to reward your players in every step they take. Start with Hello world

    With all these benefits, Solana is quickly becoming the go-to platform for game developers. Get started today!

    - - +

    Intro into gaming on Solana

    The gaming space in the Solana ecosystem is expanding rapidly. Integrating with Solana can provide numerous benefits for games, such as enabling players to own and trade their assets via NFTs in games, building a real in-game economy, creating composable game programs, and allowing players to compete for valuable assets.

    Solana is purpose-built for games, with its 400ms block time and lightning-fast confirmations making it a real-time database that's free for all. It's perfect for genres like strategy games, city builders, turn-based games, and more.

    However, not everything needs to be put on the blockchain. Smaller integrations using NFTs that represent game items, for example, can be easily done. Transaction fees are extremely cheap, and there are many tools and SDKs available to start building today. You can build your game in Javascript and Canvas, Flutter, or use one of the Solana Game SDKs for the two biggest game engines - UnitySDK, UnrealSDK, and more Game SDKs.

    There are several ways to integrate Solana into your game:

    1. Give players digital collectibles for in-game items or use them as characters. Check out Nfts in games
    2. Use tokens for in-app purchases or micro-payments in the game. See use tokens
    3. Use the player's wallet to authenticate them in the game. Sign message
    4. Run tournaments and pay out crypto rewards to your players.
    5. Develop the game entirely on-chain to reward your players in every step they take. Start with Hello world

    With all these benefits, Solana is quickly becoming the go-to platform for game developers. Get started today!

    + + \ No newline at end of file diff --git a/cookbook-zh/references/gaming/nfts-in-games/index.html b/cookbook-zh/references/gaming/nfts-in-games/index.html index 88a5c70c8..d75f3b8b7 100644 --- a/cookbook-zh/references/gaming/nfts-in-games/index.html +++ b/cookbook-zh/references/gaming/nfts-in-games/index.html @@ -9,8 +9,8 @@ - - + +
    @@ -22,8 +22,8 @@ In js using the Metaplex sdk this would look like this:

    JSON.parse(
    // For example '.config/solana/devnet.json'
    fs.readFileSync("yourKeyPair.json").toString())
    );
    let keyPair = Keypair.fromSecretKey(decodedKey);

    const metaplex = Metaplex.make(connection).use(keypairIdentity(keyPair));

    const nfts = await metaplex
    .nfts()
    .findAllByOwner({ owner: wallet.publicKey })

    let collectionNfts = []
    for (let i = 0; i < nfts.length; i++) {
    if (nfts[i].collection?.address.toString() == collectionAddress.toString()) {
    collectionNfts.push(nfts[i])
    }
    }

    Bonus Effects with NFTs

    In addition to providing new revenue streams, NFTs can also be used to provide in-game benefits and bonuses to players. For instance, a player who owns a "coin doubler" NFT may receive double the amount of coins for as long as they hold the NFT in their wallet. Additionally, NFTs can be used as consumables, allowing players to use them to gain temporary effects such as potions or spells. Once consumed, the NFT is burned, and the effect is applied to the player's character. These innovative features of NFTs provide game developers with new opportunities to create unique gameplay experiences and reward players for their ownership of valuable assets on the Solana blockchain.

    How to interact with tokens

    NFTs

    Using NFT Metadata for Player Stats

    NFTs also have Metadata, which can be used for all kind of traits for game objects. For example an NFT could represent a game character and his traits Strength/Intelligence/Agility could directly influence how strong the character is in the game. You can load NFT metadata and their attributes using the Metaplex SDK:

    import { Metaplex, keypairIdentity } from "@metaplex-foundation/js";

    JSON.parse(
    // For example '.config/solana/devnet.json'
    fs.readFileSync("yourKeyPair.json").toString())
    );
    let keyPair = Keypair.fromSecretKey(decodedKey);

    const metaplex = Metaplex.make(connection).use(keypairIdentity(keyPair));
    const nfts = await metaplex.nfts().findAllByOwner({owner: keyPair.publicKey});

    const physicalDamage = 5;
    const magicalDamage = 5;

    nfts.forEach(async nft => {
    const metaData = await metaplex.nfts().load({metadata: nft});

    metaData.json.attributes.forEach(async attribute => {
    if (attribute.trait_type == "Strength") {
    physicalDamage += parseInt(attribute.value)
    }
    if (attribute.trait_type == "Int") {
    magicalDamage += parseInt(attribute.value)
    }
    });
    })

    console.log("Player Physical Damage: " + physicalDamage)
    console.log("Player Magical Damage: " + magicalDamage)

    Fusing NFTs Together

    The Metaplex Fusion Trifle program allows you to have NFTs own other NFTs. For example you could create a plant plot NFT and then use to combine it with a water NFT and a seed NFT to create a Tomato NFT.

    Use 3D Nfts in a game

    Every NFT metadata can also have a animation url. This url can contain a video, gif or a 3d file. These 3d files usually use the format .glb or .gltf and can dynamically be loaded into a game. For unity you can use the GLTFast package and in js the -GLTFast JS. For reference a NFT metadata with glb model

      var gltf = gameObject.AddComponent<GLTFast.GltfAsset>();
    gltf.url = nft.metadata.animationUrl;
    npm install --save-dev gltf-loader-ts

    import { GltfLoader } from 'gltf-loader-ts';

    let loader = new GltfLoader();
    let uri = 'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/BoxTextured/glTF/BoxTextured.gltf';
    let asset: Asset = await loader.load(uri);
    let gltf: GlTf = asset.gltf;
    console.log(gltf);
    // -> {asset: {…}, scene: 0, scenes: Array(1), nodes: Array(2), meshes: Array(1), …}

    let data = await asset.accessorData(0); // fetches BoxTextured0.bin
    let image: Image = await asset.imageData.get(0) // fetches CesiumLogoFlat.png

    Customize NFTs with items and traits (Raindrops boots)

    With the Raindrops Boots program you can have an adventure character which owns a sword and a helmet. When the Character NFT would be sold on a market place the other NFTs it owns would be sold as well.

    How to create an NFT collection

    NFTs on Solana mostly follow the Metaplex standard. Metaplex is a company which takes care of the NFT most used standard on Solana. The most common way to create an NFT collection is to create a metaplex candy machine which lets the user mint predefined pairs of metadata and images.

    Metaplex Docs

    Setup a candy machine step by step

    NFTs

    - - +GLTFast JS. For reference a NFT metadata with glb model

      var gltf = gameObject.AddComponent<GLTFast.GltfAsset>();
    gltf.url = nft.metadata.animationUrl;
    npm install --save-dev gltf-loader-ts

    import { GltfLoader } from 'gltf-loader-ts';

    let loader = new GltfLoader();
    let uri = 'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/BoxTextured/glTF/BoxTextured.gltf';
    let asset: Asset = await loader.load(uri);
    let gltf: GlTf = asset.gltf;
    console.log(gltf);
    // -> {asset: {…}, scene: 0, scenes: Array(1), nodes: Array(2), meshes: Array(1), …}

    let data = await asset.accessorData(0); // fetches BoxTextured0.bin
    let image: Image = await asset.imageData.get(0) // fetches CesiumLogoFlat.png

    Customize NFTs with items and traits (Raindrops boots)

    With the Raindrops Boots program you can have an adventure character which owns a sword and a helmet. When the Character NFT would be sold on a market place the other NFTs it owns would be sold as well.

    How to create an NFT collection

    NFTs on Solana mostly follow the Metaplex standard. Metaplex is a company which takes care of the NFT most used standard on Solana. The most common way to create an NFT collection is to create a metaplex candy machine which lets the user mint predefined pairs of metadata and images.

    Metaplex Docs

    Setup a candy machine step by step

    NFTs

    + + \ No newline at end of file diff --git a/cookbook-zh/references/gaming/porting-anchor-to-unity/index.html b/cookbook-zh/references/gaming/porting-anchor-to-unity/index.html index 73757d623..a53b8a29e 100644 --- a/cookbook-zh/references/gaming/porting-anchor-to-unity/index.html +++ b/cookbook-zh/references/gaming/porting-anchor-to-unity/index.html @@ -9,8 +9,8 @@ - - + +
    @@ -18,8 +18,8 @@ With this IDL you can then generate different clients. For example JS or C# to Unity.
    IDL to C# Converter

    These two lines will generate a C# client for the game.

    dotnet tool install Solana.Unity.Anchor.Tool
    dotnet anchorgen -i idl/file.json -o src/ProgramCode.cs

    This will generate you a C# representation of you program, which lets you deserialize the data and easily create instructions to the program.

    Building the Transaction in Unity C#

    Within Unity game engine we can then use the Solana Unity SDK to interact with the program.

    1. First we find the on chain adress of the game data account with TryFindProgramAddress. We need to pass in this account to the transaction so that the Solana runtime knows that we want to change this account.

    2. Next we use the generated client to create a MoveRight instruction.

    3. Then we request a block hash from an RPC node. This is needed so that Solana knows how long the transaction will be valid.

    4. Next we set the fee payer to be the players wallet.

    5. Then we add the move right instruction to the Transaction. We can also add multiple instructions to a singe transaction if needed.

    6. Afterwards the transaction gets signed and then send to the RPC node for processing. -Solana has different Commitment levels. If we set the commitment level to Confirmed we will be able to get the new state already within the next 500ms.

    7. Full C# Source Code

    public async void MoveRight()
    {
    PublicKey.TryFindProgramAddress(new[]
    {
    Encoding.UTF8.GetBytes("level1")
    },
    ProgramId, out gameDataAccount, out var bump);

    MoveRightAccounts account = new MoveRightAccounts();
    account.GameDataAccount = gameDataAccount;
    TransactionInstruction moveRightInstruction = TinyAdventureProgram.MoveRight(account, ProgramId);

    var walletHolderService = ServiceFactory.Resolve<WalletHolderService>();
    var result = await walletHolderService.BaseWallet.ActiveRpcClient.GetRecentBlockHashAsync(Commitment.Confirmed);

    Transaction transaction = new Transaction();
    transaction.FeePayer = walletHolderService.BaseWallet.Account.PublicKey;
    transaction.RecentBlockHash = result.Result.Value.Blockhash;
    transaction.Signatures = new List<SignaturePubKeyPair>();
    transaction.Instructions = new List<TransactionInstruction>();
    transaction.Instructions.Add(moveRightInstruction);

    Transaction signedTransaction = await walletHolderService.BaseWallet.SignTransaction(transaction);

    RequestResult<string> signature = await walletHolderService.BaseWallet.ActiveRpcClient.SendTransactionAsync(
    Convert.ToBase64String(signedTransaction.Serialize()),
    true, Commitment.Confirmed);
    }
    - - +Solana has different Commitment levels. If we set the commitment level to Confirmed we will be able to get the new state already within the next 500ms.

  • Full C# Source Code

  • public async void MoveRight()
    {
    PublicKey.TryFindProgramAddress(new[]
    {
    Encoding.UTF8.GetBytes("level1")
    },
    ProgramId, out gameDataAccount, out var bump);

    MoveRightAccounts account = new MoveRightAccounts();
    account.GameDataAccount = gameDataAccount;
    TransactionInstruction moveRightInstruction = TinyAdventureProgram.MoveRight(account, ProgramId);

    var walletHolderService = ServiceFactory.Resolve<WalletHolderService>();
    var result = await walletHolderService.BaseWallet.ActiveRpcClient.GetRecentBlockHashAsync(Commitment.Confirmed);

    Transaction transaction = new Transaction();
    transaction.FeePayer = walletHolderService.BaseWallet.Account.PublicKey;
    transaction.RecentBlockHash = result.Result.Value.Blockhash;
    transaction.Signatures = new List<SignaturePubKeyPair>();
    transaction.Instructions = new List<TransactionInstruction>();
    transaction.Instructions.Add(moveRightInstruction);

    Transaction signedTransaction = await walletHolderService.BaseWallet.SignTransaction(transaction);

    RequestResult<string> signature = await walletHolderService.BaseWallet.ActiveRpcClient.SendTransactionAsync(
    Convert.ToBase64String(signedTransaction.Serialize()),
    true, Commitment.Confirmed);
    }
    + + \ No newline at end of file diff --git a/cookbook-zh/references/gaming/saving-game-state/index.html b/cookbook-zh/references/gaming/saving-game-state/index.html index 3a5b37130..25dccda96 100644 --- a/cookbook-zh/references/gaming/saving-game-state/index.html +++ b/cookbook-zh/references/gaming/saving-game-state/index.html @@ -9,16 +9,16 @@ - - + +

    How to save game state

    You can use Solana block chain to save the state of your game in program accounts. These are accounts that are owned by your program and they are derived from the program Id and some seeds. These can be thought of as data base entries. We can for example create a PlayerData account and use the players public key as a seed. This means every player can have one player account per wallet. These accounts can be up to 10Kb by default. If you need a bigger account look into Manage big accounts This can be done in a program like this:

    pub fn init_player(ctx: Context<InitPlayer>) -> Result<()> {
    ctx.accounts.player.energy = MAX_ENERGY;
    ctx.accounts.player.health = MAX_HEALTH;
    ctx.accounts.player.last_login = Clock::get()?.unix_timestamp;
    Ok(())
    }

    #[derive(Accounts)]
    pub struct InitPlayer <'info> {
    #[account(
    init,
    payer = signer,
    space = 1000,
    seeds = [b"player".as_ref(), signer.key().as_ref()],
    bump,
    )]
    pub player: Account<'info, PlayerData>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    #[account]
    pub struct PlayerData {
    pub name: String,
    pub level: u8,
    pub xp: u64,
    pub health: u64,
    pub log: u64,
    pub energy: u64,
    pub last_login: i64
    }

    You can then interact with this player data via transaction instructions. Lets say we want the player to get experience for killing a monster for example:

        pub fn kill_enemy(mut ctx: Context<KillEnemy>, enemyId: u8) -> Result<()> {
    let account = &mut ctx.accounts;

    ... handle energy

    if ctx.accounts.player.energy == 0 {
    return err!(ErrorCode::NotEnoughEnergy);
    }

    ... get enemy values by id and calculate battle

    ctx.accounts.player.xp = ctx.accounts.player.xp + 1;
    ctx.accounts.player.energy = ctx.accounts.player.energy - 1;

    ... handle level up

    msg!("You killed enemy and got 1 xp. You have {} xp and {} energy left.", ctx.accounts.player.xp, ctx.accounts.player.energy);
    Ok(())
    }

    This is how this would look like from a js client:

    const wallet = useAnchorWallet();
    const provider = new AnchorProvider(connection, wallet, {});
    setProvider(provider);
    const program = new Program(IDL, PROGRAM_ID, provider);

    const [pda] = PublicKey.findProgramAddressSync(
    [Buffer.from("player", "utf8"),
    publicKey.toBuffer()],
    new PublicKey(PROGRAM_ID)
    );

    try {
    const transaction = program.methods
    .initPlayer()
    .accounts({
    player: pda,
    signer: publicKey,
    systemProgram: SystemProgram.programId,
    })
    .transaction();

    const tx = await transaction;
    const txSig = await sendTransaction(tx, connection);
    await connection.confirmTransaction(txSig, "confirmed");

    How to actually build this energy system you can learn here: -Building an Energy system

    - - +Building an Energy system

    + + \ No newline at end of file diff --git a/cookbook-zh/references/gaming/store-sol-in-pda/index.html b/cookbook-zh/references/gaming/store-sol-in-pda/index.html index 2ed80f289..1050cfc90 100644 --- a/cookbook-zh/references/gaming/store-sol-in-pda/index.html +++ b/cookbook-zh/references/gaming/store-sol-in-pda/index.html @@ -9,15 +9,15 @@ - - + +

    Storing SOL in PDAs for Game Rewards

    Video Walkthrough:

    Live Version. (use devnet) -

    Tiny Adventure Anchor Program - Part Two

    In this tutorial, we will rebuild the Tiny Adventure game and introduce a chest with a reward of 0.1 SOL. The chest will "spawn" at a specific position, and when the player reaches that position, they will receive the reward. The goal of this program is to demonstrate how to store SOL within a program account and distribute it to players.

    The Tiny Adventure Two Program consists of 3 instructions:

    • initialize_level_one - This instruction initializes two on-chain accounts: one for recording the player's position and another for holding the SOL reward that represents the “reward chest”.
    • reset_level_and_spawn_chest - This instruction resets the player's position to zero and "respawns" a reward chest by transferring SOL from the user invoking the instruction to the reward chest account.
    • move_right - This instruction allows the player to move their position to the right and collect the SOL in the reward chest once they reach a specific position.

    In the following sections, we will guide you through building the program step by step. You can find the complete source code, which can be deployed directly from your browser using the Solana Playground, at this link: Open In Playground.

    Getting Started

    To start building the Tiny Adventure game, follow these steps:

    Visit the Solana Playground and create a new Anchor project. If you're new to Solana Playground, you'll also need to create a Playground Wallet.

    After creating a new project, replace the default starter code with the code below:

    use anchor_lang::prelude::*;
    use anchor_lang::solana_program::native_token::LAMPORTS_PER_SOL;
    use anchor_lang::system_program;

    declare_id!("11111111111111111111111111111111");

    #[program]
    mod tiny_adventure_two {
    use super::*;
    }

    fn print_player(player_position: u8) {
    if player_position == 0 {
    msg!("A Journey Begins!");
    msg!("o.........💎");
    } else if player_position == 1 {
    msg!("..o.......💎");
    } else if player_position == 2 {
    msg!("....o.....💎");
    } else if player_position == 3 {
    msg!("........\\o/💎");
    msg!("..........\\o/");
    msg!("You have reached the end! Super!");
    }
    }

    In this game, the player starts at position 0 and can only move right. To visualize the player's progress throughout the game, we'll use message logs to represent their journey towards the reward chest!

    Defining the Chest Vault Account

    Add the CHEST_REWARD constant at the beginning of the program. The CHEST_REWARD represents the amount of lamports that will be put into the chest and given out as rewards. Lamports are the smallest fractions of a SOL, with 1 billion lamports being equal to 1 SOL.

    To store the SOL reward, we will define a new ChestVaultAccount struct. This is an empty struct because we will be directly updating the lamports in the account. The account will hold the SOL reward and does not need to store any additional data.

    use anchor_lang::prelude::*;
    use anchor_lang::solana_program::native_token::LAMPORTS_PER_SOL;
    use anchor_lang::system_program;

    declare_id!("11111111111111111111111111111111");

    #[program]
    mod tiny_adventure_two {
    use super::*;

    // The amount of lamports that will be put into chests and given out as rewards.
    const CHEST_REWARD: u64 = LAMPORTS_PER_SOL / 10; // 0.1 SOL
    }

    ...

    // Define the Chest Vault Account structure
    #[account]
    pub struct ChestVaultAccount {}

    Defining the Game Data Account

    To keep track of the player's position within the game, we need to define a structure for the on-chain account that will store the player's position.

    The GameDataAccount struct contains a single field, player_position, which stores the player's current position as an unsigned 8-bit integer.


    use anchor_lang::prelude::*;
    use anchor_lang::solana_program::native_token::LAMPORTS_PER_SOL;
    use anchor_lang::system_program;

    declare_id!("11111111111111111111111111111111");

    #[program]
    mod tiny_adventure_two {
    use super::*;
    ...

    }

    ...

    // Define the Game Data Account structure
    #[account]
    pub struct GameDataAccount {
    player_position: u8,
    }

    With the GameDataAccount struct defined, you can now use it to store and update the player's position as they interact with the game. As the player moves right and progresses through the game, their position will be updated within the GameDataAccount, allowing you to track their progress towards the chest containing the SOL reward.

    Initialize Level One Instruction

    With the GameDataAccount and ChestVaultAccount defined, let's implement the initialize_level_one instruction. This instruction initializes both the GameDataAccount and ChestVaultAccount, sets the player's position to 0, and displays the starting message.

    The initialize_level_one instruction requires 4 accounts:

    • new_game_data_account - the GameDataAccount we are initializing to store the player’s position
    • chest_vault - the ChestVaultAccount we are initializing to store the SOL reward
    • signer - the player paying for the initialization of the accounts
    • system_program - a required account when creating a new account
    #[program]
    pub mod tiny_adventure_two {
    use super::*;

    pub fn initialize_level_one(_ctx: Context<InitializeLevelOne>) -> Result<()> {
    msg!("A Journey Begins!");
    msg!("o.......💎");
    Ok(())
    }

    ...
    }

    // Specify the accounts required by the initialize_level_one instruction
    #[derive(Accounts)]
    pub struct InitializeLevelOne<'info> {
    #[account(
    init_if_needed,
    seeds = [b"level1"],
    bump,
    payer = signer,
    space = 8 + 1
    )]
    pub new_game_data_account: Account<'info, GameDataAccount>,
    #[account(
    init_if_needed,
    seeds = [b"chestVault"],
    bump,
    payer = signer,
    space = 8
    )]
    pub chest_vault: Account<'info, ChestVaultAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    ...

    Both the GameDataAccount and ChestVaultAccount are created using a Program Derived Address (PDA) as the address of the account, allowing us to deterministically locate the address later. The init_if_needed constraint ensures that the accounts are only initialized if they don't already exist. Since the PDAs for both accounts in this instruction use a single fixed seed, our program can only create one of each type of account. In effect, the instruction would only ever need to be invoked one time.

    It's worth noting that the current implementation does not have any restrictions on who can modify the GameDataAccount, effectively turning the game into a multiplayer experience where everyone can control the player's movement.

    Alternatively, you can use the signer's address as an extra seed in the initialize instruction, allowing each player to create their own GameDataAccount.

    Reset Level and Spawn Chest Instruction

    Next, let's implement the reset_level_and_spawn_chest instruction, which resets the player's position to the start and fills up the chest with a reward of 0.1 SOL.

    The reset_level_and_spawn_chest instruction requires 4 accounts:

    • new_game_data_account - the GameDataAccount storing the player's position
    • chest_vault - the ChestVaultAccount storing the SOL reward
    • signer - the player providing the SOL reward for the chest
    • system_program - the program we'll be invoking to transfer SOL using a cross-program invocation (CPI), more on this shortly
    #[program]
    pub mod tiny_adventure_two {
    use super::*;
    ...

    pub fn reset_level_and_spawn_chest(ctx: Context<SpawnChest>) -> Result<()> {
    ctx.accounts.game_data_account.player_position = 0;

    let cpi_context = CpiContext::new(
    ctx.accounts.system_program.to_account_info(),
    system_program::Transfer {
    from: ctx.accounts.payer.to_account_info().clone(),
    to: ctx.accounts.chest_vault.to_account_info().clone(),
    },
    );
    system_program::transfer(cpi_context, CHEST_REWARD)?;

    msg!("Level Reset and Chest Spawned at position 3");

    Ok(())
    }

    ...
    }

    // Specify the accounts required by the reset_level_and_spawn_chest instruction
    #[derive(Accounts)]
    pub struct SpawnChest<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(mut, seeds = [b"chestVault"], bump)]
    pub chest_vault: Account<'info, ChestVaultAccount>,
    #[account(mut)]
    pub game_data_account: Account<'info, GameDataAccount>,
    pub system_program: Program<'info, System>,
    }

    ...

    This instruction includes a cross-program invocation (CPI) to transfer SOL from the payer to the ChestVaultAccount. A cross-program invocation is when one program invokes an instruction on another program. In this case, we use a CPI to invoke the Transfer instruction from the system_program to transfer SOL from the payer to the ChestVaultAccount.

    Cross-program invocations are a key concept in the Solana programming model, enabling programs to directly interact with instructions from other programs. For a deeper dive into of CPIs, feel free to explore the CPI lessons available in the Solana Course.

    Move Right Instruction

    Finally, let's implement the move_right instruction which includes the logic for collecting the chest reward. When a player reaches position 3 and inputs the correct “password”, the reward is transferred from the ChestVaultAccount to the player's account. If an incorrect password is entered, a custom Anchor Error is returned. If the player is already at position 3, a message will be logged. Otherwise, the position will be incremented by 1 to represent moving to the right.

    The main purpose of this "password" functionality is to demonstrate how to incorporate parameters into an instruction and the implementation of custom Anchor Errors for improved error handling. In this example, the correct password will be "gib".

    The move_right instruction requires 3 accounts:

    • new_game_data_account - the GameDataAccount storing the player's position
    • chest_vault - the ChestVaultAccount storing the SOL reward
    • player_wallet - the wallet of the player invoking the instruction and the potential recipient of SOL reward
    #[program]
    pub mod tiny_adventure_two {
    use super::*;
    ...

    // Instruction to move right
    pub fn move_right(ctx: Context<MoveRight>, password: String) -> Result<()> {
    let game_data_account = &mut ctx.accounts.game_data_account;
    if game_data_account.player_position == 3 {
    msg!("You have reached the end! Super!");
    } else if game_data_account.player_position == 2 {
    if password != "gib" {
    return err!(MyError::WrongPassword);
    }

    game_data_account.player_position = game_data_account.player_position + 1;

    msg!(
    "You made it! Here is your reward {0} lamports",
    CHEST_REWARD
    );

    **ctx
    .accounts
    .chest_vault
    .to_account_info()
    .try_borrow_mut_lamports()? -= CHEST_REWARD;
    **ctx
    .accounts
    .player
    .to_account_info()
    .try_borrow_mut_lamports()? += CHEST_REWARD;
    } else {
    game_data_account.player_position = game_data_account.player_position + 1;
    print_player(game_data_account.player_position);
    }
    Ok(())
    }

    ...
    }

    // Specify the accounts required by the move_right instruction
    #[derive(Accounts)]
    pub struct MoveRight<'info> {
    #[account(mut, seeds = [b"chestVault"], bump)]
    pub chest_vault: Account<'info, ChestVaultAccount>,
    #[account(mut)]
    pub game_data_account: Account<'info, GameDataAccount>,
    #[account(mut)]
    pub player: Signer<'info>,
    }

    // Custom Anchor Error
    #[error_code]
    pub enum MyError {
    #[msg("Password was wrong")]
    WrongPassword,
    }

    ...

    To transfer lamports from the reward chest to the player account, we can't use a Cross-Program Invocation (CPI) as we did previously, since the ChestVaultAccount isn't owned by the system program. Instead, we directly modify the lamports within the accounts by using try_borrow_mut_lamports. Keep in mind that the account you deduct lamports from must be a signer, and the runtime always makes sure that the total account balances stay equal after a transaction.

    Note that Program Derived Accounts (PDAs) offer two main features:

    1. Provide a deterministic way to find an account's address
    2. Allow the program from which a PDA is derived to "sign" for them

    This is the reason we are able to deduct lamports from the ChestVaultAccount without explicitly requiring an extra signer for the instruction.

    Build and Deploy

    Great job! You've now completed part two of the Tiny Adventure program! Your final program should look like this:

    use anchor_lang::prelude::*;
    use anchor_lang::solana_program::native_token::LAMPORTS_PER_SOL;
    use anchor_lang::system_program;

    // This is your program's public key and it will update
    // automatically when you build the project.
    declare_id!("7gZTdZg86YsYbs92Rhv63kZUAkoww1kLexJg8sNpgVQ3");

    #[program]
    mod tiny_adventure_two {
    use super::*;

    // The amount of lamports that will be put into chests and given out as rewards.
    const CHEST_REWARD: u64 = LAMPORTS_PER_SOL / 10; // 0.1 SOL

    pub fn initialize_level_one(_ctx: Context<InitializeLevelOne>) -> Result<()> {
    // Usually in your production code you would not print lots of text because it cost compute units.
    msg!("A Journey Begins!");
    msg!("o.......💎");
    Ok(())
    }

    pub fn reset_level_and_spawn_chest(ctx: Context<SpawnChest>) -> Result<()> {
    ctx.accounts.game_data_account.player_position = 0;

    let cpi_context = CpiContext::new(
    ctx.accounts.system_program.to_account_info(),
    system_program::Transfer {
    from: ctx.accounts.payer.to_account_info().clone(),
    to: ctx.accounts.chest_vault.to_account_info().clone(),
    },
    );
    system_program::transfer(cpi_context, CHEST_REWARD)?;

    msg!("Level Reset and Chest Spawned at position 3");

    Ok(())
    }

    pub fn move_right(ctx: Context<MoveRight>, password: String) -> Result<()> {
    let game_data_account = &mut ctx.accounts.game_data_account;
    if game_data_account.player_position == 3 {
    msg!("You have reached the end! Super!");
    } else if game_data_account.player_position == 2 {
    if password != "gib" {
    return err!(MyError::WrongPassword);
    }

    game_data_account.player_position = game_data_account.player_position + 1;

    msg!(
    "You made it! Here is your reward {0} lamports",
    CHEST_REWARD
    );

    **ctx
    .accounts
    .chest_vault
    .to_account_info()
    .try_borrow_mut_lamports()? -= CHEST_REWARD;
    **ctx
    .accounts
    .player
    .to_account_info()
    .try_borrow_mut_lamports()? += CHEST_REWARD;
    } else {
    game_data_account.player_position = game_data_account.player_position + 1;
    print_player(game_data_account.player_position);
    }
    Ok(())
    }
    }

    fn print_player(player_position: u8) {
    if player_position == 0 {
    msg!("A Journey Begins!");
    msg!("o.........💎");
    } else if player_position == 1 {
    msg!("..o.......💎");
    } else if player_position == 2 {
    msg!("....o.....💎");
    } else if player_position == 3 {
    msg!("........\\o/💎");
    msg!("..........\\o/");
    msg!("You have reached the end! Super!");
    }
    }

    #[derive(Accounts)]
    pub struct InitializeLevelOne<'info> {
    // We must specify the space in order to initialize an account.
    // First 8 bytes are default account discriminator,
    // next 1 byte come from NewAccount.data being type u8.
    // (u8 = 8 bits unsigned integer = 8 bytes)
    // You can also use the signer as seed [signer.key().as_ref()],
    #[account(
    init_if_needed,
    seeds = [b"level1"],
    bump,
    payer = signer,
    space = 8 + 1
    )]
    pub new_game_data_account: Account<'info, GameDataAccount>,
    // This is the PDA in which we will deposit the reward SOL and
    // from where we send it back to the first player reaching the chest.
    #[account(
    init_if_needed,
    seeds = [b"chestVault"],
    bump,
    payer = signer,
    space = 8
    )]
    pub chest_vault: Account<'info, ChestVaultAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    #[derive(Accounts)]
    pub struct SpawnChest<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(mut, seeds = [b"chestVault"], bump)]
    pub chest_vault: Account<'info, ChestVaultAccount>,
    #[account(mut)]
    pub game_data_account: Account<'info, GameDataAccount>,
    pub system_program: Program<'info, System>,
    }

    #[derive(Accounts)]
    pub struct MoveRight<'info> {
    #[account(mut, seeds = [b"chestVault"], bump)]
    pub chest_vault: Account<'info, ChestVaultAccount>,
    #[account(mut)]
    pub game_data_account: Account<'info, GameDataAccount>,
    #[account(mut)]
    pub player: Signer<'info>,
    }

    #[account]
    pub struct GameDataAccount {
    player_position: u8,
    }

    #[account]
    pub struct ChestVaultAccount {}

    #[error_code]
    pub enum MyError {
    #[msg("Password was wrong")]
    WrongPassword,
    }

    Now that the program is complete, let's build and deploy it using the Solana Playground!

    If you're new to the Solana Playground, start by creating a Playground Wallet and make sure you're connected to a Devnet endpoint. Next, run solana airdrop 2 until you have 6 SOL. Once you have enough SOL, build and deploy the program.

    Get Started with the Client

    In this section, we'll walk through a simple client-side implementation for interacting with the game. We will break down the code and provide detailed explanations for each step. To get started, navigate to the client.ts file in Solana Playground, remove the placeholder code, and add the code snippets from the following sections.

    First, let's derive the PDAs (Program Derived Addresses) for the GameDataAccount and ChestVaultAccount. A PDA is a unique address in the format of a public key, derived using the program's ID and additional seeds.

    // The PDA adress everyone will be able to control the character if the interact with your program
    const [globalLevel1GameDataAccount, bump] =
    await anchor.web3.PublicKey.findProgramAddress(
    [Buffer.from("level1", "utf8")],
    //[pg.wallet.publicKey.toBuffer()], <- You could also add the player wallet as a seed, then you would have one instance per player. Need to also change the seed in the rust part
    pg.program.programId
    );

    // This is where the program will save the sol reward for the chests and from which the reward will be payed out again
    const [chestVaultAccount, chestBump] =
    await anchor.web3.PublicKey.findProgramAddress(
    [Buffer.from("chestVault", "utf8")],
    pg.program.programId
    );

    Next, we'll call the initializeLevelOne instruction to set up the GameDataAccount and ChestVaultAccount.

    // Initialize level
    let txHash = await pg.program.methods
    .initializeLevelOne()
    .accounts({
    chestVault: chestVaultAccount,
    newGameDataAccount: globalLevel1GameDataAccount,
    signer: pg.wallet.publicKey,
    systemProgram: web3.SystemProgram.programId,
    })
    .signers([pg.wallet.keypair])
    .rpc();

    console.log(`Use 'solana confirm -v ${txHash}' to see the logs`);
    await pg.connection.confirmTransaction(txHash);

    let balance = await pg.connection.getBalance(pg.wallet.publicKey);
    console.log(
    `My balance before spawning a chest: ${balance / web3.LAMPORTS_PER_SOL} SOL`
    );

    After that, we'll use the resetLevelAndSpawnChest instruction to set the player's position to 0 and fill the ChestVaultAccount with 0.1 SOL.

    // Set the player position back to 0 and pay to fill up the chest with sol
    txHash = await pg.program.methods
    .resetLevelAndSpawnChest()
    .accounts({
    chestVault: chestVaultAccount,
    gameDataAccount: globalLevel1GameDataAccount,
    payer: pg.wallet.publicKey,
    systemProgram: web3.SystemProgram.programId,
    })
    .signers([pg.wallet.keypair])
    .rpc();

    console.log(`Use 'solana confirm -v ${txHash}' to see the logs`);
    await pg.connection.confirmTransaction(txHash);

    console.log("Level reset and chest spawned 💎");
    console.log("o........💎");

    Now we can interact with the game by calling the moveRight instruction. In this example, we'll loop through this instruction until the player reaches the position to collect the reward from the ChestVaultAccount.


    // Here we move to the right three times and collect the chest at the end of the level
    for (let i = 0; i < 3; i++) {
    txHash = await pg.program.methods
    .moveRight("gib")
    .accounts({
    chestVault: chestVaultAccount,
    gameDataAccount: globalLevel1GameDataAccount,
    player: pg.wallet.publicKey,
    })
    .signers([pg.wallet.keypair])
    .rpc();

    console.log(`Use 'solana confirm -v ${txHash}' to see the logs`);
    await pg.connection.confirmTransaction(txHash);
    let balance = await pg.connection.getBalance(pg.wallet.publicKey);
    console.log(`My balance: ${balance / web3.LAMPORTS_PER_SOL} SOL`);

    let gameDateAccount = await pg.program.account.gameDataAccount.fetch(
    globalLevel1GameDataAccount
    );

    console.log("Player position is:", gameDateAccount.playerPosition.toString());

    switch (gameDateAccount.playerPosition) {
    case 0:
    console.log("A journey begins...");
    console.log("o........💎");
    break;
    case 1:
    console.log("....o....💎");
    break;
    case 2:
    console.log("......o..💎");
    break;
    case 3:
    console.log(".........\\o/💎");
    console.log("...........\\o/");
    break;
    }
    }

    Finally, press the "Run" button in the Solana Playground to execute the client. When you input anything other than "gib" as the password for the moveRight instruction, you should encounter the following error message upon reaching the position to claim the chest reward:

    Error Code: WrongPassword. Error Number: 6000. Error Message: Password was wrong.

    However, if you enter the correct password, the output should resemble the following:

    Running client...
    client.ts:
    Use 'solana confirm -v CX8VWV5Jp1kXDkZrTdeeyibgZg3B3cXAzchzCfNHvJoqARSGHeEU5injypxFwiKFcHPcWFG9BeNSrqZAdENtL2t' to see the logs
    My balance before spawning a chest: 6.396630254 SOL
    Use 'solana confirm -v 3HwAS1RK7beL3mGoNdFYWteJXF3NdJXiEskJrHtuJ6Tu9ow67Zo3yScQBEPQyish33hP8WyuVanmq93wEFJ2LQcx' to see the logs
    Level reset and chest spawned 💎
    o........💎
    Use 'solana confirm -v 43KnGrx5VQYd8LctsNaNqN1hg69vE6wiiTbdxTC1uM3Hasnq7ZdM9zWx4JS39AKNz2FpQr9a3ZnEA7XscEzmXQ5U' to see the logs
    My balance: 6.296620254 SOL
    Player position is: 1
    ....o....💎
    Use 'solana confirm -v AGxYWDw49d4y5dLon5M42eu1qG8g2Yf7FeTr3Dpbf1uFXnMeUzp4XWmHyQP1YRNpT8acz4aTJU9f2FQpL6BSAkY' to see the logs
    My balance: 6.296615254 SOL
    Player position is: 2
    ......o..💎
    Use 'solana confirm -v 5pjAU5NrS4u91QLWZTvo9aXBtR3c6g981UGSxrWDoDW5MehXnx5LnAxu4jKLp1p75RKpVSgMBgg2zHX3WDyci7AK' to see the logs
    My balance: 6.396610254 SOL
    Player position is: 3
    .........\o/💎
    ...........\o/

    Well done! You have successfully created, deployed, and interacted with Tiny Adventure Two from the client side. You've incorporated a new feature that allows players to collect rewards by reaching the chest at the end of the level. Moreover, you've learned how to transfer SOL within an Anchor program using cross-program invocations and by directly modifying lamports in accounts.

    Feel free to continue building independently and enhance the game with additional features like new levels or alternative rewards!

    - - +

    Tiny Adventure Anchor Program - Part Two

    In this tutorial, we will rebuild the Tiny Adventure game and introduce a chest with a reward of 0.1 SOL. The chest will "spawn" at a specific position, and when the player reaches that position, they will receive the reward. The goal of this program is to demonstrate how to store SOL within a program account and distribute it to players.

    The Tiny Adventure Two Program consists of 3 instructions:

    • initialize_level_one - This instruction initializes two on-chain accounts: one for recording the player's position and another for holding the SOL reward that represents the “reward chest”.
    • reset_level_and_spawn_chest - This instruction resets the player's position to zero and "respawns" a reward chest by transferring SOL from the user invoking the instruction to the reward chest account.
    • move_right - This instruction allows the player to move their position to the right and collect the SOL in the reward chest once they reach a specific position.

    In the following sections, we will guide you through building the program step by step. You can find the complete source code, which can be deployed directly from your browser using the Solana Playground, at this link: Open In Playground.

    Getting Started

    To start building the Tiny Adventure game, follow these steps:

    Visit the Solana Playground and create a new Anchor project. If you're new to Solana Playground, you'll also need to create a Playground Wallet.

    After creating a new project, replace the default starter code with the code below:

    use anchor_lang::prelude::*;
    use anchor_lang::solana_program::native_token::LAMPORTS_PER_SOL;
    use anchor_lang::system_program;

    declare_id!("11111111111111111111111111111111");

    #[program]
    mod tiny_adventure_two {
    use super::*;
    }

    fn print_player(player_position: u8) {
    if player_position == 0 {
    msg!("A Journey Begins!");
    msg!("o.........💎");
    } else if player_position == 1 {
    msg!("..o.......💎");
    } else if player_position == 2 {
    msg!("....o.....💎");
    } else if player_position == 3 {
    msg!("........\\o/💎");
    msg!("..........\\o/");
    msg!("You have reached the end! Super!");
    }
    }

    In this game, the player starts at position 0 and can only move right. To visualize the player's progress throughout the game, we'll use message logs to represent their journey towards the reward chest!

    Defining the Chest Vault Account

    Add the CHEST_REWARD constant at the beginning of the program. The CHEST_REWARD represents the amount of lamports that will be put into the chest and given out as rewards. Lamports are the smallest fractions of a SOL, with 1 billion lamports being equal to 1 SOL.

    To store the SOL reward, we will define a new ChestVaultAccount struct. This is an empty struct because we will be directly updating the lamports in the account. The account will hold the SOL reward and does not need to store any additional data.

    use anchor_lang::prelude::*;
    use anchor_lang::solana_program::native_token::LAMPORTS_PER_SOL;
    use anchor_lang::system_program;

    declare_id!("11111111111111111111111111111111");

    #[program]
    mod tiny_adventure_two {
    use super::*;

    // The amount of lamports that will be put into chests and given out as rewards.
    const CHEST_REWARD: u64 = LAMPORTS_PER_SOL / 10; // 0.1 SOL
    }

    ...

    // Define the Chest Vault Account structure
    #[account]
    pub struct ChestVaultAccount {}

    Defining the Game Data Account

    To keep track of the player's position within the game, we need to define a structure for the on-chain account that will store the player's position.

    The GameDataAccount struct contains a single field, player_position, which stores the player's current position as an unsigned 8-bit integer.


    use anchor_lang::prelude::*;
    use anchor_lang::solana_program::native_token::LAMPORTS_PER_SOL;
    use anchor_lang::system_program;

    declare_id!("11111111111111111111111111111111");

    #[program]
    mod tiny_adventure_two {
    use super::*;
    ...

    }

    ...

    // Define the Game Data Account structure
    #[account]
    pub struct GameDataAccount {
    player_position: u8,
    }

    With the GameDataAccount struct defined, you can now use it to store and update the player's position as they interact with the game. As the player moves right and progresses through the game, their position will be updated within the GameDataAccount, allowing you to track their progress towards the chest containing the SOL reward.

    Initialize Level One Instruction

    With the GameDataAccount and ChestVaultAccount defined, let's implement the initialize_level_one instruction. This instruction initializes both the GameDataAccount and ChestVaultAccount, sets the player's position to 0, and displays the starting message.

    The initialize_level_one instruction requires 4 accounts:

    • new_game_data_account - the GameDataAccount we are initializing to store the player’s position
    • chest_vault - the ChestVaultAccount we are initializing to store the SOL reward
    • signer - the player paying for the initialization of the accounts
    • system_program - a required account when creating a new account
    #[program]
    pub mod tiny_adventure_two {
    use super::*;

    pub fn initialize_level_one(_ctx: Context<InitializeLevelOne>) -> Result<()> {
    msg!("A Journey Begins!");
    msg!("o.......💎");
    Ok(())
    }

    ...
    }

    // Specify the accounts required by the initialize_level_one instruction
    #[derive(Accounts)]
    pub struct InitializeLevelOne<'info> {
    #[account(
    init_if_needed,
    seeds = [b"level1"],
    bump,
    payer = signer,
    space = 8 + 1
    )]
    pub new_game_data_account: Account<'info, GameDataAccount>,
    #[account(
    init_if_needed,
    seeds = [b"chestVault"],
    bump,
    payer = signer,
    space = 8
    )]
    pub chest_vault: Account<'info, ChestVaultAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    ...

    Both the GameDataAccount and ChestVaultAccount are created using a Program Derived Address (PDA) as the address of the account, allowing us to deterministically locate the address later. The init_if_needed constraint ensures that the accounts are only initialized if they don't already exist. Since the PDAs for both accounts in this instruction use a single fixed seed, our program can only create one of each type of account. In effect, the instruction would only ever need to be invoked one time.

    It's worth noting that the current implementation does not have any restrictions on who can modify the GameDataAccount, effectively turning the game into a multiplayer experience where everyone can control the player's movement.

    Alternatively, you can use the signer's address as an extra seed in the initialize instruction, allowing each player to create their own GameDataAccount.

    Reset Level and Spawn Chest Instruction

    Next, let's implement the reset_level_and_spawn_chest instruction, which resets the player's position to the start and fills up the chest with a reward of 0.1 SOL.

    The reset_level_and_spawn_chest instruction requires 4 accounts:

    • new_game_data_account - the GameDataAccount storing the player's position
    • chest_vault - the ChestVaultAccount storing the SOL reward
    • signer - the player providing the SOL reward for the chest
    • system_program - the program we'll be invoking to transfer SOL using a cross-program invocation (CPI), more on this shortly
    #[program]
    pub mod tiny_adventure_two {
    use super::*;
    ...

    pub fn reset_level_and_spawn_chest(ctx: Context<SpawnChest>) -> Result<()> {
    ctx.accounts.game_data_account.player_position = 0;

    let cpi_context = CpiContext::new(
    ctx.accounts.system_program.to_account_info(),
    system_program::Transfer {
    from: ctx.accounts.payer.to_account_info().clone(),
    to: ctx.accounts.chest_vault.to_account_info().clone(),
    },
    );
    system_program::transfer(cpi_context, CHEST_REWARD)?;

    msg!("Level Reset and Chest Spawned at position 3");

    Ok(())
    }

    ...
    }

    // Specify the accounts required by the reset_level_and_spawn_chest instruction
    #[derive(Accounts)]
    pub struct SpawnChest<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(mut, seeds = [b"chestVault"], bump)]
    pub chest_vault: Account<'info, ChestVaultAccount>,
    #[account(mut)]
    pub game_data_account: Account<'info, GameDataAccount>,
    pub system_program: Program<'info, System>,
    }

    ...

    This instruction includes a cross-program invocation (CPI) to transfer SOL from the payer to the ChestVaultAccount. A cross-program invocation is when one program invokes an instruction on another program. In this case, we use a CPI to invoke the Transfer instruction from the system_program to transfer SOL from the payer to the ChestVaultAccount.

    Cross-program invocations are a key concept in the Solana programming model, enabling programs to directly interact with instructions from other programs. For a deeper dive into of CPIs, feel free to explore the CPI lessons available in the Solana Course.

    Move Right Instruction

    Finally, let's implement the move_right instruction which includes the logic for collecting the chest reward. When a player reaches position 3 and inputs the correct “password”, the reward is transferred from the ChestVaultAccount to the player's account. If an incorrect password is entered, a custom Anchor Error is returned. If the player is already at position 3, a message will be logged. Otherwise, the position will be incremented by 1 to represent moving to the right.

    The main purpose of this "password" functionality is to demonstrate how to incorporate parameters into an instruction and the implementation of custom Anchor Errors for improved error handling. In this example, the correct password will be "gib".

    The move_right instruction requires 3 accounts:

    • new_game_data_account - the GameDataAccount storing the player's position
    • chest_vault - the ChestVaultAccount storing the SOL reward
    • player_wallet - the wallet of the player invoking the instruction and the potential recipient of SOL reward
    #[program]
    pub mod tiny_adventure_two {
    use super::*;
    ...

    // Instruction to move right
    pub fn move_right(ctx: Context<MoveRight>, password: String) -> Result<()> {
    let game_data_account = &mut ctx.accounts.game_data_account;
    if game_data_account.player_position == 3 {
    msg!("You have reached the end! Super!");
    } else if game_data_account.player_position == 2 {
    if password != "gib" {
    return err!(MyError::WrongPassword);
    }

    game_data_account.player_position = game_data_account.player_position + 1;

    msg!(
    "You made it! Here is your reward {0} lamports",
    CHEST_REWARD
    );

    **ctx
    .accounts
    .chest_vault
    .to_account_info()
    .try_borrow_mut_lamports()? -= CHEST_REWARD;
    **ctx
    .accounts
    .player
    .to_account_info()
    .try_borrow_mut_lamports()? += CHEST_REWARD;
    } else {
    game_data_account.player_position = game_data_account.player_position + 1;
    print_player(game_data_account.player_position);
    }
    Ok(())
    }

    ...
    }

    // Specify the accounts required by the move_right instruction
    #[derive(Accounts)]
    pub struct MoveRight<'info> {
    #[account(mut, seeds = [b"chestVault"], bump)]
    pub chest_vault: Account<'info, ChestVaultAccount>,
    #[account(mut)]
    pub game_data_account: Account<'info, GameDataAccount>,
    #[account(mut)]
    pub player: Signer<'info>,
    }

    // Custom Anchor Error
    #[error_code]
    pub enum MyError {
    #[msg("Password was wrong")]
    WrongPassword,
    }

    ...

    To transfer lamports from the reward chest to the player account, we can't use a Cross-Program Invocation (CPI) as we did previously, since the ChestVaultAccount isn't owned by the system program. Instead, we directly modify the lamports within the accounts by using try_borrow_mut_lamports. Keep in mind that the account you deduct lamports from must be a signer, and the runtime always makes sure that the total account balances stay equal after a transaction.

    Note that Program Derived Accounts (PDAs) offer two main features:

    1. Provide a deterministic way to find an account's address
    2. Allow the program from which a PDA is derived to "sign" for them

    This is the reason we are able to deduct lamports from the ChestVaultAccount without explicitly requiring an extra signer for the instruction.

    Build and Deploy

    Great job! You've now completed part two of the Tiny Adventure program! Your final program should look like this:

    use anchor_lang::prelude::*;
    use anchor_lang::solana_program::native_token::LAMPORTS_PER_SOL;
    use anchor_lang::system_program;

    // This is your program's public key and it will update
    // automatically when you build the project.
    declare_id!("7gZTdZg86YsYbs92Rhv63kZUAkoww1kLexJg8sNpgVQ3");

    #[program]
    mod tiny_adventure_two {
    use super::*;

    // The amount of lamports that will be put into chests and given out as rewards.
    const CHEST_REWARD: u64 = LAMPORTS_PER_SOL / 10; // 0.1 SOL

    pub fn initialize_level_one(_ctx: Context<InitializeLevelOne>) -> Result<()> {
    // Usually in your production code you would not print lots of text because it cost compute units.
    msg!("A Journey Begins!");
    msg!("o.......💎");
    Ok(())
    }

    pub fn reset_level_and_spawn_chest(ctx: Context<SpawnChest>) -> Result<()> {
    ctx.accounts.game_data_account.player_position = 0;

    let cpi_context = CpiContext::new(
    ctx.accounts.system_program.to_account_info(),
    system_program::Transfer {
    from: ctx.accounts.payer.to_account_info().clone(),
    to: ctx.accounts.chest_vault.to_account_info().clone(),
    },
    );
    system_program::transfer(cpi_context, CHEST_REWARD)?;

    msg!("Level Reset and Chest Spawned at position 3");

    Ok(())
    }

    pub fn move_right(ctx: Context<MoveRight>, password: String) -> Result<()> {
    let game_data_account = &mut ctx.accounts.game_data_account;
    if game_data_account.player_position == 3 {
    msg!("You have reached the end! Super!");
    } else if game_data_account.player_position == 2 {
    if password != "gib" {
    return err!(MyError::WrongPassword);
    }

    game_data_account.player_position = game_data_account.player_position + 1;

    msg!(
    "You made it! Here is your reward {0} lamports",
    CHEST_REWARD
    );

    **ctx
    .accounts
    .chest_vault
    .to_account_info()
    .try_borrow_mut_lamports()? -= CHEST_REWARD;
    **ctx
    .accounts
    .player
    .to_account_info()
    .try_borrow_mut_lamports()? += CHEST_REWARD;
    } else {
    game_data_account.player_position = game_data_account.player_position + 1;
    print_player(game_data_account.player_position);
    }
    Ok(())
    }
    }

    fn print_player(player_position: u8) {
    if player_position == 0 {
    msg!("A Journey Begins!");
    msg!("o.........💎");
    } else if player_position == 1 {
    msg!("..o.......💎");
    } else if player_position == 2 {
    msg!("....o.....💎");
    } else if player_position == 3 {
    msg!("........\\o/💎");
    msg!("..........\\o/");
    msg!("You have reached the end! Super!");
    }
    }

    #[derive(Accounts)]
    pub struct InitializeLevelOne<'info> {
    // We must specify the space in order to initialize an account.
    // First 8 bytes are default account discriminator,
    // next 1 byte come from NewAccount.data being type u8.
    // (u8 = 8 bits unsigned integer = 8 bytes)
    // You can also use the signer as seed [signer.key().as_ref()],
    #[account(
    init_if_needed,
    seeds = [b"level1"],
    bump,
    payer = signer,
    space = 8 + 1
    )]
    pub new_game_data_account: Account<'info, GameDataAccount>,
    // This is the PDA in which we will deposit the reward SOL and
    // from where we send it back to the first player reaching the chest.
    #[account(
    init_if_needed,
    seeds = [b"chestVault"],
    bump,
    payer = signer,
    space = 8
    )]
    pub chest_vault: Account<'info, ChestVaultAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
    }

    #[derive(Accounts)]
    pub struct SpawnChest<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(mut, seeds = [b"chestVault"], bump)]
    pub chest_vault: Account<'info, ChestVaultAccount>,
    #[account(mut)]
    pub game_data_account: Account<'info, GameDataAccount>,
    pub system_program: Program<'info, System>,
    }

    #[derive(Accounts)]
    pub struct MoveRight<'info> {
    #[account(mut, seeds = [b"chestVault"], bump)]
    pub chest_vault: Account<'info, ChestVaultAccount>,
    #[account(mut)]
    pub game_data_account: Account<'info, GameDataAccount>,
    #[account(mut)]
    pub player: Signer<'info>,
    }

    #[account]
    pub struct GameDataAccount {
    player_position: u8,
    }

    #[account]
    pub struct ChestVaultAccount {}

    #[error_code]
    pub enum MyError {
    #[msg("Password was wrong")]
    WrongPassword,
    }

    Now that the program is complete, let's build and deploy it using the Solana Playground!

    If you're new to the Solana Playground, start by creating a Playground Wallet and make sure you're connected to a Devnet endpoint. Next, run solana airdrop 2 until you have 6 SOL. Once you have enough SOL, build and deploy the program.

    Get Started with the Client

    In this section, we'll walk through a simple client-side implementation for interacting with the game. We will break down the code and provide detailed explanations for each step. To get started, navigate to the client.ts file in Solana Playground, remove the placeholder code, and add the code snippets from the following sections.

    First, let's derive the PDAs (Program Derived Addresses) for the GameDataAccount and ChestVaultAccount. A PDA is a unique address in the format of a public key, derived using the program's ID and additional seeds.

    // The PDA adress everyone will be able to control the character if the interact with your program
    const [globalLevel1GameDataAccount, bump] =
    await anchor.web3.PublicKey.findProgramAddress(
    [Buffer.from("level1", "utf8")],
    //[pg.wallet.publicKey.toBuffer()], <- You could also add the player wallet as a seed, then you would have one instance per player. Need to also change the seed in the rust part
    pg.program.programId
    );

    // This is where the program will save the sol reward for the chests and from which the reward will be payed out again
    const [chestVaultAccount, chestBump] =
    await anchor.web3.PublicKey.findProgramAddress(
    [Buffer.from("chestVault", "utf8")],
    pg.program.programId
    );

    Next, we'll call the initializeLevelOne instruction to set up the GameDataAccount and ChestVaultAccount.

    // Initialize level
    let txHash = await pg.program.methods
    .initializeLevelOne()
    .accounts({
    chestVault: chestVaultAccount,
    newGameDataAccount: globalLevel1GameDataAccount,
    signer: pg.wallet.publicKey,
    systemProgram: web3.SystemProgram.programId,
    })
    .signers([pg.wallet.keypair])
    .rpc();

    console.log(`Use 'solana confirm -v ${txHash}' to see the logs`);
    await pg.connection.confirmTransaction(txHash);

    let balance = await pg.connection.getBalance(pg.wallet.publicKey);
    console.log(
    `My balance before spawning a chest: ${balance / web3.LAMPORTS_PER_SOL} SOL`
    );

    After that, we'll use the resetLevelAndSpawnChest instruction to set the player's position to 0 and fill the ChestVaultAccount with 0.1 SOL.

    // Set the player position back to 0 and pay to fill up the chest with sol
    txHash = await pg.program.methods
    .resetLevelAndSpawnChest()
    .accounts({
    chestVault: chestVaultAccount,
    gameDataAccount: globalLevel1GameDataAccount,
    payer: pg.wallet.publicKey,
    systemProgram: web3.SystemProgram.programId,
    })
    .signers([pg.wallet.keypair])
    .rpc();

    console.log(`Use 'solana confirm -v ${txHash}' to see the logs`);
    await pg.connection.confirmTransaction(txHash);

    console.log("Level reset and chest spawned 💎");
    console.log("o........💎");

    Now we can interact with the game by calling the moveRight instruction. In this example, we'll loop through this instruction until the player reaches the position to collect the reward from the ChestVaultAccount.


    // Here we move to the right three times and collect the chest at the end of the level
    for (let i = 0; i < 3; i++) {
    txHash = await pg.program.methods
    .moveRight("gib")
    .accounts({
    chestVault: chestVaultAccount,
    gameDataAccount: globalLevel1GameDataAccount,
    player: pg.wallet.publicKey,
    })
    .signers([pg.wallet.keypair])
    .rpc();

    console.log(`Use 'solana confirm -v ${txHash}' to see the logs`);
    await pg.connection.confirmTransaction(txHash);
    let balance = await pg.connection.getBalance(pg.wallet.publicKey);
    console.log(`My balance: ${balance / web3.LAMPORTS_PER_SOL} SOL`);

    let gameDateAccount = await pg.program.account.gameDataAccount.fetch(
    globalLevel1GameDataAccount
    );

    console.log("Player position is:", gameDateAccount.playerPosition.toString());

    switch (gameDateAccount.playerPosition) {
    case 0:
    console.log("A journey begins...");
    console.log("o........💎");
    break;
    case 1:
    console.log("....o....💎");
    break;
    case 2:
    console.log("......o..💎");
    break;
    case 3:
    console.log(".........\\o/💎");
    console.log("...........\\o/");
    break;
    }
    }

    Finally, press the "Run" button in the Solana Playground to execute the client. When you input anything other than "gib" as the password for the moveRight instruction, you should encounter the following error message upon reaching the position to claim the chest reward:

    Error Code: WrongPassword. Error Number: 6000. Error Message: Password was wrong.

    However, if you enter the correct password, the output should resemble the following:

    Running client...
    client.ts:
    Use 'solana confirm -v CX8VWV5Jp1kXDkZrTdeeyibgZg3B3cXAzchzCfNHvJoqARSGHeEU5injypxFwiKFcHPcWFG9BeNSrqZAdENtL2t' to see the logs
    My balance before spawning a chest: 6.396630254 SOL
    Use 'solana confirm -v 3HwAS1RK7beL3mGoNdFYWteJXF3NdJXiEskJrHtuJ6Tu9ow67Zo3yScQBEPQyish33hP8WyuVanmq93wEFJ2LQcx' to see the logs
    Level reset and chest spawned 💎
    o........💎
    Use 'solana confirm -v 43KnGrx5VQYd8LctsNaNqN1hg69vE6wiiTbdxTC1uM3Hasnq7ZdM9zWx4JS39AKNz2FpQr9a3ZnEA7XscEzmXQ5U' to see the logs
    My balance: 6.296620254 SOL
    Player position is: 1
    ....o....💎
    Use 'solana confirm -v AGxYWDw49d4y5dLon5M42eu1qG8g2Yf7FeTr3Dpbf1uFXnMeUzp4XWmHyQP1YRNpT8acz4aTJU9f2FQpL6BSAkY' to see the logs
    My balance: 6.296615254 SOL
    Player position is: 2
    ......o..💎
    Use 'solana confirm -v 5pjAU5NrS4u91QLWZTvo9aXBtR3c6g981UGSxrWDoDW5MehXnx5LnAxu4jKLp1p75RKpVSgMBgg2zHX3WDyci7AK' to see the logs
    My balance: 6.396610254 SOL
    Player position is: 3
    .........\o/💎
    ...........\o/

    Well done! You have successfully created, deployed, and interacted with Tiny Adventure Two from the client side. You've incorporated a new feature that allows players to collect rewards by reaching the chest at the end of the level. Moreover, you've learned how to transfer SOL within an Anchor program using cross-program invocations and by directly modifying lamports in accounts.

    Feel free to continue building independently and enhance the game with additional features like new levels or alternative rewards!

    + + \ No newline at end of file diff --git a/cookbook-zh/references/index.html b/cookbook-zh/references/index.html index e696e57c2..505d8adbc 100644 --- a/cookbook-zh/references/index.html +++ b/cookbook-zh/references/index.html @@ -9,13 +9,13 @@ - - + + - - + + + \ No newline at end of file diff --git a/cookbook-zh/references/keypairs-and-wallets/index.html b/cookbook-zh/references/keypairs-and-wallets/index.html index 2030d2336..c642a4ced 100644 --- a/cookbook-zh/references/keypairs-and-wallets/index.html +++ b/cookbook-zh/references/keypairs-and-wallets/index.html @@ -9,14 +9,14 @@ - - + +

    密钥对和钱包

    如何生成新的密钥对

    对于使用Solana库执行各种操作,许多操作都需要一个密钥对或钱包。如果你正在连接到一个钱包,那么你不必担心。然而,如果你需要一个密钥对,你会需要生成一个。

    let keypair = Keypair.generate();

    如何从密钥恢复密钥对

    如果你已经有了密钥,你可以通过这个密钥获取密钥对,以测试你的dApp。

    1. 从字节中:
    const keypair = Keypair.fromSecretKey(
    Uint8Array.from([
    174, 47, 154, 16, 202, 193, 206, 113, 199, 190, 53, 133, 169, 175, 31, 56,
    222, 53, 138, 189, 224, 216, 117, 173, 10, 149, 53, 45, 73, 251, 237, 246,
    15, 185, 186, 82, 177, 240, 148, 69, 241, 227, 167, 80, 141, 89, 240, 121,
    121, 35, 172, 247, 68, 251, 226, 218, 48, 63, 176, 109, 168, 89, 238, 135,
    ])
    );
    1. 从Base58字符串:
    const keypair = Keypair.fromSecretKey(
    bs58.decode(
    "5MaiiCavjCmn9Hs1o3eznqDEhRwxo7pXiAYez7keQUviUkauRiTMD8DrESdrNjN8zd9mTmVhRvBJeg5vhyvgrAhG"
    )
    );

    如何验证密钥对

    如果你有了个密钥对,你可以验证密钥对的私钥是否与给定的公钥匹配。

    const publicKey = new PublicKey("24PNhTaNtomHhoy3fTRaMhAFCRj4uHqhZEEoWrKDbR5p");
    const keypair = Keypair.fromSecretKey(
    Uint8Array.from([
    174, 47, 154, 16, 202, 193, 206, 113, 199, 190, 53, 133, 169, 175, 31, 56,
    222, 53, 138, 189, 224, 216, 117, 173, 10, 149, 53, 45, 73, 251, 237, 246,
    15, 185, 186, 82, 177, 240, 148, 69, 241, 227, 167, 80, 141, 89, 240, 121,
    121, 35, 172, 247, 68, 251, 226, 218, 48, 63, 176, 109, 168, 89, 238, 135,
    ])
    );
    console.log(keypair.publicKey.toBase58() === publicKey.toBase58());
    // true

    如何检查一个公钥是否有关联的私钥

    在某些特殊情况下(例如,派生自程序的地址(PDA)),公钥可能没有关联的私钥。你可以通过查看公钥是否位于ed25519曲线上来检查这一点。只有位于曲线上的公钥才可以由具有钱包的用户控制。

    const key = new PublicKey("5oNDL3swdJJF1g9DzJiZ4ynHXgszjAEpUkxVYejchzrY");
    console.log(PublicKey.isOnCurve(key.toBytes()));

    如何生成助记词

    如果你正在创建一个钱包,你需要生成一个助记词,以便用户可以将其保存为备份。

    const mnemonic = bip39.generateMnemonic();

    如何通过助记词恢复密钥对

    许多钱包扩展使用助记词来表示其密钥。你可以将助记词转换为密钥对以进行本地测试。

    1. BIP39 - 创建单个钱包的步骤如下:
    const mnemonic =
    "pill tomorrow foster begin walnut borrow virtual kick shift mutual shoe scatter";
    const seed = bip39.mnemonicToSeedSync(mnemonic, ""); // (mnemonic, password)
    const keypair = Keypair.fromSeed(seed.slice(0, 32));
    1. BIP44 (多个钱包,也叫HD钱包)

    你可以从一个单一种子生成多个钱包,也被称为“分层确定性钱包”或HD钱包。

    const mnemonic =
    "neither lonely flavor argue grass remind eye tag avocado spot unusual intact";
    const seed = bip39.mnemonicToSeedSync(mnemonic, ""); // (mnemonic, password)
    for (let i = 0; i < 10; i++) {
    const path = `m/44'/501'/${i}'/0'`;
    const keypair = Keypair.fromSeed(derivePath(path, seed.toString("hex")).key);
    console.log(`${path} => ${keypair.publicKey.toBase58()}`);
    }

    如何生成自定义地址(vanity address)

    自定义公钥或地址(Vanity Address)是以特定字符开头的密钥。例如,一个人可能希望公钥以 "elv1s" 或 "cook" 开头,这样可以帮助他人记住密钥所属的人,使密钥更容易识别。

    注意: 自定义地址中字符的数量越多,生成时间将会更长。

    caution

    警告 -在此任务中,您应该使用命令行界面(CLI)。Python和TypeScript的示例仅用于说明,速度比CLI慢得多。

    let keypair = Keypair.generate();
    while (!keypair.publicKey.toBase58().startsWith("elv1s")) {
    keypair = Keypair.generate();
    }

    如何使用钱包来签名和验证消息

    密钥对的主要功能是对消息进行签名并验证签名的有效性。通过验证签名,接收方可以确保数据是由特定私钥的所有者签名的。

    为此,我们将导入TweetNaCl 密码库,并按照以下步骤进行操作:

    const message = "The quick brown fox jumps over the lazy dog";
    const messageBytes = decodeUTF8(message);

    const signature = nacl.sign.detached(messageBytes, keypair.secretKey);
    const result = nacl.sign.detached.verify(
    messageBytes,
    signature,
    keypair.publicKey.toBytes()
    );

    console.log(result);

    如何连接到钱包

    Solana的钱包适配器 库使客户端管理钱包连接变得简单。

    反应

    运行以下命令来安装所需的依赖项:

    yarn add @solana/wallet-adapter-react @solana/wallet-adapter-react-ui @solana/wallet-adapter-base @solana/wallet-adapter-wallets

    React的钱包适配器库允许我们通过钩子和上下文提供程序来持久化和访问钱包连接状态,主要包括useWallet、WalletProvideruseConnection和ConnectionProviderWalletProviderConnectionProvider必须包装React应用程。

    此外,我们可以使用useWalletModal来提示用户进行连接,通过切换连接模态框的可见性,并将应用程序包装在@solana/wallet-adapter-react-ui中的WalletModalProvider中。连接模态框将处理连接流程,因此我们只需监听钱包连接的状态。当useWallet的响应具有非空的wallet属性时,我们知道钱包已连接。反之,如果该属性为空,我们知道钱包已断开连接。

    const { wallet } = useWallet();
    const { setVisible } = useWalletModal();

    const onRequestConnectWallet = () => {
    setVisible(true);
    };

    // Prompt the user to connect their wallet
    if (!wallet) {
    return <button onClick={onRequestConnectWallet}>Connect Wallet</button>;
    }

    // Displays the connected wallet address
    return (
    <main>
    <p>Wallet successfully connected!</p>
    <p>{wallet.publicKey.toBase58()}</p>
    </main>
    );w

    Vue

    运行以下命令来安装所需的依赖项:

    npm install solana-wallets-vue @solana/wallet-adapter-wallets

    Solana的Vue钱包 插件允许我们初始化钱包存储,并创建一个名为$wallet的全局属性,可以在任何组件中访问。你可以在此处 查看可以从useWallet()获取的所有属性和方法。我们还导入并渲染WalletMultiButton组件,以允许用户选择钱包并连接到它。

    <script setup>
    import { WalletMultiButton } from "solana-wallets-vue";
    </script>

    <template>
    <wallet-multi-button></wallet-multi-button>
    </template>

    Svelte

    运行以下命令来安装所需的依赖项:

    npm install @svelte-on-solana/wallet-adapter-core @svelte-on-solana/wallet-adapter-ui @solana/wallet-adapter-base @solana/wallet-adapter-wallets @solana/web3.js

    Svelte Wallet Adapter 包允许我们在使用Svelte模板或SvelteKit创建的项目中,在所有JS、TS或/和Svelte文件之间添加一个可访问的Svelte Store($walletStore)。使用 此处 的存储库引用,您可以在SSR或SPA中使用适配器。UI包含一个<WalletMultiButton />组件,允许用户选择一个钱包并连接到它。

    <script>
    import { walletStore } from "@svelte-on-solana/wallet-adapter-core";
    import { WalletMultiButton } from "@svelte-on-solana/wallet-adapter-ui";
    </script>

    {#if $walletStore?.connected} Wallet with public key {$walletStore.publicKey}
    successfully connected! {:else}
    <WalletMultiButton />
    {/if}
    - - +在此任务中,您应该使用命令行界面(CLI)。Python和TypeScript的示例仅用于说明,速度比CLI慢得多。

    let keypair = Keypair.generate();
    while (!keypair.publicKey.toBase58().startsWith("elv1s")) {
    keypair = Keypair.generate();
    }

    如何使用钱包来签名和验证消息

    密钥对的主要功能是对消息进行签名并验证签名的有效性。通过验证签名,接收方可以确保数据是由特定私钥的所有者签名的。

    为此,我们将导入TweetNaCl 密码库,并按照以下步骤进行操作:

    const message = "The quick brown fox jumps over the lazy dog";
    const messageBytes = decodeUTF8(message);

    const signature = nacl.sign.detached(messageBytes, keypair.secretKey);
    const result = nacl.sign.detached.verify(
    messageBytes,
    signature,
    keypair.publicKey.toBytes()
    );

    console.log(result);

    如何连接到钱包

    Solana的钱包适配器 库使客户端管理钱包连接变得简单。

    反应

    运行以下命令来安装所需的依赖项:

    yarn add @solana/wallet-adapter-react @solana/wallet-adapter-react-ui @solana/wallet-adapter-base @solana/wallet-adapter-wallets

    React的钱包适配器库允许我们通过钩子和上下文提供程序来持久化和访问钱包连接状态,主要包括useWallet、WalletProvideruseConnection和ConnectionProviderWalletProviderConnectionProvider必须包装React应用程。

    此外,我们可以使用useWalletModal来提示用户进行连接,通过切换连接模态框的可见性,并将应用程序包装在@solana/wallet-adapter-react-ui中的WalletModalProvider中。连接模态框将处理连接流程,因此我们只需监听钱包连接的状态。当useWallet的响应具有非空的wallet属性时,我们知道钱包已连接。反之,如果该属性为空,我们知道钱包已断开连接。

    const { wallet } = useWallet();
    const { setVisible } = useWalletModal();

    const onRequestConnectWallet = () => {
    setVisible(true);
    };

    // Prompt the user to connect their wallet
    if (!wallet) {
    return <button onClick={onRequestConnectWallet}>Connect Wallet</button>;
    }

    // Displays the connected wallet address
    return (
    <main>
    <p>Wallet successfully connected!</p>
    <p>{wallet.publicKey.toBase58()}</p>
    </main>
    );w

    Vue

    运行以下命令来安装所需的依赖项:

    npm install solana-wallets-vue @solana/wallet-adapter-wallets

    Solana的Vue钱包 插件允许我们初始化钱包存储,并创建一个名为$wallet的全局属性,可以在任何组件中访问。你可以在此处 查看可以从useWallet()获取的所有属性和方法。我们还导入并渲染WalletMultiButton组件,以允许用户选择钱包并连接到它。

    <script setup>
    import { WalletMultiButton } from "solana-wallets-vue";
    </script>

    <template>
    <wallet-multi-button></wallet-multi-button>
    </template>

    Svelte

    运行以下命令来安装所需的依赖项:

    npm install @svelte-on-solana/wallet-adapter-core @svelte-on-solana/wallet-adapter-ui @solana/wallet-adapter-base @solana/wallet-adapter-wallets @solana/web3.js

    Svelte Wallet Adapter 包允许我们在使用Svelte模板或SvelteKit创建的项目中,在所有JS、TS或/和Svelte文件之间添加一个可访问的Svelte Store($walletStore)。使用 此处 的存储库引用,您可以在SSR或SPA中使用适配器。UI包含一个<WalletMultiButton />组件,允许用户选择一个钱包并连接到它。

    <script>
    import { walletStore } from "@svelte-on-solana/wallet-adapter-core";
    import { WalletMultiButton } from "@svelte-on-solana/wallet-adapter-ui";
    </script>

    {#if $walletStore?.connected} Wallet with public key {$walletStore.publicKey}
    successfully connected! {:else}
    <WalletMultiButton />
    {/if}
    + + \ No newline at end of file diff --git a/cookbook-zh/references/local-development/index.html b/cookbook-zh/references/local-development/index.html index ed2d981ab..43ac52316 100644 --- a/cookbook-zh/references/local-development/index.html +++ b/cookbook-zh/references/local-development/index.html @@ -9,15 +9,15 @@ - - + +

    本地开发

    开启本地验证器

    在本地测试验证器比在开发网络(devnet)上进行测试更可靠,并且可以帮助你在开发网络上运行之前进行测试。

    你可以通过安装 solana工具套件 并运行以下命令来设置本地测试验证器:

    solana-test-validator

    使用本地测试验证器的好处包括:

    • 无RPC速率限制
    • 无空投限制
    • 直接在链上部署程序(--bpf-program ...
    • 从公共集群克隆账户,包括程序(--clone ...
    • 可配置的事务历史保留(--limit-ledger-size ...
    • 可配置的纪元长度(--slots-per-epoch ...
    • 跳转到任意槽位(--warp-slot ...

    连接到不同环境

    当你进行Solana开发时,你需要连接到特定的RPC API端点。Solana有三个公共的开发环境:

    const connection = new Connection(clusterApiUrl("mainnet-beta"), "confirmed");

    最后,你还可以连接到私有集群,无论是本地的还是远程运行的,使用以下方式:

    const connection = new Connection("http://127.0.0.1:8899", "confirmed");

    订阅事件

    Websockets提供了一种发布/订阅接口,你可以在其中监听特定的事件。与在固定时间间隔内对典型的HTTP端点进行轮询以获取频繁的更新不同,你可以仅在事件发生时才接收这些更新。

    Solana的web3连接 在底层生成一个websocket端点,并在创建新的Connection实例时注册一个websocket客户端(请参阅 此处) 的源代码)。

    Connection类提供了发布/订阅方法,它们都以on开头,类似于事件发射器。当您调用这些监听器方法时,它会在该Connection实例的websocket客户端中注册一个新的订阅。下面我们使用的示例发布/订阅方法是onAccountChange。 回调函数将通过参数提供更新的状态数据(例如,查看AAccountChangeCallback 作为示例)。

    // Establish new connect to devnet - websocket client connected to devnet will also be registered here
    const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

    // Create a test wallet to listen to
    const wallet = Keypair.generate();

    // Register a callback to listen to the wallet (ws subscription)
    connection.onAccountChange(
    wallet.publicKey(),
    (updatedAccountInfo, context) =>
    console.log("Updated account info: ", updatedAccountInfo),
    "confirmed"
    );

    获取测试用的SOL

    你在本地工作时,为了发送交易,你需要一些 SOL。在非主网环境中,你可以向你的地址空投 SOL,获取SOL。

    const airdropSignature = await connection.requestAirdrop(
    keypair.publicKey,
    LAMPORTS_PER_SOL
    );

    await connection.confirmTransaction(airdropSignature);

    使用主网 (Mainnet) 账户和程序

    本地测试通常依赖于仅在主网上可用的程序和账户。Solana CLI 提供了以下两个功能:

    下载程序和账户 -将程序和账户加载到本地验证器中

    如何从主网加载账户

    可以将SRM代币的铸造(mint)账户下载到文件中:

    solana account -u m --output json-compact --output-file SRM_token.json SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt

    然后,通过在启动验证器时传递该账户文件和目标地址(在本地集群上)你可以将其加载到本地网络:

    solana-test-validator --account SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt SRM_token.json --reset

    如何从主网加载程序

    同样地,我们可以下载Serum Dex v3程序:

    solana program dump -u m 9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin serum_dex_v3.so

    然后,在启动验证器时,通过传递程序的文件和目标地址(在本地集群上)来将其加载到本地网络:

    solana-test-validator --bpf-program 9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin serum_dex_v3.so --reset
    - - +将程序和账户加载到本地验证器中

    如何从主网加载账户

    可以将SRM代币的铸造(mint)账户下载到文件中:

    solana account -u m --output json-compact --output-file SRM_token.json SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt

    然后,通过在启动验证器时传递该账户文件和目标地址(在本地集群上)你可以将其加载到本地网络:

    solana-test-validator --account SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt SRM_token.json --reset

    如何从主网加载程序

    同样地,我们可以下载Serum Dex v3程序:

    solana program dump -u m 9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin serum_dex_v3.so

    然后,在启动验证器时,通过传递程序的文件和目标地址(在本地集群上)来将其加载到本地网络:

    solana-test-validator --bpf-program 9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin serum_dex_v3.so --reset
    + + \ No newline at end of file diff --git a/cookbook-zh/references/name-service/index.html b/cookbook-zh/references/name-service/index.html index a0e3b9347..f585573e4 100644 --- a/cookbook-zh/references/name-service/index.html +++ b/cookbook-zh/references/name-service/index.html @@ -9,13 +9,13 @@ - - + +
    -

    命名服务

    名称注册表

    名称注册表存储与域名有关的信息。它由两部分组成:

    • 头部 (Header)
    • 数据 (Data)

    域名的数据始终以头部为前缀。以下是头部在 JavaScript 中的结构:

    export class NameRegistryState {
    parentName: PublicKey;
    owner: PublicKey;
    class: PublicKey;
    data: Buffer | undefined;

    static HEADER_LEN = 96;

    static schema: Schema = new Map([
    [
    NameRegistryState,
    {
    kind: "struct",
    fields: [
    ["parentName", [32]],
    ["owner", [32]],
    ["class", [32]],
    ],
    },
    ],
    ]);
    constructor(obj: {
    parentName: Uint8Array;
    owner: Uint8Array;
    class: Uint8Array;
    }) {
    this.parentName = new PublicKey(obj.parentName);
    this.owner = new PublicKey(obj.owner);
    this.class = new PublicKey(obj.class);
    }
    }

    解析SOL域名

    .SOL 域名是独特的、易于理解的域名,可以转换为公钥。许多钱包使用它们作为发送代币或 SOL 的另一种选项。你可以使用以下方法将 .SOL 域名转换为对应的公钥:

    const domain = "levi.sol";
    const hashedName = await getHashedName(domain.replace(".sol", ""));
    const nameAccountKey = await getNameAccountKey(
    hashedName,
    undefined,
    new PublicKey("58PwtjSDuFHuUkYjH9BYnnQKHfwo9reZhC2zMJv9JPkx") // SOL TLD Authority
    );
    const owner = await NameRegistryState.retrieve(
    new Connection(clusterApiUrl("mainnet-beta")),
    nameAccountKey
    );
    console.log(owner.registry.owner.toBase58());
    // JUskoxS2PTiaBpxfGaAPgf3cUNhdeYFGMKdL6mZKKfR

    反向查找

    这可以用于从公钥解析域名。

    // Public key of bonfida.sol
    const domainKey = new PublicKey("Crf8hzfthWGbGbLTVCiqRqV5MVnbpHB1L9KQMd6gsinb");

    const domainName = await performReverseLookup(connection, domainKey); // bonfida

    子域名查找

    为了解析一个子域名,你需要:

    1. 域名的密钥
    2. 域名的密钥
    3. 检索账户信息
    const parentDomain = "bonfida";
    const subDomain = "demo";

    // Step 1
    const hashedParentDomain = await getHashedName(parentDomain);
    const parentDomainKey = await getNameAccountKey(
    hashedParentDomain,
    undefined,
    SOL_TLD_AUTHORITY
    );

    // Step 2
    const subDomainKey = await getDNSRecordAddress(parentDomainKey, subDomain);

    // Step 3
    const registry = await NameRegistryState.retrieve(connection, subDomainKey);

    查找由公钥拥有的所有域名

    你可以通过使用带有memcmp过滤器的getProgramAccounts请求来检索钱包的所有域名。

    export async function findOwnedNameAccountsForUser(
    connection: Connection,
    userAccount: PublicKey
    ): Promise<PublicKey[]> {
    const filters = [
    {
    memcmp: {
    offset: 32,
    bytes: userAccount.toBase58(),
    },
    },
    ];
    const accounts = await connection.getProgramAccounts(NAME_PROGRAM_ID, {
    filters,
    });
    return accounts.map((a) => a.publicKey);
    }

    解析一个Twitter用户名

    Twitter用户名可以在 [Solana名称服务上注册](https://naming.bonfida.org/#/twitter-registration 并可以像.SOL域名一样使用。

    // Pubkey of the wallet you want to retrieve the Twitter handle
    const pubkey = new PublicKey("FidaeBkZkvDqi1GXNEwB8uWmj9Ngx2HXSS5nyGRuVFcZ");

    const [handle, registryKey] = await getHandleAndRegistryKey(connection, pubkey);

    Twitter用户名的反向查找

    为了找到与Twitter用户名相关联的SOL地址,你可以进行反向查找。

    const handle = "bonfida";

    const registry = await getTwitterRegistry(connection, handle);
    - - +

    命名服务

    名称注册表

    名称注册表存储与域名有关的信息。它由两部分组成:

    • 头部 (Header)
    • 数据 (Data)

    域名的数据始终以头部为前缀。以下是头部在 JavaScript 中的结构:

    export class NameRegistryState {
    parentName: PublicKey;
    owner: PublicKey;
    class: PublicKey;
    data: Buffer | undefined;

    static HEADER_LEN = 96;

    static schema: Schema = new Map([
    [
    NameRegistryState,
    {
    kind: "struct",
    fields: [
    ["parentName", [32]],
    ["owner", [32]],
    ["class", [32]],
    ],
    },
    ],
    ]);
    constructor(obj: {
    parentName: Uint8Array;
    owner: Uint8Array;
    class: Uint8Array;
    }) {
    this.parentName = new PublicKey(obj.parentName);
    this.owner = new PublicKey(obj.owner);
    this.class = new PublicKey(obj.class);
    }
    }

    解析SOL域名

    .SOL 域名是独特的、易于理解的域名,可以转换为公钥。许多钱包使用它们作为发送代币或 SOL 的另一种选项。你可以使用以下方法将 .SOL 域名转换为对应的公钥:

    const domain = "levi.sol";
    const hashedName = await getHashedName(domain.replace(".sol", ""));
    const nameAccountKey = await getNameAccountKey(
    hashedName,
    undefined,
    new PublicKey("58PwtjSDuFHuUkYjH9BYnnQKHfwo9reZhC2zMJv9JPkx") // SOL TLD Authority
    );
    const owner = await NameRegistryState.retrieve(
    new Connection(clusterApiUrl("mainnet-beta")),
    nameAccountKey
    );
    console.log(owner.registry.owner.toBase58());
    // JUskoxS2PTiaBpxfGaAPgf3cUNhdeYFGMKdL6mZKKfR

    反向查找

    这可以用于从公钥解析域名。

    // Public key of bonfida.sol
    const domainKey = new PublicKey("Crf8hzfthWGbGbLTVCiqRqV5MVnbpHB1L9KQMd6gsinb");

    const domainName = await performReverseLookup(connection, domainKey); // bonfida

    子域名查找

    为了解析一个子域名,你需要:

    1. 域名的密钥
    2. 域名的密钥
    3. 检索账户信息
    const parentDomain = "bonfida";
    const subDomain = "demo";

    // Step 1
    const hashedParentDomain = await getHashedName(parentDomain);
    const parentDomainKey = await getNameAccountKey(
    hashedParentDomain,
    undefined,
    SOL_TLD_AUTHORITY
    );

    // Step 2
    const subDomainKey = await getDNSRecordAddress(parentDomainKey, subDomain);

    // Step 3
    const registry = await NameRegistryState.retrieve(connection, subDomainKey);

    查找由公钥拥有的所有域名

    你可以通过使用带有memcmp过滤器的getProgramAccounts请求来检索钱包的所有域名。

    export async function findOwnedNameAccountsForUser(
    connection: Connection,
    userAccount: PublicKey
    ): Promise<PublicKey[]> {
    const filters = [
    {
    memcmp: {
    offset: 32,
    bytes: userAccount.toBase58(),
    },
    },
    ];
    const accounts = await connection.getProgramAccounts(NAME_PROGRAM_ID, {
    filters,
    });
    return accounts.map((a) => a.publicKey);
    }

    解析一个Twitter用户名

    Twitter用户名可以在 [Solana名称服务上注册](https://naming.bonfida.org/#/twitter-registration 并可以像.SOL域名一样使用。

    // Pubkey of the wallet you want to retrieve the Twitter handle
    const pubkey = new PublicKey("FidaeBkZkvDqi1GXNEwB8uWmj9Ngx2HXSS5nyGRuVFcZ");

    const [handle, registryKey] = await getHandleAndRegistryKey(connection, pubkey);

    Twitter用户名的反向查找

    为了找到与Twitter用户名相关联的SOL地址,你可以进行反向查找。

    const handle = "bonfida";

    const registry = await getTwitterRegistry(connection, handle);
    + + \ No newline at end of file diff --git a/cookbook-zh/references/nfts/index.html b/cookbook-zh/references/nfts/index.html index 080667558..859106fac 100644 --- a/cookbook-zh/references/nfts/index.html +++ b/cookbook-zh/references/nfts/index.html @@ -9,14 +9,14 @@ - - + +

    非同质化代币 (NFTs)

    如何创建一个NFT

    要创建一个 NFT,你需要:

    1. 将图像上传到像 Arweave 这样的 IPFS 网络上。
    2. 将 JSON 元数据上传到像 Arweave 这样的 IPFS 网络上。
    3. 调用 Metaplex 创建一个用于该 NFT 的账户。

    上传到 Arweave

    // 1. Upload image to Arweave
    const data = fs.readFileSync("./code/nfts/arweave-upload/lowres-dog.png");

    const transaction = await arweave.createTransaction({
    data: data,
    });

    transaction.addTag("Content-Type", "image/png");

    const wallet = JSON.parse(fs.readFileSync("wallet.json", "utf-8"))
    await arweave.transactions.sign(transaction, wallet);

    const response = await arweave.transactions.post(transaction);
    console.log(response);

    const id = transaction.id;
    const imageUrl = id ? `https://arweave.net/${id}` : undefined;

    // 2. Upload metadata to Arweave

    const metadataRequest = JSON.stringify(metadata);

    const metadataTransaction = await arweave.createTransaction({
    data: metadataRequest,
    });

    metadataTransaction.addTag("Content-Type", "application/json");

    await arweave.transactions.sign(metadataTransaction, wallet);

    await arweave.transactions.post(metadataTransaction);

    铸造(Mint)该 NFT

    如果你已经上传了图像和元数据,您可以使用以下代码铸造(Mint)该 NFT。

    const mintNFTResponse = await metaplex.nfts().create({
    uri: "https://ffaaqinzhkt4ukhbohixfliubnvpjgyedi3f2iccrq4efh3s.arweave.net/KUAIIbk6p8oo4XHRcq0U__C2r0mwQaNl0gQow4Qp9yk",
    maxSupply: 1,
    });
    info

    tip 注意 -你不能使用与你钱包不同的创作者信息来铸造(Mint) NFT。如果遇到创作者的问题,请确保你的元数据中将你列为创作者。

    如何获取 NFT 元数据

    Metaplex 的 NFT 元数据存储在 Arweave 上。为了获取 Arweave 的元数据,您需要获取 Metaplex PDA(程序派生账户)并对账户数据进行解码。

    const connection = new Connection(clusterApiUrl("mainnet-beta"));
    const keypair = Keypair.generate();

    const metaplex = new Metaplex(connection);
    metaplex.use(keypairIdentity(keypair));

    const mintAddress = new PublicKey(
    "Ay1U9DWphDgc7hq58Yj1yHabt91zTzvV2YJbAWkPNbaK"
    );

    const nft = await metaplex.nfts().findByMint({ mintAddress });

    console.log(nft.json);

    如何获取NFT的所有者

    如果你拥有 NFT 的铸币密钥,你可以通过查看该铸币密钥对应的最大代币账户来找到其当前所有者。

    请记住,NFT 的供应量为 1,它们是不可分割的,这意味着在任何时刻只有一个代币账户持有该代币,而其他所有与该铸币密钥相关的代币账户的余额都为 0。

    一旦确定了最大代币账户,我们可以获取它的所有者。

    const connection = new Connection("https://api.mainnet-beta.solana.com");
    const tokenMint = "9ARngHhVaCtH5JFieRdSS5Y8cdZk2TMF4tfGSWFB9iSK";
    const largestAccounts = await connection.getTokenLargestAccounts(
    new PublicKey(tokenMint)
    );
    const largestAccountInfo = await connection.getParsedAccountInfo(
    largestAccounts.value[0].address
    );
    console.log(largestAccountInfo.value.data.parsed.info.owner);

    如何获取 NFT 的铸币地址

    如果你知道Candy Machine的公钥,你可以使用以下代码获取从该Candy Machine生成的所有 NFT 铸币地址的列表。请注意,我们可以使用以下的 memcmp 过滤器,因为在 v1 版本中,第一个创作者的地址总是Candy Machine的地址。

    Candy Machine V1

    const getMintAddresses = async (firstCreatorAddress: PublicKey) => {
    const metadataAccounts = await connection.getProgramAccounts(
    TOKEN_METADATA_PROGRAM,
    {
    // The mint address is located at byte 33 and lasts for 32 bytes.
    dataSlice: { offset: 33, length: 32 },

    filters: [
    // Only get Metadata accounts.
    { dataSize: MAX_METADATA_LEN },

    // Filter using the first creator.
    {
    memcmp: {
    offset: CREATOR_ARRAY_START,
    bytes: firstCreatorAddress.toBase58(),
    },
    },
    ],
    }
    );

    return metadataAccounts.map((metadataAccountInfo) =>
    bs58.encode(metadataAccountInfo.account.data)
    );
    };

    getMintAddresses(candyMachineId);

    Candy Machine V2

    如果你正在使用Candy Machine v2,你首先需要访问其 "Candy Machine Creator" 地址,该地址是一个简单的 PDA,使用candy_machine和Candy Machine v2 地址作为种子生成。一旦你获得了创建者地址,你可以像对待 v1 版本一样使用它。

    const getCandyMachineCreator = async (
    candyMachine: PublicKey
    ): Promise<[PublicKey, number]> =>
    PublicKey.findProgramAddress(
    [Buffer.from("candy_machine"), candyMachine.toBuffer()],
    CANDY_MACHINE_V2_PROGRAM
    );

    const candyMachineCreator = await getCandyMachineCreator(candyMachineId);
    getMintAddresses(candyMachineCreator[0]);

    如何从钱包获取所有 NFT?

    当从钱包获取所有 NFT 时,你需要获取所有的代币账户,然后解析出其中的 NFT。你可以使用 Metaplex JS 库中的 findDataByOwner 方法来完成这个过程。

    const connection = new Connection(clusterApiUrl("mainnet-beta"), "confirmed");
    const keypair = Keypair.generate();

    const metaplex = new Metaplex(connection);
    metaplex.use(keypairIdentity(keypair));

    const owner = new PublicKey("2R4bHmSBHkHAskerTHE6GE1Fxbn31kaD5gHqpsPySVd7");
    const allNFTs = await metaplex.nfts().findAllByOwner({ owner });

    console.log(allNFTs);

    Candy Machine v2

    Metaplex JS SDK 现在支持通过代码创建和更新Candy Machine v2。它使开发者能够与糖果机v2 程序进行交互,创建、更新和删除Candy Machine,并从中铸造(Mint) NFT。

    如何创建Candy Machine

    const { candyMachine } = await metaplex.candyMachinesV2().create({
    sellerFeeBasisPoints: 5, // 0.05% royalties
    price: sol(0.0001), // 0.0001 SOL
    itemsAvailable: toBigNumber(5), // 5 items available
    });

    /**
    * #1 Candy Machine ID - HSZxtWx6vgGWGsWu9SouXkHA2bAKCMtMZyMKzF2dvhrR
    */

    如何删除Candy Machine

    // creating a candy machine
    const { candyMachine } = await metaplex.candyMachinesV2().create({
    sellerFeeBasisPoints: 5, // 0.05% royalties
    price: sol(0.0001), // 0.0001 SOL
    itemsAvailable: toBigNumber(5), // 5 items available
    });

    console.log(`Candy Machine ID - ${candyMachine.address.toString()}`);

    // deleting the candy machine
    const { response } = await metaplex.candyMachinesV2().delete({
    candyMachine,
    });

    如何通过权限查找Candy Machine

    要查找所有权限为特定公钥的 Candy Machine,我们需要使用 findAllBy 函数,并将 type 参数设置为 authority

    const candyMachines = await metaplex.candyMachinesV2().findAllBy({
    type: "authority",
    publicKey: authority,
    });

    candyMachines.map((candyMachine, index) => {
    console.log(`#${index + 1} Candy Machine ID - ${candyMachine.address}`);
    });

    /**
    * #1 Candy Machine ID - HSZxtWx6vgGWGsWu9SouXkHA2bAKCMtMZyMKzF2dvhrR
    */

    如何通过钱包地址查找Candy Machine

    要通过钱包地址获取 Candy Machine 对象,我们需要使用 findAllBy 函数,并将 type 参数设置为 wallet。你可以从浏览器的 "Anchor data" 选项卡中获取 Candy Machine 的钱包地址。

    const candyMachines = await metaplex.candyMachinesV2().findAllBy({
    type: "wallet",
    publicKey: wallet,
    });

    candyMachines.map((candyMachine, index) => {
    console.log(`#${index + 1} Candy Machine ID - ${candyMachine.address}`);
    });

    如何通过Candy Machine的地址查找它

    要通过Candy Machine的地址查找Candy Machine,我们需要使用findByAddress 函数。

    const candyMachine = await metaplex.candyMachinesV2().findByAddress({
    address: candyMachineId,
    });

    如何从Candy Machine找到铸造(mint)的 NFT

    const candyMachine = await metaplex.candyMachinesV2().findMintedNfts({
    candyMachine: candyMachineId,
    });

    如何将物品插入到Candy Machine

    await metaplex.candyMachines().insertItems({
    candyMachineId,
    items: [
    { name: "My NFT #1", uri: "https://example.com/nft1" },
    { name: "My NFT #2", uri: "https://example.com/nft2" },
    { name: "My NFT #3", uri: "https://example.com/nft3" },
    ],
    });

    如何从Candy Machine铸造(Mint)一个 NFT

    默认情况下,铸造的 NFT 的所有者是metaplex.identity().publicKey。如果你希望将 NFT 铸造到其他钱包中,可以将新的钱包公钥与newOwner参数一起传递。

    // by default, the owner of the minted nft would be `metaplex.identity().publicKey`. if you want to mint the nft to some other wallet, pass that public key along with the `newOwner` parameter
    const candyMachine = await metaplex.candyMachinesV2().mint({
    candyMachine: candyMachineId,
    // newOwner: new PublicKey("some-other-public-key");
    });
    - - +你不能使用与你钱包不同的创作者信息来铸造(Mint) NFT。如果遇到创作者的问题,请确保你的元数据中将你列为创作者。

    如何获取 NFT 元数据

    Metaplex 的 NFT 元数据存储在 Arweave 上。为了获取 Arweave 的元数据,您需要获取 Metaplex PDA(程序派生账户)并对账户数据进行解码。

    const connection = new Connection(clusterApiUrl("mainnet-beta"));
    const keypair = Keypair.generate();

    const metaplex = new Metaplex(connection);
    metaplex.use(keypairIdentity(keypair));

    const mintAddress = new PublicKey(
    "Ay1U9DWphDgc7hq58Yj1yHabt91zTzvV2YJbAWkPNbaK"
    );

    const nft = await metaplex.nfts().findByMint({ mintAddress });

    console.log(nft.json);

    如何获取NFT的所有者

    如果你拥有 NFT 的铸币密钥,你可以通过查看该铸币密钥对应的最大代币账户来找到其当前所有者。

    请记住,NFT 的供应量为 1,它们是不可分割的,这意味着在任何时刻只有一个代币账户持有该代币,而其他所有与该铸币密钥相关的代币账户的余额都为 0。

    一旦确定了最大代币账户,我们可以获取它的所有者。

    const connection = new Connection("https://api.mainnet-beta.solana.com");
    const tokenMint = "9ARngHhVaCtH5JFieRdSS5Y8cdZk2TMF4tfGSWFB9iSK";
    const largestAccounts = await connection.getTokenLargestAccounts(
    new PublicKey(tokenMint)
    );
    const largestAccountInfo = await connection.getParsedAccountInfo(
    largestAccounts.value[0].address
    );
    console.log(largestAccountInfo.value.data.parsed.info.owner);

    如何获取 NFT 的铸币地址

    如果你知道Candy Machine的公钥,你可以使用以下代码获取从该Candy Machine生成的所有 NFT 铸币地址的列表。请注意,我们可以使用以下的 memcmp 过滤器,因为在 v1 版本中,第一个创作者的地址总是Candy Machine的地址。

    Candy Machine V1

    const getMintAddresses = async (firstCreatorAddress: PublicKey) => {
    const metadataAccounts = await connection.getProgramAccounts(
    TOKEN_METADATA_PROGRAM,
    {
    // The mint address is located at byte 33 and lasts for 32 bytes.
    dataSlice: { offset: 33, length: 32 },

    filters: [
    // Only get Metadata accounts.
    { dataSize: MAX_METADATA_LEN },

    // Filter using the first creator.
    {
    memcmp: {
    offset: CREATOR_ARRAY_START,
    bytes: firstCreatorAddress.toBase58(),
    },
    },
    ],
    }
    );

    return metadataAccounts.map((metadataAccountInfo) =>
    bs58.encode(metadataAccountInfo.account.data)
    );
    };

    getMintAddresses(candyMachineId);

    Candy Machine V2

    如果你正在使用Candy Machine v2,你首先需要访问其 "Candy Machine Creator" 地址,该地址是一个简单的 PDA,使用candy_machine和Candy Machine v2 地址作为种子生成。一旦你获得了创建者地址,你可以像对待 v1 版本一样使用它。

    const getCandyMachineCreator = async (
    candyMachine: PublicKey
    ): Promise<[PublicKey, number]> =>
    PublicKey.findProgramAddress(
    [Buffer.from("candy_machine"), candyMachine.toBuffer()],
    CANDY_MACHINE_V2_PROGRAM
    );

    const candyMachineCreator = await getCandyMachineCreator(candyMachineId);
    getMintAddresses(candyMachineCreator[0]);

    如何从钱包获取所有 NFT?

    当从钱包获取所有 NFT 时,你需要获取所有的代币账户,然后解析出其中的 NFT。你可以使用 Metaplex JS 库中的 findDataByOwner 方法来完成这个过程。

    const connection = new Connection(clusterApiUrl("mainnet-beta"), "confirmed");
    const keypair = Keypair.generate();

    const metaplex = new Metaplex(connection);
    metaplex.use(keypairIdentity(keypair));

    const owner = new PublicKey("2R4bHmSBHkHAskerTHE6GE1Fxbn31kaD5gHqpsPySVd7");
    const allNFTs = await metaplex.nfts().findAllByOwner({ owner });

    console.log(allNFTs);

    Candy Machine v2

    Metaplex JS SDK 现在支持通过代码创建和更新Candy Machine v2。它使开发者能够与糖果机v2 程序进行交互,创建、更新和删除Candy Machine,并从中铸造(Mint) NFT。

    如何创建Candy Machine

    const { candyMachine } = await metaplex.candyMachinesV2().create({
    sellerFeeBasisPoints: 5, // 0.05% royalties
    price: sol(0.0001), // 0.0001 SOL
    itemsAvailable: toBigNumber(5), // 5 items available
    });

    /**
    * #1 Candy Machine ID - HSZxtWx6vgGWGsWu9SouXkHA2bAKCMtMZyMKzF2dvhrR
    */

    如何删除Candy Machine

    // creating a candy machine
    const { candyMachine } = await metaplex.candyMachinesV2().create({
    sellerFeeBasisPoints: 5, // 0.05% royalties
    price: sol(0.0001), // 0.0001 SOL
    itemsAvailable: toBigNumber(5), // 5 items available
    });

    console.log(`Candy Machine ID - ${candyMachine.address.toString()}`);

    // deleting the candy machine
    const { response } = await metaplex.candyMachinesV2().delete({
    candyMachine,
    });

    如何通过权限查找Candy Machine

    要查找所有权限为特定公钥的 Candy Machine,我们需要使用 findAllBy 函数,并将 type 参数设置为 authority

    const candyMachines = await metaplex.candyMachinesV2().findAllBy({
    type: "authority",
    publicKey: authority,
    });

    candyMachines.map((candyMachine, index) => {
    console.log(`#${index + 1} Candy Machine ID - ${candyMachine.address}`);
    });

    /**
    * #1 Candy Machine ID - HSZxtWx6vgGWGsWu9SouXkHA2bAKCMtMZyMKzF2dvhrR
    */

    如何通过钱包地址查找Candy Machine

    要通过钱包地址获取 Candy Machine 对象,我们需要使用 findAllBy 函数,并将 type 参数设置为 wallet。你可以从浏览器的 "Anchor data" 选项卡中获取 Candy Machine 的钱包地址。

    const candyMachines = await metaplex.candyMachinesV2().findAllBy({
    type: "wallet",
    publicKey: wallet,
    });

    candyMachines.map((candyMachine, index) => {
    console.log(`#${index + 1} Candy Machine ID - ${candyMachine.address}`);
    });

    如何通过Candy Machine的地址查找它

    要通过Candy Machine的地址查找Candy Machine,我们需要使用findByAddress 函数。

    const candyMachine = await metaplex.candyMachinesV2().findByAddress({
    address: candyMachineId,
    });

    如何从Candy Machine找到铸造(mint)的 NFT

    const candyMachine = await metaplex.candyMachinesV2().findMintedNfts({
    candyMachine: candyMachineId,
    });

    如何将物品插入到Candy Machine

    await metaplex.candyMachines().insertItems({
    candyMachineId,
    items: [
    { name: "My NFT #1", uri: "https://example.com/nft1" },
    { name: "My NFT #2", uri: "https://example.com/nft2" },
    { name: "My NFT #3", uri: "https://example.com/nft3" },
    ],
    });

    如何从Candy Machine铸造(Mint)一个 NFT

    默认情况下,铸造的 NFT 的所有者是metaplex.identity().publicKey。如果你希望将 NFT 铸造到其他钱包中,可以将新的钱包公钥与newOwner参数一起传递。

    // by default, the owner of the minted nft would be `metaplex.identity().publicKey`. if you want to mint the nft to some other wallet, pass that public key along with the `newOwner` parameter
    const candyMachine = await metaplex.candyMachinesV2().mint({
    candyMachine: candyMachineId,
    // newOwner: new PublicKey("some-other-public-key");
    });
    + + \ No newline at end of file diff --git a/cookbook-zh/references/offline-transactions/index.html b/cookbook-zh/references/offline-transactions/index.html index 8cd290f57..8c302110d 100644 --- a/cookbook-zh/references/offline-transactions/index.html +++ b/cookbook-zh/references/offline-transactions/index.html @@ -9,13 +9,13 @@ - - + +
    -

    发送离线交易

    签署交易

    要创建离线交易,你需要签署交易,然后任何人都可以在网络上广播它。

    // there are two ways you can recover the tx
    // 3.a Recover Tranasction (use populate then addSignauture)
    {
    let recoverTx = Transaction.populate(Message.from(realDataNeedToSign));
    recoverTx.addSignature(feePayer.publicKey, Buffer.from(feePayerSignature));
    recoverTx.addSignature(alice.publicKey, Buffer.from(aliceSignature));

    // 4. Send transaction
    console.log(
    `txhash: ${await connection.sendRawTransaction(recoverTx.serialize())}`
    );
    }

    // or

    // 3.b. Recover Tranasction (use populate with signature)
    {
    let recoverTx = Transaction.populate(Message.from(realDataNeedToSign), [
    bs58.encode(feePayerSignature),
    bs58.encode(aliceSignature),
    ]);

    // 4. Send transaction
    console.log(
    `txhash: ${await connection.sendRawTransaction(recoverTx.serialize())}`
    );
    }

    部分签署交易

    当一个交易需要多个签名时,你可以部分签署它。其他签署者随后可以签署并在网络上广播该交易。

    以下是一些有用的情况示例:

    • 用支付作为交换发送 SPL 代币
    • 签署交易以便以后验证其真实性
    • 在需要你签名的自定义程序中调用交易

    在这个例子中,Bob给Alice发送了一个 SPL 代币,回报Alice的付款:

    // 1. Add an instruction to send the token from Bob to Alice
    transaction.add(
    createTransferCheckedInstruction(
    bobTokenAddress, // source
    tokenAddress, // mint
    aliceTokenAccount.address, // destination
    bobKeypair.publicKey, // owner of source account
    1 * 10 ** tokenMint.decimals, // amount to transfer
    tokenMint.decimals // decimals of token
    )
    );

    // 2. Bob partially signs the transaction
    transaction.partialSign(bobKeypair);

    // 3. Serialize the transaction without requiring all signatures
    const serializedTransaction = transaction.serialize({
    requireAllSignatures: false,
    });

    // 4. Alice can deserialize the transaction
    const recoveredTransaction = Transaction.from(
    Buffer.from(transactionBase64, "base64")
    );

    耐久性的 Nonce

    RecentBlockhash对于交易非常重要。如果你使用一个过期的最近区块哈希(在150个区块后),你的交易将被拒绝。你可以使用耐久性Nonce来获取一个永不过期的最近区块哈希。要触发这种机制,你的交易必须:

    1. 使用存储在nonce账户中的nonce作为最近的区块哈希。
    2. nonce advance操作放在第一个指令中。

    创建Nonce账户

    let tx = new Transaction().add(
    // create nonce account
    SystemProgram.createAccount({
    fromPubkey: feePayer.publicKey,
    newAccountPubkey: nonceAccount.publicKey,
    lamports: await connection.getMinimumBalanceForRentExemption(
    NONCE_ACCOUNT_LENGTH
    ),
    space: NONCE_ACCOUNT_LENGTH,
    programId: SystemProgram.programId,
    }),
    // init nonce account
    SystemProgram.nonceInitialize({
    noncePubkey: nonceAccount.publicKey, // nonce account pubkey
    authorizedPubkey: nonceAccountAuth.publicKey, // nonce account authority (for advance and close)
    })
    );

    console.log(
    `txhash: ${await connection.sendTransaction(tx, [feePayer, nonceAccount])}`
    );

    获取Nonce账户

    let accountInfo = await connection.getAccountInfo(nonceAccountPubkey);
    let nonceAccount = NonceAccount.fromAccountData(accountInfo.data);

    使用Nonce账户

    let tx = new Transaction().add(
    // nonce advance must be the first insturction
    SystemProgram.nonceAdvance({
    noncePubkey: nonceAccountPubkey,
    authorizedPubkey: nonceAccountAuth.publicKey,
    }),
    // after that, you do what you really want to do, here we append a transfer instruction as an example.
    SystemProgram.transfer({
    fromPubkey: feePayer.publicKey,
    toPubkey: nonceAccountAuth.publicKey,
    lamports: 1,
    })
    );
    // assign `nonce` as recentBlockhash
    tx.recentBlockhash = nonceAccount.nonce;
    tx.feePayer = feePayer.publicKey;
    tx.sign(
    feePayer,
    nonceAccountAuth
    ); /* fee payer + nonce account authority + ... */

    console.log(`txhash: ${await connection.sendRawTransaction(tx.serialize())}`);
    - - +

    发送离线交易

    签署交易

    要创建离线交易,你需要签署交易,然后任何人都可以在网络上广播它。

    // there are two ways you can recover the tx
    // 3.a Recover Tranasction (use populate then addSignauture)
    {
    let recoverTx = Transaction.populate(Message.from(realDataNeedToSign));
    recoverTx.addSignature(feePayer.publicKey, Buffer.from(feePayerSignature));
    recoverTx.addSignature(alice.publicKey, Buffer.from(aliceSignature));

    // 4. Send transaction
    console.log(
    `txhash: ${await connection.sendRawTransaction(recoverTx.serialize())}`
    );
    }

    // or

    // 3.b. Recover Tranasction (use populate with signature)
    {
    let recoverTx = Transaction.populate(Message.from(realDataNeedToSign), [
    bs58.encode(feePayerSignature),
    bs58.encode(aliceSignature),
    ]);

    // 4. Send transaction
    console.log(
    `txhash: ${await connection.sendRawTransaction(recoverTx.serialize())}`
    );
    }

    部分签署交易

    当一个交易需要多个签名时,你可以部分签署它。其他签署者随后可以签署并在网络上广播该交易。

    以下是一些有用的情况示例:

    • 用支付作为交换发送 SPL 代币
    • 签署交易以便以后验证其真实性
    • 在需要你签名的自定义程序中调用交易

    在这个例子中,Bob给Alice发送了一个 SPL 代币,回报Alice的付款:

    // 1. Add an instruction to send the token from Bob to Alice
    transaction.add(
    createTransferCheckedInstruction(
    bobTokenAddress, // source
    tokenAddress, // mint
    aliceTokenAccount.address, // destination
    bobKeypair.publicKey, // owner of source account
    1 * 10 ** tokenMint.decimals, // amount to transfer
    tokenMint.decimals // decimals of token
    )
    );

    // 2. Bob partially signs the transaction
    transaction.partialSign(bobKeypair);

    // 3. Serialize the transaction without requiring all signatures
    const serializedTransaction = transaction.serialize({
    requireAllSignatures: false,
    });

    // 4. Alice can deserialize the transaction
    const recoveredTransaction = Transaction.from(
    Buffer.from(transactionBase64, "base64")
    );

    耐久性的 Nonce

    RecentBlockhash对于交易非常重要。如果你使用一个过期的最近区块哈希(在150个区块后),你的交易将被拒绝。你可以使用耐久性Nonce来获取一个永不过期的最近区块哈希。要触发这种机制,你的交易必须:

    1. 使用存储在nonce账户中的nonce作为最近的区块哈希。
    2. nonce advance操作放在第一个指令中。

    创建Nonce账户

    let tx = new Transaction().add(
    // create nonce account
    SystemProgram.createAccount({
    fromPubkey: feePayer.publicKey,
    newAccountPubkey: nonceAccount.publicKey,
    lamports: await connection.getMinimumBalanceForRentExemption(
    NONCE_ACCOUNT_LENGTH
    ),
    space: NONCE_ACCOUNT_LENGTH,
    programId: SystemProgram.programId,
    }),
    // init nonce account
    SystemProgram.nonceInitialize({
    noncePubkey: nonceAccount.publicKey, // nonce account pubkey
    authorizedPubkey: nonceAccountAuth.publicKey, // nonce account authority (for advance and close)
    })
    );

    console.log(
    `txhash: ${await connection.sendTransaction(tx, [feePayer, nonceAccount])}`
    );

    获取Nonce账户

    let accountInfo = await connection.getAccountInfo(nonceAccountPubkey);
    let nonceAccount = NonceAccount.fromAccountData(accountInfo.data);

    使用Nonce账户

    let tx = new Transaction().add(
    // nonce advance must be the first insturction
    SystemProgram.nonceAdvance({
    noncePubkey: nonceAccountPubkey,
    authorizedPubkey: nonceAccountAuth.publicKey,
    }),
    // after that, you do what you really want to do, here we append a transfer instruction as an example.
    SystemProgram.transfer({
    fromPubkey: feePayer.publicKey,
    toPubkey: nonceAccountAuth.publicKey,
    lamports: 1,
    })
    );
    // assign `nonce` as recentBlockhash
    tx.recentBlockhash = nonceAccount.nonce;
    tx.feePayer = feePayer.publicKey;
    tx.sign(
    feePayer,
    nonceAccountAuth
    ); /* fee payer + nonce account authority + ... */

    console.log(`txhash: ${await connection.sendRawTransaction(tx.serialize())}`);
    + + \ No newline at end of file diff --git a/cookbook-zh/references/programs/index.html b/cookbook-zh/references/programs/index.html index 351868b55..7b0251fb7 100644 --- a/cookbook-zh/references/programs/index.html +++ b/cookbook-zh/references/programs/index.html @@ -9,13 +9,13 @@ - - + +
    -

    编写程序

    如何在程序中转移 SOL

    你的Solana程序可以在不"调用"系统程序的情况下将lamports从一个账户转移给另一个账户。基本规则是,你的程序可以将lamports从你的程序所拥有的任何账户转移到任何账户。

    接收方账户不一定要是你的程序所拥有的账户。

    /// Transfers lamports from one account (must be program owned)
    /// to another account. The recipient can by any account
    fn transfer_service_fee_lamports(
    from_account: &AccountInfo,
    to_account: &AccountInfo,
    amount_of_lamports: u64,
    ) -> ProgramResult {
    // Does the from account have enough lamports to transfer?
    if **from_account.try_borrow_lamports()? < amount_of_lamports {
    return Err(CustomError::InsufficientFundsForTransaction.into());
    }
    // Debit from_account and credit to_account
    **from_account.try_borrow_mut_lamports()? -= amount_of_lamports;
    **to_account.try_borrow_mut_lamports()? += amount_of_lamports;
    Ok(())
    }

    /// Primary function handler associated with instruction sent
    /// to your program
    fn instruction_handler(accounts: &[AccountInfo]) -> ProgramResult {
    // Get the 'from' and 'to' accounts
    let account_info_iter = &mut accounts.iter();
    let from_account = next_account_info(account_info_iter)?;
    let to_service_account = next_account_info(account_info_iter)?;

    // Extract a service 'fee' of 5 lamports for performing this instruction
    transfer_service_fee_lamports(from_account, to_service_account, 5u64)?;

    // Perform the primary instruction
    // ... etc.

    Ok(())
    }

    如何在程序中获取时钟

    获取时钟的方法有两种:

    1. SYSVAR_CLOCK_PUBKEY作为指令的参数传入。
    2. 在指令内部直接访问时钟。

    了解这两种方法会对你有好处,因为一些传统的程序仍然将SYSVAR_CLOCK_PUBKEY作为一个账户来使用。

    在指令中将时钟作为一个账户传递

    让我们创建一个指令,该指令接收一个账户用于初始化,并接收 SYSVAR 的公钥。

    let clock = Clock::from_account_info(&sysvar_clock_pubkey)?;
    let current_timestamp = clock.unix_timestamp;

    现在,我们通过客户端传递时钟的 SYSVAR 公共地址:

    (async () => {
    const programId = new PublicKey(
    "77ezihTV6mTh2Uf3ggwbYF2NyGJJ5HHah1GrdowWJVD3"
    );

    // Passing Clock Sys Var
    const passClockIx = new TransactionInstruction({
    programId: programId,
    keys: [
    {
    isSigner: false,
    isWritable: true,
    pubkey: helloAccount.publicKey,
    },
    {
    is_signer: false,
    is_writable: false,
    pubkey: SYSVAR_CLOCK_PUBKEY,
    },
    ],
    });

    const transaction = new Transaction();
    transaction.add(passClockIx);

    const txHash = await connection.sendTransaction(transaction, [
    feePayer,
    helloAccount,
    ]);

    console.log(`Transaction succeeded. TxHash: ${txHash}`);
    })();

    在指令内部直接访问时钟

    让我们创建同样的指令,但这次我们不需要从客户端传递SYSVAR_CLOCK_PUBKEY

    let clock = Clock::get()?;
    let current_timestamp = clock.unix_timestamp;

    现在,客户端只需要传递状态和支付账户的指令:

    (async () => {
    const programId = new PublicKey(
    "4ZEdbCtb5UyCSiAMHV5eSHfyjq3QwbG3yXb6oHD7RYjk"
    );

    // No more requirement to pass clock sys var key
    const initAccountIx = new TransactionInstruction({
    programId: programId,
    keys: [
    {
    isSigner: false,
    isWritable: true,
    pubkey: helloAccount.publicKey,
    },
    ],
    });

    const transaction = new Transaction();
    transaction.add(initAccountIx);

    const txHash = await connection.sendTransaction(transaction, [
    feePayer,
    helloAccount,
    ]);

    console.log(`Transaction succeeded. TxHash: ${txHash}`);
    })();

    如何更改账户大小

    你可以使用realloc函数来更改程序拥有的账户的大小。realloc函数可以将账户的大小调整到最大10KB。当你使用realloc增加账户的大小时,你需要转移lamports以保持该账户的租金免除状态。

    // adding a publickey to the account
    let new_size = pda_account.data.borrow().len() + 32;

    let rent = Rent::get()?;
    let new_minimum_balance = rent.minimum_balance(new_size);

    let lamports_diff = new_minimum_balance.saturating_sub(pda_account.lamports());
    invoke(
    &system_instruction::transfer(funding_account.key, pda_account.key, lamports_diff),
    &[
    funding_account.clone(),
    pda_account.clone(),
    system_program.clone(),
    ],
    )?;

    pda_account.realloc(new_size, false)?;

    跨程序调用的方法

    跨程序调用,简单来说,就是在我们的程序中调用另一个程序的指令。一个很好的例子是Uniswapswap功能。UniswapV2Router合约调用必要的逻辑进行交换,并调用ERC20合约的transfer函数将代币从一个人转移到另一个人。同样的方式,我们可以调用程序的指令来实现多种目的。

    让我们来看看我们的第一个例子,即SPL Token Programtransfer指令。进行转账所需的账户包括:

    1. 源代币账户(我们持有代币的账户)
    2. 目标代币账户(我们要将代币转移至的账户)
    3. 源代币账户的持有者(我们将为其签名的钱包地址)
    let token_transfer_amount = instruction_data
    .get(..8)
    .and_then(|slice| slice.try_into().ok())
    .map(u64::from_le_bytes)
    .ok_or(ProgramError::InvalidAccountData)?;

    let transfer_tokens_instruction = transfer(
    &token_program.key,
    &source_token_account.key,
    &destination_token_account.key,
    &source_token_account_holder.key,
    &[&source_token_account_holder.key],
    token_transfer_amount,
    )?;

    let required_accounts_for_transfer = [
    source_token_account.clone(),
    destination_token_account.clone(),
    source_token_account_holder.clone(),
    ];

    invoke(
    &transfer_tokens_instruction,
    &required_accounts_for_transfer,
    )?;

    相应的客户端指令如下所示。有关了解铸币和代币创建指令,请参考附近的完整代码。

    (async () => {
    const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
    const programId = new PublicKey(
    "EfYK91eN3AqTwY1C34W6a33qGAtQ8HJYVhNv7cV4uMZj"
    );

    const transferTokensIx = new TransactionInstruction({
    programId: programId,
    data: TOKEN_TRANSFER_AMOUNT_BUFFER,
    keys: [
    {
    isSigner: false,
    isWritable: true,
    pubkey: SOURCE_TOKEN_ACCOUNT.publicKey,
    },
    {
    isSigner: false,
    isWritable: true,
    pubkey: DESTINATION_TOKEN_ACCOUNT.publicKey,
    },
    {
    isSigner: true,
    isWritable: true,
    pubkey: PAYER_KEYPAIR.publicKey,
    },
    {
    isSigner: false,
    isWritable: false,
    pubkey: TOKEN_PROGRAM_ID,
    },
    ],
    });

    const transaction = new Transaction();
    transaction.add(transferTokensIx);

    const txHash = await connection.sendTransaction(transaction, [
    PAYER_KEYPAIR,
    TOKEN_MINT_ACCOUNT,
    SOURCE_TOKEN_ACCOUNT,
    DESTINATION_TOKEN_ACCOUNT,
    ]);

    console.log(`Token transfer CPI success: ${txHash}`);
    })();

    现在让我们来看另一个例子,即System Programcreate_account指令。这里与上面提到的指令有一点不同。在上述例子中,我们不需要在invoke函数中将token_program作为账户之一传递。然而,在某些情况下,您需要传递调用指令的program_id。在我们的例子中,它将是System Programprogram_id("11111111111111111111111111111111")。所以现在所需的账户包括:

    let account_span = instruction_data
    .get(..8)
    .and_then(|slice| slice.try_into().ok())
    .map(u64::from_le_bytes)
    .ok_or(ProgramError::InvalidAccountData)?;

    let lamports_required = (Rent::get()?).minimum_balance(account_span as usize);

    let create_account_instruction = create_account(
    &payer_account.key,
    &general_state_account.key,
    lamports_required,
    account_span,
    program_id,
    );

    let required_accounts_for_create = [
    payer_account.clone(),
    general_state_account.clone(),
    system_program.clone(),
    ];

    invoke(&create_account_instruction, &required_accounts_for_create)?;

    对应的客户端代码如下所示:

    (async () => {
    const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
    const programId = new PublicKey(
    "DkuQ5wsndkzXfgqDB6Lgf4sDjBi4gkLSak1dM5Mn2RuQ"
    );

    // Airdropping some SOL
    await connection.confirmTransaction(
    await connection.requestAirdrop(PAYER_KEYPAIR.publicKey, LAMPORTS_PER_SOL)
    );

    // Our program's CPI instruction (create_account)
    const creataAccountIx = new TransactionInstruction({
    programId: programId,
    data: ACCOUNT_SPACE_BUFFER,
    keys: [
    {
    isSigner: true,
    isWritable: true,
    pubkey: PAYER_KEYPAIR.publicKey,
    },
    {
    isSigner: true,
    isWritable: true,
    pubkey: GENERAL_STATE_KEYPAIR.publicKey,
    },
    {
    isSigner: false,
    isWritable: false,
    pubkey: SystemProgram.programId,
    },
    ],
    });

    const transaction = new Transaction();
    // Adding up all the above instructions
    transaction.add(creataAccountIx);

    const txHash = await connection.sendTransaction(transaction, [
    PAYER_KEYPAIR,
    GENERAL_STATE_KEYPAIR,
    ]);

    console.log(`Create Account CPI Success: ${txHash}`);
    })();

    如何创建PDA

    程序派生地址(Program Derived Address,PDA)是程序拥有的账户,但没有私钥。相反,它的签名是通过一组种子和一个阻碍值(一个确保其不在曲线上的随机数)获取的。"生成"程序地址与"创建"它是不同的。可以使用Pubkey::find_program_address来生成PDA。创建PDA实质上意味着初始化该地址的空间并将其状态设置为初始状态。普通的密钥对账户可以在我们的程序之外创建,然后将其用于初始化PDA的状态。不幸的是,对于PDA来说,它必须在链上创建,因为它本身无法代表自己进行签名。因此,我们使用invoke_signed来传递PDA的种子,以及资金账户的签名,从而实现了PDA的账户创建。

    let create_pda_account_ix = system_instruction::create_account(
    &funding_account.key,
    &pda_account.key,
    lamports_required,
    ACCOUNT_DATA_LEN.try_into().unwrap(),
    &program_id,
    );

    invoke_signed(
    &create_pda_account_ix,
    &[funding_account.clone(), pda_account.clone()],
    &[signers_seeds],
    )?;

    可以通过客户端按如下方式发送所需的账户:

    const PAYER_KEYPAIR = Keypair.generate();

    (async () => {
    const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
    const programId = new PublicKey(
    "6eW5nnSosr2LpkUGCdznsjRGDhVb26tLmiM1P8RV1QQp"
    );

    const [pda, bump] = await PublicKey.findProgramAddress(
    [Buffer.from("customaddress"), PAYER_KEYPAIR.publicKey.toBuffer()],
    programId
    );

    const createPDAIx = new TransactionInstruction({
    programId: programId,
    data: Buffer.from(Uint8Array.of(bump)),
    keys: [
    {
    isSigner: true,
    isWritable: true,
    pubkey: PAYER_KEYPAIR.publicKey,
    },
    {
    isSigner: false,
    isWritable: true,
    pubkey: pda,
    },
    {
    isSigner: false,
    isWritable: false,
    pubkey: SystemProgram.programId,
    },
    ],
    });

    const transaction = new Transaction();
    transaction.add(createPDAIx);

    const txHash = await connection.sendTransaction(transaction, [PAYER_KEYPAIR]);
    })();

    如何读取账户

    在Solana中,几乎所有的指令都至少需要2-3个账户,并且在指令处理程序中会说明它期望的账户顺序。如果我们利用Rust中的iter()方法,而不是手动索引账户,那么这将非常简单。next_account_info方法基本上是对可迭代对象的第一个索引进行切片,并返回账户数组中存在的账户。让我们看一个简单的指令,它期望一堆账户并需要解析每个账户。

    pub fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    _instruction_data: &[u8],
    ) -> ProgramResult {
    // Fetching all the accounts as a iterator (facilitating for loops and iterations)
    let accounts_iter = &mut accounts.iter();
    // Payer account
    let payer_account = next_account_info(accounts_iter)?;
    // Hello state account
    let hello_state_account = next_account_info(accounts_iter)?;
    // Rent account
    let rent_account = next_account_info(accounts_iter)?;
    // System Program
    let system_program = next_account_info(accounts_iter)?;

    Ok(())
    }

    如何验证账户

    由于Solana中的程序是无状态的,作为程序创建者,我们必须尽可能验证传递的账户,以避免任何恶意账户的进入。可以进行的基本检查包括:

    1. 检查预期的签名账户是否已签名。
    2. 检查预期的状态账户是否已标记为可写。
    3. 检查预期的状态账户的所有者是否为调用程序的程序ID。
    4. 如果首次初始化状态,请检查账户是否已经初始化。
    5. 检查是否按预期传递了任何跨程序的ID(在需要时)。

    下面是一个基本的指令,它使用上述检查初始化英雄状态账户的示例:

    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    _instruction_data: &[u8],
    ) -> ProgramResult {
    let accounts_iter = &mut accounts.iter();
    let payer_account = next_account_info(accounts_iter)?;
    let hello_state_account = next_account_info(accounts_iter)?;
    let system_program = next_account_info(accounts_iter)?;

    let rent = Rent::get()?;

    // Checking if payer account is the signer
    if !payer_account.is_signer {
    return Err(ProgramError::MissingRequiredSignature);
    }

    // Checking if hello state account is rent exempt
    if !rent.is_exempt(hello_state_account.lamports(), 1) {
    return Err(ProgramError::AccountNotRentExempt);
    }

    // Checking if hello state account is writable
    if !hello_state_account.is_writable {
    return Err(ProgramError::InvalidAccountData);
    }

    // Checking if hello state account's owner is the current program
    if hello_state_account.owner.ne(&program_id) {
    return Err(ProgramError::IllegalOwner);
    }

    // Checking if the system program is valid
    if system_program.key.ne(&SYSTEM_PROGRAM_ID) {
    return Err(ProgramError::IncorrectProgramId);
    }

    let mut hello_state = HelloState::try_from_slice(&hello_state_account.data.borrow())?;

    // Checking if the state has already been initialized
    if hello_state.is_initialized {
    return Err(ProgramError::AccountAlreadyInitialized);
    }

    hello_state.is_initialized = true;
    hello_state.serialize(&mut &mut hello_state_account.data.borrow_mut()[..])?;
    msg!("Account initialized :)");

    Ok(())
    }

    如何从一个交易中读取多个指令

    Solana允许我们查看当前交易中的所有指令。我们可以将它们存储在一个变量中,并对其进行迭代。我们可以利用这一点做许多事情,比如检查可疑的交易。

    let mut idx = 0;
    let num_instructions = read_u16(&mut idx, &instruction_sysvar)
    .map_err(|_| MyError::NoInstructionFound)?;


    for index in 0..num_instructions {

    let mut current = 2 + (index * 2) as usize;
    let start = read_u16(&mut current, &instruction_sysvar).unwrap();

    current = start as usize;
    let num_accounts = read_u16(&mut current, &instruction_sysvar).unwrap();
    current += (num_accounts as usize) * (1 + 32);

    }
    - - +

    编写程序

    如何在程序中转移 SOL

    你的Solana程序可以在不"调用"系统程序的情况下将lamports从一个账户转移给另一个账户。基本规则是,你的程序可以将lamports从你的程序所拥有的任何账户转移到任何账户。

    接收方账户不一定要是你的程序所拥有的账户。

    /// Transfers lamports from one account (must be program owned)
    /// to another account. The recipient can by any account
    fn transfer_service_fee_lamports(
    from_account: &AccountInfo,
    to_account: &AccountInfo,
    amount_of_lamports: u64,
    ) -> ProgramResult {
    // Does the from account have enough lamports to transfer?
    if **from_account.try_borrow_lamports()? < amount_of_lamports {
    return Err(CustomError::InsufficientFundsForTransaction.into());
    }
    // Debit from_account and credit to_account
    **from_account.try_borrow_mut_lamports()? -= amount_of_lamports;
    **to_account.try_borrow_mut_lamports()? += amount_of_lamports;
    Ok(())
    }

    /// Primary function handler associated with instruction sent
    /// to your program
    fn instruction_handler(accounts: &[AccountInfo]) -> ProgramResult {
    // Get the 'from' and 'to' accounts
    let account_info_iter = &mut accounts.iter();
    let from_account = next_account_info(account_info_iter)?;
    let to_service_account = next_account_info(account_info_iter)?;

    // Extract a service 'fee' of 5 lamports for performing this instruction
    transfer_service_fee_lamports(from_account, to_service_account, 5u64)?;

    // Perform the primary instruction
    // ... etc.

    Ok(())
    }

    如何在程序中获取时钟

    获取时钟的方法有两种:

    1. SYSVAR_CLOCK_PUBKEY作为指令的参数传入。
    2. 在指令内部直接访问时钟。

    了解这两种方法会对你有好处,因为一些传统的程序仍然将SYSVAR_CLOCK_PUBKEY作为一个账户来使用。

    在指令中将时钟作为一个账户传递

    让我们创建一个指令,该指令接收一个账户用于初始化,并接收 SYSVAR 的公钥。

    let clock = Clock::from_account_info(&sysvar_clock_pubkey)?;
    let current_timestamp = clock.unix_timestamp;

    现在,我们通过客户端传递时钟的 SYSVAR 公共地址:

    (async () => {
    const programId = new PublicKey(
    "77ezihTV6mTh2Uf3ggwbYF2NyGJJ5HHah1GrdowWJVD3"
    );

    // Passing Clock Sys Var
    const passClockIx = new TransactionInstruction({
    programId: programId,
    keys: [
    {
    isSigner: false,
    isWritable: true,
    pubkey: helloAccount.publicKey,
    },
    {
    is_signer: false,
    is_writable: false,
    pubkey: SYSVAR_CLOCK_PUBKEY,
    },
    ],
    });

    const transaction = new Transaction();
    transaction.add(passClockIx);

    const txHash = await connection.sendTransaction(transaction, [
    feePayer,
    helloAccount,
    ]);

    console.log(`Transaction succeeded. TxHash: ${txHash}`);
    })();

    在指令内部直接访问时钟

    让我们创建同样的指令,但这次我们不需要从客户端传递SYSVAR_CLOCK_PUBKEY

    let clock = Clock::get()?;
    let current_timestamp = clock.unix_timestamp;

    现在,客户端只需要传递状态和支付账户的指令:

    (async () => {
    const programId = new PublicKey(
    "4ZEdbCtb5UyCSiAMHV5eSHfyjq3QwbG3yXb6oHD7RYjk"
    );

    // No more requirement to pass clock sys var key
    const initAccountIx = new TransactionInstruction({
    programId: programId,
    keys: [
    {
    isSigner: false,
    isWritable: true,
    pubkey: helloAccount.publicKey,
    },
    ],
    });

    const transaction = new Transaction();
    transaction.add(initAccountIx);

    const txHash = await connection.sendTransaction(transaction, [
    feePayer,
    helloAccount,
    ]);

    console.log(`Transaction succeeded. TxHash: ${txHash}`);
    })();

    如何更改账户大小

    你可以使用realloc函数来更改程序拥有的账户的大小。realloc函数可以将账户的大小调整到最大10KB。当你使用realloc增加账户的大小时,你需要转移lamports以保持该账户的租金免除状态。

    // adding a publickey to the account
    let new_size = pda_account.data.borrow().len() + 32;

    let rent = Rent::get()?;
    let new_minimum_balance = rent.minimum_balance(new_size);

    let lamports_diff = new_minimum_balance.saturating_sub(pda_account.lamports());
    invoke(
    &system_instruction::transfer(funding_account.key, pda_account.key, lamports_diff),
    &[
    funding_account.clone(),
    pda_account.clone(),
    system_program.clone(),
    ],
    )?;

    pda_account.realloc(new_size, false)?;

    跨程序调用的方法

    跨程序调用,简单来说,就是在我们的程序中调用另一个程序的指令。一个很好的例子是Uniswapswap功能。UniswapV2Router合约调用必要的逻辑进行交换,并调用ERC20合约的transfer函数将代币从一个人转移到另一个人。同样的方式,我们可以调用程序的指令来实现多种目的。

    让我们来看看我们的第一个例子,即SPL Token Programtransfer指令。进行转账所需的账户包括:

    1. 源代币账户(我们持有代币的账户)
    2. 目标代币账户(我们要将代币转移至的账户)
    3. 源代币账户的持有者(我们将为其签名的钱包地址)
    let token_transfer_amount = instruction_data
    .get(..8)
    .and_then(|slice| slice.try_into().ok())
    .map(u64::from_le_bytes)
    .ok_or(ProgramError::InvalidAccountData)?;

    let transfer_tokens_instruction = transfer(
    &token_program.key,
    &source_token_account.key,
    &destination_token_account.key,
    &source_token_account_holder.key,
    &[&source_token_account_holder.key],
    token_transfer_amount,
    )?;

    let required_accounts_for_transfer = [
    source_token_account.clone(),
    destination_token_account.clone(),
    source_token_account_holder.clone(),
    ];

    invoke(
    &transfer_tokens_instruction,
    &required_accounts_for_transfer,
    )?;

    相应的客户端指令如下所示。有关了解铸币和代币创建指令,请参考附近的完整代码。

    (async () => {
    const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
    const programId = new PublicKey(
    "EfYK91eN3AqTwY1C34W6a33qGAtQ8HJYVhNv7cV4uMZj"
    );

    const transferTokensIx = new TransactionInstruction({
    programId: programId,
    data: TOKEN_TRANSFER_AMOUNT_BUFFER,
    keys: [
    {
    isSigner: false,
    isWritable: true,
    pubkey: SOURCE_TOKEN_ACCOUNT.publicKey,
    },
    {
    isSigner: false,
    isWritable: true,
    pubkey: DESTINATION_TOKEN_ACCOUNT.publicKey,
    },
    {
    isSigner: true,
    isWritable: true,
    pubkey: PAYER_KEYPAIR.publicKey,
    },
    {
    isSigner: false,
    isWritable: false,
    pubkey: TOKEN_PROGRAM_ID,
    },
    ],
    });

    const transaction = new Transaction();
    transaction.add(transferTokensIx);

    const txHash = await connection.sendTransaction(transaction, [
    PAYER_KEYPAIR,
    TOKEN_MINT_ACCOUNT,
    SOURCE_TOKEN_ACCOUNT,
    DESTINATION_TOKEN_ACCOUNT,
    ]);

    console.log(`Token transfer CPI success: ${txHash}`);
    })();

    现在让我们来看另一个例子,即System Programcreate_account指令。这里与上面提到的指令有一点不同。在上述例子中,我们不需要在invoke函数中将token_program作为账户之一传递。然而,在某些情况下,您需要传递调用指令的program_id。在我们的例子中,它将是System Programprogram_id("11111111111111111111111111111111")。所以现在所需的账户包括:

    let account_span = instruction_data
    .get(..8)
    .and_then(|slice| slice.try_into().ok())
    .map(u64::from_le_bytes)
    .ok_or(ProgramError::InvalidAccountData)?;

    let lamports_required = (Rent::get()?).minimum_balance(account_span as usize);

    let create_account_instruction = create_account(
    &payer_account.key,
    &general_state_account.key,
    lamports_required,
    account_span,
    program_id,
    );

    let required_accounts_for_create = [
    payer_account.clone(),
    general_state_account.clone(),
    system_program.clone(),
    ];

    invoke(&create_account_instruction, &required_accounts_for_create)?;

    对应的客户端代码如下所示:

    (async () => {
    const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
    const programId = new PublicKey(
    "DkuQ5wsndkzXfgqDB6Lgf4sDjBi4gkLSak1dM5Mn2RuQ"
    );

    // Airdropping some SOL
    await connection.confirmTransaction(
    await connection.requestAirdrop(PAYER_KEYPAIR.publicKey, LAMPORTS_PER_SOL)
    );

    // Our program's CPI instruction (create_account)
    const creataAccountIx = new TransactionInstruction({
    programId: programId,
    data: ACCOUNT_SPACE_BUFFER,
    keys: [
    {
    isSigner: true,
    isWritable: true,
    pubkey: PAYER_KEYPAIR.publicKey,
    },
    {
    isSigner: true,
    isWritable: true,
    pubkey: GENERAL_STATE_KEYPAIR.publicKey,
    },
    {
    isSigner: false,
    isWritable: false,
    pubkey: SystemProgram.programId,
    },
    ],
    });

    const transaction = new Transaction();
    // Adding up all the above instructions
    transaction.add(creataAccountIx);

    const txHash = await connection.sendTransaction(transaction, [
    PAYER_KEYPAIR,
    GENERAL_STATE_KEYPAIR,
    ]);

    console.log(`Create Account CPI Success: ${txHash}`);
    })();

    如何创建PDA

    程序派生地址(Program Derived Address,PDA)是程序拥有的账户,但没有私钥。相反,它的签名是通过一组种子和一个阻碍值(一个确保其不在曲线上的随机数)获取的。"生成"程序地址与"创建"它是不同的。可以使用Pubkey::find_program_address来生成PDA。创建PDA实质上意味着初始化该地址的空间并将其状态设置为初始状态。普通的密钥对账户可以在我们的程序之外创建,然后将其用于初始化PDA的状态。不幸的是,对于PDA来说,它必须在链上创建,因为它本身无法代表自己进行签名。因此,我们使用invoke_signed来传递PDA的种子,以及资金账户的签名,从而实现了PDA的账户创建。

    let create_pda_account_ix = system_instruction::create_account(
    &funding_account.key,
    &pda_account.key,
    lamports_required,
    ACCOUNT_DATA_LEN.try_into().unwrap(),
    &program_id,
    );

    invoke_signed(
    &create_pda_account_ix,
    &[funding_account.clone(), pda_account.clone()],
    &[signers_seeds],
    )?;

    可以通过客户端按如下方式发送所需的账户:

    const PAYER_KEYPAIR = Keypair.generate();

    (async () => {
    const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
    const programId = new PublicKey(
    "6eW5nnSosr2LpkUGCdznsjRGDhVb26tLmiM1P8RV1QQp"
    );

    const [pda, bump] = await PublicKey.findProgramAddress(
    [Buffer.from("customaddress"), PAYER_KEYPAIR.publicKey.toBuffer()],
    programId
    );

    const createPDAIx = new TransactionInstruction({
    programId: programId,
    data: Buffer.from(Uint8Array.of(bump)),
    keys: [
    {
    isSigner: true,
    isWritable: true,
    pubkey: PAYER_KEYPAIR.publicKey,
    },
    {
    isSigner: false,
    isWritable: true,
    pubkey: pda,
    },
    {
    isSigner: false,
    isWritable: false,
    pubkey: SystemProgram.programId,
    },
    ],
    });

    const transaction = new Transaction();
    transaction.add(createPDAIx);

    const txHash = await connection.sendTransaction(transaction, [PAYER_KEYPAIR]);
    })();

    如何读取账户

    在Solana中,几乎所有的指令都至少需要2-3个账户,并且在指令处理程序中会说明它期望的账户顺序。如果我们利用Rust中的iter()方法,而不是手动索引账户,那么这将非常简单。next_account_info方法基本上是对可迭代对象的第一个索引进行切片,并返回账户数组中存在的账户。让我们看一个简单的指令,它期望一堆账户并需要解析每个账户。

    pub fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    _instruction_data: &[u8],
    ) -> ProgramResult {
    // Fetching all the accounts as a iterator (facilitating for loops and iterations)
    let accounts_iter = &mut accounts.iter();
    // Payer account
    let payer_account = next_account_info(accounts_iter)?;
    // Hello state account
    let hello_state_account = next_account_info(accounts_iter)?;
    // Rent account
    let rent_account = next_account_info(accounts_iter)?;
    // System Program
    let system_program = next_account_info(accounts_iter)?;

    Ok(())
    }

    如何验证账户

    由于Solana中的程序是无状态的,作为程序创建者,我们必须尽可能验证传递的账户,以避免任何恶意账户的进入。可以进行的基本检查包括:

    1. 检查预期的签名账户是否已签名。
    2. 检查预期的状态账户是否已标记为可写。
    3. 检查预期的状态账户的所有者是否为调用程序的程序ID。
    4. 如果首次初始化状态,请检查账户是否已经初始化。
    5. 检查是否按预期传递了任何跨程序的ID(在需要时)。

    下面是一个基本的指令,它使用上述检查初始化英雄状态账户的示例:

    pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    _instruction_data: &[u8],
    ) -> ProgramResult {
    let accounts_iter = &mut accounts.iter();
    let payer_account = next_account_info(accounts_iter)?;
    let hello_state_account = next_account_info(accounts_iter)?;
    let system_program = next_account_info(accounts_iter)?;

    let rent = Rent::get()?;

    // Checking if payer account is the signer
    if !payer_account.is_signer {
    return Err(ProgramError::MissingRequiredSignature);
    }

    // Checking if hello state account is rent exempt
    if !rent.is_exempt(hello_state_account.lamports(), 1) {
    return Err(ProgramError::AccountNotRentExempt);
    }

    // Checking if hello state account is writable
    if !hello_state_account.is_writable {
    return Err(ProgramError::InvalidAccountData);
    }

    // Checking if hello state account's owner is the current program
    if hello_state_account.owner.ne(&program_id) {
    return Err(ProgramError::IllegalOwner);
    }

    // Checking if the system program is valid
    if system_program.key.ne(&SYSTEM_PROGRAM_ID) {
    return Err(ProgramError::IncorrectProgramId);
    }

    let mut hello_state = HelloState::try_from_slice(&hello_state_account.data.borrow())?;

    // Checking if the state has already been initialized
    if hello_state.is_initialized {
    return Err(ProgramError::AccountAlreadyInitialized);
    }

    hello_state.is_initialized = true;
    hello_state.serialize(&mut &mut hello_state_account.data.borrow_mut()[..])?;
    msg!("Account initialized :)");

    Ok(())
    }

    如何从一个交易中读取多个指令

    Solana允许我们查看当前交易中的所有指令。我们可以将它们存储在一个变量中,并对其进行迭代。我们可以利用这一点做许多事情,比如检查可疑的交易。

    let mut idx = 0;
    let num_instructions = read_u16(&mut idx, &instruction_sysvar)
    .map_err(|_| MyError::NoInstructionFound)?;


    for index in 0..num_instructions {

    let mut current = 2 + (index * 2) as usize;
    let start = read_u16(&mut current, &instruction_sysvar).unwrap();

    current = start as usize;
    let num_accounts = read_u16(&mut current, &instruction_sysvar).unwrap();
    current += (num_accounts as usize) * (1 + 32);

    }
    + + \ No newline at end of file diff --git a/cookbook-zh/references/staking/index.html b/cookbook-zh/references/staking/index.html index 532fe2038..377f68a6f 100644 --- a/cookbook-zh/references/staking/index.html +++ b/cookbook-zh/references/staking/index.html @@ -9,13 +9,13 @@ - - + +
    -

    质押

    获取当前验证器

    我们可以质押 SOL 并通过帮助保护网络来获得奖励。要进行质押,我们将 SOL 委托给验证器,而验证器则处理交易。

    import { clusterApiUrl, Connection } from "@solana/web3.js";

    (async () => {
    const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

    // Get all validators, categorized by current (i.e. active) and deliquent (i.e. inactive)
    const { current, delinquent } = await connection.getVoteAccounts();
    console.log("current validators: ", current);
    console.log("all validators: ", current.concat(delinquent));
    })();

    创建质押账户

    所有的质押指令由质押程序 (Stake Program) 处理。首先,我们创建一个质押账户, 该账户与标准系统账户创建和管理方式不同。特别是,我们需要设置账户的Stake AuthorityWithdrawal Authority

    // Setup a transaction to create our stake account
    // Note: `StakeProgram.createAccount` returns a `Transaction` preconfigured with the necessary `TransactionInstruction`s
    const createStakeAccountTx = StakeProgram.createAccount({
    authorized: new Authorized(wallet.publicKey, wallet.publicKey), // Here we set two authorities: Stake Authority and Withdrawal Authority. Both are set to our wallet.
    fromPubkey: wallet.publicKey,
    lamports: amountToStake,
    lockup: new Lockup(0, 0, wallet.publicKey), // Optional. We'll set this to 0 for demonstration purposes.
    stakePubkey: stakeAccount.publicKey,
    });

    const createStakeAccountTxId = await sendAndConfirmTransaction(
    connection,
    createStakeAccountTx,
    [
    wallet,
    stakeAccount, // Since we're creating a new stake account, we have that account sign as well
    ]
    );
    console.log(`Stake account created. Tx Id: ${createStakeAccountTxId}`);

    // Check our newly created stake account balance. This should be 0.5 SOL.
    let stakeBalance = await connection.getBalance(stakeAccount.publicKey);
    console.log(`Stake account balance: ${stakeBalance / LAMPORTS_PER_SOL} SOL`);

    // Verify the status of our stake account. This will start as inactive and will take some time to activate.
    let stakeStatus = await connection.getStakeActivation(stakeAccount.publicKey);
    console.log(`Stake account status: ${stakeStatus.state}`);

    委托质押

    一旦质押账户得到资金支持,Stake Authority可以将其委托给一个验证者。每个质押账户一次只能委托给一个验证者。此外,账户中的所有代币必须要么被委托,要么取消委托。一旦委托成功,质押账户需要经过几个时期才能变为活跃状态。

    // With a validator selected, we can now setup a transaction that delegates our stake to their vote account.
    const delegateTx = StakeProgram.delegate({
    stakePubkey: stakeAccount.publicKey,
    authorizedPubkey: wallet.publicKey,
    votePubkey: selectedValidatorPubkey,
    });

    const delegateTxId = await sendAndConfirmTransaction(connection, delegateTx, [
    wallet,
    ]);
    console.log(
    `Stake account delegated to ${selectedValidatorPubkey}. Tx Id: ${delegateTxId}`
    );

    // Check in on our stake account. It should now be activating.
    stakeStatus = await connection.getStakeActivation(stakeAccount.publicKey);
    console.log(`Stake account status: ${stakeStatus.state}`);

    通过验证器获取委托人

    多个账户可能已经质押给了特定的验证账户。为了获取所有的质押人,我们可以使用 getProgramAccountsgetParsedProgramAccounts API。请参考指南部分 获取更多信息。质押账户长度为200字节,选民公钥从第124字节开始。参考资料

    const STAKE_PROGRAM_ID = new PublicKey(
    "Stake11111111111111111111111111111111111111"
    );
    const VOTE_PUB_KEY = "27MtjMSAQ2BGkXNuJEJkxFyCJT8dugGAaHJ9T7Gc6x4x";

    const connection = new Connection(clusterApiUrl("mainnet-beta"), "confirmed");
    const accounts = await connection.getParsedProgramAccounts(STAKE_PROGRAM_ID, {
    filters: [
    {
    dataSize: 200, // number of bytes
    },
    {
    memcmp: {
    offset: 124, // number of bytes
    bytes: VOTE_PUB_KEY, // base58 encoded string
    },
    },
    ],
    });

    console.log(`Accounts for program ${STAKE_PROGRAM_ID}: `);
    console.log(
    `Total number of delegators found for ${VOTE_PUB_KEY} is: ${accounts.length}`
    );
    if (accounts.length)
    console.log(`Sample delegator:`, JSON.stringify(accounts[0]));

    /*
    // Output

    Accounts for program Stake11111111111111111111111111111111111111:
    Total number of delegators found for 27MtjMSAQ2BGkXNuJEJkxFyCJT8dugGAaHJ9T7Gc6x4x is: 184
    Sample delegator:
    {
    "account": {
    "data": {
    "parsed": {
    "info": {
    "meta": {
    "authorized": {
    "staker": "3VDVh3rHTLkNJp6FVYbuFcaihYBFCQX5VSBZk23ckDGV",
    "withdrawer": "EhYXq3ANp5nAerUpbSgd7VK2RRcxK1zNuSQ755G5Mtxx"
    },
    "lockup": {
    "custodian": "3XdBZcURF5nKg3oTZAcfQZg8XEc5eKsx6vK8r3BdGGxg",
    "epoch": 0,
    "unixTimestamp": 1822867200
    },
    "rentExemptReserve": "2282880"
    },
    "stake": {
    "creditsObserved": 58685367,
    "delegation": {
    "activationEpoch": "208",
    "deactivationEpoch": "18446744073709551615",
    "stake": "433005300621",
    "voter": "27MtjMSAQ2BGkXNuJEJkxFyCJT8dugGAaHJ9T7Gc6x4x",
    "warmupCooldownRate": 0.25
    }
    }
    },
    "type": "delegated"
    },
    "program": "stake",
    "space": 200
    },
    "executable": false,
    "lamports": 433012149261,
    "owner": {
    "_bn": "06a1d8179137542a983437bdfe2a7ab2557f535c8a78722b68a49dc000000000"
    },
    "rentEpoch": 264
    },
    "pubkey": {
    "_bn": "0dc8b506f95e52c9ac725e714c7078799dd3268df562161411fe0916a4dc0a43"
    }
    }

    */

    停用质押

    在质押账户委托后的任何时候,Stake Authority可以选择停用该账户。停用过程可能需要多个时期才能完成,并且在提取任何 SOL 之前必须完成停用操作。

    // At anytime we can choose to deactivate our stake. Our stake account must be inactive before we can withdraw funds.
    const deactivateTx = StakeProgram.deactivate({
    stakePubkey: stakeAccount.publicKey,
    authorizedPubkey: wallet.publicKey,
    });
    const deactivateTxId = await sendAndConfirmTransaction(
    connection,
    deactivateTx,
    [wallet]
    );
    console.log(`Stake account deactivated. Tx Id: ${deactivateTxId}`);

    // Check in on our stake account. It should now be inactive.
    stakeStatus = await connection.getStakeActivation(stakeAccount.publicKey);
    console.log(`Stake account status: ${stakeStatus.state}`);

    提取质押

    一旦停用了,Withdrawal Authority可以将 SOL 提取回系统账户。一旦质押账户不再委托并且余额为 0 SOL,它将被销毁了。

    // Once deactivated, we can withdraw our SOL back to our main wallet
    const withdrawTx = StakeProgram.withdraw({
    stakePubkey: stakeAccount.publicKey,
    authorizedPubkey: wallet.publicKey,
    toPubkey: wallet.publicKey,
    lamports: stakeBalance, // Withdraw the full balance at the time of the transaction
    });

    const withdrawTxId = await sendAndConfirmTransaction(connection, withdrawTx, [
    wallet,
    ]);
    console.log(`Stake account withdrawn. Tx Id: ${withdrawTxId}`);

    // Confirm that our stake account balance is now 0
    stakeBalance = await connection.getBalance(stakeAccount.publicKey);
    console.log(`Stake account balance: ${stakeBalance / LAMPORTS_PER_SOL} SOL`);
    - - +

    质押

    获取当前验证器

    我们可以质押 SOL 并通过帮助保护网络来获得奖励。要进行质押,我们将 SOL 委托给验证器,而验证器则处理交易。

    import { clusterApiUrl, Connection } from "@solana/web3.js";

    (async () => {
    const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

    // Get all validators, categorized by current (i.e. active) and deliquent (i.e. inactive)
    const { current, delinquent } = await connection.getVoteAccounts();
    console.log("current validators: ", current);
    console.log("all validators: ", current.concat(delinquent));
    })();

    创建质押账户

    所有的质押指令由质押程序 (Stake Program) 处理。首先,我们创建一个质押账户, 该账户与标准系统账户创建和管理方式不同。特别是,我们需要设置账户的Stake AuthorityWithdrawal Authority

    // Setup a transaction to create our stake account
    // Note: `StakeProgram.createAccount` returns a `Transaction` preconfigured with the necessary `TransactionInstruction`s
    const createStakeAccountTx = StakeProgram.createAccount({
    authorized: new Authorized(wallet.publicKey, wallet.publicKey), // Here we set two authorities: Stake Authority and Withdrawal Authority. Both are set to our wallet.
    fromPubkey: wallet.publicKey,
    lamports: amountToStake,
    lockup: new Lockup(0, 0, wallet.publicKey), // Optional. We'll set this to 0 for demonstration purposes.
    stakePubkey: stakeAccount.publicKey,
    });

    const createStakeAccountTxId = await sendAndConfirmTransaction(
    connection,
    createStakeAccountTx,
    [
    wallet,
    stakeAccount, // Since we're creating a new stake account, we have that account sign as well
    ]
    );
    console.log(`Stake account created. Tx Id: ${createStakeAccountTxId}`);

    // Check our newly created stake account balance. This should be 0.5 SOL.
    let stakeBalance = await connection.getBalance(stakeAccount.publicKey);
    console.log(`Stake account balance: ${stakeBalance / LAMPORTS_PER_SOL} SOL`);

    // Verify the status of our stake account. This will start as inactive and will take some time to activate.
    let stakeStatus = await connection.getStakeActivation(stakeAccount.publicKey);
    console.log(`Stake account status: ${stakeStatus.state}`);

    委托质押

    一旦质押账户得到资金支持,Stake Authority可以将其委托给一个验证者。每个质押账户一次只能委托给一个验证者。此外,账户中的所有代币必须要么被委托,要么取消委托。一旦委托成功,质押账户需要经过几个时期才能变为活跃状态。

    // With a validator selected, we can now setup a transaction that delegates our stake to their vote account.
    const delegateTx = StakeProgram.delegate({
    stakePubkey: stakeAccount.publicKey,
    authorizedPubkey: wallet.publicKey,
    votePubkey: selectedValidatorPubkey,
    });

    const delegateTxId = await sendAndConfirmTransaction(connection, delegateTx, [
    wallet,
    ]);
    console.log(
    `Stake account delegated to ${selectedValidatorPubkey}. Tx Id: ${delegateTxId}`
    );

    // Check in on our stake account. It should now be activating.
    stakeStatus = await connection.getStakeActivation(stakeAccount.publicKey);
    console.log(`Stake account status: ${stakeStatus.state}`);

    通过验证器获取委托人

    多个账户可能已经质押给了特定的验证账户。为了获取所有的质押人,我们可以使用 getProgramAccountsgetParsedProgramAccounts API。请参考指南部分 获取更多信息。质押账户长度为200字节,选民公钥从第124字节开始。参考资料

    const STAKE_PROGRAM_ID = new PublicKey(
    "Stake11111111111111111111111111111111111111"
    );
    const VOTE_PUB_KEY = "27MtjMSAQ2BGkXNuJEJkxFyCJT8dugGAaHJ9T7Gc6x4x";

    const connection = new Connection(clusterApiUrl("mainnet-beta"), "confirmed");
    const accounts = await connection.getParsedProgramAccounts(STAKE_PROGRAM_ID, {
    filters: [
    {
    dataSize: 200, // number of bytes
    },
    {
    memcmp: {
    offset: 124, // number of bytes
    bytes: VOTE_PUB_KEY, // base58 encoded string
    },
    },
    ],
    });

    console.log(`Accounts for program ${STAKE_PROGRAM_ID}: `);
    console.log(
    `Total number of delegators found for ${VOTE_PUB_KEY} is: ${accounts.length}`
    );
    if (accounts.length)
    console.log(`Sample delegator:`, JSON.stringify(accounts[0]));

    /*
    // Output

    Accounts for program Stake11111111111111111111111111111111111111:
    Total number of delegators found for 27MtjMSAQ2BGkXNuJEJkxFyCJT8dugGAaHJ9T7Gc6x4x is: 184
    Sample delegator:
    {
    "account": {
    "data": {
    "parsed": {
    "info": {
    "meta": {
    "authorized": {
    "staker": "3VDVh3rHTLkNJp6FVYbuFcaihYBFCQX5VSBZk23ckDGV",
    "withdrawer": "EhYXq3ANp5nAerUpbSgd7VK2RRcxK1zNuSQ755G5Mtxx"
    },
    "lockup": {
    "custodian": "3XdBZcURF5nKg3oTZAcfQZg8XEc5eKsx6vK8r3BdGGxg",
    "epoch": 0,
    "unixTimestamp": 1822867200
    },
    "rentExemptReserve": "2282880"
    },
    "stake": {
    "creditsObserved": 58685367,
    "delegation": {
    "activationEpoch": "208",
    "deactivationEpoch": "18446744073709551615",
    "stake": "433005300621",
    "voter": "27MtjMSAQ2BGkXNuJEJkxFyCJT8dugGAaHJ9T7Gc6x4x",
    "warmupCooldownRate": 0.25
    }
    }
    },
    "type": "delegated"
    },
    "program": "stake",
    "space": 200
    },
    "executable": false,
    "lamports": 433012149261,
    "owner": {
    "_bn": "06a1d8179137542a983437bdfe2a7ab2557f535c8a78722b68a49dc000000000"
    },
    "rentEpoch": 264
    },
    "pubkey": {
    "_bn": "0dc8b506f95e52c9ac725e714c7078799dd3268df562161411fe0916a4dc0a43"
    }
    }

    */

    停用质押

    在质押账户委托后的任何时候,Stake Authority可以选择停用该账户。停用过程可能需要多个时期才能完成,并且在提取任何 SOL 之前必须完成停用操作。

    // At anytime we can choose to deactivate our stake. Our stake account must be inactive before we can withdraw funds.
    const deactivateTx = StakeProgram.deactivate({
    stakePubkey: stakeAccount.publicKey,
    authorizedPubkey: wallet.publicKey,
    });
    const deactivateTxId = await sendAndConfirmTransaction(
    connection,
    deactivateTx,
    [wallet]
    );
    console.log(`Stake account deactivated. Tx Id: ${deactivateTxId}`);

    // Check in on our stake account. It should now be inactive.
    stakeStatus = await connection.getStakeActivation(stakeAccount.publicKey);
    console.log(`Stake account status: ${stakeStatus.state}`);

    提取质押

    一旦停用了,Withdrawal Authority可以将 SOL 提取回系统账户。一旦质押账户不再委托并且余额为 0 SOL,它将被销毁了。

    // Once deactivated, we can withdraw our SOL back to our main wallet
    const withdrawTx = StakeProgram.withdraw({
    stakePubkey: stakeAccount.publicKey,
    authorizedPubkey: wallet.publicKey,
    toPubkey: wallet.publicKey,
    lamports: stakeBalance, // Withdraw the full balance at the time of the transaction
    });

    const withdrawTxId = await sendAndConfirmTransaction(connection, withdrawTx, [
    wallet,
    ]);
    console.log(`Stake account withdrawn. Tx Id: ${withdrawTxId}`);

    // Confirm that our stake account balance is now 0
    stakeBalance = await connection.getBalance(stakeAccount.publicKey);
    console.log(`Stake account balance: ${stakeBalance / LAMPORTS_PER_SOL} SOL`);
    + + \ No newline at end of file diff --git a/cookbook-zh/references/token/index.html b/cookbook-zh/references/token/index.html index 3ddcd17d7..05bad7341 100644 --- a/cookbook-zh/references/token/index.html +++ b/cookbook-zh/references/token/index.html @@ -9,14 +9,14 @@ - - + +

    代币

    我需要什么才能开始使用SPL代币?

    每当你在Solana上与代币进行交互时,实际上你正在与Solana程序库代币(SPL-Token)或SPL代币标准交互。SPL代币标准需要使用特定的库,你可以根据你使用的编程语言在下面找到相应的库。

    "@solana/spl-token": "^0.2.0"

    如何创建一个新的代币

    创建代币是通过创建所谓的“铸币账户”来完成的。这个铸币账户随后用于向用户的代币账户铸造代币。

    // 1) use build-in function
    let mintPubkey = await createMint(
    connection, // conneciton
    feePayer, // fee payer
    alice.publicKey, // mint authority
    alice.publicKey, // freeze authority (you can use `null` to disable it. when you disable it, you can't turn it on again)
    8 // decimals
    );

    // or

    // 2) compose by yourself
    let tx = new Transaction().add(
    // create mint account
    SystemProgram.createAccount({
    fromPubkey: feePayer.publicKey,
    newAccountPubkey: mint.publicKey,
    space: MINT_SIZE,
    lamports: await getMinimumBalanceForRentExemptMint(connection),
    programId: TOKEN_PROGRAM_ID,
    }),
    // init mint account
    createInitializeMintInstruction(
    mint.publicKey, // mint pubkey
    8, // decimals
    alice.publicKey, // mint authority
    alice.publicKey // freeze authority (you can use `null` to disable it. when you disable it, you can't turn it on again)
    )
    );

    如何获得一个代币铸币账户

    为了获得代币的当前供应量、授权信息或小数位数,你需要获取代币铸币账户的账户信息。

    let mintAccount = await getMint(connection, mintAccountPublicKey);

    如何创建一个代币账户

    用户需要一个代币账户来持有代币。

    对于用户所拥有的每种类型的代币,他们将至少拥有一个代币账户。

    关联代币账户(Associated Token Accounts, ATA) 是根据每个密钥对确定性地创建的账户。关联代币账户是管理代币账户的推荐方法。

    // 1) use build-in function
    {
    let ata = await createAssociatedTokenAccount(
    connection, // connection
    feePayer, // fee payer
    mintPubkey, // mint
    alice.publicKey // owner,
    );
    }

    // or

    // 2) composed by yourself
    {
    let tx = new Transaction().add(
    createAssociatedTokenAccountInstruction(
    feePayer.publicKey, // payer
    ata, // ata
    alice.publicKey, // owner
    mintPubkey // mint
    )
    );
    }

    如何获得一个代币账户

    每个代币账户都包含有关代币的信息,例如所有者、铸币账户、数量(余额)和小数位数。

    let tokenAccount = await getAccount(connection, tokenAccountPubkey);

    如何获得一个代币账户的余额

    每个代币账户都包含有关代币的信息,例如所有者、铸币账户、数量(余额)和小数位数。

    let tokenAmount = await connection.getTokenAccountBalance(tokenAccount);
    info

    贴士 -一个代币账户只能持有一种铸币。当您指定一个代币账户时,您也需要指定一个铸币。

    如何铸造(mint)代币

    当你铸造代币时,你会增加供应量并将新代币转移到特定的代币账户。

    // 1) use build-in function
    {
    let txhash = await mintToChecked(
    connection, // connection
    feePayer, // fee payer
    mintPubkey, // mint
    tokenAccountPubkey, // receiver (sholud be a token account)
    alice, // mint authority
    1e8, // amount. if your decimals is 8, you mint 10^8 for 1 token.
    8 // decimals
    );
    }

    // or

    // 2) compose by yourself
    {
    let tx = new Transaction().add(
    createMintToCheckedInstruction(
    mintPubkey, // mint
    tokenAccountPubkey, // receiver (sholud be a token account)
    alice.publicKey, // mint authority
    1e8, // amount. if your decimals is 8, you mint 10^8 for 1 token.
    8 // decimals
    // [signer1, signer2 ...], // only multisig account will use
    )
    );
    }

    如何转移代币

    你可以将代币从一个代币账户转移到另一个代币账户。

    // 1) use build-in function
    {
    let txhash = await transferChecked(
    connection, // connection
    feePayer, // payer
    tokenAccountXPubkey, // from (should be a token account)
    mintPubkey, // mint
    tokenAccountYPubkey, // to (should be a token account)
    alice, // from's owner
    1e8, // amount, if your deciamls is 8, send 10^8 for 1 token
    8 // decimals
    );
    }

    // or

    // 2) compose by yourself
    {
    let tx = new Transaction().add(
    createTransferCheckedInstruction(
    tokenAccountXPubkey, // from (should be a token account)
    mintPubkey, // mint
    tokenAccountYPubkey, // to (should be a token account)
    alice.publicKey, // from's owner
    1e8, // amount, if your deciamls is 8, send 10^8 for 1 token
    8 // decimals
    )
    );
    }

    如何销代币

    如果你是代币的所有者,你可以销毁代币。

    // 1) use build-in function
    {
    let txhash = await burnChecked(
    connection, // connection
    feePayer, // payer
    tokenAccountPubkey, // token account
    mintPubkey, // mint
    alice, // owner
    1e8, // amount, if your deciamls is 8, 10^8 for 1 token
    8
    );
    }

    // or

    // 2) compose by yourself
    {
    let tx = new Transaction().add(
    createBurnCheckedInstruction(
    tokenAccountPubkey, // token account
    mintPubkey, // mint
    alice.publicKey, // owner of token account
    1e8, // amount, if your deciamls is 8, 10^8 for 1 token
    8 // decimals
    )
    );
    }

    如何关闭代币账户

    如果你不再需要使用某个代币账户,你可以关闭它。有两种情况:

    1. 包装的 SOL(Wrapped SOL)- 关闭操作会将包装的 SOL 转换为 SOL。
    2. 其他代币(Other Tokens)- 只有当代币账户的余额为0时,你才能关闭它。
    // 1) use build-in function
    {
    let txhash = await closeAccount(
    connection, // connection
    feePayer, // payer
    tokenAccountPubkey, // token account which you want to close
    alice.publicKey, // destination
    alice // owner of token account
    );
    }

    // or

    // 2) compose by yourself
    {
    let tx = new Transaction().add(
    createCloseAccountInstruction(
    tokenAccountPubkey, // token account which you want to close
    alice.publicKey, // destination
    alice.publicKey // owner of token account
    )
    );
    }

    如何在代币账户或铸币账户上设置权限

    你可以设置/更新权限。有四种类型:

    1. MintTokens(铸币账户):用于控制在铸币账户上铸造代币的权限。
    2. FreezeAccount(铸币账户):用于冻结或解冻铸币账户的权限。
    3. AccountOwner(代币账户):用于控制代币账户所有权的权限。
    4. CloseAccount(代币账户):用于关闭代币账户的权限。
    // 1) use build-in function
    {
    let txhash = await setAuthority(
    connection, // connection
    feePayer, // payer
    mintPubkey, // mint account || token account
    alice, // current authority
    AuthorityType.MintTokens, // authority type
    randomGuy.publicKey // new authority (you can pass `null` to close it)
    );
    }

    // or

    // 2) compose by yourself
    {
    let tx = new Transaction().add(
    createSetAuthorityInstruction(
    mintPubkey, // mint acocunt || token account
    alice.publicKey, // current auth
    AuthorityType.MintTokens, // authority type
    randomGuy.publicKey // new auth (you can pass `null` to close it)
    )
    );
    }

    如何批准代币委托

    你可以设置一个委托代理,并指定一个允许的代币数量。设置后,委托代理就像代币账户的另一个所有者。一个代币账户在同一时间只能委托给一个账户。

    // 1) use build-in function
    {
    let txhash = await approveChecked(
    connection, // connection
    feePayer, // fee payer
    mintPubkey, // mint
    tokenAccountPubkey, // token account
    randomGuy.publicKey, // delegate
    alice, // owner of token account
    1e8, // amount, if your deciamls is 8, 10^8 for 1 token
    8 // decimals
    );
    }
    // or

    // 2) compose by yourself
    {
    let tx = new Transaction().add(
    createApproveCheckedInstruction(
    tokenAccountPubkey, // token account
    mintPubkey, // mint
    randomGuy.publicKey, // delegate
    alice.publicKey, // owner of token account
    1e8, // amount, if your deciamls is 8, 10^8 for 1 token
    8 // decimals
    )
    );
    }

    如何撤销代币委托

    撤销操作将把代币委托设置为空,并将委托的代币数量设置为0。

    // 1) use build-in function
    {
    let txhash = await revoke(
    connection, // connection
    feePayer, // payer
    tokenAccountPubkey, // token account
    alice // owner of token account
    );
    }

    // or

    // 2) compose by yourself
    {
    let tx = new Transaction().add(
    createRevokeInstruction(
    tokenAccountPubkey, // token account
    alice.publicKey // owner of token account
    )
    );
    }

    如何管理包装的SOL

    包装的 SOL与其他代币铸币类似,区别在于使用 syncNative 并在 NATIVE_MINT 地址上专门创建代币账户。

    创建代币账户

    创建代币账户 但将mint替换为NATIVE_MINT

    import { NATIVE_MINT } from "@solana/spl-token";

    增加余额

    有两种方法可以增加包装的 SOL 的余额:

    1. 通过 SOL 转账方式

    let tx = new Transaction().add(
    // trasnfer SOL
    SystemProgram.transfer({
    fromPubkey: alice.publicKey,
    toPubkey: ata,
    lamports: amount,
    }),
    // sync wrapped SOL balance
    createSyncNativeInstruction(ata)
    );

    2. 通过代币转账方式

    let tx = new Transaction().add(
    // create token account
    SystemProgram.createAccount({
    fromPubkey: alice.publicKey,
    newAccountPubkey: auxAccount.publicKey,
    space: ACCOUNT_SIZE,
    lamports:
    (await getMinimumBalanceForRentExemptAccount(connection)) + amount, // rent + amount
    programId: TOKEN_PROGRAM_ID,
    }),
    // init token account
    createInitializeAccountInstruction(
    auxAccount.publicKey,
    NATIVE_MINT,
    alice.publicKey
    ),
    // transfer WSOL
    createTransferInstruction(auxAccount.publicKey, ata, alice.publicKey, amount),
    // close aux account
    createCloseAccountInstruction(
    auxAccount.publicKey,
    alice.publicKey,
    alice.publicKey
    )
    );

    如何通过所有者获取所有代币账户

    你可以通过所有者获取代币账户。有两种方法可以实现。

    1. 获取所有代币账户
    let response = await connection.getParsedTokenAccountsByOwner(owner, {
    programId: TOKEN_PROGRAM_ID,
    });
    1. 按照铸币进行过滤
    let response = await connection.getParsedTokenAccountsByOwner(owner, {
    mint: mint,
    });
    - - +一个代币账户只能持有一种铸币。当您指定一个代币账户时,您也需要指定一个铸币。

    如何铸造(mint)代币

    当你铸造代币时,你会增加供应量并将新代币转移到特定的代币账户。

    // 1) use build-in function
    {
    let txhash = await mintToChecked(
    connection, // connection
    feePayer, // fee payer
    mintPubkey, // mint
    tokenAccountPubkey, // receiver (sholud be a token account)
    alice, // mint authority
    1e8, // amount. if your decimals is 8, you mint 10^8 for 1 token.
    8 // decimals
    );
    }

    // or

    // 2) compose by yourself
    {
    let tx = new Transaction().add(
    createMintToCheckedInstruction(
    mintPubkey, // mint
    tokenAccountPubkey, // receiver (sholud be a token account)
    alice.publicKey, // mint authority
    1e8, // amount. if your decimals is 8, you mint 10^8 for 1 token.
    8 // decimals
    // [signer1, signer2 ...], // only multisig account will use
    )
    );
    }

    如何转移代币

    你可以将代币从一个代币账户转移到另一个代币账户。

    // 1) use build-in function
    {
    let txhash = await transferChecked(
    connection, // connection
    feePayer, // payer
    tokenAccountXPubkey, // from (should be a token account)
    mintPubkey, // mint
    tokenAccountYPubkey, // to (should be a token account)
    alice, // from's owner
    1e8, // amount, if your deciamls is 8, send 10^8 for 1 token
    8 // decimals
    );
    }

    // or

    // 2) compose by yourself
    {
    let tx = new Transaction().add(
    createTransferCheckedInstruction(
    tokenAccountXPubkey, // from (should be a token account)
    mintPubkey, // mint
    tokenAccountYPubkey, // to (should be a token account)
    alice.publicKey, // from's owner
    1e8, // amount, if your deciamls is 8, send 10^8 for 1 token
    8 // decimals
    )
    );
    }

    如何销代币

    如果你是代币的所有者,你可以销毁代币。

    // 1) use build-in function
    {
    let txhash = await burnChecked(
    connection, // connection
    feePayer, // payer
    tokenAccountPubkey, // token account
    mintPubkey, // mint
    alice, // owner
    1e8, // amount, if your deciamls is 8, 10^8 for 1 token
    8
    );
    }

    // or

    // 2) compose by yourself
    {
    let tx = new Transaction().add(
    createBurnCheckedInstruction(
    tokenAccountPubkey, // token account
    mintPubkey, // mint
    alice.publicKey, // owner of token account
    1e8, // amount, if your deciamls is 8, 10^8 for 1 token
    8 // decimals
    )
    );
    }

    如何关闭代币账户

    如果你不再需要使用某个代币账户,你可以关闭它。有两种情况:

    1. 包装的 SOL(Wrapped SOL)- 关闭操作会将包装的 SOL 转换为 SOL。
    2. 其他代币(Other Tokens)- 只有当代币账户的余额为0时,你才能关闭它。
    // 1) use build-in function
    {
    let txhash = await closeAccount(
    connection, // connection
    feePayer, // payer
    tokenAccountPubkey, // token account which you want to close
    alice.publicKey, // destination
    alice // owner of token account
    );
    }

    // or

    // 2) compose by yourself
    {
    let tx = new Transaction().add(
    createCloseAccountInstruction(
    tokenAccountPubkey, // token account which you want to close
    alice.publicKey, // destination
    alice.publicKey // owner of token account
    )
    );
    }

    如何在代币账户或铸币账户上设置权限

    你可以设置/更新权限。有四种类型:

    1. MintTokens(铸币账户):用于控制在铸币账户上铸造代币的权限。
    2. FreezeAccount(铸币账户):用于冻结或解冻铸币账户的权限。
    3. AccountOwner(代币账户):用于控制代币账户所有权的权限。
    4. CloseAccount(代币账户):用于关闭代币账户的权限。
    // 1) use build-in function
    {
    let txhash = await setAuthority(
    connection, // connection
    feePayer, // payer
    mintPubkey, // mint account || token account
    alice, // current authority
    AuthorityType.MintTokens, // authority type
    randomGuy.publicKey // new authority (you can pass `null` to close it)
    );
    }

    // or

    // 2) compose by yourself
    {
    let tx = new Transaction().add(
    createSetAuthorityInstruction(
    mintPubkey, // mint acocunt || token account
    alice.publicKey, // current auth
    AuthorityType.MintTokens, // authority type
    randomGuy.publicKey // new auth (you can pass `null` to close it)
    )
    );
    }

    如何批准代币委托

    你可以设置一个委托代理,并指定一个允许的代币数量。设置后,委托代理就像代币账户的另一个所有者。一个代币账户在同一时间只能委托给一个账户。

    // 1) use build-in function
    {
    let txhash = await approveChecked(
    connection, // connection
    feePayer, // fee payer
    mintPubkey, // mint
    tokenAccountPubkey, // token account
    randomGuy.publicKey, // delegate
    alice, // owner of token account
    1e8, // amount, if your deciamls is 8, 10^8 for 1 token
    8 // decimals
    );
    }
    // or

    // 2) compose by yourself
    {
    let tx = new Transaction().add(
    createApproveCheckedInstruction(
    tokenAccountPubkey, // token account
    mintPubkey, // mint
    randomGuy.publicKey, // delegate
    alice.publicKey, // owner of token account
    1e8, // amount, if your deciamls is 8, 10^8 for 1 token
    8 // decimals
    )
    );
    }

    如何撤销代币委托

    撤销操作将把代币委托设置为空,并将委托的代币数量设置为0。

    // 1) use build-in function
    {
    let txhash = await revoke(
    connection, // connection
    feePayer, // payer
    tokenAccountPubkey, // token account
    alice // owner of token account
    );
    }

    // or

    // 2) compose by yourself
    {
    let tx = new Transaction().add(
    createRevokeInstruction(
    tokenAccountPubkey, // token account
    alice.publicKey // owner of token account
    )
    );
    }

    如何管理包装的SOL

    包装的 SOL与其他代币铸币类似,区别在于使用 syncNative 并在 NATIVE_MINT 地址上专门创建代币账户。

    创建代币账户

    创建代币账户 但将mint替换为NATIVE_MINT

    import { NATIVE_MINT } from "@solana/spl-token";

    增加余额

    有两种方法可以增加包装的 SOL 的余额:

    1. 通过 SOL 转账方式

    let tx = new Transaction().add(
    // trasnfer SOL
    SystemProgram.transfer({
    fromPubkey: alice.publicKey,
    toPubkey: ata,
    lamports: amount,
    }),
    // sync wrapped SOL balance
    createSyncNativeInstruction(ata)
    );

    2. 通过代币转账方式

    let tx = new Transaction().add(
    // create token account
    SystemProgram.createAccount({
    fromPubkey: alice.publicKey,
    newAccountPubkey: auxAccount.publicKey,
    space: ACCOUNT_SIZE,
    lamports:
    (await getMinimumBalanceForRentExemptAccount(connection)) + amount, // rent + amount
    programId: TOKEN_PROGRAM_ID,
    }),
    // init token account
    createInitializeAccountInstruction(
    auxAccount.publicKey,
    NATIVE_MINT,
    alice.publicKey
    ),
    // transfer WSOL
    createTransferInstruction(auxAccount.publicKey, ata, alice.publicKey, amount),
    // close aux account
    createCloseAccountInstruction(
    auxAccount.publicKey,
    alice.publicKey,
    alice.publicKey
    )
    );

    如何通过所有者获取所有代币账户

    你可以通过所有者获取代币账户。有两种方法可以实现。

    1. 获取所有代币账户
    let response = await connection.getParsedTokenAccountsByOwner(owner, {
    programId: TOKEN_PROGRAM_ID,
    });
    1. 按照铸币进行过滤
    let response = await connection.getParsedTokenAccountsByOwner(owner, {
    mint: mint,
    });
    + + \ No newline at end of file diff --git a/cookbook-zh/tags/account-map/index.html b/cookbook-zh/tags/account-map/index.html index 07151105d..1a7f1548f 100644 --- a/cookbook-zh/tags/account-map/index.html +++ b/cookbook-zh/tags/account-map/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "account-map"

    View All Tags

    账户映射

    在编程中,我们经常使用映射(Map)这种数据结构,将一个键与某种值关联起来。键和值可以是任意类型的数据,键用作标识要保存的特定值的标识符。通过键,我们可以高效地插入、检索和更新这些值。

    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/account/index.html b/cookbook-zh/tags/account/index.html index 79a8af3f9..378fb2db4 100644 --- a/cookbook-zh/tags/account/index.html +++ b/cookbook-zh/tags/account/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "account"

    View All Tags

    获取程序帐户

    一个返回程序所拥有的账户的RPC方法。目前不支持分页。请求getProgramAccounts应该包括dataSlice和/或filters参数,以提高响应时间并返回只有预期结果的内容。

    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/accounts/index.html b/cookbook-zh/tags/accounts/index.html index 6f617cfdd..ccc2c6b4c 100644 --- a/cookbook-zh/tags/accounts/index.html +++ b/cookbook-zh/tags/accounts/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "accounts"

    View All Tags

    账户

    在Solana中,账户是用来存储状态的。账户是Solana开发中非常重要的构成要素。

    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/contribute/index.html b/cookbook-zh/tags/contribute/index.html index 609873852..f8cc09f0d 100644 --- a/cookbook-zh/tags/contribute/index.html +++ b/cookbook-zh/tags/contribute/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "contribute"

    View All Tags

    贡献

    欢迎任何人对这本食谱进行贡献。在贡献新的代码片段时,请参考项目的风格。

    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/cpi/index.html b/cookbook-zh/tags/cpi/index.html index bbf74bf46..3bf8b7759 100644 --- a/cookbook-zh/tags/cpi/index.html +++ b/cookbook-zh/tags/cpi/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "cpi"

    View All Tags

    Cross Program Invocations (CPIs)

    A Cross-Program Invocation (CPI) is a direct call from one program into another, allowing for the composability of Solana programs. Just as any client can call any program using the JSON RPC, any program can call any other program via a CPI. CPIs essentially turn the entire Solana ecosystem into one giant API that is at your disposal as a developer.

    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/data-migration/index.html b/cookbook-zh/tags/data-migration/index.html index 91a0dc16d..4dd3a5db6 100644 --- a/cookbook-zh/tags/data-migration/index.html +++ b/cookbook-zh/tags/data-migration/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "data-migration"

    View All Tags
    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/debug/index.html b/cookbook-zh/tags/debug/index.html index 71a4ee6ca..ce5ff3958 100644 --- a/cookbook-zh/tags/debug/index.html +++ b/cookbook-zh/tags/debug/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "debug"

    View All Tags
    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/distrbution/index.html b/cookbook-zh/tags/distrbution/index.html index 7aee1bff0..3ab326f4c 100644 --- a/cookbook-zh/tags/distrbution/index.html +++ b/cookbook-zh/tags/distrbution/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "distrbution"

    View All Tags

    Distribution

    Distribution of your game depends highly on the platform you are using. With Solana, there are game SDKs you can build for IOS, Android, Web and Native Windows or Mac. Using the Unity SDK you could even connect Nintendo Switch or XBox to Solana theoretically. Many game companies are pivoting to a mobile first approach because there are so many people with mobile phones in the world. Mobile comes with its own complications though, so you should pick what fits best to your game.

    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/energy-system/index.html b/cookbook-zh/tags/energy-system/index.html index b47ec04e0..3e3ece14d 100644 --- a/cookbook-zh/tags/energy-system/index.html +++ b/cookbook-zh/tags/energy-system/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "energy-system"

    View All Tags

    Energy System

    Casual games commonly use energy systems, meaning that actions in the game cost energy which refills over time. In this guide we will walk through how to build one on Solana.

    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/example/index.html b/cookbook-zh/tags/example/index.html index 225eadfb2..bc51f9af8 100644 --- a/cookbook-zh/tags/example/index.html +++ b/cookbook-zh/tags/example/index.html @@ -9,13 +9,13 @@ - - + +

    2 docs tagged with "example"

    View All Tags
    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/game/index.html b/cookbook-zh/tags/game/index.html index 4941af277..5668c0d34 100644 --- a/cookbook-zh/tags/game/index.html +++ b/cookbook-zh/tags/game/index.html @@ -9,13 +9,13 @@ - - + +

    11 docs tagged with "game"

    View All Tags

    Distribution

    Distribution of your game depends highly on the platform you are using. With Solana, there are game SDKs you can build for IOS, Android, Web and Native Windows or Mac. Using the Unity SDK you could even connect Nintendo Switch or XBox to Solana theoretically. Many game companies are pivoting to a mobile first approach because there are so many people with mobile phones in the world. Mobile comes with its own complications though, so you should pick what fits best to your game.

    Energy System

    Casual games commonly use energy systems, meaning that actions in the game cost energy which refills over time. In this guide we will walk through how to build one on Solana.

    How interact with tokens in program

    Tokens on Solana can serve various purposes, such as in-game rewards, incentives, or other applications. For example, you can create tokens and distribute them to players when they complete specific in-game actions.

    Intro into gaming on Solana

    The gaming space in the Solana ecosystem is expanding rapidly. Integrating with Solana can provide numerous benefits for games, such as enabling players to own and trade their assets via NFTs in games, building a real in-game economy, creating composable game programs, and allowing players to compete for valuable assets.

    NFTs In Games

    Non-fungible tokens (NFTs) are rapidly gaining popularity as a means of integrating Solana into games.

    Porting a program to Unity

    When you have written a solana program you now maybe want to use it in the Unity Game engine. Fortunately there is a code generator which lets you port a anchor idl (a json representation of a solana program) to C#

    save game state

    You can use Solana block chain to save the state of your game in program accounts. These are accounts that are owned by your program and they are derived from the program Id and some seeds. These can be thought of as data base entries.

    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/hello-world/index.html b/cookbook-zh/tags/hello-world/index.html index 9a38c8115..4b5ed1cd2 100644 --- a/cookbook-zh/tags/hello-world/index.html +++ b/cookbook-zh/tags/hello-world/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "hello-world"

    View All Tags
    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/index.html b/cookbook-zh/tags/index.html index 6b0c706ea..927704cd4 100644 --- a/cookbook-zh/tags/index.html +++ b/cookbook-zh/tags/index.html @@ -9,13 +9,13 @@ - - + + - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/intro/index.html b/cookbook-zh/tags/intro/index.html index 0429dfc0c..8f2f500f4 100644 --- a/cookbook-zh/tags/intro/index.html +++ b/cookbook-zh/tags/intro/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "intro"

    View All Tags

    Intro into gaming on Solana

    The gaming space in the Solana ecosystem is expanding rapidly. Integrating with Solana can provide numerous benefits for games, such as enabling players to own and trade their assets via NFTs in games, building a real in-game economy, creating composable game programs, and allowing players to compete for valuable assets.

    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/keypair/index.html b/cookbook-zh/tags/keypair/index.html index 9e92acc4e..2d75f2832 100644 --- a/cookbook-zh/tags/keypair/index.html +++ b/cookbook-zh/tags/keypair/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "keypair"

    View All Tags
    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/learn/index.html b/cookbook-zh/tags/learn/index.html index d3752a301..ef70c193a 100644 --- a/cookbook-zh/tags/learn/index.html +++ b/cookbook-zh/tags/learn/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "learn"

    View All Tags
    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/local-development/index.html b/cookbook-zh/tags/local-development/index.html index e58584611..c28e1de75 100644 --- a/cookbook-zh/tags/local-development/index.html +++ b/cookbook-zh/tags/local-development/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "local-development"

    View All Tags
    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/name-service/index.html b/cookbook-zh/tags/name-service/index.html index eb320ff49..68eb414f2 100644 --- a/cookbook-zh/tags/name-service/index.html +++ b/cookbook-zh/tags/name-service/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "name-service"

    View All Tags
    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/nft/index.html b/cookbook-zh/tags/nft/index.html index f3514d841..94c3f2784 100644 --- a/cookbook-zh/tags/nft/index.html +++ b/cookbook-zh/tags/nft/index.html @@ -9,13 +9,13 @@ - - + +

    2 docs tagged with "nft"

    View All Tags

    NFTs In Games

    Non-fungible tokens (NFTs) are rapidly gaining popularity as a means of integrating Solana into games.

    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/pda/index.html b/cookbook-zh/tags/pda/index.html index 70927653a..7e03e6336 100644 --- a/cookbook-zh/tags/pda/index.html +++ b/cookbook-zh/tags/pda/index.html @@ -9,13 +9,13 @@ - - + +

    2 docs tagged with "pda"

    View All Tags

    程序派生账户(PDA)

    程序派生账户(PDA)是为了让特定程序可以控制一些账户而设计出来的。使用PDA,程序可以通过编程方法为一些地址进行签名,而不一定用到私钥。

    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/program/index.html b/cookbook-zh/tags/program/index.html index 595d1fcc2..85f1d64be 100644 --- a/cookbook-zh/tags/program/index.html +++ b/cookbook-zh/tags/program/index.html @@ -9,13 +9,13 @@ - - + +

    4 docs tagged with "program"

    View All Tags

    How interact with tokens in program

    Tokens on Solana can serve various purposes, such as in-game rewards, incentives, or other applications. For example, you can create tokens and distribute them to players when they complete specific in-game actions.

    Porting a program to Unity

    When you have written a solana program you now maybe want to use it in the Unity Game engine. Fortunately there is a code generator which lets you port a anchor idl (a json representation of a solana program) to C#

    程序

    任何开发者都可以在Solana链上编写以及部署程序。Solana程序(在其他链上叫做智能合约),是所有链上活动的基础。

    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/sdk/index.html b/cookbook-zh/tags/sdk/index.html index 7f33c5218..1f57b536f 100644 --- a/cookbook-zh/tags/sdk/index.html +++ b/cookbook-zh/tags/sdk/index.html @@ -9,13 +9,13 @@ - - + + - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/serialization/index.html b/cookbook-zh/tags/serialization/index.html index b11f3297f..dfdd46cf8 100644 --- a/cookbook-zh/tags/serialization/index.html +++ b/cookbook-zh/tags/serialization/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "serialization"

    View All Tags

    序列数据

    当我们谈论序列化时,我们指的是数据的序列化和反序列化。

    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/solana-cook-book/index.html b/cookbook-zh/tags/solana-cook-book/index.html index 554658882..b1c3a38cb 100644 --- a/cookbook-zh/tags/solana-cook-book/index.html +++ b/cookbook-zh/tags/solana-cook-book/index.html @@ -9,13 +9,13 @@ - - + +

    37 docs tagged with "solana-cook-book"

    View All Tags

    Cross Program Invocations (CPIs)

    A Cross-Program Invocation (CPI) is a direct call from one program into another, allowing for the composability of Solana programs. Just as any client can call any program using the JSON RPC, any program can call any other program via a CPI. CPIs essentially turn the entire Solana ecosystem into one giant API that is at your disposal as a developer.

    Distribution

    Distribution of your game depends highly on the platform you are using. With Solana, there are game SDKs you can build for IOS, Android, Web and Native Windows or Mac. Using the Unity SDK you could even connect Nintendo Switch or XBox to Solana theoretically. Many game companies are pivoting to a mobile first approach because there are so many people with mobile phones in the world. Mobile comes with its own complications though, so you should pick what fits best to your game.

    Energy System

    Casual games commonly use energy systems, meaning that actions in the game cost energy which refills over time. In this guide we will walk through how to build one on Solana.

    How interact with tokens in program

    Tokens on Solana can serve various purposes, such as in-game rewards, incentives, or other applications. For example, you can create tokens and distribute them to players when they complete specific in-game actions.

    Intro into gaming on Solana

    The gaming space in the Solana ecosystem is expanding rapidly. Integrating with Solana can provide numerous benefits for games, such as enabling players to own and trade their assets via NFTs in games, building a real in-game economy, creating composable game programs, and allowing players to compete for valuable assets.

    NFTs In Games

    Non-fungible tokens (NFTs) are rapidly gaining popularity as a means of integrating Solana into games.

    Porting a program to Unity

    When you have written a solana program you now maybe want to use it in the Unity Game engine. Fortunately there is a code generator which lets you port a anchor idl (a json representation of a solana program) to C#

    save game state

    You can use Solana block chain to save the state of your game in program accounts. These are accounts that are owned by your program and they are derived from the program Id and some seeds. These can be thought of as data base entries.

    Solana秘籍

    《Solana 秘籍》中文取自:Solana Cookbook仓库里面已经有的中文版本。后期会根据官方的更新及时更新中文版本,由All in One Solana 社区维护。

    交易

    客户端可以通过向一个集群提交交易来调用程序。一个交易可以包含多个指令,每个指令可以针对不同的程序。

    代币

    我需要什么才能开始使用SPL代币?

    功能相等测试

    当测试程序时,确保它在各个集群中以相同的方式运行对于确保质量和产生预期结果非常重要。

    序列数据

    当我们谈论序列化时,我们指的是数据的序列化和反序列化。

    程序

    任何开发者都可以在Solana链上编写以及部署程序。Solana程序(在其他链上叫做智能合约),是所有链上活动的基础。

    程序派生账户(PDA)

    程序派生账户(PDA)是为了让特定程序可以控制一些账户而设计出来的。使用PDA,程序可以通过编程方法为一些地址进行签名,而不一定用到私钥。

    获取程序帐户

    一个返回程序所拥有的账户的RPC方法。目前不支持分页。请求getProgramAccounts应该包括dataSlice和/或filters参数,以提高响应时间并返回只有预期结果的内容。

    贡献

    欢迎任何人对这本食谱进行贡献。在贡献新的代码片段时,请参考项目的风格。

    账户

    在Solana中,账户是用来存储状态的。账户是Solana开发中非常重要的构成要素。

    账户

    如何创建系统账户

    账户映射

    在编程中,我们经常使用映射(Map)这种数据结构,将一个键与某种值关联起来。键和值可以是任意类型的数据,键用作标识要保存的特定值的标识符。通过键,我们可以高效地插入、检索和更新这些值。

    重试交易

    在某些情况下,一个看似有效的交易可能在输入区块之前会被丢弃。这种情况最常发生在网络拥堵期间,当一个RPC节点无法将交易重新广播给区块链的领导节点时。对于最终用户来说,他们的交易可能会完全消失。虽然RPC节点配备了通用的重新广播算法,但应用程序开发人员也可以开发自己的自定义重新广播逻辑。

    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/solana/index.html b/cookbook-zh/tags/solana/index.html index 4b34758e7..13a9dc7fb 100644 --- a/cookbook-zh/tags/solana/index.html +++ b/cookbook-zh/tags/solana/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "solana"

    View All Tags

    Solana秘籍

    《Solana 秘籍》中文取自:Solana Cookbook仓库里面已经有的中文版本。后期会根据官方的更新及时更新中文版本,由All in One Solana 社区维护。

    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/staking/index.html b/cookbook-zh/tags/staking/index.html index 41702d89b..7fa1ac9cd 100644 --- a/cookbook-zh/tags/staking/index.html +++ b/cookbook-zh/tags/staking/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "staking"

    View All Tags
    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/store-sol/index.html b/cookbook-zh/tags/store-sol/index.html index 69fef28e5..0416bc316 100644 --- a/cookbook-zh/tags/store-sol/index.html +++ b/cookbook-zh/tags/store-sol/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "store-sol"

    View All Tags
    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/test/index.html b/cookbook-zh/tags/test/index.html index 62103032f..a7f3450b8 100644 --- a/cookbook-zh/tags/test/index.html +++ b/cookbook-zh/tags/test/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "test"

    View All Tags

    功能相等测试

    当测试程序时,确保它在各个集群中以相同的方式运行对于确保质量和产生预期结果非常重要。

    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/token/index.html b/cookbook-zh/tags/token/index.html index 853364e19..9263396ca 100644 --- a/cookbook-zh/tags/token/index.html +++ b/cookbook-zh/tags/token/index.html @@ -9,13 +9,13 @@ - - + +

    2 docs tagged with "token"

    View All Tags

    How interact with tokens in program

    Tokens on Solana can serve various purposes, such as in-game rewards, incentives, or other applications. For example, you can create tokens and distribute them to players when they complete specific in-game actions.

    代币

    我需要什么才能开始使用SPL代币?

    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/transaction/index.html b/cookbook-zh/tags/transaction/index.html index 83097a864..71e9271a2 100644 --- a/cookbook-zh/tags/transaction/index.html +++ b/cookbook-zh/tags/transaction/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "transaction"

    View All Tags
    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/transactions/index.html b/cookbook-zh/tags/transactions/index.html index 55aab7e66..a1c1fd67c 100644 --- a/cookbook-zh/tags/transactions/index.html +++ b/cookbook-zh/tags/transactions/index.html @@ -9,13 +9,13 @@ - - + +

    5 docs tagged with "transactions"

    View All Tags

    交易

    客户端可以通过向一个集群提交交易来调用程序。一个交易可以包含多个指令,每个指令可以针对不同的程序。

    账户

    如何创建系统账户

    重试交易

    在某些情况下,一个看似有效的交易可能在输入区块之前会被丢弃。这种情况最常发生在网络拥堵期间,当一个RPC节点无法将交易重新广播给区块链的领导节点时。对于最终用户来说,他们的交易可能会完全消失。虽然RPC节点配备了通用的重新广播算法,但应用程序开发人员也可以开发自己的自定义重新广播逻辑。

    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/unity/index.html b/cookbook-zh/tags/unity/index.html index d39e98f92..8f06a28ee 100644 --- a/cookbook-zh/tags/unity/index.html +++ b/cookbook-zh/tags/unity/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "unity"

    View All Tags

    Porting a program to Unity

    When you have written a solana program you now maybe want to use it in the Unity Game engine. Fortunately there is a code generator which lets you port a anchor idl (a json representation of a solana program) to C#

    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/wallet/index.html b/cookbook-zh/tags/wallet/index.html index d5f78abb2..e770428f0 100644 --- a/cookbook-zh/tags/wallet/index.html +++ b/cookbook-zh/tags/wallet/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "wallet"

    View All Tags
    - - + + \ No newline at end of file diff --git a/cookbook-zh/tags/web-3-js/index.html b/cookbook-zh/tags/web-3-js/index.html index 1b48ed8f5..dd18ac3b5 100644 --- a/cookbook-zh/tags/web-3-js/index.html +++ b/cookbook-zh/tags/web-3-js/index.html @@ -9,13 +9,13 @@ - - + +

    One doc tagged with "web3.js"

    View All Tags
    - - + + \ No newline at end of file diff --git a/index.html b/index.html index c22ce9038..7691ed19d 100644 --- a/index.html +++ b/index.html @@ -9,13 +9,13 @@ - - + +

    Solana Co Learn

    Power by 706 & Rustycab

    Solana CookBook Zh

    📖Solana CookBook 中文翻译版本

    Solana Co Learn

    💾Solana 共学学习资料

    - - + + \ No newline at end of file diff --git a/markdown-page/index.html b/markdown-page/index.html index 7f368e80d..a660f2fb6 100644 --- a/markdown-page/index.html +++ b/markdown-page/index.html @@ -9,13 +9,13 @@ - - + +

    Markdown page example

    You don't need React to write simple standalone pages.

    - - + + \ No newline at end of file diff --git a/search/index.html b/search/index.html index d33c48ecf..f14d6b2f5 100644 --- a/search/index.html +++ b/search/index.html @@ -9,13 +9,13 @@ - - + + - - + + \ No newline at end of file diff --git a/solana-development-course/index.html b/solana-development-course/index.html index d9fee581a..61f9e8175 100644 --- a/solana-development-course/index.html +++ b/solana-development-course/index.html @@ -9,13 +9,13 @@ - - + +
    -

    Solana 开发课程

    info

    欢迎来到希望学习 Web3 开发的 Web 开发人员的最佳起点。 Solana 的高速、低成本和能源效率使其成为理想的学习网络。

    source is : https://www.soldev.app/course

    Module 0 : 介绍

    • 入门

    Module 1: 密码学和 Solana 客户端简介

    • 密码学基础知识
    • 从网络读取数据
    • 将数据写入网络
    • 与钱包互动
    • 序列化数据
    • 反序列化数据
    • 页面、顺序和过滤程序数据

    Module 2: 客户端与常见 Solana 程序的交互

    • 使用token program 创建token
    • 通过token swap 交换token
    • 使用Metaplex创建solana nft

    Module 3: 基本 Solana 程序开发

    • hello world
    • 创建基本程序,第 1 部分 - 处理指令数据
    • 创建基本程序,第 2 部分 - 状态管理
    • 创建基本程序,第 3 部分 - 基本安全性和验证

    Module 4: 中级 Solana 程序开发

    • 本地程序开发
    • 程序派生地址
    • 跨程序调用

    Module 5: Anchor 项目开发

    • Anchor 开发简介
    • 客户端 Anchor 开发简介
    • Anchor DPA与账户
    • Anchor CPI和错误

    Modul 6: 超越基础

    • Solana 程序中的环境变量
    • Solana Pay
    • 版本化事务和查找表
    • Rust程序宏

    Module 7: Solana 程序安全

    • 如何使用程序安全模块
    • 签名人授权
    • 账户数据匹配
    • 重新初始化攻击
    • 重复的可变账户
    • 类型角色扮演
    • 任意CPI
    • Bump seed canonicalizatio
    • Closing accounts and revival attacks
    • PDA 共享

    Module 8: 高级 Solana 编程

    • 程序架构
    • 预言机和预言机网络
    • 可验证随机函数
    • 压缩NFT
    - - +

    Solana 开发课程

    info

    欢迎来到希望学习 Web3 开发的 Web 开发人员的最佳起点。 Solana 的高速、低成本和能源效率使其成为理想的学习网络。

    source is : https://www.soldev.app/course

    Module 0 : 介绍

    • 入门

    Module 1: 密码学和 Solana 客户端简介

    • 密码学基础知识
    • 从网络读取数据
    • 将数据写入网络
    • 与钱包互动
    • 序列化数据
    • 反序列化数据
    • 页面、顺序和过滤程序数据

    Module 2: 客户端与常见 Solana 程序的交互

    • 使用token program 创建token
    • 通过token swap 交换token
    • 使用Metaplex创建solana nft

    Module 3: 基本 Solana 程序开发

    • hello world
    • 创建基本程序,第 1 部分 - 处理指令数据
    • 创建基本程序,第 2 部分 - 状态管理
    • 创建基本程序,第 3 部分 - 基本安全性和验证

    Module 4: 中级 Solana 程序开发

    • 本地程序开发
    • 程序派生地址
    • 跨程序调用

    Module 5: Anchor 项目开发

    • Anchor 开发简介
    • 客户端 Anchor 开发简介
    • Anchor DPA与账户
    • Anchor CPI和错误

    Modul 6: 超越基础

    • Solana 程序中的环境变量
    • Solana Pay
    • 版本化事务和查找表
    • Rust程序宏

    Module 7: Solana 程序安全

    • 如何使用程序安全模块
    • 签名人授权
    • 账户数据匹配
    • 重新初始化攻击
    • 重复的可变账户
    • 类型角色扮演
    • 任意CPI
    • Bump seed canonicalizatio
    • Closing accounts and revival attacks
    • PDA 共享

    Module 8: 高级 Solana 编程

    • 程序架构
    • 预言机和预言机网络
    • 可验证随机函数
    • 压缩NFT
    + + \ No newline at end of file diff --git a/solana-development-course/module0/index.html b/solana-development-course/module0/index.html index f640885c4..9da6f307a 100644 --- a/solana-development-course/module0/index.html +++ b/solana-development-course/module0/index.html @@ -9,14 +9,14 @@ - - + +

    课程指南

    欢迎!

    欢迎来到最适合希望学习 Web3 和区块链的开发人员的起点!

    什么是 Web3?

    通常,在旧系统中,人们通过第三方平台相互交互:

    • 用户帐户存储在 Google、X(以前称为 Twitter)和 Meta(Facebook、Instagram)等大型平台上。这些帐户可以由公司任意删除,并且这些帐户“拥有”的项目可能会永久丢失。

    • 存储价值的帐户(如支付卡、银行帐户和交易帐户)由大型平台(如信用卡公司、汇款组织和股票交易所)处理。在许多情况下,这些公司会收取其平台上发生的每笔交易的一部分(约为 1% 到 3%)。他们通常会放慢交易结算速度,以便使组织受益。在某些情况下,被转移的物品可能根本不属于收件人,而只是由收件人代为保管。

    Web3 是互联网的演变,它允许人们直接相互交易:

    • 用户拥有自己的帐户,由他们的钱包代表。

    • 价值转移可以直接在用户之间进行。

    • 代表货币、数字艺术、活动门票、房地产或其他任何事物的代币完全由用户保管。

    Web3 的常见用途包括:

    • 以接近零的费用和即时结算在线销售商品和服务。
    • 销售数字或实体物品,确保每个物品都是真品,并且副本可以与原始物品区分开来。
    • 即时全球支付,无需“汇款”公司的時間和费用。

    什么是 Solana?

    Solana 是第一个可扩展的 Layer 1 区块链。

    与比特币和以太坊等旧平台相比,Solana 具有以下优势:

    • 显著更快 - 大多数交易在一两秒内完成。
    • 大幅更便宜 - 交易费用(在旧网络中称为“gas 费”)通常为 0.000025 美元(是的,远远不到一美分),无论转移什么价值。
    • 高度去中心化,具有所有权益证明网络中最高的 Nakamoto 系数(去中心化评分)之一。 -Solana 上的许多常见用例由于旧区块链的高成本和缓慢的交易时间而在 Solana 上才有可能。

    我将在这门课程中学习什么?

    在这门课程中,您将:

    • 创建允许人们使用 Web3 钱包登录的 Web 应用程序
    • 在人与人之间转让代币(如 USDC,一种代表美元的代币)
    • 将 Solana pay 等工具集成到您现有的应用程序中
    • 构建一个电影评论应用程序,该应用程序在 Solana 区块链上实时运行。您将构建一个 Web 前端和应用程序的后端程序和数据库
    • 铸造大规模 NFT 系列

    等等。我们正在不断更新这门课程,因此随着新技术加入 Solana 生态系统,您将在此处找到课程。

    开始之前我需要什么?

    您不需要以前的区块链经验即可参加这门课程!

    • Linux、Mac 或 Windows 计算机。Windows 计算机应安装 Windows Terminal WSL。
    • 基本的 JavaScript/TypeScript 编程经验。我们还会使用一些 Rust,但我们会边讲 Rust 边讲。
    • 安装了 node.js 18
    • 安装了 Rust
    • 命令行的基本使用

    这门课程的结构如何?

    模块涵盖特定主题。这些模块被分解为单独的课程。

    每个课程都从列出课程目标开始,即您将在课程中学习的内容。

    然后有一个简短的“TL;DR”,以便您可以浏览一下,了解课程涵盖的内容,并决定课程是否适合您。

    然后每个课程都有三个部分:

    • 概述 - 概述包含说明性文本、示例和代码片段。您不需要按照此处显示的任何示例进行编码。目标只是阅读并初步了解课程主题。
    • 演示 - 演示是一个教程式的项目。您绝对应该按照此部分进行编码。这是您第二次接触内容,也是您第一次有机会深入了解并去做。
    • 挑战 - 挑战包含一个与演示类似的项目,只是没有引导您完成它,而是只留下了一些简单的提示,您应该独立完成。

    这种结构借鉴了一种称为 IWY 循环的教学方法。IWY 代表“我做,我们做,你做”。沿途的每个步骤都会增加您对主题的了解,并减少您获得的指导量。

    如何有效地使用这门课程?

    这里的课程非常有效,但每个人都来自不同的背景和能力,静态内容无法考虑这些因素。考虑到这一点,以下是有关如何充分利用课程的三项建议:

    • 对自己诚实——这可能听起来有点含糊,但对自己诚实,了解自己对某个主题的理解程度对于掌握它至关重要。读到一件事并想“是的,是的,我明白了”,然后才意识到你实际上没有明白,这真的很容易。在学习每一课时要对自己诚实。如果您需要,请毫不犹豫地重复某些部分,或者当课程措辞不太适合您时进行外部研究。

    • 做每一个演示和挑战——这支持了第一点。当你强迫自己尝试做某件事时,很难对自己撒谎说你对某件事有多了解。进行每个演示和每个挑战来测试您所处的位置,并根据需要重复它们。我们为所有内容提供解决方案代码,但请务必将其用作有用的资源而不是拐杖。

    • 超越——我知道这听起来很陈词滥调,但不要仅仅停留在演示和挑战要求你做的事情上。发挥创意!把这些项目变成你自己的。超越他们。你练习得越多,你就会越好。

    好了,我的励志演讲就到此为止。追上它吧!

    - - +Solana 上的许多常见用例由于旧区块链的高成本和缓慢的交易时间而在 Solana 上才有可能。

    我将在这门课程中学习什么?

    在这门课程中,您将:

    • 创建允许人们使用 Web3 钱包登录的 Web 应用程序
    • 在人与人之间转让代币(如 USDC,一种代表美元的代币)
    • 将 Solana pay 等工具集成到您现有的应用程序中
    • 构建一个电影评论应用程序,该应用程序在 Solana 区块链上实时运行。您将构建一个 Web 前端和应用程序的后端程序和数据库
    • 铸造大规模 NFT 系列

    等等。我们正在不断更新这门课程,因此随着新技术加入 Solana 生态系统,您将在此处找到课程。

    开始之前我需要什么?

    您不需要以前的区块链经验即可参加这门课程!

    • Linux、Mac 或 Windows 计算机。Windows 计算机应安装 Windows Terminal WSL。
    • 基本的 JavaScript/TypeScript 编程经验。我们还会使用一些 Rust,但我们会边讲 Rust 边讲。
    • 安装了 node.js 18
    • 安装了 Rust
    • 命令行的基本使用

    这门课程的结构如何?

    模块涵盖特定主题。这些模块被分解为单独的课程。

    每个课程都从列出课程目标开始,即您将在课程中学习的内容。

    然后有一个简短的“TL;DR”,以便您可以浏览一下,了解课程涵盖的内容,并决定课程是否适合您。

    然后每个课程都有三个部分:

    • 概述 - 概述包含说明性文本、示例和代码片段。您不需要按照此处显示的任何示例进行编码。目标只是阅读并初步了解课程主题。
    • 演示 - 演示是一个教程式的项目。您绝对应该按照此部分进行编码。这是您第二次接触内容,也是您第一次有机会深入了解并去做。
    • 挑战 - 挑战包含一个与演示类似的项目,只是没有引导您完成它,而是只留下了一些简单的提示,您应该独立完成。

    这种结构借鉴了一种称为 IWY 循环的教学方法。IWY 代表“我做,我们做,你做”。沿途的每个步骤都会增加您对主题的了解,并减少您获得的指导量。

    如何有效地使用这门课程?

    这里的课程非常有效,但每个人都来自不同的背景和能力,静态内容无法考虑这些因素。考虑到这一点,以下是有关如何充分利用课程的三项建议:

    • 对自己诚实——这可能听起来有点含糊,但对自己诚实,了解自己对某个主题的理解程度对于掌握它至关重要。读到一件事并想“是的,是的,我明白了”,然后才意识到你实际上没有明白,这真的很容易。在学习每一课时要对自己诚实。如果您需要,请毫不犹豫地重复某些部分,或者当课程措辞不太适合您时进行外部研究。

    • 做每一个演示和挑战——这支持了第一点。当你强迫自己尝试做某件事时,很难对自己撒谎说你对某件事有多了解。进行每个演示和每个挑战来测试您所处的位置,并根据需要重复它们。我们为所有内容提供解决方案代码,但请务必将其用作有用的资源而不是拐杖。

    • 超越——我知道这听起来很陈词滥调,但不要仅仅停留在演示和挑战要求你做的事情上。发挥创意!把这些项目变成你自己的。超越他们。你练习得越多,你就会越好。

    好了,我的励志演讲就到此为止。追上它吧!

    + + \ No newline at end of file diff --git a/solana-development-course/module1/cryptography_and_solana/index.html b/solana-development-course/module1/cryptography_and_solana/index.html index 89b25fd33..fdb97a590 100644 --- a/solana-development-course/module1/cryptography_and_solana/index.html +++ b/solana-development-course/module1/cryptography_and_solana/index.html @@ -9,13 +9,13 @@ - - + +
    -

    cryptography_and_solana

    TL;DR

    • 密钥对是一对匹配的公钥和秘密密钥。
    • 公钥用作指向 Solana 网络上帐户的“地址”。公钥可以与任何人共享。
    • 密钥用于验证帐户的权限。顾名思义,您应该始终对密钥保密。
    • @solana/web3.js 提供了用于创建全新密钥对或使用现有密钥构建密钥对的辅助函数。

    概述

    对称和非对称密码学

    “密码学”字面意思是隐藏信息的研究。您每天会遇到两种主要类型的密码学:

    对称密码术是使用相同的密钥来加密和解密。它已有数百年历史,从古埃及人到伊丽莎白一世女王,每个人都在使用它。

    对称加密算法有多种,但今天最常见的是 AES 和 Chacha20。

    非对称密码学

    • 非对称加密技术 - 也称为“公钥加密技术”,于 20 世纪 70 年代开发。在非对称加密中,参与者拥有密钥对(或密钥对)。每个密钥对由一个秘密密钥和一个公钥组成。非对称加密的工作原理与对称加密不同,并且可以做不同的事情
    • 加密:如果使用公钥加密,则只能使用同一密钥对中的秘密密钥来读取它
    • 签名:如果使用密钥加密,则可以使用同一密钥对中的公钥来证明密钥的持有者对其进行了签名。
    • 您甚至可以使用非对称加密技术来计算出用于对称加密技术的好密钥!这称为密钥交换,您使用自己的公钥和接收者的公钥来生成“会话”密钥。
    • 对称加密算法有多种,但今天最常见的是 ECC 或 RSA 的变体。

    非对称加密非常流行:

    • 您的银行卡内有一个密钥,用于签署交易。
    • 您的银行可以通过使用匹配的公钥进行检查来确认您是否进行了交易。
    • 网站在其证书中包含公钥,您的浏览器将使用此公钥来加密发送到网页的数据(例如个人信息、登录详细信息和信用卡号)。 网站有匹配的私钥,因此网站可以读取数据。
    • 您的电子护照由签发国家签署,以确保护照不被伪造。电子护照门可以使用您的签发国的公钥来确认这一点。
    • 手机上的消息应用程序使用密钥交换来创建会话密钥。

    简而言之,密码学就在我们身边。 Solana 以及其他区块链只是密码学的一种用途。

    Solana 使用公钥作为地址

    参与 Solana 网络的人至少拥有一对密钥。在索拉纳:

    • 公钥用作指向 Solana 网络上帐户的“地址”。即使是友好的名称 - 例如 example.sol - 也指向 dDCQNnDmNbFVi8cQhKAgXhyhXeJ625tvwsunRyRc7c8 这样的地址
    • 私钥用于验证该密钥对的权限。如果您拥有某个地址的密钥,您就可以控制该地址内的代币。因此,顾名思义,您应该始终对密钥保密。

    使用 @solana/web3.js 制作密钥对

    您可以通过浏览器或带有 @solana/web3.js npm 模块的 Node.js 使用 Solana 区块链。按照平常的方式设置一个项目,然后使用 npm 安装 @solana/web3.js

    npm i @solana/web3.js

    我们将在本课程中逐步介绍许多 web3.js 的内容,但您也可以查看官方 web3.js 文档

    要发送令牌、发送 NFTS 或读取和写入数据 Solana,您需要自己的密钥对。要创建新的密钥对,请使用 @solana/web3.js 中的 Keypair.generate() 函数:

    import { Keypair } from "@solana/web3.js";

    const keypair = Keypair.generate();

    console.log(`The public key is: `, keypair.publicKey.toBase58());
    console.log(`The secret key is: `, keypair.secretKey);

    ⚠️ 不要在源代码中包含密钥

    由于密钥对可以从密钥重新生成,因此我们通常只存储密钥,并从密钥恢复密钥对。

    此外,由于密钥赋予了地址权限,因此我们不会将密钥存储在源代码中。相反,我们:

    • 将密钥放入 .env 文件中
    • .env 添加到 .gitignore,这样 .env 文件就不会被提交。

    加载现有密钥对

    如果您已经有想要使用的密钥对,则可以从文件系统或 .env 文件中存储的现有密钥加载密钥对。在node.js中,@solana-developers/node-helpers npm包包含一些额外的功能:

    • 要使用 .env 文件,请使用 getKeypairFromEnvironment()
    • 要使用 Solana CLI 文件,请使用 `getKeypairFromFile()``
    import * as dotenv from "dotenv";
    import { getKeypairFromEnvironment } from "@solana-developers/node-helpers";

    dotenv.config();

    const keypair = getKeypairFromEnvironment("SECRET_KEY");

    您知道如何制作和加载密钥对!让我们练习一下我们所学的内容。

    演示

    安装

    创建一个新目录,安装 TypeScriptSolana web3.jsesrun

    mkdir generate-keypair
    cd generate-keypair
    npm init -y
    npm install typescript @solana/web3.js @digitak/esrun @solana-developers/node-helpers

    创建一个名为generate-keypair.ts的新文件

    import { Keypair } from "@solana/web3.js";
    const keypair = Keypair.generate();
    console.log(`✅ Generated keypair!`)

    运行 npx esrungenerate-keypair.ts。您应该看到文本:

    ✅ Generated keypair!

    每个密钥对都有一个 publicKeySecretKey 属性。更新文件:

    import { Keypair } from "@solana/web3.js";

    const keypair = Keypair.generate();

    console.log(`The public key is: `, keypair.publicKey.toBase58());
    console.log(`The secret key is: `, keypair.secretKey);
    console.log(`✅ Finished!`);

    运行 npx esrungenerate-keypair.ts。您应该看到文本:

    The public key is:  764CksEAZvm7C1mg2uFmpeFvifxwgjqxj2bH6Ps7La4F
    The secret key is: Uint8Array(64) [
    (a long series of numbers)
    ]
    ✅ Finished!

    .env 文件加载现有密钥对

    为了确保您的密钥安全,我们建议使用 .env 文件注入密钥:

    使用您之前创建的密钥的内容创建一个名为 .env 的新文件:

    SECRET_KEY="[(a series of numbers)]"

    然后我们可以从环境中加载密钥对。更新generate-keypair.ts

    import * as dotenv from "dotenv";
    import { getKeypairFromEnvironment } from "@solana-developers/node-helpers";

    dotenv.config();

    const keypair = getKeypairFromEnvironment("SECRET_KEY");

    console.log(
    `✅ Finished! We've loaded our secret key securely, using an env file!`
    );

    运行 npx esrungenerate-keypair.ts。您应该看到以下结果:

    ✅ Finished! We've loaded our secret key securely, using an env file!

    我们现在已经了解了密钥对,以及如何在 Solana 上安全地存储密钥。在下一章中,我们将使用它们!

    - - +

    cryptography_and_solana

    TL;DR

    • 密钥对是一对匹配的公钥和秘密密钥。
    • 公钥用作指向 Solana 网络上帐户的“地址”。公钥可以与任何人共享。
    • 密钥用于验证帐户的权限。顾名思义,您应该始终对密钥保密。
    • @solana/web3.js 提供了用于创建全新密钥对或使用现有密钥构建密钥对的辅助函数。

    概述

    对称和非对称密码学

    “密码学”字面意思是隐藏信息的研究。您每天会遇到两种主要类型的密码学:

    对称密码术是使用相同的密钥来加密和解密。它已有数百年历史,从古埃及人到伊丽莎白一世女王,每个人都在使用它。

    对称加密算法有多种,但今天最常见的是 AES 和 Chacha20。

    非对称密码学

    • 非对称加密技术 - 也称为“公钥加密技术”,于 20 世纪 70 年代开发。在非对称加密中,参与者拥有密钥对(或密钥对)。每个密钥对由一个秘密密钥和一个公钥组成。非对称加密的工作原理与对称加密不同,并且可以做不同的事情
    • 加密:如果使用公钥加密,则只能使用同一密钥对中的秘密密钥来读取它
    • 签名:如果使用密钥加密,则可以使用同一密钥对中的公钥来证明密钥的持有者对其进行了签名。
    • 您甚至可以使用非对称加密技术来计算出用于对称加密技术的好密钥!这称为密钥交换,您使用自己的公钥和接收者的公钥来生成“会话”密钥。
    • 对称加密算法有多种,但今天最常见的是 ECC 或 RSA 的变体。

    非对称加密非常流行:

    • 您的银行卡内有一个密钥,用于签署交易。
    • 您的银行可以通过使用匹配的公钥进行检查来确认您是否进行了交易。
    • 网站在其证书中包含公钥,您的浏览器将使用此公钥来加密发送到网页的数据(例如个人信息、登录详细信息和信用卡号)。 网站有匹配的私钥,因此网站可以读取数据。
    • 您的电子护照由签发国家签署,以确保护照不被伪造。电子护照门可以使用您的签发国的公钥来确认这一点。
    • 手机上的消息应用程序使用密钥交换来创建会话密钥。

    简而言之,密码学就在我们身边。 Solana 以及其他区块链只是密码学的一种用途。

    Solana 使用公钥作为地址

    参与 Solana 网络的人至少拥有一对密钥。在索拉纳:

    • 公钥用作指向 Solana 网络上帐户的“地址”。即使是友好的名称 - 例如 example.sol - 也指向 dDCQNnDmNbFVi8cQhKAgXhyhXeJ625tvwsunRyRc7c8 这样的地址
    • 私钥用于验证该密钥对的权限。如果您拥有某个地址的密钥,您就可以控制该地址内的代币。因此,顾名思义,您应该始终对密钥保密。

    使用 @solana/web3.js 制作密钥对

    您可以通过浏览器或带有 @solana/web3.js npm 模块的 Node.js 使用 Solana 区块链。按照平常的方式设置一个项目,然后使用 npm 安装 @solana/web3.js

    npm i @solana/web3.js

    我们将在本课程中逐步介绍许多 web3.js 的内容,但您也可以查看官方 web3.js 文档

    要发送令牌、发送 NFTS 或读取和写入数据 Solana,您需要自己的密钥对。要创建新的密钥对,请使用 @solana/web3.js 中的 Keypair.generate() 函数:

    import { Keypair } from "@solana/web3.js";

    const keypair = Keypair.generate();

    console.log(`The public key is: `, keypair.publicKey.toBase58());
    console.log(`The secret key is: `, keypair.secretKey);

    ⚠️ 不要在源代码中包含密钥

    由于密钥对可以从密钥重新生成,因此我们通常只存储密钥,并从密钥恢复密钥对。

    此外,由于密钥赋予了地址权限,因此我们不会将密钥存储在源代码中。相反,我们:

    • 将密钥放入 .env 文件中
    • .env 添加到 .gitignore,这样 .env 文件就不会被提交。

    加载现有密钥对

    如果您已经有想要使用的密钥对,则可以从文件系统或 .env 文件中存储的现有密钥加载密钥对。在node.js中,@solana-developers/node-helpers npm包包含一些额外的功能:

    • 要使用 .env 文件,请使用 getKeypairFromEnvironment()
    • 要使用 Solana CLI 文件,请使用 `getKeypairFromFile()``
    import * as dotenv from "dotenv";
    import { getKeypairFromEnvironment } from "@solana-developers/node-helpers";

    dotenv.config();

    const keypair = getKeypairFromEnvironment("SECRET_KEY");

    您知道如何制作和加载密钥对!让我们练习一下我们所学的内容。

    演示

    安装

    创建一个新目录,安装 TypeScriptSolana web3.jsesrun

    mkdir generate-keypair
    cd generate-keypair
    npm init -y
    npm install typescript @solana/web3.js @digitak/esrun @solana-developers/node-helpers

    创建一个名为generate-keypair.ts的新文件

    import { Keypair } from "@solana/web3.js";
    const keypair = Keypair.generate();
    console.log(`✅ Generated keypair!`)

    运行 npx esrungenerate-keypair.ts。您应该看到文本:

    ✅ Generated keypair!

    每个密钥对都有一个 publicKeySecretKey 属性。更新文件:

    import { Keypair } from "@solana/web3.js";

    const keypair = Keypair.generate();

    console.log(`The public key is: `, keypair.publicKey.toBase58());
    console.log(`The secret key is: `, keypair.secretKey);
    console.log(`✅ Finished!`);

    运行 npx esrungenerate-keypair.ts。您应该看到文本:

    The public key is:  764CksEAZvm7C1mg2uFmpeFvifxwgjqxj2bH6Ps7La4F
    The secret key is: Uint8Array(64) [
    (a long series of numbers)
    ]
    ✅ Finished!

    .env 文件加载现有密钥对

    为了确保您的密钥安全,我们建议使用 .env 文件注入密钥:

    使用您之前创建的密钥的内容创建一个名为 .env 的新文件:

    SECRET_KEY="[(a series of numbers)]"

    然后我们可以从环境中加载密钥对。更新generate-keypair.ts

    import * as dotenv from "dotenv";
    import { getKeypairFromEnvironment } from "@solana-developers/node-helpers";

    dotenv.config();

    const keypair = getKeypairFromEnvironment("SECRET_KEY");

    console.log(
    `✅ Finished! We've loaded our secret key securely, using an env file!`
    );

    运行 npx esrungenerate-keypair.ts。您应该看到以下结果:

    ✅ Finished! We've loaded our secret key securely, using an env file!

    我们现在已经了解了密钥对,以及如何在 Solana 上安全地存储密钥。在下一章中,我们将使用它们!

    + + \ No newline at end of file diff --git a/solana-development-course/module1/index.html b/solana-development-course/module1/index.html index b16f9613e..a9f897168 100644 --- a/solana-development-course/module1/index.html +++ b/solana-development-course/module1/index.html @@ -9,13 +9,13 @@ - - + +
    -

    密码学和 Solana 客户端简介

    • 密码学基础知识
    • 从网络读取数据
    • 将数据写入网络
    • 与钱包互动
    • 序列化数据
    • 反序列化数据
    • 页面、顺序和过滤程序数据
    - - +

    密码学和 Solana 客户端简介

    • 密码学基础知识
    • 从网络读取数据
    • 将数据写入网络
    • 与钱包互动
    • 序列化数据
    • 反序列化数据
    • 页面、顺序和过滤程序数据
    + + \ No newline at end of file diff --git a/solana-development-course/module1/read_data_from_solana/index.html b/solana-development-course/module1/read_data_from_solana/index.html index 84317892e..562fe3c99 100644 --- a/solana-development-course/module1/read_data_from_solana/index.html +++ b/solana-development-course/module1/read_data_from_solana/index.html @@ -9,13 +9,13 @@ - - + +
    -

    从 Solana 网络读取数据

    TL;DR

    • SOL 是 Solana 原生代币的名称。每个 Sol 由 10 亿个 Lamports 组成。
    • 账户存储代币、NFT、程序和数据。现在我们将重点关注存储 SOL 的帐户。
    • 地址指向 Solana 网络上的帐户。任何人都可以读取给定地址中的数据。大多数地址也是公钥

    概述

    账户

    Solana 上存储的所有数据都存储在帐户中。帐户可以存储:

    • SOL
    • 其他代币,例如 USDC
    • NFT
    • Program,比如我们这门课做的影评Program!
    • Program 数据,例如对上述节目的特定电影的评论!

    SOL

    SOL 是 Solana 的原生代币 - SOL 用于支付交易费用、支付账户租金等。 SOL 有时用 ◎ 符号显示。每个 SOL 由 10 亿个 Lamports 组成。与金融应用程序通常以美分(美元)、便士(英镑)进行数学计算的方式相同,Solana 应用程序通常使用 Lamports 进行数学计算,并且仅转换为 SOL 来显示数据。

    地址

    地址唯一标识帐户。地址通常显示为 base-58 编码字符串,例如 dDCQNnDmNbFVi8cQhKAgXhyhXeJ625tvwsunRyRc7c8。 Solana 上的大多数地址也是公钥。正如上一章提到的,谁控制了匹配的密钥,谁就控制了该帐户——例如,拥有密钥的人可以从该帐户发送代币。

    从 Solana 区块链读取

    安装

    我们使用名为 @solana/web3.js 的 npm 包来完成 Solana 的大部分工作。我们还将安装 TypeScript 和 esrun,以便我们可以运行命令行:

    npm install typescript @solana/web3.js @digitak/esrun

    连接到网络

    使用 @solana/web3.js 与 Solana 网络的每次交互都将通过 Connection 对象进行。 Connection 对象与特定 Solana 网络(称为“集群”)建立连接。

    现在我们将使用 Devnet 集群而不是Mainnet。顾名思义,Devnet 集群是为开发人员使用和测试而设计的。

    import { Connection, clusterApiUrl } from "@solana/web3.js";

    const connection = new Connection(clusterApiUrl("devnet"));
    console.log(`✅ Connected!`)

    运行此 TypeScript (npx esrun example.ts) 显示:

    ✅ Connected!

    从网络读取

    读取账户余额:

    import { Connection, PublicKey, clusterApiUrl } from "@solana/web3.js";

    const connection = new Connection(clusterApiUrl("devnet"));
    const address = new PublicKey('CenYq6bDRB7p73EjsPEpiYN7uveyPUTdXkDkgUduboaN');
    const balance = await connection.getBalance(address);

    console.log(`The balance of the account at ${address} is ${balance} lamports`);
    console.log(`✅ Finished!`)

    退回的余额存放在灯箱中。 lamport 是 Sol 的小单位,就像美分对美元或便士对英镑一样。单个 lamport 代表 0.000000001 SOL。大多数时候,我们会将 SOL 作为 Lamport 进行传输、花费、存储和处理,仅转换为完整的 SOL 来显示给用户。 Web3.js 提供了常量 LAMPORTS_PER_SOL 来进行快速转换。

    import { Connection, PublicKey, clusterApiUrl, LAMPORTS_PER_SOL } from "@solana/web3.js";

    const connection = new Connection(clusterApiUrl("devnet"));
    const address = new PublicKey('CenYq6bDRB7p73EjsPEpiYN7uveyPUTdXkDkgUduboaN');
    const balance = await connection.getBalance(address);
    const balanceInSol = balance / LAMPORTS_PER_SOL;

    console.log(`The balance of the account at ${address} is ${balanceInSol} SOL`);
    console.log(`✅ Finished!`)

    运行 npx esrun example.ts 将显示类似以下内容:

    The balance of the account at CenYq6bDRB7p73EjsPEpiYN7uveyPUTdXkDkgUduboaN is 0.00114144 SOL
    ✅ Finished!

    ...就像这样,我们正在从 Solana 区块链读取数据!

    演示

    让我们练习所学的内容,并创建一个简单的网站,让用户检查特定地址的余额。

    它看起来像这样:

    为了紧扣主题,我们不会完全从头开始工作,因此请下载入门代码。入门项目使用 Next.js 和 Typescript。如果您习惯了不同的堆栈,请不要担心!您将在这些课程中学到的 web3 和 Solana 原则适用于您最熟悉的任何前端堆栈。

    1. 确定方向

    获得起始代码后,请四处查看。使用 npm install 安装依赖项,然后使用 npm run dev 运行应用程序。请注意,无论您在地址字段中输入什么内容,当您单击“检查 SOL 余额”时,余额都将是占位符值 1000。

    从结构上讲,该应用程序由index.tsxAddressForm.tsx组成。当用户提交表单时,index.tsx 中的 addressSubscribedHandler 被调用。这就是我们将添加逻辑来更新 UI 其余部分的地方。

    2.安装依赖

    使用 npm install @solana/web3.js 安装对 Solana web3 库的依赖项。

    3.设置地址余额

    首先,在index.tsx顶部导入@solana/web3.js

    现在该库已可用,让我们进入 addressSubscribedHandler() 并使用表单输入中的地址值创建 PublicKey 的实例。接下来,创建 Connection 的实例并使用它来调用 getBalance()。传入您刚刚创建的公钥的值。最后,调用setBalance(),传入getBalance的结果。如果您愿意,请独立尝试,而不是从下面的代码片段中复制。

    import type { NextPage } from 'next'
    import { useState } from 'react'
    import styles from '../styles/Home.module.css'
    import AddressForm from '../components/AddressForm'
    import * as web3 from '@solana/web3.js'

    const Home: NextPage = () => {
    const [balance, setBalance] = useState(0)
    const [address, setAddress] = useState('')

    const addressSubmittedHandler = async (address: string) => {
    setAddress(address)
    const key = new web3.PublicKey(address)
    const connection = new web3.Connection(web3.clusterApiUrl('devnet'));
    const balance = await connection.getBalance(key);
    setBalance(balance / web3.LAMPORTS_PER_SOL);
    }
    ...
    }

    大多数时候,在处理 SOL 时,系统会使用 lamports 而不是 SOL。由于计算机更擅长处理整数而不是分数,因此我们通常在整数中进行大部分交易,仅转换回 Sol 来向用户显示值。这就是为什么我们将 Solana 返回的余额除以 LAMPORTS_PER_SOL

    在将其设置为我们的状态之前,我们还使用 LAMPORTS_PER_SOL 常量将其转换为 SOL。

    此时,您应该能够在表单字段中输入有效地址,然后单击“检查 SOL 余额”以查看下面填充的地址和余额。

    4. 处理无效地址

    我们即将完成。唯一剩下的问题是,使用无效地址不会显示任何错误消息或更改显示的余额。如果打开开发者控制台,您将看到错误:无效的公钥输入。使用 PublicKey 构造函数时,需要传入有效的地址,否则会出现此错误。

    为了解决这个问题,让我们将所有内容包装在 try-catch 块中,并在用户输入无效时提醒用户。

    const addressSubmittedHandler = async (address: string) => {
    try {
    setAddress(address);
    const key = new web3.PublicKey(address);
    const connection = new web3.Connection(web3.clusterApiUrl("devnet"));
    const balance = await connection.getBalance(key)
    setBalance(balance / web3.LAMPORTS_PER_SOL);
    } catch (error) {
    setAddress("");
    setBalance(0);
    alert(error);
    }
    };

    请注意,在 catch 块中,我们还清除了地址和余额以避免混淆。

    我们做到了!我们有一个正常运行的站点,可以从 Solana 网络读取 SOL 余额。您正在 Solana 上实现您的宏伟抱负。如果您需要花更多时间查看此代码以更好地理解它,请查看完整的解决方案代码。坚持住,这些课程会很快增加。

    挑战

    由于这是第一个挑战,我们将保持简单。继续添加到我们已经创建的前端,在“余额”之后添加一个行项目。让行项目显示该帐户是否是可执行帐户。提示:有一个 getAccountInfo() 方法。

    由于这是 DevNet,您的常规主网钱包地址将无法执行,因此如果您想要一个可执行的地址用于测试,请使用 CenYq6bDRB7p73EjsPEpiYN7uveyPUTdXkDkgUduboaN

    如果您遇到困难,请随时查看解决方案代码

    - - +

    从 Solana 网络读取数据

    TL;DR

    • SOL 是 Solana 原生代币的名称。每个 Sol 由 10 亿个 Lamports 组成。
    • 账户存储代币、NFT、程序和数据。现在我们将重点关注存储 SOL 的帐户。
    • 地址指向 Solana 网络上的帐户。任何人都可以读取给定地址中的数据。大多数地址也是公钥

    概述

    账户

    Solana 上存储的所有数据都存储在帐户中。帐户可以存储:

    • SOL
    • 其他代币,例如 USDC
    • NFT
    • Program,比如我们这门课做的影评Program!
    • Program 数据,例如对上述节目的特定电影的评论!

    SOL

    SOL 是 Solana 的原生代币 - SOL 用于支付交易费用、支付账户租金等。 SOL 有时用 ◎ 符号显示。每个 SOL 由 10 亿个 Lamports 组成。与金融应用程序通常以美分(美元)、便士(英镑)进行数学计算的方式相同,Solana 应用程序通常使用 Lamports 进行数学计算,并且仅转换为 SOL 来显示数据。

    地址

    地址唯一标识帐户。地址通常显示为 base-58 编码字符串,例如 dDCQNnDmNbFVi8cQhKAgXhyhXeJ625tvwsunRyRc7c8。 Solana 上的大多数地址也是公钥。正如上一章提到的,谁控制了匹配的密钥,谁就控制了该帐户——例如,拥有密钥的人可以从该帐户发送代币。

    从 Solana 区块链读取

    安装

    我们使用名为 @solana/web3.js 的 npm 包来完成 Solana 的大部分工作。我们还将安装 TypeScript 和 esrun,以便我们可以运行命令行:

    npm install typescript @solana/web3.js @digitak/esrun

    连接到网络

    使用 @solana/web3.js 与 Solana 网络的每次交互都将通过 Connection 对象进行。 Connection 对象与特定 Solana 网络(称为“集群”)建立连接。

    现在我们将使用 Devnet 集群而不是Mainnet。顾名思义,Devnet 集群是为开发人员使用和测试而设计的。

    import { Connection, clusterApiUrl } from "@solana/web3.js";

    const connection = new Connection(clusterApiUrl("devnet"));
    console.log(`✅ Connected!`)

    运行此 TypeScript (npx esrun example.ts) 显示:

    ✅ Connected!

    从网络读取

    读取账户余额:

    import { Connection, PublicKey, clusterApiUrl } from "@solana/web3.js";

    const connection = new Connection(clusterApiUrl("devnet"));
    const address = new PublicKey('CenYq6bDRB7p73EjsPEpiYN7uveyPUTdXkDkgUduboaN');
    const balance = await connection.getBalance(address);

    console.log(`The balance of the account at ${address} is ${balance} lamports`);
    console.log(`✅ Finished!`)

    退回的余额存放在灯箱中。 lamport 是 Sol 的小单位,就像美分对美元或便士对英镑一样。单个 lamport 代表 0.000000001 SOL。大多数时候,我们会将 SOL 作为 Lamport 进行传输、花费、存储和处理,仅转换为完整的 SOL 来显示给用户。 Web3.js 提供了常量 LAMPORTS_PER_SOL 来进行快速转换。

    import { Connection, PublicKey, clusterApiUrl, LAMPORTS_PER_SOL } from "@solana/web3.js";

    const connection = new Connection(clusterApiUrl("devnet"));
    const address = new PublicKey('CenYq6bDRB7p73EjsPEpiYN7uveyPUTdXkDkgUduboaN');
    const balance = await connection.getBalance(address);
    const balanceInSol = balance / LAMPORTS_PER_SOL;

    console.log(`The balance of the account at ${address} is ${balanceInSol} SOL`);
    console.log(`✅ Finished!`)

    运行 npx esrun example.ts 将显示类似以下内容:

    The balance of the account at CenYq6bDRB7p73EjsPEpiYN7uveyPUTdXkDkgUduboaN is 0.00114144 SOL
    ✅ Finished!

    ...就像这样,我们正在从 Solana 区块链读取数据!

    演示

    让我们练习所学的内容,并创建一个简单的网站,让用户检查特定地址的余额。

    它看起来像这样:

    为了紧扣主题,我们不会完全从头开始工作,因此请下载入门代码。入门项目使用 Next.js 和 Typescript。如果您习惯了不同的堆栈,请不要担心!您将在这些课程中学到的 web3 和 Solana 原则适用于您最熟悉的任何前端堆栈。

    1. 确定方向

    获得起始代码后,请四处查看。使用 npm install 安装依赖项,然后使用 npm run dev 运行应用程序。请注意,无论您在地址字段中输入什么内容,当您单击“检查 SOL 余额”时,余额都将是占位符值 1000。

    从结构上讲,该应用程序由index.tsxAddressForm.tsx组成。当用户提交表单时,index.tsx 中的 addressSubscribedHandler 被调用。这就是我们将添加逻辑来更新 UI 其余部分的地方。

    2.安装依赖

    使用 npm install @solana/web3.js 安装对 Solana web3 库的依赖项。

    3.设置地址余额

    首先,在index.tsx顶部导入@solana/web3.js

    现在该库已可用,让我们进入 addressSubscribedHandler() 并使用表单输入中的地址值创建 PublicKey 的实例。接下来,创建 Connection 的实例并使用它来调用 getBalance()。传入您刚刚创建的公钥的值。最后,调用setBalance(),传入getBalance的结果。如果您愿意,请独立尝试,而不是从下面的代码片段中复制。

    import type { NextPage } from 'next'
    import { useState } from 'react'
    import styles from '../styles/Home.module.css'
    import AddressForm from '../components/AddressForm'
    import * as web3 from '@solana/web3.js'

    const Home: NextPage = () => {
    const [balance, setBalance] = useState(0)
    const [address, setAddress] = useState('')

    const addressSubmittedHandler = async (address: string) => {
    setAddress(address)
    const key = new web3.PublicKey(address)
    const connection = new web3.Connection(web3.clusterApiUrl('devnet'));
    const balance = await connection.getBalance(key);
    setBalance(balance / web3.LAMPORTS_PER_SOL);
    }
    ...
    }

    大多数时候,在处理 SOL 时,系统会使用 lamports 而不是 SOL。由于计算机更擅长处理整数而不是分数,因此我们通常在整数中进行大部分交易,仅转换回 Sol 来向用户显示值。这就是为什么我们将 Solana 返回的余额除以 LAMPORTS_PER_SOL

    在将其设置为我们的状态之前,我们还使用 LAMPORTS_PER_SOL 常量将其转换为 SOL。

    此时,您应该能够在表单字段中输入有效地址,然后单击“检查 SOL 余额”以查看下面填充的地址和余额。

    4. 处理无效地址

    我们即将完成。唯一剩下的问题是,使用无效地址不会显示任何错误消息或更改显示的余额。如果打开开发者控制台,您将看到错误:无效的公钥输入。使用 PublicKey 构造函数时,需要传入有效的地址,否则会出现此错误。

    为了解决这个问题,让我们将所有内容包装在 try-catch 块中,并在用户输入无效时提醒用户。

    const addressSubmittedHandler = async (address: string) => {
    try {
    setAddress(address);
    const key = new web3.PublicKey(address);
    const connection = new web3.Connection(web3.clusterApiUrl("devnet"));
    const balance = await connection.getBalance(key)
    setBalance(balance / web3.LAMPORTS_PER_SOL);
    } catch (error) {
    setAddress("");
    setBalance(0);
    alert(error);
    }
    };

    请注意,在 catch 块中,我们还清除了地址和余额以避免混淆。

    我们做到了!我们有一个正常运行的站点,可以从 Solana 网络读取 SOL 余额。您正在 Solana 上实现您的宏伟抱负。如果您需要花更多时间查看此代码以更好地理解它,请查看完整的解决方案代码。坚持住,这些课程会很快增加。

    挑战

    由于这是第一个挑战,我们将保持简单。继续添加到我们已经创建的前端,在“余额”之后添加一个行项目。让行项目显示该帐户是否是可执行帐户。提示:有一个 getAccountInfo() 方法。

    由于这是 DevNet,您的常规主网钱包地址将无法执行,因此如果您想要一个可执行的地址用于测试,请使用 CenYq6bDRB7p73EjsPEpiYN7uveyPUTdXkDkgUduboaN

    如果您遇到困难,请随时查看解决方案代码

    + + \ No newline at end of file diff --git a/solana-development-course/tags/blockchain/index.html b/solana-development-course/tags/blockchain/index.html index fc5ee9be0..1524e7711 100644 --- a/solana-development-course/tags/blockchain/index.html +++ b/solana-development-course/tags/blockchain/index.html @@ -9,13 +9,13 @@ - - + + - - + + \ No newline at end of file diff --git a/solana-development-course/tags/cryptography/index.html b/solana-development-course/tags/cryptography/index.html index 1a2ce92eb..e2ab9c132 100644 --- a/solana-development-course/tags/cryptography/index.html +++ b/solana-development-course/tags/cryptography/index.html @@ -9,13 +9,13 @@ - - + + - - + + \ No newline at end of file diff --git a/solana-development-course/tags/index.html b/solana-development-course/tags/index.html index 6bfa23b65..854e1f2a8 100644 --- a/solana-development-course/tags/index.html +++ b/solana-development-course/tags/index.html @@ -9,13 +9,13 @@ - - + + - - + + \ No newline at end of file diff --git a/solana-development-course/tags/introduction/index.html b/solana-development-course/tags/introduction/index.html index 7385fd9ee..35d35b3af 100644 --- a/solana-development-course/tags/introduction/index.html +++ b/solana-development-course/tags/introduction/index.html @@ -9,13 +9,13 @@ - - + + - - + + \ No newline at end of file diff --git a/solana-development-course/tags/read-data/index.html b/solana-development-course/tags/read-data/index.html index 23261fd07..a07091900 100644 --- a/solana-development-course/tags/read-data/index.html +++ b/solana-development-course/tags/read-data/index.html @@ -9,13 +9,13 @@ - - + + - - + + \ No newline at end of file diff --git a/solana-development-course/tags/solana/index.html b/solana-development-course/tags/solana/index.html index cf7cf8fd9..c09a762ee 100644 --- a/solana-development-course/tags/solana/index.html +++ b/solana-development-course/tags/solana/index.html @@ -9,13 +9,13 @@ - - + + - - + + \ No newline at end of file