Skip to content

Commit

Permalink
feat!: add compose method to anchor requests
Browse files Browse the repository at this point in the history
Now multiple anchor transactions for the same program can be composed to
gether using the `compose` method.
  • Loading branch information
ifiokjr committed Oct 7, 2024
1 parent 15d9368 commit 173d24d
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 36 deletions.
135 changes: 99 additions & 36 deletions crates/wasm_client_anchor/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,29 @@
macro_rules! base_create_request_builder {
($program:path, $program_struct:path, $name_prefix:ident, $accounts:ident) => {
$crate::__private::paste::paste! {
pub type [<$name_prefix RequestBuilderPartial>]<'a, W> =
[<$name_prefix RequestBuilder>]<
'a,
W,
(
(&'a $program_struct<W>,),
(&'a W,),
(),
(),
(),
(),
(),
(),
(),
),
>;

#[derive($crate::__private::typed_builder::TypedBuilder)]
#[builder(mutators(
/// Add signers to the request method. This can be added multiple times in the builder.
pub fn signers(
&mut self,
mut signers: Vec<&'a dyn $crate::__private::solana_sdk::signer::Signer>
) {
self._signers.append(&mut signers);
}
/// Add instructions to the request method. This can be added multiple times in the builder.
pub fn instructions(
&mut self,
mut instructions: Vec<$crate::__private::solana_sdk::instruction::Instruction>
) {
self._instructions.append(&mut instructions);
}
))]
pub struct [<$name_prefix Request>]<
'a,
W: $crate::WalletAnchor + 'a,
> {
/// This is the launchpad program.
pub launchpad: &'a $program_struct<W>,
/// This is the anchor client for interacting with this program.
pub program_client: &'a $program_struct<W>,
/// This is the wallet / payer that will always sign the transaction. It should implement [`wasm_client_anchor::WalletAnchor`] to allow for async signing via wallets.
pub wallet: &'a W,
/// Provide the args to the anchor program endpoint. This will be transformed into the instruction data when processing the transaction.
Expand All @@ -37,11 +36,11 @@ macro_rules! base_create_request_builder {
#[builder(default)]
pub remaining_accounts: Vec<$crate::__private::solana_sdk::instruction::AccountMeta>,
/// Signers that can sign the data synchronously
#[builder(default)]
pub signers: Vec<&'a dyn $crate::__private::solana_sdk::signer::Signer>,
#[builder(default)]
#[builder(via_mutators(init = vec![]))]
_signers: Vec<&'a dyn $crate::__private::solana_sdk::signer::Signer>,
/// Instructions that are run before the anchor instruction.
pub instructions: Vec<$crate::__private::solana_sdk::instruction::Instruction>,
#[builder(via_mutators(init = vec![]))]
_instructions: Vec<$crate::__private::solana_sdk::instruction::Instruction>,
#[builder(default)]
/// Instructions that are run after the anchor instruction is completed.
pub extra_instructions: Vec<$crate::__private::solana_sdk::instruction::Instruction>,
Expand All @@ -50,8 +49,6 @@ macro_rules! base_create_request_builder {
pub options: $crate::__private::wallet_standard::SolanaSignAndSendTransactionOptions,
}

impl<'a, W: $crate::WalletAnchor + 'a> [<$name_prefix Request>]<'a, W> {}

#[$crate::__private::async_trait::async_trait(?Send)]
impl<'a, W: $crate::WalletAnchor + 'a> $crate::AnchorRequestMethods<'a, W>
for [<$name_prefix Request>]<'a, W>
Expand All @@ -65,24 +62,24 @@ macro_rules! base_create_request_builder {
}

fn rpc(&self) -> &'a $crate::__private::wasm_client_solana::SolanaRpcClient {
self.launchpad.rpc()
self.program_client.rpc()
}

fn signers(&self) -> Vec<&'a dyn $crate::__private::solana_sdk::signer::Signer> {
self.signers.clone()
self._signers.clone()
}

fn instructions(&self) -> Vec<$crate::__private::solana_sdk::instruction::Instruction> {
use $crate::__private::anchor_lang::InstructionData;
use $crate::__private::anchor_lang::ToAccountMetas;

let mut accounts = self.accounts.to_account_metas(None);
let mut instructions = self.instructions.clone();
let mut instructions = self._instructions.clone();

accounts.append(&mut self.remaining_accounts.clone());

instructions.push($crate::__private::solana_sdk::instruction::Instruction {
program_id: self.launchpad.id(),
program_id: self.program_client.id(),
accounts,
data: self.args.data(),
});
Expand All @@ -92,6 +89,20 @@ macro_rules! base_create_request_builder {
instructions
}
}

impl<'a, W: $crate::WalletAnchor + 'a> [<$name_prefix Request>]<'a, W> {
/// Compose multiple instructions from the current anchor program client.
pub fn compose(&self) -> [<$program_struct Composer>]<'a, W> {
use $crate::AnchorRequestMethods;

[<$program_struct Composer>] {
program_client: self.program_client,
wallet: self.wallet,
instructions: self.instructions(),
signers: self.signers(),
}
}
}
}
};
}
Expand All @@ -102,7 +113,7 @@ macro_rules! create_request_builder {
($program:path, $program_struct:path, $name_prefix:ident, $accounts:ident, "optional:args") => {
$crate::base_create_request_builder!($program, $program_struct, $name_prefix, $accounts);
$crate::__private::paste::paste! {
pub type [<$name_prefix RequestBuilderArgsPartial>]<'a, W> =
pub type [<$name_prefix RequestBuilderOptionalArgs>]<'a, W> =
[<$name_prefix RequestBuilder>]<
'a,
W,
Expand All @@ -112,41 +123,83 @@ macro_rules! create_request_builder {
(::$program::instruction::$name_prefix,),
(),
(),
(),
(),
(std::vec::Vec<&'a dyn $crate::prelude::Signer>,),
(std::vec::Vec<$crate::__private::solana_sdk::instruction::Instruction>,),
(),
(),
),
>;
impl<W: $crate::WalletAnchor> $program_struct<W> {
pub fn [<$name_prefix:snake>](&self) -> [<$name_prefix RequestBuilderArgsPartial>]<'_, W> {
pub fn [<$name_prefix:snake>](&self) -> [<$name_prefix RequestBuilderOptionalArgs>]<'_, W> {
[<$name_prefix Request>]::builder()
.launchpad(self)
.program_client(self)
.wallet(self.wallet())
.args(::$program::instruction::$name_prefix {})
}
}

impl<'a, W: $crate::WalletAnchor + 'a> [<$program_struct Composer>]<'a, W> {
pub fn [<$name_prefix:snake>](self) -> [<$name_prefix RequestBuilderOptionalArgs>]<'a, W> {
[<$name_prefix Request>]::builder()
.program_client(self.program_client)
.wallet(self.wallet)
.args(::$program::instruction::$name_prefix {})
.instructions(self.instructions)
.signers(self.signers)
}
}
}
};

($program:path, $program_struct:path, $name_prefix:ident, $accounts:ident, "required:args") => {
$crate::base_create_request_builder!($program, $program_struct, $name_prefix, $accounts);
$crate::__private::paste::paste! {
pub type [<$name_prefix RequestBuilderRequiredArgs>]<'a, W> =
[<$name_prefix RequestBuilder>]<
'a,
W,
(
(&'a $program_struct<W>,),
(&'a W,),
(),
(),
(),
(std::vec::Vec<&'a dyn $crate::prelude::Signer>,),
(std::vec::Vec<$crate::__private::solana_sdk::instruction::Instruction>,),
(),
(),
),
>;

impl<W: $crate::WalletAnchor> $program_struct<W> {
pub fn [<$name_prefix:snake>](&self) -> [<$name_prefix RequestBuilderPartial>]<'_, W> {
pub fn [<$name_prefix:snake>](&self) -> [<$name_prefix RequestBuilderRequiredArgs>]<'_, W> {
[<$name_prefix Request>]::builder()
.launchpad(self)
.program_client(self)
.wallet(self.wallet())

}
}

impl<'a, W: $crate::WalletAnchor + 'a> [<$program_struct Composer>]<'a, W> {
pub fn [<$name_prefix:snake>](self) -> [<$name_prefix RequestBuilderRequiredArgs>]<'a, W> {
[<$name_prefix Request>]::builder()
.program_client(self.program_client)
.wallet(self.wallet)
.instructions(self.instructions)
.signers(self.signers)
}
}
}
};

($program:path, $program_struct:path, $name_prefix:ident, $accounts:ident) => {
$crate::create_request_builder!($program, $program_struct, $name_prefix, $accounts, "required:args");
};

($program:path, $program_struct:path, $name_prefix:ident, "optional:args") => {
$crate::create_request_builder!($program, $program_struct, $name_prefix, $name_prefix, "optional:args");
};

($program:path, $program_struct:path, $name_prefix:ident) => {
$crate::create_request_builder!($program, $program_struct, $name_prefix, $name_prefix, "required:args");
};
Expand Down Expand Up @@ -279,6 +332,16 @@ macro_rules! create_program_client {
Ok(signature)
}
}

/// This struct is used to compose different request methods together.
pub struct [<$program_client_name Composer>]<'a, W: $crate::WalletAnchor + 'a> {
/// This is the anchor client for interacting with this program.
program_client: &'a $program_client_name<W>,
/// This is the wallet / payer that will always sign the transaction. It should implement [`wasm_client_anchor::WalletAnchor`] to allow for async signing via wallets.
wallet: &'a W,
instructions: Vec<$crate::__private::solana_sdk::instruction::Instruction>,
signers: Vec<&'a dyn $crate::__private::solana_sdk::signer::Signer>,
}
}
};
}
1 change: 1 addition & 0 deletions programs/example_client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ use wasm_client_anchor::create_program_client_macro;
create_program_client!(example_program::ID_CONST, ExampleProgramClient);
create_program_client_macro!(example_program, ExampleProgramClient);
example_program_client_request_builder!(Initialize, "optional:args");
example_program_client_request_builder!(Another);
43 changes: 43 additions & 0 deletions programs/example_client/tests/client.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use anyhow::Result;
use assert2::check;
use example_client::ExampleProgramClient;
use example_client::IntoExampleProgramClient;
use solana_sdk::account::Account;
use solana_sdk::commitment_config::CommitmentConfig;
use solana_sdk::native_token::sol_to_lamports;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::Keypair;
use test_utils::SECRET_KEY_WALLET;
use test_utils_solana::ProgramTest;
Expand Down Expand Up @@ -41,6 +43,47 @@ async fn initialize() -> Result<()> {
Ok(())
}

#[test_log::test(tokio::test)]
async fn composition() -> Result<()> {
let unchecked = Pubkey::new_unique();
let signer_keypair = Keypair::new();
let signer = signer_keypair.pubkey();
let keypair = get_wallet_keypair();
let (mut ctx, rpc) = create_program_test().await;
let mut wallet = MemoryWallet::new(rpc.clone(), &[keypair]);

wallet.connect().await?;

let program = ExampleProgramClient::builder()
.wallet(wallet.clone())
.rpc(rpc.clone())
.build()
.into_example_program_client();

let request = program
.another()
.args(10)
.accounts(example_program::accounts::Another { signer })
.signers(vec![&signer_keypair])
.build()
.compose()
.initialize()
.accounts(example_program::accounts::Initialize { unchecked })
.build();

let simulation = request
.sign_and_simulate_banks_client_transaction(&mut ctx.banks_client)
.await?;

check!(simulation.result.unwrap().is_ok());

request
.sign_and_process_banks_client_transaction(&mut ctx.banks_client)
.await?;

Ok(())
}

async fn create_program_test() -> (ProgramTestContext, SolanaRpcClient) {
let pubkey = get_wallet_keypair().pubkey();
let mut program_test = ProgramTest::new(
Expand Down
17 changes: 17 additions & 0 deletions programs/example_program/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,27 @@ pub mod example_program {
msg!("Greetings from: {:?}", ctx.program_id);
Ok(())
}

pub fn another(ctx: Context<Another>, useless: u32) -> Result<()> {
msg!("another useless: {}, program: {}", useless, ctx.program_id);
Ok(())
}
}

#[derive(Accounts)]
pub struct Initialize<'info> {
/// CHECK: for testing purposes
pub unchecked: UncheckedAccount<'info>,
}

#[derive(Accounts)]
pub struct Another<'info> {
/// signer for testing purposes
pub signer: Signer<'info>,
}

impl From<u32> for instruction::Another {
fn from(useless: u32) -> Self {
instruction::Another { useless }
}
}

0 comments on commit 173d24d

Please sign in to comment.