diff --git a/Cargo.lock b/Cargo.lock index 9314c64b..3efd2068 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2178,6 +2178,18 @@ dependencies = [ "solana-program-error", ] +[[package]] +name = "solana-instruction-view" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab7a27d0c4214b9f7389c3dd00b68c93093a67f1dcc5b7893aebe299bbcbb47" +dependencies = [ + "solana-account-view", + "solana-address 2.6.1", + "solana-define-syscall 5.1.0", + "solana-program-error", +] + [[package]] name = "solana-instructions-sysvar" version = "3.0.0" @@ -2844,8 +2856,11 @@ dependencies = [ name = "spl-memo-interface" version = "2.1.0" dependencies = [ + "solana-account-view", + "solana-address 2.6.1", "solana-instruction", - "solana-pubkey 4.2.0", + "solana-instruction-view", + "solana-program-error", ] [[package]] diff --git a/interface/Cargo.toml b/interface/Cargo.toml index 253191ed..bf4df09b 100644 --- a/interface/Cargo.toml +++ b/interface/Cargo.toml @@ -7,9 +7,19 @@ repository = "https://github.com/solana-program/memo" license = "Apache-2.0" edition = "2021" +[features] +cpi = ["dep:solana-account-view", "dep:solana-instruction-view", "dep:solana-program-error"] +instruction = ["dep:solana-instruction"] + [dependencies] -solana-instruction = "3.4.0" -solana-pubkey = "4.2.0" +solana-account-view = { version = "2.0", optional = true } +solana-address = { version = "2.6", features = ["decode"] } +solana-instruction = { version = "3.4.0", optional = true } +solana-instruction-view = { version = "2.1", features = ["cpi"], optional = true } +solana-program-error = { version = "3.0", optional = true } + +[dev-dependencies] +solana-address = { version = "2.6", features = ["atomic"] } [lib] crate-type = ["lib"] diff --git a/interface/src/cpi.rs b/interface/src/cpi.rs new file mode 100644 index 00000000..196cb530 --- /dev/null +++ b/interface/src/cpi.rs @@ -0,0 +1,79 @@ +use { + core::{mem::MaybeUninit, slice::from_raw_parts}, + solana_account_view::AccountView, + solana_address::Address, + solana_instruction_view::{ + cpi::{invoke_signed_unchecked, CpiAccount, Signer, MAX_STATIC_CPI_ACCOUNTS}, + InstructionAccount, InstructionView, + }, + solana_program_error::{ProgramError, ProgramResult}, +}; + +/// Writes a message to the transaction log, validating that +/// provided accounts are signers. +/// +/// ### Accounts: +/// 0. `..+N` `[SIGNER]` N signing accounts +pub struct Memo<'memo, 'signers, S: AsRef> { + /// The message to log. + pub message: &'memo str, + + /// Signing accounts. + pub signers: &'signers [S], + + /// The Memo program to invoke. + pub program_id: &'static Address, +} + +impl> Memo<'_, '_, S> { + /// Invokes the Memo program with the provided message and signing accounts. + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + /// Invokes the Memo program with the provided message and signing accounts. + /// + /// Seeds for signing accounts can be provided via `signers` parameter. + #[inline(always)] + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + let mut instruction_accounts = + [const { MaybeUninit::::uninit() }; MAX_STATIC_CPI_ACCOUNTS]; + + let expected_account = self.signers.len(); + + if expected_account > MAX_STATIC_CPI_ACCOUNTS { + return Err(ProgramError::InvalidArgument); + } + + let mut accounts = [const { MaybeUninit::::uninit() }; MAX_STATIC_CPI_ACCOUNTS]; + + for i in 0..expected_account { + // SAFETY: `expected_account` is less than MAX_STATIC_CPI_ACCOUNTS. + unsafe { + let signer = self.signers.get_unchecked(i); + + instruction_accounts.get_unchecked_mut(i).write( + InstructionAccount::readonly_signer(signer.as_ref().address()), + ); + + CpiAccount::init_from_account_view(signer.as_ref(), accounts.get_unchecked_mut(i)); + } + } + + // SAFETY: both `instruction_accounts` and `accounts` are initialized. + unsafe { + invoke_signed_unchecked( + &InstructionView { + program_id: self.program_id, + accounts: from_raw_parts(instruction_accounts.as_ptr() as _, expected_account), + data: self.message.as_bytes(), + }, + from_raw_parts(accounts.as_ptr() as _, expected_account), + signers, + ); + } + + Ok(()) + } +} diff --git a/interface/src/instruction.rs b/interface/src/instruction.rs index 079bd0ba..c98a0067 100644 --- a/interface/src/instruction.rs +++ b/interface/src/instruction.rs @@ -1,22 +1,22 @@ use { + solana_address::Address, solana_instruction::{AccountMeta, Instruction}, - solana_pubkey::Pubkey, }; -/// Build a memo instruction, possibly signed +/// Build a memo instruction, possibly signed. /// /// Accounts expected by this instruction: /// /// 0. `..0+N` `[signer]` Expected signers; if zero provided, instruction will /// be processed as a normal, unsigned spl-memo -pub fn build_memo(program_id: &Pubkey, memo: &[u8], signer_pubkeys: &[&Pubkey]) -> Instruction { +pub fn memo(program_id: &Address, message: &[u8], signer_pubkeys: &[&Address]) -> Instruction { Instruction { program_id: *program_id, accounts: signer_pubkeys .iter() .map(|&pubkey| AccountMeta::new_readonly(*pubkey, true)) .collect(), - data: memo.to_vec(), + data: message.to_vec(), } } @@ -25,12 +25,13 @@ mod tests { use super::*; #[test] - fn test_build_memo() { - let program_id = Pubkey::new_unique(); - let signer_pubkey = Pubkey::new_unique(); - let memo = "🐆".as_bytes(); - let instruction = build_memo(&program_id, memo, &[&signer_pubkey]); - assert_eq!(memo, instruction.data); + fn test_memo() { + let program_id = Address::new_unique(); + let signer_pubkey = Address::new_unique(); + let message = "🐆".as_bytes(); + let instruction = memo(&program_id, message, &[&signer_pubkey]); + + assert_eq!(message, instruction.data); assert_eq!(instruction.accounts.len(), 1); assert_eq!(instruction.accounts[0].pubkey, signer_pubkey); } diff --git a/interface/src/lib.rs b/interface/src/lib.rs index 775ef1af..317978d6 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -1,22 +1,28 @@ +#![no_std] #![deny(missing_docs)] //! An interface for programs that accept a string of encoded characters and //! verifies that it parses, while verifying and logging signers. -/// Instruction type +/// CPI interface for invoking the Memo program. +#[cfg(feature = "cpi")] +pub mod cpi; + +/// Instruction definition for the Memo program. +#[cfg(feature = "instruction")] pub mod instruction; /// Legacy symbols from Memo version 1 pub mod v1 { - solana_pubkey::declare_id!("Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo"); + solana_address::declare_id!("Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo"); } /// Symbols from Memo version 3 pub mod v3 { - solana_pubkey::declare_id!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"); + solana_address::declare_id!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"); } /// Symbols from Memo version 4 pub mod v4 { - solana_pubkey::declare_id!("Memo4c2pN8afCj432Lb7RMVKi9PbQnnW7ewFFaV3oAH"); + solana_address::declare_id!("Memo4c2pN8afCj432Lb7RMVKi9PbQnnW7ewFFaV3oAH"); } diff --git a/program/Cargo.toml b/program/Cargo.toml index 48ce388b..8c4dcbef 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -28,4 +28,4 @@ solana-account = "3.4.0" solana-instruction = "3.4.0" solana-program-error = "3.0.1" solana-pubkey = "4.2.0" -spl-memo-interface = { path = "../interface" } +spl-memo-interface = { path = "../interface", features = ["instruction"] } diff --git a/program/tests/functional.rs b/program/tests/functional.rs index 8e335741..c72c5623 100644 --- a/program/tests/functional.rs +++ b/program/tests/functional.rs @@ -5,12 +5,12 @@ use { solana_instruction::{error::InstructionError, AccountMeta, Instruction}, solana_program_error::ProgramError, solana_pubkey::Pubkey, - spl_memo_interface::{instruction::build_memo, v4::id}, + spl_memo_interface::{instruction::memo, v4::id}, }; #[test] fn test_memo_signing() { - let memo = "🐆".as_bytes(); + let message = "🐆".as_bytes(); let mollusk = Mollusk::new(&id(), "pinocchio_memo_program"); let first_address = Pubkey::new_unique(); @@ -21,7 +21,7 @@ fn test_memo_signing() { // Test complete signing let signer_key_refs: Vec<&Pubkey> = pubkeys.iter().collect(); mollusk.process_and_validate_instruction( - &build_memo(&id(), memo, &signer_key_refs), + &memo(&id(), message, &signer_key_refs), &[ (first_address, Account::default()), (second_address, Account::default()), @@ -31,11 +31,7 @@ fn test_memo_signing() { ); // Test unsigned memo - mollusk.process_and_validate_instruction( - &build_memo(&id(), memo, &[]), - &[], - &[Check::success()], - ); + mollusk.process_and_validate_instruction(&memo(&id(), message, &[]), &[], &[Check::success()]); // Test missing signer(s) mollusk.process_and_validate_instruction( @@ -46,7 +42,7 @@ fn test_memo_signing() { AccountMeta::new_readonly(second_address, false), AccountMeta::new_readonly(third_address, true), ], - data: memo.to_vec(), + data: message.to_vec(), }, &[ (first_address, Account::default()), @@ -64,7 +60,7 @@ fn test_memo_signing() { AccountMeta::new_readonly(second_address, false), AccountMeta::new_readonly(third_address, false), ], - data: memo.to_vec(), + data: message.to_vec(), }, &[ (first_address, Account::default()), @@ -77,7 +73,7 @@ fn test_memo_signing() { // Test invalid utf-8; demonstrate log let invalid_utf8 = [0xF0, 0x9F, 0x90, 0x86, 0xF0, 0x9F, 0xFF, 0x86]; mollusk.process_and_validate_instruction( - &build_memo(&id(), &invalid_utf8, &[]), + &memo(&id(), &invalid_utf8, &[]), &[], &[Check::instruction_err( InstructionError::ProgramFailedToComplete, @@ -90,44 +86,44 @@ fn test_memo_compute_limits() { let mollusk = Mollusk::new(&id(), "pinocchio_memo_program"); // Test memo length - let mut memo = vec![]; + let mut message = vec![]; for _ in 0..1000 { let mut vec = vec![0x53, 0x4F, 0x4C]; - memo.append(&mut vec); + message.append(&mut vec); } mollusk.process_and_validate_instruction( - &build_memo(&id(), &memo[..450], &[]), + &memo(&id(), &message[..450], &[]), &[], &[Check::success()], ); mollusk.process_and_validate_instruction( - &build_memo(&id(), &memo[..600], &[]), + &memo(&id(), &message[..600], &[]), &[], &[Check::success(), Check::compute_units(800)], ); - let mut memo = vec![]; + let mut message = vec![]; for _ in 0..100 { let mut vec = vec![0xE2, 0x97, 0x8E]; - memo.append(&mut vec); + message.append(&mut vec); } mollusk.process_and_validate_instruction( - &build_memo(&id(), &memo[..60], &[]), + &memo(&id(), &message[..60], &[]), &[], &[Check::success()], ); mollusk.process_and_validate_instruction( - &build_memo(&id(), &memo[..63], &[]), + &memo(&id(), &message[..63], &[]), &[], &[Check::success(), Check::compute_units(287)], ); // Test num signers with 32-byte memo - let memo = [b'1'; 32]; + let message = [b'1'; 32]; let mut pubkeys = vec![]; for _ in 0..20 { pubkeys.push(Pubkey::new_unique()); @@ -135,7 +131,7 @@ fn test_memo_compute_limits() { let signer_key_refs: Vec<&Pubkey> = pubkeys.iter().collect(); mollusk.process_and_validate_instruction( - &build_memo(&id(), &memo, &signer_key_refs[..12]), + &memo(&id(), &message, &signer_key_refs[..12]), pubkeys .iter() .take(12) @@ -146,7 +142,7 @@ fn test_memo_compute_limits() { ); mollusk.process_and_validate_instruction( - &build_memo(&id(), &memo, &signer_key_refs[..15]), + &memo(&id(), &message, &signer_key_refs[..15]), pubkeys .iter() .take(15)