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}