From 41a8d17789a1c5dcc68104d9c04446858ace05d0 Mon Sep 17 00:00:00 2001 From: Blockiosaurus Date: Mon, 2 Dec 2024 13:28:04 -0500 Subject: [PATCH] Adding batch IX processing. --- clients/js/baseline.json | 92 +++++ clients/js/bench/transfer.ts | 88 ++++- .../js/src/generated/instructions/batchV1.ts | 114 ++++++ .../js/src/generated/instructions/index.ts | 1 + clients/js/src/instructions/batch.ts | 38 ++ clients/js/src/instructions/index.ts | 1 + clients/js/test/batch.test.ts | 63 +++ .../src/generated/instructions/batch_v1.rs | 363 ++++++++++++++++++ .../rust/src/generated/instructions/mod.rs | 2 + idls/mpl_core.json | 42 ++ programs/mpl-core/src/instruction.rs | 6 +- programs/mpl-core/src/processor/batch.rs | 138 +++++++ programs/mpl-core/src/processor/mod.rs | 6 + 13 files changed, 952 insertions(+), 2 deletions(-) create mode 100644 clients/js/baseline.json create mode 100644 clients/js/src/generated/instructions/batchV1.ts create mode 100644 clients/js/src/instructions/batch.ts create mode 100644 clients/js/test/batch.test.ts create mode 100644 clients/rust/src/generated/instructions/batch_v1.rs create mode 100644 programs/mpl-core/src/processor/batch.rs diff --git a/clients/js/baseline.json b/clients/js/baseline.json new file mode 100644 index 00000000..badadba0 --- /dev/null +++ b/clients/js/baseline.json @@ -0,0 +1,92 @@ +[ + { + "name": "CU: create a new, empty asset", + "unit": "Compute Units", + "value": 9863 + }, + { + "name": "Space: create a new, empty asset", + "unit": "Bytes", + "value": 91 + }, + { + "name": "CU: create a new, empty asset with empty collection", + "unit": "Compute Units", + "value": 21491 + }, + { + "name": "Space: create a new, empty asset with empty collection", + "unit": "Bytes", + "value": 91 + }, + { + "name": "CU: create a new asset with plugins", + "unit": "Compute Units", + "value": 39333 + }, + { + "name": "Space: create a new asset with plugins", + "unit": "Bytes", + "value": 194 + }, + { + "name": "CU: create a new asset with plugins and empty collection", + "unit": "Compute Units", + "value": 45133 + }, + { + "name": "Space: create a new asset with plugins and empty collection", + "unit": "Bytes", + "value": 194 + }, + { + "name": "CU: list an asset", + "unit": "Compute Units", + "value": 30912 + }, + { + "name": "CU: sell an asset", + "unit": "Compute Units", + "value": 37720 + }, + { + "name": "CU: list an asset with empty collection", + "unit": "Compute Units", + "value": 39073 + }, + { + "name": "CU: sell an asset with empty collection", + "unit": "Compute Units", + "value": 50953 + }, + { + "name": "CU: list an asset with collection royalties", + "unit": "Compute Units", + "value": 40358 + }, + { + "name": "CU: sell an asset with collection royalties", + "unit": "Compute Units", + "value": 56517 + }, + { + "name": "CU: transfer an empty asset", + "unit": "Compute Units", + "value": 5292 + }, + { + "name": "CU: transfer an empty asset with empty collection", + "unit": "Compute Units", + "value": 8066 + }, + { + "name": "CU: transfer an asset with plugins", + "unit": "Compute Units", + "value": 11493 + }, + { + "name": "CU: transfer an asset with plugins and empty collection", + "unit": "Compute Units", + "value": 14267 + } +] \ No newline at end of file diff --git a/clients/js/bench/transfer.ts b/clients/js/bench/transfer.ts index 248b0937..bfafca8a 100644 --- a/clients/js/bench/transfer.ts +++ b/clients/js/bench/transfer.ts @@ -1,7 +1,7 @@ import { generateSigner, TransactionBuilder } from "@metaplex-foundation/umi"; import test from "ava"; import { existsSync, readFileSync, writeFileSync } from "fs"; -import { createCollectionV1, createV1, pluginAuthorityPair, ruleSet, transferV1 } from "../src"; +import { batch, createCollectionV1, createV1, pluginAuthorityPair, ruleSet, transferV1 } from "../src"; import { createUmi } from "./_setup"; test('transfer an empty asset', async (t) => { @@ -204,5 +204,91 @@ test('transfer an asset with plugins and empty collection', async (t) => { // Write the array to output.json writeFileSync("./output.json", JSON.stringify(output, null, 2)); + t.pass(); +}); + +test('transfer an empty asset 36 times separate instructions', async (t) => { + // Given an Umi instance and a new signer. + const umi = await createUmi(); + const assetAddress = generateSigner(umi); + const newOwner = generateSigner(umi); + + await createV1(umi, { + asset: assetAddress, + name: "Test", + uri: "www.test.com", + }).sendAndConfirm(umi); + + let builder = new TransactionBuilder(); + + for (let i = 0; i < 18; i += 1) { + builder = builder.add(transferV1(umi, { asset: assetAddress.publicKey, newOwner: newOwner.publicKey })); + builder = builder.add(transferV1(umi, { asset: assetAddress.publicKey, newOwner: umi.identity.publicKey, authority: newOwner })); + } + + const tx = await builder.sendAndConfirm(umi); + + const compute = Number((await umi.rpc.getTransaction(tx.signature))?.meta.computeUnitsConsumed); + + const cuResult = { + name: `CU: ${t.title}`, + unit: "Compute Units", + value: compute, + } + + // Read the results array from output.json + let output = []; + if (existsSync("./output.json")) { + output = JSON.parse(readFileSync("./output.json", 'utf-8')); + } + + // Push the result to the array + output.push(cuResult); + // Write the array to output.json + writeFileSync("./output.json", JSON.stringify(output, null, 2)); + + t.pass(); +}); + +test('transfer an empty asset 36 times in a batch instruction', async (t) => { + // Given an Umi instance and a new signer. + const umi = await createUmi(); + const assetAddress = generateSigner(umi); + const newOwner = generateSigner(umi); + + await createV1(umi, { + asset: assetAddress, + name: "Test", + uri: "www.test.com", + }).sendAndConfirm(umi); + + let builder = new TransactionBuilder(); + + for (let i = 0; i < 18; i += 1) { + builder = builder.add(transferV1(umi, { asset: assetAddress.publicKey, newOwner: newOwner.publicKey })); + builder = builder.add(transferV1(umi, { asset: assetAddress.publicKey, newOwner: umi.identity.publicKey, authority: newOwner })); + } + + const tx = await batch(umi, builder).sendAndConfirm(umi); + + const compute = Number((await umi.rpc.getTransaction(tx.signature))?.meta.computeUnitsConsumed); + + const cuResult = { + name: `CU: ${t.title}`, + unit: "Compute Units", + value: compute, + } + + // Read the results array from output.json + let output = []; + if (existsSync("./output.json")) { + output = JSON.parse(readFileSync("./output.json", 'utf-8')); + } + + // Push the result to the array + output.push(cuResult); + // Write the array to output.json + writeFileSync("./output.json", JSON.stringify(output, null, 2)); + t.pass(); }); \ No newline at end of file diff --git a/clients/js/src/generated/instructions/batchV1.ts b/clients/js/src/generated/instructions/batchV1.ts new file mode 100644 index 00000000..525b7421 --- /dev/null +++ b/clients/js/src/generated/instructions/batchV1.ts @@ -0,0 +1,114 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/metaplex-foundation/kinobi + */ + +import { + Context, + Pda, + PublicKey, + TransactionBuilder, + transactionBuilder, +} from '@metaplex-foundation/umi'; +import { + Serializer, + bytes, + mapSerializer, + struct, + u32, + u8, +} from '@metaplex-foundation/umi/serializers'; +import { + ResolvedAccount, + ResolvedAccountsWithIndices, + getAccountMetasAndSigners, +} from '../shared'; + +// Accounts. +export type BatchV1InstructionAccounts = { + /** Dummy account */ + dummy?: PublicKey | Pda; +}; + +// Data. +export type BatchV1InstructionData = { + discriminator: number; + numAccounts: Uint8Array; + instructions: Uint8Array; +}; + +export type BatchV1InstructionDataArgs = { + numAccounts: Uint8Array; + instructions: Uint8Array; +}; + +export function getBatchV1InstructionDataSerializer(): Serializer< + BatchV1InstructionDataArgs, + BatchV1InstructionData +> { + return mapSerializer( + struct( + [ + ['discriminator', u8()], + ['numAccounts', bytes({ size: u32() })], + ['instructions', bytes({ size: u32() })], + ], + { description: 'BatchV1InstructionData' } + ), + (value) => ({ ...value, discriminator: 31 }) + ) as Serializer; +} + +// Args. +export type BatchV1InstructionArgs = BatchV1InstructionDataArgs; + +// Instruction. +export function batchV1( + context: Pick, + input: BatchV1InstructionAccounts & BatchV1InstructionArgs +): TransactionBuilder { + // Program ID. + const programId = context.programs.getPublicKey( + 'mplCore', + 'CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d' + ); + + // Accounts. + const resolvedAccounts = { + dummy: { + index: 0, + isWritable: false as boolean, + value: input.dummy ?? null, + }, + } satisfies ResolvedAccountsWithIndices; + + // Arguments. + const resolvedArgs: BatchV1InstructionArgs = { ...input }; + + // Accounts in order. + const orderedAccounts: ResolvedAccount[] = Object.values( + resolvedAccounts + ).sort((a, b) => a.index - b.index); + + // Keys and Signers. + const [keys, signers] = getAccountMetasAndSigners( + orderedAccounts, + 'programId', + programId + ); + + // Data. + const data = getBatchV1InstructionDataSerializer().serialize( + resolvedArgs as BatchV1InstructionDataArgs + ); + + // Bytes Created On Chain. + const bytesCreatedOnChain = 0; + + return transactionBuilder([ + { instruction: { keys, programId, data }, signers, bytesCreatedOnChain }, + ]); +} diff --git a/clients/js/src/generated/instructions/index.ts b/clients/js/src/generated/instructions/index.ts index f52941cd..2ec30a48 100644 --- a/clients/js/src/generated/instructions/index.ts +++ b/clients/js/src/generated/instructions/index.ts @@ -12,6 +12,7 @@ export * from './addExternalPluginAdapterV1'; export * from './addPluginV1'; export * from './approveCollectionPluginAuthorityV1'; export * from './approvePluginAuthorityV1'; +export * from './batchV1'; export * from './burnCollectionV1'; export * from './burnV1'; export * from './collect'; diff --git a/clients/js/src/instructions/batch.ts b/clients/js/src/instructions/batch.ts new file mode 100644 index 00000000..a926e2d2 --- /dev/null +++ b/clients/js/src/instructions/batch.ts @@ -0,0 +1,38 @@ +import { + AccountMeta, + Context, + Signer, + TransactionBuilder, +} from '@metaplex-foundation/umi'; +import { batchV1 } from '../generated'; + +export const batch = ( + context: Pick, + builder: TransactionBuilder +) => { + const ixes = builder.items; + let numAccounts = Buffer.alloc(0); + let instructions = Buffer.alloc(0); + const remainingAccounts: AccountMeta[] = []; + const signers: Signer[] = []; + + ixes.forEach((ix) => { + numAccounts = Buffer.concat([ + numAccounts, + Buffer.from([ix.instruction.keys.length]), + ]); + instructions = Buffer.concat([instructions, ix.instruction.data]); + remainingAccounts.push(...ix.instruction.keys); + signers.push(...ix.signers); + }); + + const batchBuilder = batchV1(context, { + numAccounts, + instructions, + }).addRemainingAccounts(remainingAccounts); + + const batchBuilderItems = batchBuilder.items; + batchBuilderItems[0].signers.push(...signers); + batchBuilder.setItems(batchBuilderItems); + return batchBuilder; +}; diff --git a/clients/js/src/instructions/index.ts b/clients/js/src/instructions/index.ts index d1d747ba..c695dff7 100644 --- a/clients/js/src/instructions/index.ts +++ b/clients/js/src/instructions/index.ts @@ -12,3 +12,4 @@ export * from './approvePluginAuthority'; export * from './revokePluginAuthority'; export * from './collection'; export * from './writeData'; +export * from './batch'; diff --git a/clients/js/test/batch.test.ts b/clients/js/test/batch.test.ts new file mode 100644 index 00000000..d90305d8 --- /dev/null +++ b/clients/js/test/batch.test.ts @@ -0,0 +1,63 @@ +import { generateSigner, TransactionBuilder } from '@metaplex-foundation/umi'; +import test from 'ava'; + +import { batch, create, transfer } from '../src'; +import { assertAsset, createUmi, DEFAULT_ASSET } from './_setupRaw'; +import { createAsset } from './_setupSdk'; + +test('it can batch create assets', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const assets = []; + + let builder = new TransactionBuilder(); + + for (let i = 0; i < 5; i += 1) { + assets.push(generateSigner(umi)); + builder = builder.add(create(umi, { asset: assets[i], ...DEFAULT_ASSET })); + } + + await batch(umi, builder).sendAndConfirm(umi); + + // eslint-disable-next-line no-restricted-syntax + for (const asset of assets) { + // eslint-disable-next-line no-await-in-loop + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + }); + } +}); + +test('it can batch transfer assets', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const newOwner = generateSigner(umi); + const assetSigner = generateSigner(umi); + + const asset = await createAsset(umi, { asset: assetSigner }); + + let builder = new TransactionBuilder(); + + for (let i = 0; i < 18; i += 1) { + builder = builder.add( + transfer(umi, { asset, newOwner: newOwner.publicKey }) + ); + builder = builder.add( + transfer(umi, { + asset, + newOwner: umi.identity.publicKey, + authority: newOwner, + }) + ); + } + + await batch(umi, builder).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + }); +}); diff --git a/clients/rust/src/generated/instructions/batch_v1.rs b/clients/rust/src/generated/instructions/batch_v1.rs new file mode 100644 index 00000000..f3c9ab76 --- /dev/null +++ b/clients/rust/src/generated/instructions/batch_v1.rs @@ -0,0 +1,363 @@ +//! This code was AUTOGENERATED using the kinobi library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun kinobi to update it. +//! +//! [https://github.com/metaplex-foundation/kinobi] +//! + +#[cfg(feature = "anchor")] +use anchor_lang::prelude::{AnchorDeserialize, AnchorSerialize}; +#[cfg(not(feature = "anchor"))] +use borsh::{BorshDeserialize, BorshSerialize}; + +/// Accounts. +pub struct BatchV1 { + /// Dummy account + pub dummy: Option, +} + +impl BatchV1 { + pub fn instruction( + &self, + args: BatchV1InstructionArgs, + ) -> solana_program::instruction::Instruction { + self.instruction_with_remaining_accounts(args, &[]) + } + #[allow(clippy::vec_init_then_push)] + pub fn instruction_with_remaining_accounts( + &self, + args: BatchV1InstructionArgs, + remaining_accounts: &[solana_program::instruction::AccountMeta], + ) -> solana_program::instruction::Instruction { + let mut accounts = Vec::with_capacity(1 + remaining_accounts.len()); + if let Some(dummy) = self.dummy { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + dummy, false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + crate::MPL_CORE_ID, + false, + )); + } + accounts.extend_from_slice(remaining_accounts); + let mut data = BatchV1InstructionData::new().try_to_vec().unwrap(); + let mut args = args.try_to_vec().unwrap(); + data.append(&mut args); + + solana_program::instruction::Instruction { + program_id: crate::MPL_CORE_ID, + accounts, + data, + } + } +} + +#[cfg_attr(not(feature = "anchor"), derive(BorshSerialize, BorshDeserialize))] +#[cfg_attr(feature = "anchor", derive(AnchorSerialize, AnchorDeserialize))] +pub struct BatchV1InstructionData { + discriminator: u8, +} + +impl BatchV1InstructionData { + pub fn new() -> Self { + Self { discriminator: 31 } + } +} + +#[cfg_attr(not(feature = "anchor"), derive(BorshSerialize, BorshDeserialize))] +#[cfg_attr(feature = "anchor", derive(AnchorSerialize, AnchorDeserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BatchV1InstructionArgs { + pub num_accounts: Vec, + pub instructions: Vec, +} + +/// Instruction builder for `BatchV1`. +/// +/// ### Accounts: +/// +/// 0. `[optional]` dummy +#[derive(Default)] +pub struct BatchV1Builder { + dummy: Option, + num_accounts: Option>, + instructions: Option>, + __remaining_accounts: Vec, +} + +impl BatchV1Builder { + pub fn new() -> Self { + Self::default() + } + /// `[optional account]` + /// Dummy account + #[inline(always)] + pub fn dummy(&mut self, dummy: Option) -> &mut Self { + self.dummy = dummy; + self + } + #[inline(always)] + pub fn num_accounts(&mut self, num_accounts: Vec) -> &mut Self { + self.num_accounts = Some(num_accounts); + self + } + #[inline(always)] + pub fn instructions(&mut self, instructions: Vec) -> &mut Self { + self.instructions = Some(instructions); + self + } + /// Add an aditional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: solana_program::instruction::AccountMeta, + ) -> &mut Self { + self.__remaining_accounts.push(account); + self + } + /// Add additional accounts to the instruction. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[solana_program::instruction::AccountMeta], + ) -> &mut Self { + self.__remaining_accounts.extend_from_slice(accounts); + self + } + #[allow(clippy::clone_on_copy)] + pub fn instruction(&self) -> solana_program::instruction::Instruction { + let accounts = BatchV1 { dummy: self.dummy }; + let args = BatchV1InstructionArgs { + num_accounts: self.num_accounts.clone().expect("num_accounts is not set"), + instructions: self.instructions.clone().expect("instructions is not set"), + }; + + accounts.instruction_with_remaining_accounts(args, &self.__remaining_accounts) + } +} + +/// `batch_v1` CPI accounts. +pub struct BatchV1CpiAccounts<'a, 'b> { + /// Dummy account + pub dummy: Option<&'b solana_program::account_info::AccountInfo<'a>>, +} + +/// `batch_v1` CPI instruction. +pub struct BatchV1Cpi<'a, 'b> { + /// The program to invoke. + pub __program: &'b solana_program::account_info::AccountInfo<'a>, + /// Dummy account + pub dummy: Option<&'b solana_program::account_info::AccountInfo<'a>>, + /// The arguments for the instruction. + pub __args: BatchV1InstructionArgs, +} + +impl<'a, 'b> BatchV1Cpi<'a, 'b> { + pub fn new( + program: &'b solana_program::account_info::AccountInfo<'a>, + accounts: BatchV1CpiAccounts<'a, 'b>, + args: BatchV1InstructionArgs, + ) -> Self { + Self { + __program: program, + dummy: accounts.dummy, + __args: args, + } + } + #[inline(always)] + pub fn invoke(&self) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], &[]) + } + #[inline(always)] + pub fn invoke_with_remaining_accounts( + &self, + remaining_accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], remaining_accounts) + } + #[inline(always)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(signers_seeds, &[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed_with_remaining_accounts( + &self, + signers_seeds: &[&[&[u8]]], + remaining_accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program::entrypoint::ProgramResult { + let mut accounts = Vec::with_capacity(1 + remaining_accounts.len()); + if let Some(dummy) = self.dummy { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *dummy.key, false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + crate::MPL_CORE_ID, + false, + )); + } + remaining_accounts.iter().for_each(|remaining_account| { + accounts.push(solana_program::instruction::AccountMeta { + pubkey: *remaining_account.0.key, + is_signer: remaining_account.1, + is_writable: remaining_account.2, + }) + }); + let mut data = BatchV1InstructionData::new().try_to_vec().unwrap(); + let mut args = self.__args.try_to_vec().unwrap(); + data.append(&mut args); + + let instruction = solana_program::instruction::Instruction { + program_id: crate::MPL_CORE_ID, + accounts, + data, + }; + let mut account_infos = Vec::with_capacity(1 + 1 + remaining_accounts.len()); + account_infos.push(self.__program.clone()); + if let Some(dummy) = self.dummy { + account_infos.push(dummy.clone()); + } + remaining_accounts + .iter() + .for_each(|remaining_account| account_infos.push(remaining_account.0.clone())); + + if signers_seeds.is_empty() { + solana_program::program::invoke(&instruction, &account_infos) + } else { + solana_program::program::invoke_signed(&instruction, &account_infos, signers_seeds) + } + } +} + +/// Instruction builder for `BatchV1` via CPI. +/// +/// ### Accounts: +/// +/// 0. `[optional]` dummy +pub struct BatchV1CpiBuilder<'a, 'b> { + instruction: Box>, +} + +impl<'a, 'b> BatchV1CpiBuilder<'a, 'b> { + pub fn new(program: &'b solana_program::account_info::AccountInfo<'a>) -> Self { + let instruction = Box::new(BatchV1CpiBuilderInstruction { + __program: program, + dummy: None, + num_accounts: None, + instructions: None, + __remaining_accounts: Vec::new(), + }); + Self { instruction } + } + /// `[optional account]` + /// Dummy account + #[inline(always)] + pub fn dummy( + &mut self, + dummy: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ) -> &mut Self { + self.instruction.dummy = dummy; + self + } + #[inline(always)] + pub fn num_accounts(&mut self, num_accounts: Vec) -> &mut Self { + self.instruction.num_accounts = Some(num_accounts); + self + } + #[inline(always)] + pub fn instructions(&mut self, instructions: Vec) -> &mut Self { + self.instruction.instructions = Some(instructions); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: &'b solana_program::account_info::AccountInfo<'a>, + is_writable: bool, + is_signer: bool, + ) -> &mut Self { + self.instruction + .__remaining_accounts + .push((account, is_writable, is_signer)); + self + } + /// Add additional accounts to the instruction. + /// + /// Each account is represented by a tuple of the `AccountInfo`, a `bool` indicating whether the account is writable or not, + /// and a `bool` indicating whether the account is a signer or not. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> &mut Self { + self.instruction + .__remaining_accounts + .extend_from_slice(accounts); + self + } + #[inline(always)] + pub fn invoke(&self) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed(&[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program::entrypoint::ProgramResult { + let args = BatchV1InstructionArgs { + num_accounts: self + .instruction + .num_accounts + .clone() + .expect("num_accounts is not set"), + instructions: self + .instruction + .instructions + .clone() + .expect("instructions is not set"), + }; + let instruction = BatchV1Cpi { + __program: self.instruction.__program, + + dummy: self.instruction.dummy, + __args: args, + }; + instruction.invoke_signed_with_remaining_accounts( + signers_seeds, + &self.instruction.__remaining_accounts, + ) + } +} + +struct BatchV1CpiBuilderInstruction<'a, 'b> { + __program: &'b solana_program::account_info::AccountInfo<'a>, + dummy: Option<&'b solana_program::account_info::AccountInfo<'a>>, + num_accounts: Option>, + instructions: Option>, + /// Additional instruction accounts `(AccountInfo, is_writable, is_signer)`. + __remaining_accounts: Vec<( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )>, +} diff --git a/clients/rust/src/generated/instructions/mod.rs b/clients/rust/src/generated/instructions/mod.rs index b42e06ac..60bb9a9d 100644 --- a/clients/rust/src/generated/instructions/mod.rs +++ b/clients/rust/src/generated/instructions/mod.rs @@ -11,6 +11,7 @@ pub(crate) mod r#add_external_plugin_adapter_v1; pub(crate) mod r#add_plugin_v1; pub(crate) mod r#approve_collection_plugin_authority_v1; pub(crate) mod r#approve_plugin_authority_v1; +pub(crate) mod r#batch_v1; pub(crate) mod r#burn_collection_v1; pub(crate) mod r#burn_v1; pub(crate) mod r#collect; @@ -43,6 +44,7 @@ pub use self::r#add_external_plugin_adapter_v1::*; pub use self::r#add_plugin_v1::*; pub use self::r#approve_collection_plugin_authority_v1::*; pub use self::r#approve_plugin_authority_v1::*; +pub use self::r#batch_v1::*; pub use self::r#burn_collection_v1::*; pub use self::r#burn_v1::*; pub use self::r#collect::*; diff --git a/idls/mpl_core.json b/idls/mpl_core.json index 4ea16d18..c88ca909 100644 --- a/idls/mpl_core.json +++ b/idls/mpl_core.json @@ -2006,6 +2006,32 @@ "type": "u8", "value": 30 } + }, + { + "name": "BatchV1", + "accounts": [ + { + "name": "dummy", + "isMut": false, + "isSigner": false, + "isOptional": true, + "docs": [ + "Dummy account" + ] + } + ], + "args": [ + { + "name": "batchV1Args", + "type": { + "defined": "BatchV1Args" + } + } + ], + "discriminant": { + "type": "u8", + "value": 31 + } } ], "accounts": [ @@ -3210,6 +3236,22 @@ ] } }, + { + "name": "BatchV1Args", + "type": { + "kind": "struct", + "fields": [ + { + "name": "numAccounts", + "type": "bytes" + }, + { + "name": "instructions", + "type": "bytes" + } + ] + } + }, { "name": "BurnV1Args", "type": { diff --git a/programs/mpl-core/src/instruction.rs b/programs/mpl-core/src/instruction.rs index ff657069..774b2d81 100644 --- a/programs/mpl-core/src/instruction.rs +++ b/programs/mpl-core/src/instruction.rs @@ -5,7 +5,7 @@ use shank::{ShankContext, ShankInstruction}; use crate::processor::{ AddCollectionExternalPluginAdapterV1Args, AddCollectionPluginV1Args, AddExternalPluginAdapterV1Args, AddPluginV1Args, ApproveCollectionPluginAuthorityV1Args, - ApprovePluginAuthorityV1Args, BurnCollectionV1Args, BurnV1Args, CompressV1Args, + ApprovePluginAuthorityV1Args, BatchV1Args, BurnCollectionV1Args, BurnV1Args, CompressV1Args, CreateCollectionV1Args, CreateCollectionV2Args, CreateV1Args, CreateV2Args, DecompressV1Args, RemoveCollectionExternalPluginAdapterV1Args, RemoveCollectionPluginV1Args, RemoveExternalPluginAdapterV1Args, RemovePluginV1Args, RevokeCollectionPluginAuthorityV1Args, @@ -292,4 +292,8 @@ pub(crate) enum MplAssetInstruction { #[account(5, name="system_program", desc = "The system program")] #[account(6, optional, name="log_wrapper", desc = "The SPL Noop Program")] UpdateV2(UpdateV2Args), + + /// Execute multiple instructions. + #[account(0, optional, name="dummy", desc = "Dummy account")] + BatchV1(BatchV1Args), } diff --git a/programs/mpl-core/src/processor/batch.rs b/programs/mpl-core/src/processor/batch.rs new file mode 100644 index 00000000..ef2df61b --- /dev/null +++ b/programs/mpl-core/src/processor/batch.rs @@ -0,0 +1,138 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError, +}; + +use crate::instruction::accounts::BatchV1Accounts; + +use super::{ + add_collection_external_plugin_adapter, add_collection_plugin, add_external_plugin_adapter, + add_plugin, approve_collection_plugin_authority, approve_plugin_authority, burn, + burn_collection, collect, compress, create_collection_v1, create_collection_v2, create_v1, + create_v2, decompress, remove_collection_external_plugin_adapter, remove_collection_plugin, + remove_external_plugin_adapter, remove_plugin, revoke_collection_plugin_authority, + revoke_plugin_authority, transfer, update_collection, + update_collection_external_plugin_adapter, update_collection_plugin, + update_external_plugin_adapter, update_plugin, update_v1, update_v2, + write_collection_external_plugin_adapter_data, write_external_plugin_adapter_data, + MplAssetInstruction, +}; + +#[repr(C)] +#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] +pub(crate) struct BatchV1Args { + pub(crate) num_accounts: Vec, + pub(crate) instructions: Vec, +} + +pub(crate) fn batch_v1<'a>(accounts: &'a [AccountInfo<'a>], args: BatchV1Args) -> ProgramResult { + // Accounts. + let ctx = BatchV1Accounts::context(accounts)?; + let mut index = 0; + let mut accounts_index = 0; + + let mut ix_slice = args.instructions.as_slice(); + while !ix_slice.is_empty() { + let ix = MplAssetInstruction::deserialize(&mut ix_slice)?; + match ix { + MplAssetInstruction::CreateV1(args) => { + create_v1(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::CreateCollectionV1(args) => { + create_collection_v1(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::AddPluginV1(args) => { + add_plugin(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::AddCollectionPluginV1(args) => { + add_collection_plugin(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::RemovePluginV1(args) => { + remove_plugin(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::RemoveCollectionPluginV1(args) => { + remove_collection_plugin(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::UpdatePluginV1(args) => { + update_plugin(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::UpdateCollectionPluginV1(args) => { + update_collection_plugin(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::ApprovePluginAuthorityV1(args) => { + approve_plugin_authority(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::ApproveCollectionPluginAuthorityV1(args) => { + approve_collection_plugin_authority(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::RevokePluginAuthorityV1(args) => { + revoke_plugin_authority(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::RevokeCollectionPluginAuthorityV1(args) => { + revoke_collection_plugin_authority(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::BurnV1(args) => burn(&ctx.remaining_accounts[index..], args), + MplAssetInstruction::BurnCollectionV1(args) => { + burn_collection(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::TransferV1(args) => { + transfer(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::UpdateV1(args) => { + update_v1(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::UpdateCollectionV1(args) => { + update_collection(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::CompressV1(args) => { + compress(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::DecompressV1(args) => { + decompress(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::Collect => collect(accounts), + MplAssetInstruction::CreateV2(args) => { + create_v2(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::CreateCollectionV2(args) => { + create_collection_v2(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::AddExternalPluginAdapterV1(args) => { + add_external_plugin_adapter(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::AddCollectionExternalPluginAdapterV1(args) => { + add_collection_external_plugin_adapter(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::RemoveExternalPluginAdapterV1(args) => { + remove_external_plugin_adapter(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::RemoveCollectionExternalPluginAdapterV1(args) => { + remove_collection_external_plugin_adapter(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::UpdateExternalPluginAdapterV1(args) => { + update_external_plugin_adapter(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::UpdateCollectionExternalPluginAdapterV1(args) => { + update_collection_external_plugin_adapter(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::WriteExternalPluginAdapterDataV1(args) => { + write_external_plugin_adapter_data(&ctx.remaining_accounts[index..], args) + } + MplAssetInstruction::WriteCollectionExternalPluginAdapterDataV1(args) => { + write_collection_external_plugin_adapter_data( + &ctx.remaining_accounts[index..], + args, + ) + } + MplAssetInstruction::UpdateV2(args) => { + update_v2(&ctx.remaining_accounts[index..], args) + } + _ => Err(ProgramError::InvalidInstructionData), + }?; + + index += args.num_accounts[accounts_index] as usize; + accounts_index += 1; + } + + Ok(()) +} diff --git a/programs/mpl-core/src/processor/mod.rs b/programs/mpl-core/src/processor/mod.rs index e3177a48..670155e2 100644 --- a/programs/mpl-core/src/processor/mod.rs +++ b/programs/mpl-core/src/processor/mod.rs @@ -1,6 +1,7 @@ mod add_external_plugin_adapter; mod add_plugin; mod approve_plugin_authority; +mod batch; mod burn; mod collect; mod compress; @@ -19,6 +20,7 @@ mod write_external_plugin_adapter_data; pub(crate) use add_external_plugin_adapter::*; pub(crate) use add_plugin::*; pub(crate) use approve_plugin_authority::*; +pub(crate) use batch::*; pub(crate) use burn::*; pub(crate) use collect::*; pub(crate) use compress::*; @@ -168,5 +170,9 @@ pub fn process_instruction<'a>( msg!("Instruction: UpdateV2"); update_v2(accounts, args) } + MplAssetInstruction::BatchV1(args) => { + msg!("Instruction: BatchV1"); + batch_v1(accounts, args) + } } }