From c295dff319d4e9b3035abeeb3152324104f9d9af Mon Sep 17 00:00:00 2001 From: febo Date: Fri, 28 Mar 2025 16:43:00 +0000 Subject: [PATCH 1/5] Rename to try_from_bytes --- program/src/processor/initialize.rs | 2 +- program/src/processor/set_authority.rs | 2 +- program/src/processor/set_data.rs | 8 +++++--- program/src/state/mod.rs | 10 +++++----- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/program/src/processor/initialize.rs b/program/src/processor/initialize.rs index f149c98..4e324d0 100644 --- a/program/src/processor/initialize.rs +++ b/program/src/processor/initialize.rs @@ -75,7 +75,7 @@ pub fn initialize(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramR let discriminator = { // SAFETY: scoped immutable borrow of `metadata` account data. - AccountDiscriminator::from_bytes(unsafe { metadata.borrow_data_unchecked() })? + AccountDiscriminator::try_from_bytes(unsafe { metadata.borrow_data_unchecked() })? }; let data_length = match discriminator { diff --git a/program/src/processor/set_authority.rs b/program/src/processor/set_authority.rs index cf57c0f..899bb84 100644 --- a/program/src/processor/set_authority.rs +++ b/program/src/processor/set_authority.rs @@ -38,7 +38,7 @@ pub fn set_authority(accounts: &[AccountInfo], instruction_data: &[u8]) -> Progr // SAFETY: single mutable borrow of `account` account data. let account_data = unsafe { account.borrow_mut_data_unchecked() }; - match AccountDiscriminator::from_bytes(account_data)? { + match AccountDiscriminator::try_from_bytes(account_data)? { Some(AccountDiscriminator::Buffer) => { let buffer = Buffer::from_bytes_mut(account_data)?; diff --git a/program/src/processor/set_data.rs b/program/src/processor/set_data.rs index fa61b61..f5eaab4 100644 --- a/program/src/processor/set_data.rs +++ b/program/src/processor/set_data.rs @@ -60,12 +60,14 @@ pub fn set_data(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramRes let data = match (optional_data, has_buffer) { (Some((data_source, Some(remaining_data))), false) => Some((data_source, remaining_data)), (Some((data_source, None)), true) => { + // SAFETY: singe immutable borrow of `buffer` account data. let buffer_data = unsafe { buffer.borrow_data_unchecked() }; - match AccountDiscriminator::from_bytes(buffer_data)? { - Some(AccountDiscriminator::Buffer) => (), + match AccountDiscriminator::try_from_bytes(buffer_data)? { + Some(AccountDiscriminator::Buffer) => { + Some((data_source, &buffer_data[Header::LEN..])) + } _ => return Err(ProgramError::InvalidAccountData), } - Some((data_source, &buffer_data[Header::LEN..])) } (None, false) => None, _ => return Err(ProgramError::InvalidInstructionData), diff --git a/program/src/state/mod.rs b/program/src/state/mod.rs index 1656321..559b27b 100644 --- a/program/src/state/mod.rs +++ b/program/src/state/mod.rs @@ -69,12 +69,12 @@ pub enum AccountDiscriminator { } impl AccountDiscriminator { - pub fn from_bytes(bytes: &[u8]) -> Result, ProgramError> { - if bytes.is_empty() { - Ok(None) + pub fn try_from_bytes(bytes: &[u8]) -> Result, ProgramError> { + Ok(if let Some(discriminator) = bytes.first() { + Some(AccountDiscriminator::try_from(*discriminator)?) } else { - Ok(Some(AccountDiscriminator::try_from(bytes[0])?)) - } + None + }) } } From 477a19538ba8f6ec2854c973bdf53bd397c6261c Mon Sep 17 00:00:00 2001 From: febo Date: Fri, 28 Mar 2025 16:43:17 +0000 Subject: [PATCH 2/5] Add optional buffer account --- program/src/instruction.rs | 3 +- program/src/processor/write.rs | 51 ++++++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/program/src/instruction.rs b/program/src/instruction.rs index eed4573..01eee2a 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -12,11 +12,12 @@ pub enum ProgramMetadataInstruction { /// /// 0. `[w]` Buffer to write to. /// 1. `[s]` Authority account. + /// 2. `[o]` Buffer to copy the data from. /// /// Instruction data: /// /// - `u32`: offset to write to - /// - `[u8]`: bytes to write + /// - `[u8]`: (optional) bytes to write Write, /// Initializes a metadata account. diff --git a/program/src/processor/write.rs b/program/src/processor/write.rs index 67ac1b8..82fd862 100644 --- a/program/src/processor/write.rs +++ b/program/src/processor/write.rs @@ -4,7 +4,7 @@ use pinocchio::{ account_info::AccountInfo, memory::sol_memcpy, program_error::ProgramError, ProgramResult, }; -use crate::state::{buffer::Buffer, AccountDiscriminator}; +use crate::state::{buffer::Buffer, header::Header, AccountDiscriminator}; /// Processor for the [`Write`](`crate::instruction::ProgramMetadataInstruction::Write`) /// instruction. @@ -16,7 +16,7 @@ pub fn write(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult // Access accounts. - let [buffer, authority, _remaining @ ..] = accounts else { + let [target_buffer, authority, source_buffer, _remaining @ ..] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; @@ -29,15 +29,15 @@ pub fn write(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult return Err(ProgramError::MissingRequiredSignature); } - // buffer + // target_buffer // - must be initialized // - must be rent exempt (pre-funded account) since we are reallocating the buffer // account (this is tested implicity) - let required_length = { + let (required_length, source_data) = { // SAFETY: scoped immutable borrow of `buffer` account data. There // are no other borrows active. - let data = unsafe { buffer.borrow_data_unchecked() }; + let data = unsafe { target_buffer.borrow_data_unchecked() }; if data.is_empty() || data[0] != AccountDiscriminator::Buffer as u8 { return Err(ProgramError::InvalidAccountData); @@ -50,22 +50,43 @@ pub fn write(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult return Err(ProgramError::IncorrectAuthority); } - max(data.len(), offset + args.data().len()) + // Determine from where to copy the data. + let instruction_data = match args.data() { + source_data if !source_data.is_empty() => Some(source_data), + _ => None, + }; + + let buffer_data = if source_buffer.key() != &crate::ID { + // SAFETY: singe immutable borrow of `source_buffer` account data. + Some(unsafe { source_buffer.borrow_data_unchecked() }) + } else { + None + }; + + let source_data = match (instruction_data, buffer_data) { + (Some(instruction_data), None) => instruction_data, + (None, Some(buffer_data)) => match AccountDiscriminator::try_from_bytes(buffer_data)? { + Some(AccountDiscriminator::Buffer) => &buffer_data[Header::LEN..], + _ => return Err(ProgramError::InvalidAccountData), + }, + _ => return Err(ProgramError::InvalidInstructionData), + }; + + (max(data.len(), offset + source_data.len()), source_data) }; - // Writes the instruction data to the buffer account. + // Writes the source data to the buffer account. - buffer.realloc(required_length, false)?; + target_buffer.realloc(required_length, false)?; // SAFETY: single mutable borrow of `buffer` account data. There // are no other borrows active. - let data = unsafe { buffer.borrow_mut_data_unchecked() }; - let instruction_data = args.data(); + let data = unsafe { target_buffer.borrow_mut_data_unchecked() }; unsafe { sol_memcpy( - &mut data[offset..], - instruction_data, - instruction_data.len(), + data.get_unchecked_mut(offset..), + source_data, + source_data.len(), ); } @@ -86,8 +107,8 @@ impl Write<'_> { pub fn try_from_bytes(bytes: &[u8]) -> Result { // The minimum expected size of the instruction data. // - offset (4 bytes) - // - data (...n bytes) - if bytes.len() < 5 { + // - data (...n bytes, optional) + if bytes.len() < 4 { return Err(ProgramError::InvalidInstructionData); } From 90dd09394f15e615347b18b9734e31de2e338792 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 29 Mar 2025 11:02:16 +0000 Subject: [PATCH 3/5] Add "sourceBuffer" account to "write" instruction in IDL --- .../js/src/generated/instructions/write.ts | 38 +++++++++-- .../rust/src/generated/instructions/write.rs | 66 ++++++++++++++++++- program/idl.json | 8 +++ 3 files changed, 105 insertions(+), 7 deletions(-) diff --git a/clients/js/src/generated/instructions/write.ts b/clients/js/src/generated/instructions/write.ts index d8a75f9..18d9cd5 100644 --- a/clients/js/src/generated/instructions/write.ts +++ b/clients/js/src/generated/instructions/write.ts @@ -26,6 +26,7 @@ import { type IInstruction, type IInstructionWithAccounts, type IInstructionWithData, + type ReadonlyAccount, type ReadonlySignerAccount, type ReadonlyUint8Array, type TransactionSigner, @@ -44,6 +45,7 @@ export type WriteInstruction< TProgram extends string = typeof PROGRAM_METADATA_PROGRAM_ADDRESS, TAccountBuffer extends string | IAccountMeta = string, TAccountAuthority extends string | IAccountMeta = string, + TAccountSourceBuffer extends string | IAccountMeta = string, TRemainingAccounts extends readonly IAccountMeta[] = [], > = IInstruction & IInstructionWithData & @@ -56,6 +58,9 @@ export type WriteInstruction< ? ReadonlySignerAccount & IAccountSignerMeta : TAccountAuthority, + TAccountSourceBuffer extends string + ? ReadonlyAccount + : TAccountSourceBuffer, ...TRemainingAccounts, ] >; @@ -105,11 +110,14 @@ export function getWriteInstructionDataCodec(): Codec< export type WriteInput< TAccountBuffer extends string = string, TAccountAuthority extends string = string, + TAccountSourceBuffer extends string = string, > = { /** The buffer to write to. */ buffer: Address; /** The authority of the buffer. */ authority: TransactionSigner; + /** Buffer to copy the data from. */ + sourceBuffer?: Address; offset: WriteInstructionDataArgs['offset']; data: WriteInstructionDataArgs['data']; }; @@ -117,11 +125,17 @@ export type WriteInput< export function getWriteInstruction< TAccountBuffer extends string, TAccountAuthority extends string, + TAccountSourceBuffer extends string, TProgramAddress extends Address = typeof PROGRAM_METADATA_PROGRAM_ADDRESS, >( - input: WriteInput, + input: WriteInput, config?: { programAddress?: TProgramAddress } -): WriteInstruction { +): WriteInstruction< + TProgramAddress, + TAccountBuffer, + TAccountAuthority, + TAccountSourceBuffer +> { // Program address. const programAddress = config?.programAddress ?? PROGRAM_METADATA_PROGRAM_ADDRESS; @@ -130,6 +144,7 @@ export function getWriteInstruction< const originalAccounts = { buffer: { value: input.buffer ?? null, isWritable: true }, authority: { value: input.authority ?? null, isWritable: false }, + sourceBuffer: { value: input.sourceBuffer ?? null, isWritable: false }, }; const accounts = originalAccounts as Record< keyof typeof originalAccounts, @@ -144,12 +159,18 @@ export function getWriteInstruction< accounts: [ getAccountMeta(accounts.buffer), getAccountMeta(accounts.authority), + getAccountMeta(accounts.sourceBuffer), ], programAddress, data: getWriteInstructionDataEncoder().encode( args as WriteInstructionDataArgs ), - } as WriteInstruction; + } as WriteInstruction< + TProgramAddress, + TAccountBuffer, + TAccountAuthority, + TAccountSourceBuffer + >; return instruction; } @@ -164,6 +185,8 @@ export type ParsedWriteInstruction< buffer: TAccountMetas[0]; /** The authority of the buffer. */ authority: TAccountMetas[1]; + /** Buffer to copy the data from. */ + sourceBuffer?: TAccountMetas[2] | undefined; }; data: WriteInstructionData; }; @@ -176,7 +199,7 @@ export function parseWriteInstruction< IInstructionWithAccounts & IInstructionWithData ): ParsedWriteInstruction { - if (instruction.accounts.length < 2) { + if (instruction.accounts.length < 3) { // TODO: Coded error. throw new Error('Not enough accounts'); } @@ -186,11 +209,18 @@ export function parseWriteInstruction< accountIndex += 1; return accountMeta; }; + const getNextOptionalAccount = () => { + const accountMeta = getNextAccount(); + return accountMeta.address === PROGRAM_METADATA_PROGRAM_ADDRESS + ? undefined + : accountMeta; + }; return { programAddress: instruction.programAddress, accounts: { buffer: getNextAccount(), authority: getNextAccount(), + sourceBuffer: getNextOptionalAccount(), }, data: getWriteInstructionDataDecoder().decode(instruction.data), }; diff --git a/clients/rust/src/generated/instructions/write.rs b/clients/rust/src/generated/instructions/write.rs index 73968b8..47decb5 100644 --- a/clients/rust/src/generated/instructions/write.rs +++ b/clients/rust/src/generated/instructions/write.rs @@ -16,6 +16,8 @@ pub struct Write { pub buffer: solana_program::pubkey::Pubkey, /// The authority of the buffer. pub authority: solana_program::pubkey::Pubkey, + /// Buffer to copy the data from. + pub source_buffer: Option, } impl Write { @@ -32,7 +34,7 @@ impl Write { args: WriteInstructionArgs, remaining_accounts: &[solana_program::instruction::AccountMeta], ) -> solana_program::instruction::Instruction { - let mut accounts = Vec::with_capacity(2 + remaining_accounts.len()); + let mut accounts = Vec::with_capacity(3 + remaining_accounts.len()); accounts.push(solana_program::instruction::AccountMeta::new( self.buffer, false, @@ -41,6 +43,17 @@ impl Write { self.authority, true, )); + if let Some(source_buffer) = self.source_buffer { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + source_buffer, + false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + crate::PROGRAM_METADATA_ID, + false, + )); + } accounts.extend_from_slice(remaining_accounts); let mut data = borsh::to_vec(&WriteInstructionData::new()).unwrap(); let mut args = borsh::to_vec(&args).unwrap(); @@ -85,10 +98,12 @@ pub struct WriteInstructionArgs { /// /// 0. `[writable]` buffer /// 1. `[signer]` authority +/// 2. `[optional]` source_buffer #[derive(Clone, Debug, Default)] pub struct WriteBuilder { buffer: Option, authority: Option, + source_buffer: Option, offset: Option, data: Option>, __remaining_accounts: Vec, @@ -110,6 +125,16 @@ impl WriteBuilder { self.authority = Some(authority); self } + /// `[optional account]` + /// Buffer to copy the data from. + #[inline(always)] + pub fn source_buffer( + &mut self, + source_buffer: Option, + ) -> &mut Self { + self.source_buffer = source_buffer; + self + } #[inline(always)] pub fn offset(&mut self, offset: u32) -> &mut Self { self.offset = Some(offset); @@ -143,6 +168,7 @@ impl WriteBuilder { let accounts = Write { buffer: self.buffer.expect("buffer is not set"), authority: self.authority.expect("authority is not set"), + source_buffer: self.source_buffer, }; let args = WriteInstructionArgs { offset: self.offset.clone().expect("offset is not set"), @@ -159,6 +185,8 @@ pub struct WriteCpiAccounts<'a, 'b> { pub buffer: &'b solana_program::account_info::AccountInfo<'a>, /// The authority of the buffer. pub authority: &'b solana_program::account_info::AccountInfo<'a>, + /// Buffer to copy the data from. + pub source_buffer: Option<&'b solana_program::account_info::AccountInfo<'a>>, } /// `write` CPI instruction. @@ -169,6 +197,8 @@ pub struct WriteCpi<'a, 'b> { pub buffer: &'b solana_program::account_info::AccountInfo<'a>, /// The authority of the buffer. pub authority: &'b solana_program::account_info::AccountInfo<'a>, + /// Buffer to copy the data from. + pub source_buffer: Option<&'b solana_program::account_info::AccountInfo<'a>>, /// The arguments for the instruction. pub __args: WriteInstructionArgs, } @@ -183,6 +213,7 @@ impl<'a, 'b> WriteCpi<'a, 'b> { __program: program, buffer: accounts.buffer, authority: accounts.authority, + source_buffer: accounts.source_buffer, __args: args, } } @@ -220,7 +251,7 @@ impl<'a, 'b> WriteCpi<'a, 'b> { bool, )], ) -> solana_program::entrypoint::ProgramResult { - let mut accounts = Vec::with_capacity(2 + remaining_accounts.len()); + let mut accounts = Vec::with_capacity(3 + remaining_accounts.len()); accounts.push(solana_program::instruction::AccountMeta::new( *self.buffer.key, false, @@ -229,6 +260,17 @@ impl<'a, 'b> WriteCpi<'a, 'b> { *self.authority.key, true, )); + if let Some(source_buffer) = self.source_buffer { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *source_buffer.key, + false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + crate::PROGRAM_METADATA_ID, + false, + )); + } remaining_accounts.iter().for_each(|remaining_account| { accounts.push(solana_program::instruction::AccountMeta { pubkey: *remaining_account.0.key, @@ -245,10 +287,13 @@ impl<'a, 'b> WriteCpi<'a, 'b> { accounts, data, }; - let mut account_infos = Vec::with_capacity(3 + remaining_accounts.len()); + let mut account_infos = Vec::with_capacity(4 + remaining_accounts.len()); account_infos.push(self.__program.clone()); account_infos.push(self.buffer.clone()); account_infos.push(self.authority.clone()); + if let Some(source_buffer) = self.source_buffer { + account_infos.push(source_buffer.clone()); + } remaining_accounts .iter() .for_each(|remaining_account| account_infos.push(remaining_account.0.clone())); @@ -267,6 +312,7 @@ impl<'a, 'b> WriteCpi<'a, 'b> { /// /// 0. `[writable]` buffer /// 1. `[signer]` authority +/// 2. `[optional]` source_buffer #[derive(Clone, Debug)] pub struct WriteCpiBuilder<'a, 'b> { instruction: Box>, @@ -278,6 +324,7 @@ impl<'a, 'b> WriteCpiBuilder<'a, 'b> { __program: program, buffer: None, authority: None, + source_buffer: None, offset: None, data: None, __remaining_accounts: Vec::new(), @@ -302,6 +349,16 @@ impl<'a, 'b> WriteCpiBuilder<'a, 'b> { self.instruction.authority = Some(authority); self } + /// `[optional account]` + /// Buffer to copy the data from. + #[inline(always)] + pub fn source_buffer( + &mut self, + source_buffer: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ) -> &mut Self { + self.instruction.source_buffer = source_buffer; + self + } #[inline(always)] pub fn offset(&mut self, offset: u32) -> &mut Self { self.instruction.offset = Some(offset); @@ -363,6 +420,8 @@ impl<'a, 'b> WriteCpiBuilder<'a, 'b> { buffer: self.instruction.buffer.expect("buffer is not set"), authority: self.instruction.authority.expect("authority is not set"), + + source_buffer: self.instruction.source_buffer, __args: args, }; instruction.invoke_signed_with_remaining_accounts( @@ -377,6 +436,7 @@ struct WriteCpiBuilderInstruction<'a, 'b> { __program: &'b solana_program::account_info::AccountInfo<'a>, buffer: Option<&'b solana_program::account_info::AccountInfo<'a>>, authority: Option<&'b solana_program::account_info::AccountInfo<'a>>, + source_buffer: Option<&'b solana_program::account_info::AccountInfo<'a>>, offset: Option, data: Option>, /// Additional instruction accounts `(AccountInfo, is_writable, is_signer)`. diff --git a/program/idl.json b/program/idl.json index 1ec047e..f8194f2 100644 --- a/program/idl.json +++ b/program/idl.json @@ -311,6 +311,14 @@ "docs": ["The authority of the buffer."], "isSigner": true, "isWritable": false + }, + { + "kind": "instructionAccountNode", + "name": "sourceBuffer", + "docs": ["Buffer to copy the data from."], + "isSigner": false, + "isWritable": false, + "isOptional": true } ], "arguments": [ From 86fd72c8287750b8b28d0479d15d7d66776450a7 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 29 Mar 2025 11:11:56 +0000 Subject: [PATCH 4/5] Make "data" arg of "write" instruction optional in IDL --- .../js/src/generated/instructions/write.ts | 40 +++++++++++++++---- .../rust/src/generated/instructions/write.rs | 17 +++++--- program/idl.json | 16 ++++++-- 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/clients/js/src/generated/instructions/write.ts b/clients/js/src/generated/instructions/write.ts index 18d9cd5..8ab73af 100644 --- a/clients/js/src/generated/instructions/write.ts +++ b/clients/js/src/generated/instructions/write.ts @@ -10,12 +10,15 @@ import { combineCodec, getBytesDecoder, getBytesEncoder, + getOptionDecoder, + getOptionEncoder, getStructDecoder, getStructEncoder, getU32Decoder, getU32Encoder, getU8Decoder, getU8Encoder, + none, transformEncoder, type Address, type Codec, @@ -26,6 +29,8 @@ import { type IInstruction, type IInstructionWithAccounts, type IInstructionWithData, + type Option, + type OptionOrNullable, type ReadonlyAccount, type ReadonlySignerAccount, type ReadonlyUint8Array, @@ -69,13 +74,21 @@ export type WriteInstructionData = { discriminator: number; /** The offset to write to. */ offset: number; - data: ReadonlyUint8Array; + /** + * The data to write at the provided offset. + * You may use the `source_buffer` account instead of this argument to copy from an existing buffer. + */ + data: Option; }; export type WriteInstructionDataArgs = { /** The offset to write to. */ offset: number; - data: ReadonlyUint8Array; + /** + * The data to write at the provided offset. + * You may use the `source_buffer` account instead of this argument to copy from an existing buffer. + */ + data?: OptionOrNullable; }; export function getWriteInstructionDataEncoder(): Encoder { @@ -83,9 +96,13 @@ export function getWriteInstructionDataEncoder(): Encoder ({ ...value, discriminator: WRITE_DISCRIMINATOR }) + (value) => ({ + ...value, + discriminator: WRITE_DISCRIMINATOR, + data: value.data ?? none(), + }) ); } @@ -93,7 +110,7 @@ export function getWriteInstructionDataDecoder(): Decoder return getStructDecoder([ ['discriminator', getU8Decoder()], ['offset', getU32Decoder()], - ['data', getBytesDecoder()], + ['data', getOptionDecoder(getBytesDecoder(), { prefix: null })], ]); } @@ -116,10 +133,13 @@ export type WriteInput< buffer: Address; /** The authority of the buffer. */ authority: TransactionSigner; - /** Buffer to copy the data from. */ + /** + * Buffer to copy the data from. + * You may use the `data` argument instead of this account to pass data directly. + */ sourceBuffer?: Address; offset: WriteInstructionDataArgs['offset']; - data: WriteInstructionDataArgs['data']; + data?: WriteInstructionDataArgs['data']; }; export function getWriteInstruction< @@ -185,7 +205,11 @@ export type ParsedWriteInstruction< buffer: TAccountMetas[0]; /** The authority of the buffer. */ authority: TAccountMetas[1]; - /** Buffer to copy the data from. */ + /** + * Buffer to copy the data from. + * You may use the `data` argument instead of this account to pass data directly. + */ + sourceBuffer?: TAccountMetas[2] | undefined; }; data: WriteInstructionData; diff --git a/clients/rust/src/generated/instructions/write.rs b/clients/rust/src/generated/instructions/write.rs index 47decb5..f4d4086 100644 --- a/clients/rust/src/generated/instructions/write.rs +++ b/clients/rust/src/generated/instructions/write.rs @@ -5,9 +5,9 @@ //! //! +use crate::hooked::RemainderOptionBytes; use borsh::BorshDeserialize; use borsh::BorshSerialize; -use kaigan::types::RemainderVec; /// Accounts. #[derive(Debug)] @@ -17,6 +17,7 @@ pub struct Write { /// The authority of the buffer. pub authority: solana_program::pubkey::Pubkey, /// Buffer to copy the data from. + /// You may use the `data` argument instead of this account to pass data directly. pub source_buffer: Option, } @@ -89,7 +90,7 @@ impl Default for WriteInstructionData { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct WriteInstructionArgs { pub offset: u32, - pub data: RemainderVec, + pub data: RemainderOptionBytes, } /// Instruction builder for `Write`. @@ -105,7 +106,7 @@ pub struct WriteBuilder { authority: Option, source_buffer: Option, offset: Option, - data: Option>, + data: Option, __remaining_accounts: Vec, } @@ -127,6 +128,7 @@ impl WriteBuilder { } /// `[optional account]` /// Buffer to copy the data from. + /// You may use the `data` argument instead of this account to pass data directly. #[inline(always)] pub fn source_buffer( &mut self, @@ -141,7 +143,7 @@ impl WriteBuilder { self } #[inline(always)] - pub fn data(&mut self, data: RemainderVec) -> &mut Self { + pub fn data(&mut self, data: RemainderOptionBytes) -> &mut Self { self.data = Some(data); self } @@ -186,6 +188,7 @@ pub struct WriteCpiAccounts<'a, 'b> { /// The authority of the buffer. pub authority: &'b solana_program::account_info::AccountInfo<'a>, /// Buffer to copy the data from. + /// You may use the `data` argument instead of this account to pass data directly. pub source_buffer: Option<&'b solana_program::account_info::AccountInfo<'a>>, } @@ -198,6 +201,7 @@ pub struct WriteCpi<'a, 'b> { /// The authority of the buffer. pub authority: &'b solana_program::account_info::AccountInfo<'a>, /// Buffer to copy the data from. + /// You may use the `data` argument instead of this account to pass data directly. pub source_buffer: Option<&'b solana_program::account_info::AccountInfo<'a>>, /// The arguments for the instruction. pub __args: WriteInstructionArgs, @@ -351,6 +355,7 @@ impl<'a, 'b> WriteCpiBuilder<'a, 'b> { } /// `[optional account]` /// Buffer to copy the data from. + /// You may use the `data` argument instead of this account to pass data directly. #[inline(always)] pub fn source_buffer( &mut self, @@ -365,7 +370,7 @@ impl<'a, 'b> WriteCpiBuilder<'a, 'b> { self } #[inline(always)] - pub fn data(&mut self, data: RemainderVec) -> &mut Self { + pub fn data(&mut self, data: RemainderOptionBytes) -> &mut Self { self.instruction.data = Some(data); self } @@ -438,7 +443,7 @@ struct WriteCpiBuilderInstruction<'a, 'b> { authority: Option<&'b solana_program::account_info::AccountInfo<'a>>, source_buffer: Option<&'b solana_program::account_info::AccountInfo<'a>>, offset: Option, - data: Option>, + data: Option, /// Additional instruction accounts `(AccountInfo, is_writable, is_signer)`. __remaining_accounts: Vec<( &'b solana_program::account_info::AccountInfo<'a>, diff --git a/program/idl.json b/program/idl.json index f8194f2..31a938e 100644 --- a/program/idl.json +++ b/program/idl.json @@ -315,7 +315,10 @@ { "kind": "instructionAccountNode", "name": "sourceBuffer", - "docs": ["Buffer to copy the data from."], + "docs": [ + "Buffer to copy the data from.", + "You may use the `data` argument instead of this account to pass data directly." + ], "isSigner": false, "isWritable": false, "isOptional": true @@ -347,8 +350,15 @@ { "kind": "instructionArgumentNode", "name": "data", - "docs": [], - "type": { "kind": "bytesTypeNode" } + "docs": [ + "The data to write at the provided offset.", + "You may use the `source_buffer` account instead of this argument to copy from an existing buffer." + ], + "defaultValue": { "kind": "noneValueNode" }, + "type": { + "kind": "remainderOptionTypeNode", + "item": { "kind": "bytesTypeNode" } + } } ], "discriminators": [ From 70297de78d9a1f52ac4b2bbfdef82492d6388740 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 29 Mar 2025 11:27:05 +0000 Subject: [PATCH 5/5] Add JS test for writing to buffers using source buffers --- clients/js/test/write.test.ts | 46 +++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/clients/js/test/write.test.ts b/clients/js/test/write.test.ts index b76d212..ff8ff9e 100644 --- a/clients/js/test/write.test.ts +++ b/clients/js/test/write.test.ts @@ -163,3 +163,49 @@ test('it appends to the end of buffers when doing multiple writes', async (t) => data: new Uint8Array([...dataChunk1, ...dataChunk2]), }); }); + +test('it writes to buffers using other buffers', async (t) => { + // Given the following authority and deployed program. + const client = createDefaultSolanaClient(); + const authority = await generateKeyPairSignerWithSol(client); + const [program, programData] = await createDeployedProgram(client, authority); + + // And an existing keypair buffer account with data. + const data = getUtf8Encoder().encode('Hello, World!'); + const sourceBuffer = await createKeypairBuffer(client, { + payer: authority, + data, + }); + + // And the following pre-allocated pre-funded empty canonical buffer account. + const seed = 'dummy'; + const [buffer] = await createCanonicalBuffer(client, { + authority, + program, + programData, + seed, + dataLength: data.length, + }); + + // When we write some data to the canonical buffer account + // using the keypair buffer account as source. + const writeIx = getWriteInstruction({ + buffer, + authority, + offset: 0, + sourceBuffer: sourceBuffer.address, + }); + await pipe( + await createDefaultTransaction(client, authority), + (tx) => appendTransactionMessageInstruction(writeIx, tx), + (tx) => signAndSendTransaction(client, tx) + ); + + // Then we expect the buffer account to contain the data from the source buffer. + const bufferAccount = await fetchBuffer(client.rpc, buffer); + t.like(bufferAccount.data, >{ + discriminator: AccountDiscriminator.Buffer, + canonical: true, + data, + }); +});