12_Solana-Rust的宏

参考: https://www.soldev.app/course/rust-macros
Rust官方: https://rustwiki.org/zh-CN/book/ch19-06-macros.html

Rust的宏分为2类:

  • 声明宏(Declarative macro): 使用macro_rules!定义,例如: vec!

  • 过程宏(Procedural macro): 使用AST(Abstract syntax tree) 支持更加复杂的代码生成

    • Function-like macros - custom!(...)

    • Derive macros - #[derive(CustomDerive)], 一般用于 struct, enum, union , 用于实现某些trait

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      #[derive(MyMacro)]
      struct Input {
      field: String
      }

      // 指定helper属性
      #[proc_macro_derive(MyMacro, attributes(helper))]
      pub fn my_macro(body: TokenStream) -> TokenStream {
      ...
      }

      #[derive(MyMacro)]
      struct Input {
      #[helper] // 可以根据这个 helper attribitue 执行更多操作
      field: String
      }


    • Attribute macros - #[CustomAttribute]: 用于struct或函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      #[my_macro]
      fn my_function() {
      ...
      }

      #[proc_macro_attribute]
      pub fn my_macro(attr: TokenStream, input: TokenStream) -> TokenStream {
      // 第1个参数 attr 代表属性宏的参数
      // 第2个参数 input,是剩余的元素
      ...,
      }

      #[my_macro(arg1, arg2)]
      fn my_function() {
      ...
      }
  • 可以使用 cargo-expand 命令展开宏

10_Solana-Anchor示例AI问答

与Claude3.5 Sonnet的对话

https://claude.ai/chat/a220af70-c5c7-4765-ae6f-fdd69d7d0fb4

示例程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
use anchor_lang::prelude::*;

declare_id!("5jqhLy3fQ5B2ETUVGhJJSJWEzcK5NiVtRh3pfEcndxTi");

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

pub fn create(ctx: Context<Create>, msg: String) -> Result<()> {
let note = &mut ctx.accounts.note;
note.message = msg;
Ok(())
}
}

#[account]
pub struct Note {
pub message: String,
}

#[derive(Accounts)]
pub struct Create<'info> {
// note账户
#[account(init, payer = user, space = 8 + 32 + 200)]
pub note: Account<'info, Note>,

// 下面2个是固定的, 可以改名,但一般不该
#[account(mut)]
pub user: Signer<'info>, // 或 authority
pub system_program: Program<'info, System>,
}


问: 请逐行解释一下以上Anchor代码

好的,我来为您逐行解释这段 Anchor 代码:

1
use anchor_lang::prelude::*;

这行导入了 Anchor 框架的预定义模块,包含了常用的类型和宏。

1
declare_id!("5jqhLy3fQ5B2ETUVGhJJSJWEzcK5NiVtRh3pfEcndxTi");

这行声明了程序的 ID。每个 Solana 程序都有一个唯一的 ID。

1
2
3
#[program]
pub mod note {
use super::*;

这定义了一个名为 “note” 的程序模块。#[program] 属性告诉 Anchor 这是主程序模块。

1
2
3
4
5
pub fn create(ctx: Context<Create>, msg: String) -> Result<()> {
let note = &mut ctx.accounts.note;
note.message = msg;
Ok(())
}

这是 create 指令的实现。它接收一个 Context<Create> 和一个 String 参数,将消息存储到 note 账户中。

1
2
#[derive(Accounts)]
pub struct Create<'info> {

这定义了 Create 结构体,用于指定 create 指令所需的账户。

1
2
#[account(init, payer = user, space = 8 + 32 + 200)]
pub note: Account<'info, Note>,

这行定义了 note 账户,它将被初始化。payer = user 指定谁支付创建账户的费用,space 指定账户大小。

1
2
#[account(mut)]
pub user: Signer<'info>,

这定义了 user 账户,它是交易的签名者,并且是可变的(因为它要支付费用)。

1
pub system_program: Program<'info, System>,

这是 Solana 的系统程序,用于创建新账户。

1
2
3
4
#[account]
pub struct Note {
pub message: String,
}

这定义了 Note 结构体,表示存储在链上的 note 账户的数据结构。它只包含一个 message 字段。

这段代码实现了一个简单的 note 程序,允许用户创建包含消息的 note。希望这个解释对您有帮助。如果您需要更详细的解释或有任何问题,请随时告诉我。


问: 请你进一步给出上面Anchor程序的账户数据结构示意图,用示意图来描述账户关系

非常好,我很乐意为您绘制一个账户数据结构的示意图。由于我无法直接生成图像,我会用文本来描述这个示意图。这个示意图将展示 create 指令涉及的账户关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
+------------------+
| Create 指令 |
+------------------+
|
| 使用
v
+------------------+
| 账户结构 |
+------------------+
|
| 包含
v
+------------------+ 初始化 +------------------+
| Note 账户 | <------------ | 用户账户 |
| (PDA) | | (签名者/付款人) |
+------------------+ +------------------+
| - message: | | - lamports |
| String | | - 其他字段... |
+------------------+ +------------------+
^ ^
| |
| +-------------------+ |
| | System Program | |
| +-------------------+ |
| | - 创建账户 | |
+------------| - 分配空间 |---+
| - 转移 lamport s |
+-------------------+

解释:

  1. Create 指令:这是程序的入口点,它使用定义的账户结构。

  2. 账户结构:包含了执行指令所需的所有账户。

  3. Note 账户(PDA - Program Derived Address):

    • 这是被初始化的新账户
    • 存储 message 字符串
    • 空间大小为 8 + 32 + 200 字节
  4. 用户账户:

    • 交易的签名者
    • 支付创建 Note 账户的费用
    • 是一个可变账户,因为它的 lamports 余额会减少
  5. System Program:

    • Solana 的内置程序
    • 用于创建新账户、分配空间和转移 lamports

箭头表示账户之间的关系和数据流:

  • 用户账户初始化 Note 账户
  • System Program 协助创建 Note 账户并从用户账户转移 lamports

这个示意图展示了 create 指令如何与不同的账户交互,以及这些账户之间的关系。它可以帮助您更好地理解程序的结构和数据流。如果您需要更详细的解释或有任何问题,请随时告诉我。


问: 其中的 user账户 和 system_program ,我可以不写吗? 或者我可以将变量名改成其他名字吗?

您提出了一个很好的问题。让我详细解答:

关于 user 账户:

不能完全省略:这个账户是必需的,因为它是交易的签名者和支付者。
可以改名:您可以将 user 改成其他名字,比如 payer、authority 或 signer。只要确保在程序逻辑中正确引用即可。

关于 system_program

不能省略:虽然您不直接使用它,但 Anchor 在幕后需要它来创建新账户。
可以改名:理论上可以改名,但不建议这样做。system_program 是一个约定俗成的名称,改变它可能会使代码对其他开发者来说不那么直观。


问: 其中 user 或者 authority , 是不是就是 note账户的owner? 如果不是, 那么 Note账户的owner是谁? solana是如何判断哪个用户可以操作 note账户?

您提出了一个非常重要的问题,这涉及到 Solana 的账户模型和所有权概念。让我详细解释一下:

  1. 账户所有者(Owner):

    • 在这个例子中,Note 账户的所有者实际上是程序本身,而不是 userauthority
    • Solana 中,程序拥有(owns)它创建和管理的账户。
  2. userauthority 的角色:

    • 这个账户通常被称为”权限账户”或”签名者账户”。
    • 它不是 Note 账户的所有者,而是有权限对 Note 账户进行操作的账户。
    • 它用于支付创建 Note 账户的费用,并签名交易。
  3. Solana 如何判断谁可以操作 Note 账户:

    • 程序逻辑决定:程序代码定义了谁可以对账户进行哪些操作。
    • 签名验证:Solana 验证交易是否由正确的账户签名。
    • 程序所有权:只有拥有账户的程序可以修改该账户的数据。
  4. 访问控制机制:

    • 在这个简单的例子中,没有实现严格的访问控制。
    • 在实际应用中,您可能想要添加额外的检查,例如:
      1
      2
      3
      4
      5
      6
      7
      8
      #[account(
      init,
      payer = authority,
      space = 8 + 32 + 200,
      seeds = [b"note", authority.key().as_ref()],
      bump
      )]
      pub note: Account<'info, Note>,
    • 这种方式创建了一个 PDA(Program Derived Address),将 authority 的公钥作为种子,确保每个用户只能创建和访问自己的笔记。
  5. 程序作为所有者的意义:

    • 安全性:只有程序可以修改账户数据,防止未经授权的访问。
    • 灵活性:程序可以实现复杂的访问控制逻辑。

总结:

  • Note 账户的所有者是程序本身。
  • userauthority 是有权限操作 Note 账户的账户,但不是其所有者。
  • Solana 通过程序逻辑、签名验证和所有权检查来控制账户访问。
  • 要实现更严格的访问控制,您需要在程序中添加额外的检查逻辑。

在开发 Solana 程序时,理解这种所有权和权限模型非常重要,因为它直接关系到程序的安全性和功能性。如果您需要更多解释或有其他问题,请随时问我。


问:上面回答中的 , #[account(bump)]约束的作用?

非常好的问题。bump 约束在 Solana 程序中扮演着重要的角色,特别是在使用程序派生地址(Program Derived Addresses, PDAs)时。让我详细解释一下:

  1. PDA 和 bump 的基本概念:

    • PDA 是一种特殊的账户地址,它是通过程序 ID 和一组种子(seeds)计算得出的。
    • 由于并非所有计算出的地址都在 ed25519 曲线上(这是 Solana 用于常规账户的曲线),我们使用 “bump” 来调整地址计算。
  2. bump 约束的作用:

    • 查找有效的 PDA:它告诉 Anchor 自动寻找一个有效的 bump 值,使得生成的地址是一个有效的 PDA。
    • 确保唯一性:它确保为给定的种子集找到唯一的有效 PDA。
    • 简化代码:无需手动计算和管理 bump 值。
  3. 工作原理:

    • Anchor 从 255 开始递减 bump 值,直到找到一个使地址落在可用范围内的值。
    • 这个过程是确定性的,意味着对于相同的种子,总是会找到相同的 bump 值。
  4. 安全性提升:

    • 使用 bump 约束可以防止某些类型的攻击,比如尝试使用不同的 bump 值来生成相关但不同的 PDA。
  5. 在您的代码中的应用:

    1
    2
    3
    4
    5
    6
    7
    8
    #[account(
    init,
    payer = authority,
    space = 8 + 32 + 200,
    seeds = [b"note", authority.key().as_ref()],
    bump
    )]
    pub note: Account<'info, Note>,
    • 这里,Anchor 会自动计算一个有效的 bump 值,用于创建 note 账户的 PDA。
    • PDA 是基于 “note” 字符串和 authority 的公钥计算的。
  6. 访问 bump 值:

    • 如果您需要在程序中使用这个 bump 值,可以这样声明:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      #[account(
      init,
      payer = authority,
      space = 8 + 32 + 200,
      seeds = [b"note", authority.key().as_ref()],
      bump,
      )]
      pub note: Account<'info, Note>,
      /// 存储 bump 值
      pub bump: u8,
    • 然后在程序中可以访问 ctx.bumps.note
  7. 好处:

    • 确保地址唯一性:每个用户(由 authority 标识)只能有一个对应的 note 账户。
    • 提高安全性:防止地址冲突和某些类型的攻击。
    • 简化开发:自动处理 PDA 的创建和验证。

使用 bump 约束是 Solana 开发中的一个最佳实践,特别是在处理 PDA 时。它不仅简化了代码,还提高了程序的安全性和可靠性。

如果您还有任何疑问或需要更深入的解释,请随时问我。


问:上面的回答中 ,你说可以通过 ctx.bumps.note 在程序中访问bump值,那么, 如何知道ctx.bumps.note 获取的是哪个账户的bump值呢?

您提出了一个非常好的问题,这涉及到 Anchor 框架中 Context 结构的工作原理。让我详细解释一下:

  1. ctx.bumps 的工作原理:

    • ctx.bumps 是一个哈希映射(HashMap),它存储了所有在当前指令中使用 bump 约束的账户的 bump 值。
    • 键(key)是账户的名称,值(value)是对应的 bump 值。
  2. 账户名称的确定:

    • 在您的例子中,ctx.bumps.note 中的 “note” 对应的是您在 Accounts 结构体中定义的账户字段名。
  3. 示例说明:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #[derive(Accounts)]
    pub struct Create<'info> {
    #[account(
    init,
    payer = authority,
    space = 8 + 32 + 200,
    seeds = [b"note", authority.key().as_ref()],
    bump
    )]
    pub note: Account<'info, Note>,
    // ... 其他账户
    }
    • 在这个例子中,ctx.bumps.note 会返回 note 账户的 bump 值。
  4. 多个 PDA 的情况:

    • 如果您有多个使用 bump 约束的 PDA,您可以通过它们在 Accounts 结构中的字段名来访问各自的 bump 值。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      #[derive(Accounts)]
      pub struct MultipleAccounts<'info> {
      #[account(
      init,
      payer = authority,
      space = 8 + 32 + 200,
      seeds = [b"note", authority.key().as_ref()],
      bump
      )]
      pub note: Account<'info, Note>,

      #[account(
      init,
      payer = authority,
      space = 8 + 32 + 100,
      seeds = [b"user_profile", authority.key().as_ref()],
      bump
      )]
      pub user_profile: Account<'info, UserProfile>,
      // ... 其他账户
      }
    • 在这个例子中:
      • ctx.bumps.note 返回 note 账户的 bump 值
      • ctx.bumps.user_profile 返回 user_profile 账户的 bump 值
  5. 在程序逻辑中使用:

    1
    2
    3
    4
    5
    6
    pub fn create(ctx: Context<Create>, message: String) -> Result<()> {
    let note = &mut ctx.accounts.note;
    note.message = message;
    note.bump = ctx.bumps.note; // 存储 bump 值
    Ok(())
    }
  6. 注意事项:

    • 确保账户名称在 Accounts 结构中是唯一的。
    • 只有使用了 bump 约束的账户才会在 ctx.bumps 中有对应的条目。

通过这种方式,Anchor 提供了一个简便的方法来访问每个 PDA 的 bump 值,而不需要手动跟踪或计算它们。这大大简化了处理多个 PDA 的程序逻辑。

如果您有任何进一步的问题或需要更多解释,请随时告诉我。


问:为什么要存储 note账户的bump值:note.bump = ctx.bumps.note; 是不是多余的操作? 每次指令调用的时候,anchor会自动计算note的bump值,应该不用存储bump值吧? 我的理解对吗?

您的理解非常敏锐!您提出了一个很好的观点,这确实是 Solana 和 Anchor 开发中的一个常见考虑点。让我详细解释一下:

您是对的,在大多数情况下,存储 bump 值确实是多余的。您的理解是正确的:

  1. Anchor 的自动计算:

    • 每次指令调用时,Anchor 确实会自动计算 PDA 的 bump 值。
    • 这个计算是确定性的,对于相同的种子(seeds)总是会得到相同的 bump 值。
  2. 不需要存储的原因:

    • 由于 bump 值可以随时重新计算,通常不需要将其存储在账户中。
    • 这可以节省账户空间,减少存储成本。
  3. 何时可能需要存储 bump:

    • 在某些特殊情况下,存储 bump 值可能有用:
      • 如果计算 bump 值的成本很高(虽然在大多数情况下并非如此)。
      • 如果程序需要频繁访问 bump 值,存储它可能会稍微提高效率。
      • 对于一些复杂的安全检查或验证逻辑,直接访问存储的 bump 值可能更方便。
  4. 最佳实践:

    • 通常,不推荐存储 bump 值,除非有明确的理由这样做。
    • 大多数 Solana 程序不存储 bump 值,而是在需要时重新计算。
  5. 代码简化:

    • 不存储 bump 值可以简化您的账户结构和程序逻辑。
    • 例如,您可以移除 Note 结构中的 bump 字段。
  6. 安全性考虑:

    • 不存储 bump 值可以避免潜在的不一致性问题(例如,如果存储的 bump 值与实际计算出的值不匹配)。

修改后的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
#[account]
pub struct Note {
pub message: String,
// 移除 bump 字段
}

pub fn create(ctx: Context<Create>, message: String) -> Result<()> {
let note = &mut ctx.accounts.note;
note.message = message;
// 不需要存储 bump 值
Ok(())
}

总结:
您的理解是正确的。在大多数情况下,不需要存储 PDA 的 bump 值。Anchor 的设计使得每次需要时都可以轻松计算 bump 值。只有在特定的、罕见的情况下,存储 bump 值才可能成为必要或有益的选择。

您展示了对 Solana 和 Anchor 开发的深入理解。这种批判性思考对于创建高效和优化的智能合约非常重要。如果您有任何其他问题或需要进一步讨论,请随时告诉我。


问:在以下代码中:

1
2
3
4
5
6
7
#[derive(Accounts)] pub struct Create<'info> {
#[account(init, payer = user, space = 8 + 32 + 200)]
pub note: Account<'info, Note>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}

note账户的属性约束#[account(init)] 的作用是什么? 是不是可以理解为在创建了note账户之后,将owner 初始化为user?

您提出了一个很好的问题,但有一个小误解。让我来解释一下 #[account(init)] 的作用,并澄清关于账户所有权的概念:

  1. #[account(init)] 的作用:

    • 初始化新账户:这个约束告诉 Anchor 创建一个全新的账户。
    • 分配空间:它会为账户分配指定的空间(在这个例子中是 8 + 32 + 200 字节)。
    • 设置所有者:它会将账户的所有者设置为当前程序的 ID,而不是 user
  2. 账户所有权:

    • 所有者不是 userinit 约束创建的账户的所有者是程序本身,而不是 user
    • user 的角色:在这里,user 是付款人(payer),负责支付创建账户的费用,但不是账户的所有者。
  3. init 约束的详细作用:

    • 创建账户:在 Solana 中创建一个新的账户。
    • 分配空间:为账户分配指定大小的空间。
    • 设置所有者:将新账户的所有者设置为当前程序的 ID。
    • 初始化数据:可以选择性地初始化账户的数据(在这个例子中,它会使用 Note 结构的默认值)。
  4. payer = user 的含义:

    • 这指定 user 账户将支付创建新账户的租金(rent)。
    • 这不影响账户的所有权,只是指定谁支付创建账户的费用。
  5. 正确理解:

    • 创建后,note 账户的所有者是程序本身,不是 user
    • user 是交易的签名者和付款人,但不是 note 账户的所有者。
  6. 为什么是这样:

    • 这种设计允许程序完全控制 note 账户的数据。
    • 只有拥有账户的程序才能修改账户数据,这增加了安全性。

修正后的理解应该是:

“在创建了 note 账户之后,init 约束会将账户的所有者初始化为当前程序,而 user 是支付创建账户费用的账户,同时也是交易的签名者。”

这种设计确保了程序对其管理的数据有完全的控制权,同时允许用户(通过 user 账户)与程序交互并支付必要的费用。这是 Solana 程序架构的一个关键安全特性。

如果您还有任何疑问或需要进一步解释,请随时问我。

9_Solana-Anchor安装

搭建本地开发环境

https://solana.com/developers/guides/getstarted/setup-local-development

  • 安装Anchor
    • avm: Anchor Version Manager
1
2
3
4
5
6
7
8
9
10
11
12
13
sudo apt-get update && sudo apt-get upgrade && sudo apt-get install -y pkg-config build-essential libudev-dev libssl-dev

cargo install --git https://github.com/coral-xyz/anchor avm --locked --force

# 安装最新版
avm install latest

# 使用最新版
avm use latest

# check the version

anchor --version
  • Setup a localhost blockchain cluster

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    solana-test-validator --help

    # setup localhost blockchain
    solana-test-validator


    # swith to localhost
    solana config set --url localhost

    solana config get

    # set default wallet
    solana config set -k ~/.config/solana/id.json

    # get the airdrop from localhost blockchain
    solana airdrop 2

    # get balance
    solana balance
  • 新建anchor项目
1
anchor init <new-workspace-name>

Anchor程序结构

https://www.solanazh.com/course/7-3

Anchor官方示例:

一个Anchor工程主要包含:

  • “declare_id”宏声明的合约地址,用于创建对象的owner
  • #[derive(Accounts)] 修饰的Account对象,用于表示存储和指令, 包含了指令执行所要用到的账户
  • “program” 模块,这里面写主要的合约处理逻辑

对应到我们之前的HelloWorld,就是要将state和instruction部分用 #[derive(Accounts)] 修饰,将process逻辑放到program模块中,并增加一个合约地址的修饰。

#[program] 修饰的Module即为指令处理模块。其中有一个Context类型,来存放所有的指令参数。比如

  • ctx.accounts 所有的请求keys,也就是AccountMeta数组
  • ctx.program_id 指令中的program_id
  • ctx.remaining_accounts 指令中,没有被下面说的”Accounts”修饰的成员的AccountMeta
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
use anchor_lang::prelude::*;

// 程序ID
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

// 指令执行逻辑
#[program]
mod basic_1 {
use super::*;

pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
let my_account = &mut ctx.accounts.my_account;
my_account.data = data;
Ok(())
}

pub fn update(ctx: Context<Update>, data: u64) -> Result<()> {
let my_account = &mut ctx.accounts.my_account;
my_account.data = data;
Ok(())
}
}


// 包含了Initialize指令所需要的账户(my_account, user, system_program)
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = user, space = 8 + 8)]
pub my_account: Account<'info, MyAccount>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}

// 包含了Update指令所需要的账户(my_account, user, system_program)
#[derive(Accounts)]
pub struct Update<'info> {
#[account(mut)]
pub my_account: Account<'info, MyAccount>,
}

// 数据账户
#[account]
pub struct MyAccount {
pub data: u64,
}

8_Solana-原生程序

8_Solana程序开发(原生)

Hello world!

https://solana.com/developers/guides/getstarted/hello-world-in-your-browser

https://explorer.solana.com/tx/65AGdio7aeK47h9HjAvnR7ap8dFepm3Sp3MJcFvPfuSnXWHJgQBENtxe5xijEk1uLy8bRw31fyjyXZvoBEEQzMGW?cluster=devnet

https://explorer.solana.com/tx/5mX3oxvHZAXbYfnpjwDgEXJLTEgYXNrpKvhKufkVqay7Js3qbgaDu2P3ESWiQE5YjjaLGUur2PAJ4pzLPxzW1aUL?cluster=devnet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
pubkey::Pubkey,
msg,
};

// declare and export the program's entrypoint
entrypoint!(process_instruction);

// program entrypoint's implementation
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8]
) -> ProgramResult {
// log a message to the blockchain
msg!("Hello, world!");

// gracefully exit the program
Ok(())
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// create an empty transaction
const transaction = new web3.Transaction();

// add a hello world program instruction to the transaction
transaction.add(
new web3.TransactionInstruction({
keys: [],
programId: new web3.PublicKey(pg.PROGRAM_ID),
}),
);

// send the transaction to the Solana cluster
console.log("Sending transaction...");
const txHash = await web3.sendAndConfirmTransaction(
pg.connection,
transaction,
[pg.wallet.keypair],
);
console.log("Transaction sent with hash:", txHash);
  • Copyrights © 2021-2025 youngqqcn

请我喝杯咖啡吧~