Skip to content

Latest commit

 

History

History
781 lines (654 loc) · 20.8 KB

File metadata and controls

781 lines (654 loc) · 20.8 KB
title Dependency Free Composability
description Learn how to use Anchor's declare_program macro to interact with programs without additional dependencies.

The declare_program!() macro simplifies the process of interacting with Anchor programs by generating Rust modules (from a program's IDL) that can be used in both on-chain and off-chain code. You can find an example program here.

The following modules are generated by the declare_program!() macro:

Module Description
cpi Helper functions for making cross-program invocations (CPIs) to the program from other on-chain programs
client Accounts and arguments required to build program instructions to add to client-side transactions
account Account data types (program state) defined in the program
program Program ID constant used to identify the program
constants Program constants defined in the program
events Program events defined in the program
types Program types defined in the program
error Program errors defined in the program
parsers Parsers for program accounts, instructions and events

Examples

The following examples demonstrate how to use the declare_program!() macro in two scenarios:

  1. Making Cross Program Invocations (CPIs) from one program to another program
  2. Building client-side transactions to invoke a program's instructions

Both examples show how the modules generated by the declare_program!() macro simplify program interactions, whether you're writing on-chain or off-chain code.

On-chain CPI

To use the declare_program!() macro, you need the IDL file for the target program. The IDL file must be placed in a directory named /idls in your project. The /idls directory can be located at any level in your project structure. For example, your project could have this layout:

Below is the source code (lib.rs) for the target (callee) program that generates the example.json IDL file shown above.

Using the program's IDL file, another program can use the declare_program!() macro to generate a CPI module, enabling it to make CPIs to this program's instructions.

<Tabs items={["Callee Program", "IDL"]}>

use anchor_lang::prelude::*;

declare_id!("8HupNBr7SBhBLcBsLhbtes3tCarBm6Bvpqp5AfVjHuj8");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let counter = &ctx.accounts.counter;
        msg!("Counter account created! Current count: {}", counter.count);
        Ok(())
    }

    pub fn increment(ctx: Context<Increment>) -> Result<()> {
        let counter = &mut ctx.accounts.counter;
        msg!("Previous counter: {}", counter.count);

        counter.count += 1;
        msg!("Counter incremented! Current count: {}", counter.count);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,

    #[account(
        init,
        payer = payer,
        space = 8 + 8
    )]
    pub counter: Account<'info, Counter>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Increment<'info> {
    #[account(mut)]
    pub counter: Account<'info, Counter>,
}

#[account]
pub struct Counter {
    pub count: u64,
}
{
  "address": "8HupNBr7SBhBLcBsLhbtes3tCarBm6Bvpqp5AfVjHuj8",
  "metadata": {
    "name": "example",
    "version": "0.1.0",
    "spec": "0.1.0",
    "description": "Created with Anchor"
  },
  "instructions": [
    {
      "name": "increment",
      "discriminator": [
        11,
        18,
        104,
        9,
        104,
        174,
        59,
        33
      ],
      "accounts": [
        {
          "name": "counter",
          "writable": true
        }
      ],
      "args": []
    },
    {
      "name": "initialize",
      "discriminator": [
        175,
        175,
        109,
        31,
        13,
        152,
        155,
        237
      ],
      "accounts": [
        {
          "name": "payer",
          "writable": true,
          "signer": true
        },
        {
          "name": "counter",
          "writable": true,
          "signer": true
        },
        {
          "name": "system_program",
          "address": "11111111111111111111111111111111"
        }
      ],
      "args": []
    }
  ],
  "accounts": [
    {
      "name": "Counter",
      "discriminator": [
        255,
        176,
        4,
        245,
        188,
        253,
        124,
        25
      ]
    }
  ],
  "types": [
    {
      "name": "Counter",
      "type": {
        "kind": "struct",
        "fields": [
          {
            "name": "count",
            "type": "u64"
          }
        ]
      }
    }
  ]
}

Below is the source code (lib.rs) for the caller program (example-cpi) that uses the declare_program!() macro to generate a CPI module to invoke the instructions defined in the callee program above.

<Tabs items={["Caller Program", "Test"]}>

use anchor_lang::prelude::*;

declare_id!("GENmb1D59wqCKRwujq4PJ8461EccQ5srLHrXyXp4HMTH");

// [!code word:declare_program]
// [!code highlight:9]
declare_program!(example);
use example::{
    accounts::Counter,
    cpi::{
        self,
        accounts::{Increment, Initialize},
    },
    program::Example,
};

#[program]
pub mod example_cpi {

    use super::*;

    pub fn initialize_cpi(ctx: Context<InitializeCpi>) -> Result<()> {
        // Create CPI context for initialize
        let cpi_ctx = CpiContext::new(
            ctx.accounts.example_program.key(),
            Initialize {
                payer: ctx.accounts.payer.to_account_info(),
                counter: ctx.accounts.counter.to_account_info(),
                system_program: ctx.accounts.system_program.to_account_info(),
            },
        );

        // Invoke the initialize instruction
        cpi::initialize(cpi_ctx)?;
        Ok(())
    }

    pub fn increment_cpi(ctx: Context<IncrementCpi>) -> Result<()> {
        // Create CPI context for increment
        let cpi_ctx = CpiContext::new(
            ctx.accounts.example_program.key(),
            Increment {
                counter: ctx.accounts.counter.to_account_info(),
            },
        );

        // Invoke the increment instruction
        cpi::increment(cpi_ctx)?;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct InitializeCpi<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(mut)]
    pub counter: Signer<'info>,
    pub system_program: Program<'info, System>,
    pub example_program: Program<'info, Example>,
}

#[derive(Accounts)]
pub struct IncrementCpi<'info> {
    #[account(mut)]
    pub counter: Account<'info, Counter>,
    pub example_program: Program<'info, Example>,
}
import * as anchor from "@anchor-lang/core";
import { Program } from "@anchor-lang/core";
import { Example } from "../target/types/example";
import { ExampleCpi } from "../target/types/example_cpi";
import { Keypair } from "@solana/web3.js";

describe("example", () => {
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.Example as Program<Example>;
  const cpiProgram = anchor.workspace.ExampleCpi as Program<ExampleCpi>;

  const counterAccount = Keypair.generate();

  it("Is initialized!", async () => {
    const transactionSignature = await cpiProgram.methods
      .initializeCpi()
      .accounts({
        counter: counterAccount.publicKey,
      })
      .signers([counterAccount])
      .rpc({ skipPreflight: true });

    const accountData = await program.account.counter.fetch(
      counterAccount.publicKey,
    );

    console.log(`Transaction Signature: ${transactionSignature}`);
    console.log(`Count: ${accountData.count}`);
  });

  it("Increment", async () => {
    const transactionSignature = await cpiProgram.methods
      .incrementCpi()
      .accounts({
        counter: counterAccount.publicKey,
      })
      .rpc();

    const accountData = await program.account.counter.fetch(
      counterAccount.publicKey,
    );

    console.log(`Transaction Signature: ${transactionSignature}`);
    console.log(`Count: ${accountData.count}`);
  });
});

Explanation

The declare_program!() macro takes a single argument - the name of the program's IDL file (e.g. example.json):

declare_program!(example);  // Looks for /idls/example.json

Bring into scope the generated modules:

use example::{
    accounts::Counter, // Account types
    cpi::{             // Cross program invocation helpers
        self,
        accounts::{Increment, Initialize},
    },
    program::Example,  // Program type
};

Use the imported types in the account validation structs:

#[derive(Accounts)]
pub struct IncrementCpi<'info> {
    // Counter type from accounts module
    #[account(mut)]
    // [!code word:Counter]
    // [!code highlight]
    pub counter: Account<'info, Counter>,

    // Example type from program module
    // [!code word:Example]
    // [!code highlight]
    pub example_program: Program<'info, Example>,
}

Use the CPI module to invoke the program's instructions:

pub fn initialize_cpi(ctx: Context<InitializeCpi>) -> Result<()> {
    // Create CPI context for initialize
    let cpi_ctx = CpiContext::new(
        ctx.accounts.example_program.key(),
        Initialize {
            payer: ctx.accounts.payer.to_account_info(),
            counter: ctx.accounts.counter.to_account_info(),
            system_program: ctx.accounts.system_program.to_account_info(),
        },
    );

    // Invoke the initialize instruction
    // [!code highlight]
    cpi::initialize(cpi_ctx)?;
    Ok(())
}
pub fn increment_cpi(ctx: Context<IncrementCpi>) -> Result<()> {
    // Create CPI context for increment
    let cpi_ctx = CpiContext::new(
        ctx.accounts.example_program.key(),
        Increment {
            counter: ctx.accounts.counter.to_account_info(),
        },
    );

    // Invoke the increment instruction
    // [!code highlight]
    cpi::increment(cpi_ctx)?;
    Ok(())
}

Off-chain Client

To use the declare_program!() macro, you need the IDL file for the target program. The IDL file must be placed in a directory named /idls in your project. The /idls directory can be located at any level in your project structure. For example, your project could have this layout:

Below is the source code (lib.rs) for the target program that generates the example.json IDL file shown above. The program's IDL can then be used in a client script along with the declare_program!() macro to generate a Client module to build the program's instructions.

<Tabs items={["Callee Program", "IDL"]}>

use anchor_lang::prelude::*;

declare_id!("6khKp4BeJpCjBY1Eh39ybiqbfRnrn2UzWeUARjQLXYRC");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let counter = &ctx.accounts.counter;
        msg!("Counter account created! Current count: {}", counter.count);
        Ok(())
    }

    pub fn increment(ctx: Context<Increment>) -> Result<()> {
        let counter = &mut ctx.accounts.counter;
        msg!("Previous counter: {}", counter.count);

        counter.count += 1;
        msg!("Counter incremented! Current count: {}", counter.count);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,

    #[account(
        init,
        payer = payer,
        space = 8 + 8
    )]
    pub counter: Account<'info, Counter>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Increment<'info> {
    #[account(mut)]
    pub counter: Account<'info, Counter>,
}

#[account]
pub struct Counter {
    pub count: u64,
}
{
  "address": "6khKp4BeJpCjBY1Eh39ybiqbfRnrn2UzWeUARjQLXYRC",
  "metadata": {
    "name": "example",
    "version": "0.1.0",
    "spec": "0.1.0",
    "description": "Created with Anchor"
  },
  "instructions": [
    {
      "name": "increment",
      "discriminator": [11, 18, 104, 9, 104, 174, 59, 33],
      "accounts": [
        {
          "name": "counter",
          "writable": true
        }
      ],
      "args": []
    },
    {
      "name": "initialize",
      "discriminator": [175, 175, 109, 31, 13, 152, 155, 237],
      "accounts": [
        {
          "name": "payer",
          "writable": true,
          "signer": true
        },
        {
          "name": "counter",
          "writable": true,
          "signer": true
        },
        {
          "name": "system_program",
          "address": "11111111111111111111111111111111"
        }
      ],
      "args": []
    }
  ],
  "accounts": [
    {
      "name": "Counter",
      "discriminator": [255, 176, 4, 245, 188, 253, 124, 25]
    }
  ],
  "types": [
    {
      "name": "Counter",
      "type": {
        "kind": "struct",
        "fields": [
          {
            "name": "count",
            "type": "u64"
          }
        ]
      }
    }
  ]
}

Below is the client script (main.rs) that uses the declare_program!() macro to generate a Client module to build the program's instructions.

<Tabs items={["Client Script", "Dependencies"]}>

use anchor_client::{
    solana_client::rpc_client::RpcClient,
    solana_sdk::{
        commitment_config::CommitmentConfig, native_token::LAMPORTS_PER_SOL, signature::Keypair,
        system_program,
    },
    solana_signer::Signer,
    Client, Cluster,
};
use anchor_lang::prelude::*;
use std::rc::Rc;

declare_program!(example);
use example::{accounts::Counter, client::accounts, client::args};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let connection = RpcClient::new_with_commitment(
        "http://127.0.0.1:8899", // Local validator URL
        CommitmentConfig::confirmed(),
    );

    // Generate Keypairs and request airdrop
    let payer = Keypair::new();
    let counter = Keypair::new();
    println!("Generated Keypairs:");
    println!("   Payer: {}", payer.pubkey());
    println!("   Counter: {}", counter.pubkey());

    println!("\nRequesting 1 SOL airdrop to payer");
    let airdrop_signature = connection.request_airdrop(&payer.pubkey(), LAMPORTS_PER_SOL)?;

    // Wait for airdrop confirmation
    while !connection.confirm_transaction(&airdrop_signature)? {
        std::thread::sleep(std::time::Duration::from_millis(100));
    }
    println!("   Airdrop confirmed!");

    // Create program client
    let provider = Client::new_with_options(
        Cluster::Localnet,
        Rc::new(payer),
        CommitmentConfig::confirmed(),
    );
    let program = provider.program(example::ID)?;

    // Build and send instructions
    println!("\nSend transaction with initialize and increment instructions");
    let initialize_ix = program
        .request()
        .accounts(accounts::Initialize {
            counter: counter.pubkey(),
            payer: program.payer(),
            system_program: system_program::ID,
        })
        .args(args::Initialize)
        .instructions()?
        .remove(0);

    let increment_ix = program
        .request()
        .accounts(accounts::Increment {
            counter: counter.pubkey(),
        })
        .args(args::Increment)
        .instructions()?
        .remove(0);

    let signature = program
        .request()
        .instruction(initialize_ix)
        .instruction(increment_ix)
        .signer(&counter)
        .send()
        .await?;
    println!("   Transaction confirmed: {}", signature);

    println!("\nFetch counter account data");
    let counter_account: Counter = program.account::<Counter>(counter.pubkey()).await?;
    println!("   Counter value: {}", counter_account.count);
    Ok(())
}
[package]
name = "rs"
version = "0.1.0"
edition = "2021"

[dependencies]
anchor-client = { version = "1.0.2", features = ["async"] }
anchor-lang = "1.0.2"
anyhow = "1.0.93"
tokio = { version = "1.0", features = ["full"] }

The declare_program!() macro takes a single argument - the name of the program's IDL file (e.g. example.json):

declare_program!(example);  // Looks for /idls/example.json

Bring into scope the generated modules:

use example::{
  accounts::Counter,  // Program Account types
  client::accounts,   // Accounts for program instructions
  client::args,       // Arguments for program instructions
};

Use the Client module to build the program's instructions:

// Build initialize instruction
let initialize_ix = program
    .request()
    // Accounts required for initialize instruction
    .accounts(accounts::Initialize {
        counter: counter.pubkey(),
        payer: program.payer(),
        system_program: system_program::ID,
    })
    // Arguments for initialize instruction (discriminator)
    .args(args::Initialize)
    .instructions()?
    .remove(0);
// Build increment instruction
let increment_ix = program
    .request()
    // Accounts required for increment instruction
    .accounts(accounts::Increment {
        counter: counter.pubkey(),
    })
    // Arguments for increment instruction (discriminator)
    .args(args::Increment)
    .instructions()?
    .remove(0);

Add the program's instructions to a transaction and send the transaction:

let signature = program
    .request()
    .instruction(initialize_ix)
    .instruction(increment_ix)
    .signer(&counter)
    .send()
    .await?;

Use the Account module to fetch and deserialize the program's account types:

// Counter type from accounts module
let counter_account: Counter = program.account::<Counter>(counter.pubkey()).await?;