Skip to main content

hopper_memo/
lib.rs

1//! Hopper-owned SPL Memo program builder.
2//!
3//! The SPL Memo program records arbitrary UTF-8 byte payloads in
4//! transaction logs and asserts that a list of accounts signed the
5//! containing transaction. It is the canonical primitive for on-chain
6//! metadata stamping (off-chain reference numbers, orderbook IDs,
7//! arbitrary protocol tags) without spinning up program-owned state.
8//!
9//! ## Programs
10//!
11//! - `MEMO_PROGRAM_ID` - Memo v2, the default and overwhelming majority case.
12//! - [`v1::MEMO_V1_PROGRAM_ID`] - legacy Memo v1, kept available for
13//!   protocols still pinned to the original program. New code should
14//!   prefer Memo v2.
15//!
16//! ## Quick start
17//!
18//! ```ignore
19//! use hopper_memo::Memo;
20//!
21//! Memo {
22//!     signers: &[user_view],
23//!     memo: b"order=42",
24//!     program_id: None,
25//! }
26//! .invoke()?;
27//! ```
28//!
29//! Memo strings can be empty; the program enforces only the signer
30//! constraints. The memo body is passed verbatim as the instruction
31//! data. UTF-8 framing is the caller's responsibility.
32
33#![no_std]
34#![deny(unsafe_op_in_unsafe_fn)]
35
36use core::mem::MaybeUninit;
37
38use hopper_runtime::account::AccountView;
39use hopper_runtime::address::Address;
40use hopper_runtime::error::ProgramError;
41use hopper_runtime::instruction::{InstructionAccount, InstructionView, Signer};
42use hopper_runtime::ProgramResult;
43
44/// SPL Memo v2 program id: `MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr`.
45///
46/// This is the default Memo program. Use [`v1::MEMO_V1_PROGRAM_ID`] only
47/// for legacy compatibility.
48pub const MEMO_PROGRAM_ID: Address =
49    hopper_runtime::address!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
50
51/// Maximum signer accounts a single memo invocation may cite.
52///
53/// Matches Pinocchio's `MAX_STATIC_CPI_ACCOUNTS` ceiling. The Memo
54/// program itself accepts an unbounded list, but heap-free CPI on
55/// SBF requires a static cap.
56pub const MAX_MEMO_SIGNERS: usize = 16;
57
58/// Legacy SPL Memo v1 helpers.
59///
60/// The v1 program (`Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo`) is
61/// frozen and only kept here for protocols anchored to it. New code
62/// should prefer v2 via [`MEMO_PROGRAM_ID`].
63pub mod v1 {
64    use hopper_runtime::address::Address;
65
66    /// SPL Memo v1 program id: `Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo`.
67    pub const MEMO_V1_PROGRAM_ID: Address =
68        hopper_runtime::address!("Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo");
69}
70
71/// SPL Memo CPI builder.
72///
73/// `signers` are the accounts the memo program will assert signed the
74/// surrounding transaction; pass an empty slice for unauthenticated
75/// memos (the program then only logs the bytes). `memo` is the raw
76/// payload - UTF-8 framing is the caller's responsibility.
77///
78/// `program_id` selects the target program. Default (`None`) uses
79/// [`MEMO_PROGRAM_ID`] (Memo v2). Pass `Some(&v1::MEMO_V1_PROGRAM_ID)`
80/// for the legacy program.
81///
82/// The struct holds borrowed references only; nothing is allocated on
83/// the heap.
84pub struct Memo<'a, 'b, 'c> {
85    /// Signing accounts the Memo program will validate.
86    pub signers: &'a [&'a AccountView],
87    /// Raw memo payload.
88    pub memo: &'b [u8],
89    /// Target program. `None` = Memo v2 (default).
90    pub program_id: Option<&'c Address>,
91}
92
93impl Memo<'_, '_, '_> {
94    /// Invoke the Memo program with no PDA signer seeds.
95    #[inline]
96    pub fn invoke(&self) -> ProgramResult {
97        self.invoke_signed(&[])
98    }
99
100    /// Invoke the Memo program, supplying PDA signer seeds.
101    ///
102    /// Any signer in `self.signers` whose address is a PDA must have
103    /// its derivation seeds in `signers_seeds`; the runtime will sign
104    /// the inner CPI on its behalf.
105    pub fn invoke_signed(&self, signers_seeds: &[Signer]) -> ProgramResult {
106        let n = self.signers.len();
107        if n > MAX_MEMO_SIGNERS {
108            return Err(ProgramError::InvalidArgument);
109        }
110
111        // Build the InstructionAccount array on the stack. We use
112        // MaybeUninit so we don't need a Default / Copy bound on
113        // InstructionAccount, mirroring the Pinocchio shape.
114        let mut accounts: [MaybeUninit<InstructionAccount>; MAX_MEMO_SIGNERS] =
115            [const { MaybeUninit::uninit() }; MAX_MEMO_SIGNERS];
116
117        let mut i = 0;
118        while i < n {
119            accounts[i].write(InstructionAccount::readonly_signer(
120                self.signers[i].address(),
121            ));
122            i += 1;
123        }
124
125        // SAFETY: the first `n` slots have been initialised in the
126        // loop above; we hand only that prefix to InstructionView.
127        let accounts_slice: &[InstructionAccount] = unsafe {
128            core::slice::from_raw_parts(accounts.as_ptr() as *const InstructionAccount, n)
129        };
130
131        let pid = self.program_id.unwrap_or(&MEMO_PROGRAM_ID);
132        let instruction = InstructionView {
133            program_id: pid,
134            data: self.memo,
135            accounts: accounts_slice,
136        };
137
138        macro_rules! invoke_with_signers {
139            ($n:literal, [$($idx:literal),*]) => {{
140                let account_views: [&AccountView; $n] = [$(self.signers[$idx]),*];
141                hopper_runtime::cpi::invoke_signed::<$n>(&instruction, &account_views, signers_seeds)
142            }};
143        }
144
145        match n {
146            0 => invoke_with_signers!(0, []),
147            1 => invoke_with_signers!(1, [0]),
148            2 => invoke_with_signers!(2, [0, 1]),
149            3 => invoke_with_signers!(3, [0, 1, 2]),
150            4 => invoke_with_signers!(4, [0, 1, 2, 3]),
151            5 => invoke_with_signers!(5, [0, 1, 2, 3, 4]),
152            6 => invoke_with_signers!(6, [0, 1, 2, 3, 4, 5]),
153            7 => invoke_with_signers!(7, [0, 1, 2, 3, 4, 5, 6]),
154            8 => invoke_with_signers!(8, [0, 1, 2, 3, 4, 5, 6, 7]),
155            9 => invoke_with_signers!(9, [0, 1, 2, 3, 4, 5, 6, 7, 8]),
156            10 => invoke_with_signers!(10, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
157            11 => invoke_with_signers!(11, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
158            12 => invoke_with_signers!(12, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]),
159            13 => invoke_with_signers!(13, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
160            14 => invoke_with_signers!(14, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]),
161            15 => invoke_with_signers!(15, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]),
162            16 => invoke_with_signers!(16, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]),
163            _ => Err(ProgramError::InvalidArgument),
164        }
165    }
166}