跨程序调用和 PDAs——Anchor 上两种强大机制的结合

  • aseneca
  • 发布于 5天前
  • 阅读 563

本文主要介绍了 Solana 上 Anchor 开发中跨程序调用(CPIs)的概念、使用场景和方法。CPIs 允许 Solana 程序在执行期间调用其他程序,实现不同程序之间的交互,从而实现诸如 token 转移等功能。同时介绍了如何使用程序派生地址(PDAs)使程序能够作为签名者。

作为在 Solana 上构建的中间 anchor 开发者,你很可能会遇到这样一种情况:你的程序相互交互更有利。Solana 中的这种可组合性是通过一种称为 Cross-Program Invocations(CPIs) 的机制实现的。

让我们首先分解什么是 CPI,你应该如何使用它们,以及在使用此功能时应该注意什么。

Cross-Program Invocation(CPIs)

Solana 独特的集成架构使其能够在利用全球去中心化网络的同时,每秒处理数千笔交易。这种设计为一种模块化结构提供了空间,同时作为一个单链区块链,使构建在其之上的应用程序能够继承可组合性。这种可组合的拓扑结构使得应用程序(和程序)能够相互交互和构建,从而消除了复杂的链上活动,例如桥接和流动性碎片化。

CPI 是 Solana 程序(或智能合约)在其执行期间调用区块链上的另一个程序的内部机制之一。它是一种强大的机制,使不同的程序能够交互并执行诸如代币转账、数据操作或源程序定义的其他操作。

何时应该使用 CPI?

当你需要程序利用另一个程序来执行功能时,你应该使用 CPI。例如,像执行代币转账一样,使用 CPI 来利用 Token Program 将节省我们大量的计算资源,而不是重新实现代币转账功能,或者将 NFT 从一个帐户转移到另一个帐户。

如何使用 CPI?

基本上,使用 CPI 涉及以下步骤:

首先确定你希望与之交互的程序。我们将以 Token program 为例。

然后使用 Anchor 的 #[account] 宏定义 CPI 所需的必要帐户,并指定定义如何将它们传递到 CPI 的约束,如下例所示:

#[derive(Accounts)]
pub struct List<'info> {
    #[account(mut)]
    maker: Signer<'info>,
    #[account(
        seeds = [b"marketplace", marketplace.name.as_str().as_bytes()],\
        bump = marketplace.bump,
    )]
    marketplace: Box<Account<'info, Marketplace>>,
    maker_mint: Box<InterfaceAccount<'info, Mint>>,
    collection_mint: Box<InterfaceAccount<'info, Mint>>,
    #[account(
        mut,
        associated_token::authority = maker,
        associated_token::mint = maker_mint,
    )]
    maker_ata: Box<InterfaceAccount<'info, TokenAccount>>, //stores the maker on the heap
    #[account(
        init_if_needed,
        payer = maker,
        associated_token::mint = maker_mint,
        associated_token::authority = listing,
    )]
    vault: Box<InterfaceAccount<'info, TokenAccount>>,
    #[account(
        init,
        payer = maker,
        space = 8 + Listing::INIT_SPACE,
        seeds = [marketplace.key().as_ref(), maker_mint.key().as_ref()],
        bump
    )]
    listing: Box<Account<'info, Listing>>,
    #[account(
        seeds = [
            b"metadata",
            metadata_program.key().as_ref(),
            maker_mint.key().as_ref()
        ],
        seeds::program = metadata_program.key(),
        bump,
        constraint = metadata.collection.as_ref().unwrap().key.as_ref() == collection_mint.key().as_ref(),
        constraint = metadata.collection.as_ref().unwrap().verified == true,
    )]
    metadata: Box<Account<'info, MetadataAccount>>,
    #[account(
        seeds = [
            b"metadata",
            metadata_program.key().as_ref(),
            maker_mint.key().as_ref(),
            b"edition"
        ],
        seeds::program = metadata_program.key(),
        bump,
    )]
    master_edition: Box<Account<'info, MasterEditionAccount>>,
    metadata_program: Program<'info, Metadata>,
    associated_token_program: Program<'info, AssociatedToken>,
    system_program: Program<'info, System>,
    token_program: Interface<'info, TokenInterface>,
}

下一步将涉及定义指令数据,例如,定义从 maker_ata 帐户到 vault 帐户的传输机制,建立 maker 帐户作为授权机构,以及 maker_mint 帐户作为 mint。将 NFT 存入 vault 所需的帐户是 maker_ata account, vault account, maker 和 maker_mint account

然后我们创建 CPI context 并调用 token program transfer 指令

pub fn deposit_nft(&mut self) -> Result<()> {
        let accounts = TransferChecked {
            from: self.maker_ata.to_account_info(),
            to: self.vault.to_account_info(),
            authority: self.maker.to_account_info(),
            mint: self.maker_mint.to_account_info(),
        };

        //create a Cpi context
        let cpi_ctx = CpiContext::new(self.token_program.to_account_info(), accounts);

         // Invoke the token program’s transfer instruction
        transfer_checked(cpi_ctx, 1, self.maker_mint.decimals)
    }
}

上面的示例也是 CPI 的一个重要好处,称为 Privilege Extensions,顾名思义,它“扩展”了调用者对被调用者的特权。在我们上面的示例中,原始授权机构 maker account 暂时将授权机构的控制权“扩展”到 Token Program,使其能够代表其执行传输操作。

Program as Signers

很可能在某些情况下,而且通常是这种情况,你需要程序本身拥有对资产的授权。在上面的 NFT 市场示例中,你可能希望程序代表你取消列表甚至购买 NFT,或者贷款协议程序需要管理存入的抵押品,而自动化做市商程序需要管理投入其流动资金池的代币。

我们可以使用 Program Derived Addresses (PDAs) 来完成此操作。我们将在后续文章中讨论 PDA,但简单定义:PDA 是没有公钥因此也没有关联私钥的特殊地址。它们有两个优点:在链上构建类似哈希表的结构,并允许程序在没有私钥的情况下签署指令。

PDA 与 CPI 相结合是 anchor 开发中非常强大的功能,因为它们允许我们安全地管理资产和数据,从而确保只有拥有程序才能通过 CPI 签署影响特定帐户的交易,从而防止程序间通信期间的未经授权的签名。

要通过 CPI Context 使用 PDA 签署交易,而不是调用 CpiContext::new(cpi_program, accounts) 方法,我们需要使用 CpiContext::new_with_signer(cpi_program, accounts, seeds),其中种子参数是创建 PDA 时的种子和 bump,如下面的 close_mint_vault 实现所示:

 pub fn close_mint_vault(&mut self) -> Result<()> {
        let seeds = &[
            &self.marketplace.key().to_bytes()[..],
            &self.maker_mint.key().to_bytes()[..],
            &[self.listing.bump],
        ];
        let signer_seeds = &[&seeds[..]];

        let accounts = CloseAccount {
            account: self.vault.to_account_info(),
            destination: self.maker.to_account_info(),
            authority: self.listing.to_account_info(),
        };
        //Sign with the pda
        let cpi_ctx = CpiContext::new_with_signer(
            self.token_program.to_account_info(),
            accounts,
            signer_seeds
        );

        close_account(cpi_ctx)
    }

这里发生的情况是,在执行期间,Solana 运行时将检查 hash(seeds, current_program_id) == account address 是否为真。如果是,则该帐户的 is_signer 标志将设置为 true。这意味着从某个程序派生的 PDA 只能用于签署源自该特定程序的 CPI。

总而言之,结合使用 CPI 和 PDA 使我们能够利用 Anchor 强大的抽象功能,同时使我们能够通过 PDA 减少我们需要的帐户数量,并确保应用程序上不同程序之间的可组合交互。

  • 原文链接: dev.to/aseneca/cross-pro...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
aseneca
aseneca
江湖只有他的大名,没有他的介绍。