@layerzerolabs/oapp-solana-contracts 1.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/Anchor.toml +23 -0
  2. package/Cargo.toml +26 -0
  3. package/README.md +181 -0
  4. package/idls/endpoint.json +3520 -0
  5. package/macros/Cargo.toml +23 -0
  6. package/macros/src/composer.rs +43 -0
  7. package/macros/src/instructions/composer/lz_compose.rs +17 -0
  8. package/macros/src/instructions/composer/lz_compose_types_info.rs +29 -0
  9. package/macros/src/instructions/composer/lz_compose_types_v2.rs +17 -0
  10. package/macros/src/instructions/composer/mod.rs +23 -0
  11. package/macros/src/instructions/mod.rs +4 -0
  12. package/macros/src/instructions/oapp/accept_default_admin_transfer.rs +50 -0
  13. package/macros/src/instructions/oapp/get_enforced_options.rs +57 -0
  14. package/macros/src/instructions/oapp/get_peer.rs +55 -0
  15. package/macros/src/instructions/oapp/init_enforced_options.rs +87 -0
  16. package/macros/src/instructions/oapp/is_compose_msg_sender.rs +44 -0
  17. package/macros/src/instructions/oapp/lz_receive.rs +33 -0
  18. package/macros/src/instructions/oapp/lz_receive_types_info.rs +28 -0
  19. package/macros/src/instructions/oapp/lz_receive_types_v2.rs +17 -0
  20. package/macros/src/instructions/oapp/mod.rs +41 -0
  21. package/macros/src/instructions/oapp/next_nonce.rs +52 -0
  22. package/macros/src/instructions/oapp/set_enforced_options.rs +101 -0
  23. package/macros/src/instructions/oapp/set_peer.rs +74 -0
  24. package/macros/src/lib.rs +81 -0
  25. package/macros/src/oapp.rs +86 -0
  26. package/macros/src/state_types.rs +79 -0
  27. package/package.json +61 -0
  28. package/rust-toolchain.toml +3 -0
  29. package/rustfmt.toml +23 -0
  30. package/src/common.rs +156 -0
  31. package/src/endpoint_cpi.rs +386 -0
  32. package/src/errors.rs +16 -0
  33. package/src/events.rs +26 -0
  34. package/src/lib.rs +82 -0
  35. package/src/lz_compose_types_v2.rs +88 -0
  36. package/src/lz_receive_types_v2.rs +147 -0
  37. package/src/oapp_info.rs +278 -0
  38. package/src/options.rs +147 -0
  39. package/src/types.rs +114 -0
  40. package/src/utils.rs +14 -0
@@ -0,0 +1,147 @@
1
+ use crate::{
2
+ common::{AccountMetaRef, AddressLocator},
3
+ COMPOSED_MESSAGE_HASH_SEED, ENDPOINT_SEED, EVENT_SEED, NONCE_SEED, OAPP_SEED,
4
+ PAYLOAD_HASH_SEED,
5
+ };
6
+ use anchor_lang::{prelude::*, solana_program::system_program::ID as SYSTEM_ID};
7
+ use solana_keccak_hasher::hash;
8
+
9
+ /// Version identifier returned by `lz_receive_types_info` for the V2 flow.
10
+ pub const LZ_RECEIVE_TYPES_VERSION: u8 = 2;
11
+
12
+ /// Payload returned from `lz_receive_types_info` when the version is V2.
13
+ ///
14
+ /// The executor uses these accounts to construct the follow-up
15
+ /// `lz_receive_types_v2` call.
16
+ #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
17
+ pub struct LzReceiveTypesV2Accounts {
18
+ pub accounts: Vec<Pubkey>,
19
+ }
20
+
21
+ /// Output of the lz_receive_types_v2 instruction.
22
+ ///
23
+ /// This structure enables the multi-instruction execution model where OApps can
24
+ /// define multiple instructions to be executed atomically by the Executor.
25
+ /// The Executor constructs a single transaction containing all returned instructions.
26
+ #[derive(AnchorSerialize, AnchorDeserialize, Clone)]
27
+ pub struct LzReceiveTypesV2Result {
28
+ /// The version of context account
29
+ pub context_version: u8,
30
+ /// ALTs required for this execution context
31
+ /// Used by the Executor to resolve AltIndex references in AccountMetaRef
32
+ /// Enables efficient account list compression for complex transactions
33
+ pub alts: Vec<Pubkey>,
34
+ /// The complete list of instructions required for LzReceive execution
35
+ /// MUST include exactly one LzReceive instruction
36
+ /// MAY include additional Standard instructions for preprocessing/postprocessing
37
+ /// Instructions are executed in the order returned
38
+ pub instructions: Vec<Instruction>,
39
+ }
40
+
41
+ /// The list of instructions that can be executed in the LzReceive transaction.
42
+ ///
43
+ /// V2's multi-instruction model enables complex patterns such as:
44
+ /// - Preprocessing steps before lz_receive (e.g., account initialization)
45
+ /// - Postprocessing steps after lz_receive (e.g., verification, cleanup)
46
+ /// - ABA messaging patterns with additional LayerZero sends
47
+ /// - Conditional execution flows based on message content
48
+ #[derive(AnchorSerialize, AnchorDeserialize, Clone)]
49
+ pub enum Instruction {
50
+ /// The main LzReceive instruction (exactly one required per transaction)
51
+ /// This instruction processes the incoming cross-chain message
52
+ LzReceive {
53
+ /// Account list for the lz_receive instruction
54
+ /// Uses AddressLocator for flexible address resolution
55
+ accounts: Vec<AccountMetaRef>,
56
+ },
57
+ /// Arbitrary custom instruction for preprocessing/postprocessing
58
+ /// Enables OApps to implement complex execution flows
59
+ Standard {
60
+ /// Target program ID for the custom instruction
61
+ program_id: Pubkey,
62
+ /// Account list for the custom instruction
63
+ /// Uses same AddressLocator system as LzReceive
64
+ accounts: Vec<AccountMetaRef>,
65
+ /// Instruction data payload
66
+ /// Raw bytes containing the instruction's parameters
67
+ data: Vec<u8>,
68
+ },
69
+ }
70
+
71
+ /// V2 version of get_accounts_for_clear that returns AccountMetaRef
72
+ pub fn get_accounts_for_clear(
73
+ endpoint_program: Pubkey,
74
+ receiver: &Pubkey,
75
+ src_eid: u32,
76
+ sender: &[u8; 32],
77
+ nonce: u64,
78
+ ) -> Vec<AccountMetaRef> {
79
+ let (nonce_account, _) = Pubkey::find_program_address(
80
+ &[NONCE_SEED, &receiver.to_bytes(), &src_eid.to_be_bytes(), sender],
81
+ &endpoint_program,
82
+ );
83
+
84
+ let (payload_hash_account, _) = Pubkey::find_program_address(
85
+ &[
86
+ PAYLOAD_HASH_SEED,
87
+ &receiver.to_bytes(),
88
+ &src_eid.to_be_bytes(),
89
+ sender,
90
+ &nonce.to_be_bytes(),
91
+ ],
92
+ &endpoint_program,
93
+ );
94
+
95
+ let (oapp_registry_account, _) =
96
+ Pubkey::find_program_address(&[OAPP_SEED, &receiver.to_bytes()], &endpoint_program);
97
+ let (event_authority_account, _) =
98
+ Pubkey::find_program_address(&[EVENT_SEED], &endpoint_program);
99
+ let (endpoint_settings_account, _) =
100
+ Pubkey::find_program_address(&[ENDPOINT_SEED], &endpoint_program);
101
+
102
+ vec![
103
+ AccountMetaRef { pubkey: endpoint_program.into(), is_writable: false },
104
+ AccountMetaRef { pubkey: (*receiver).into(), is_writable: false },
105
+ AccountMetaRef { pubkey: oapp_registry_account.into(), is_writable: false },
106
+ AccountMetaRef { pubkey: nonce_account.into(), is_writable: false },
107
+ AccountMetaRef { pubkey: payload_hash_account.into(), is_writable: true },
108
+ AccountMetaRef { pubkey: endpoint_settings_account.into(), is_writable: true },
109
+ AccountMetaRef { pubkey: event_authority_account.into(), is_writable: false },
110
+ AccountMetaRef { pubkey: endpoint_program.into(), is_writable: false },
111
+ ]
112
+ }
113
+
114
+ /// V2 version of get_accounts_for_send_compose that returns AccountMetaRef
115
+ pub fn get_accounts_for_send_compose(
116
+ endpoint_program: Pubkey,
117
+ from: &Pubkey,
118
+ to: &Pubkey,
119
+ guid: &[u8; 32],
120
+ index: u16,
121
+ composed_message: &[u8],
122
+ ) -> Vec<AccountMetaRef> {
123
+ let (composed_message_account, _) = Pubkey::find_program_address(
124
+ &[
125
+ COMPOSED_MESSAGE_HASH_SEED,
126
+ &from.to_bytes(),
127
+ &to.to_bytes(),
128
+ &guid[..],
129
+ &index.to_be_bytes(),
130
+ &hash(composed_message).to_bytes(),
131
+ ],
132
+ &endpoint_program,
133
+ );
134
+
135
+ let (event_authority_account, _) =
136
+ Pubkey::find_program_address(&[EVENT_SEED], &endpoint_program);
137
+
138
+ vec![
139
+ AccountMetaRef { pubkey: endpoint_program.into(), is_writable: false },
140
+ AccountMetaRef { pubkey: (*from).into(), is_writable: false },
141
+ AccountMetaRef { pubkey: AddressLocator::Payer, is_writable: true },
142
+ AccountMetaRef { pubkey: composed_message_account.into(), is_writable: true },
143
+ AccountMetaRef { pubkey: SYSTEM_ID.into(), is_writable: false },
144
+ AccountMetaRef { pubkey: event_authority_account.into(), is_writable: false },
145
+ AccountMetaRef { pubkey: endpoint_program.into(), is_writable: false },
146
+ ]
147
+ }
@@ -0,0 +1,278 @@
1
+ use anchor_lang::{
2
+ prelude::{borsh, AccountInfo, AnchorDeserialize, AnchorSerialize, InitSpace},
3
+ Space,
4
+ };
5
+ use anchor_spl::token_interface::spl_token_metadata_interface::solana_borsh;
6
+ use spl_discriminator::SplDiscriminate;
7
+ use spl_type_length_value::{
8
+ state::TlvStateMut, variable_len_pack::VariableLenPack, SplBorshVariableLenPack,
9
+ };
10
+
11
+ use crate::errors::OAppError;
12
+
13
+ /// TLV entry header size in bytes: 8-byte type discriminator + 4-byte u32 length.
14
+ pub const TLV_HEADER_BYTES: usize = 12;
15
+
16
+ /// Total on-wire size of a TLV entry: header overhead plus the Borsh-serialized
17
+ /// value bytes reported by `Space::INIT_SPACE` (auto-derived via `InitSpace`).
18
+ ///
19
+ /// Blanket-implemented for every `Space + SplDiscriminate` type, so any struct
20
+ /// that derives `InitSpace` and `SplDiscriminate` gets `TLV_SPACE` for free —
21
+ /// use it when sizing the containing Anchor account.
22
+ pub trait TlvSpace {
23
+ const TLV_SPACE: usize;
24
+ }
25
+
26
+ impl<T: Space + SplDiscriminate> TlvSpace for T {
27
+ const TLV_SPACE: usize = TLV_HEADER_BYTES + <T as Space>::INIT_SPACE;
28
+ }
29
+
30
+ /// Borsh schema version of `OAppBase`. See `docs/oapp-info.md` § `schema_version` Bump Rules.
31
+ pub const OAPP_BASE_SCHEMA_VERSION: u8 = 1;
32
+
33
+ /// IDL version (instructions + events) for this build. See `docs/oapp-info.md` § `idl_version`
34
+ /// Bump Rules.
35
+ pub const OAPP_BASE_IDL_VERSION: IdlVersion = IdlVersion { major: 1, minor: 0 };
36
+
37
+ /// Sentinel: no app-layer TLV entry follows `OAppBase`.
38
+ pub const NO_APP_DISCRIMINATOR: [u8; 8] = [0u8; 8];
39
+
40
+ #[derive(Clone, Copy, AnchorSerialize, AnchorDeserialize, InitSpace, PartialEq, Eq, Debug)]
41
+ pub struct IdlVersion {
42
+ pub major: u8,
43
+ pub minor: u8,
44
+ }
45
+
46
+ /// TLV entry type for the OApp framework layer. Written via `init_oapp_base!`.
47
+ #[derive(
48
+ Clone, AnchorSerialize, AnchorDeserialize, InitSpace, SplDiscriminate, SplBorshVariableLenPack,
49
+ )]
50
+ #[discriminator_hash_input("oapp::base")]
51
+ pub struct OAppBase {
52
+ /// Borsh schema version of this entry. Bumps when fields are added, removed, reordered, or
53
+ /// change type. The SDK reads this first to select the correct parser for the remaining bytes.
54
+ pub schema_version: u8,
55
+
56
+ /// IDL version (instructions + events). Bumps independently of `schema_version`.
57
+ pub idl_version: IdlVersion,
58
+
59
+ /// Discriminator of the sibling app-layer TLV entry — the SDK reads this to find the app
60
+ /// entry. Set to the entry type's `SplDiscriminate::SPL_DISCRIMINATOR`
61
+ /// (e.g., `OftInfo::SPL_DISCRIMINATOR`), or `NO_APP_DISCRIMINATOR` when no app entry is
62
+ /// present.
63
+ pub app_discriminator: [u8; 8],
64
+ }
65
+
66
+ /// Allocates and writes a new TLV entry into an OAppInfo account's data buffer.
67
+ /// Call this once per entry type during account initialization. Fails if the
68
+ /// type already exists.
69
+ ///
70
+ /// **Precondition:** the account must be freshly allocated and zero-filled in
71
+ /// the same instruction — typically via Anchor `#[account(init, ...)]`. Calling
72
+ /// on a reused or partially-written account can leave undetected residual TLV
73
+ /// entries beyond the ones written here, since the underlying TLV parser stops
74
+ /// at the first all-zero discriminator and does not scan trailing bytes.
75
+ pub fn init_tlv_entry<T: SplDiscriminate + VariableLenPack>(
76
+ info: &AccountInfo,
77
+ entry: &T,
78
+ ) -> anchor_lang::Result<()> {
79
+ let mut raw = info.try_borrow_mut_data()?;
80
+ let mut state = TlvStateMut::unpack(&mut raw[..])?;
81
+ state.alloc_and_pack_variable_len_entry(entry, false)?;
82
+ Ok(())
83
+ }
84
+
85
+ /// Overwrites an existing TLV entry in-place. Returns `TlvLengthMismatch` if
86
+ /// the new value's packed length differs from the existing entry's.
87
+ pub fn update_tlv_entry<T: SplDiscriminate + VariableLenPack>(
88
+ info: &AccountInfo,
89
+ entry: &T,
90
+ ) -> anchor_lang::Result<()> {
91
+ let new_len = entry.get_packed_len()?;
92
+ let mut raw = info.try_borrow_mut_data()?;
93
+ let mut state = TlvStateMut::unpack(&mut raw[..])?;
94
+
95
+ let slot = state.get_first_bytes_mut::<T>()?;
96
+ anchor_lang::require!(new_len == slot.len(), OAppError::TlvLengthMismatch);
97
+ entry.pack(slot)?;
98
+ Ok(())
99
+ }
100
+
101
+ /// Initializes an OAppInfo account by writing the framework `OAppBase` TLV
102
+ /// entry.
103
+ ///
104
+ /// Anchor manages account lifecycle (init, seeds, bump); TLV occupies the full
105
+ /// account data from byte 0 (no Anchor discriminator).
106
+ ///
107
+ /// ```ignore
108
+ /// // Framework entry only — generic OApp, no app-layer sibling.
109
+ /// oapp::init_oapp_base!(&oapp_info);
110
+ ///
111
+ /// // Framework entry pointing to an app-layer TLV entry type.
112
+ /// oapp::init_oapp_base!(&oapp_info, MyAppInfo);
113
+ /// oapp::oapp_info::init_tlv_entry(&oapp_info, &my_app_info)?;
114
+ /// ```
115
+ ///
116
+ /// Passing an app-layer type sets `OAppBase.app_discriminator` only; it does not write that
117
+ /// app-layer TLV entry. The program's own initialization helper must call `init_tlv_entry` for
118
+ /// the referenced entry in the same initialize path, so `OAppBase` does not advertise a missing
119
+ /// sibling entry.
120
+ #[macro_export]
121
+ macro_rules! init_oapp_base {
122
+ // init_oapp_base!(&oapp_info)
123
+ ($oapp_info:expr $(,)?) => {{
124
+ $crate::init_oapp_base!(@inner $oapp_info, $crate::oapp_info::NO_APP_DISCRIMINATOR)
125
+ }};
126
+
127
+ // init_oapp_base!(&oapp_info, AppType)
128
+ ($oapp_info:expr, $app_type:ty $(,)?) => {{
129
+ $crate::init_oapp_base!(
130
+ @inner $oapp_info,
131
+ <[u8; 8]>::from(<$app_type as ::spl_discriminator::SplDiscriminate>::SPL_DISCRIMINATOR)
132
+ )
133
+ }};
134
+
135
+ // Single implementation — not part of the public API.
136
+ (@inner $oapp_info:expr, $disc:expr) => {{
137
+ let _entry = $crate::oapp_info::OAppBase {
138
+ schema_version: $crate::oapp_info::OAPP_BASE_SCHEMA_VERSION,
139
+ idl_version: $crate::oapp_info::OAPP_BASE_IDL_VERSION,
140
+ app_discriminator: $disc,
141
+ };
142
+ $crate::oapp_info::init_tlv_entry($oapp_info, &_entry)?;
143
+ }};
144
+ }
145
+
146
+ #[cfg(test)]
147
+ mod tests {
148
+ use super::*;
149
+ use anchor_lang::prelude::Pubkey;
150
+ use spl_type_length_value::state::{TlvState, TlvStateBorrowed, TlvStateMut};
151
+
152
+ fn write_entry(entry: OAppBase) -> Vec<u8> {
153
+ let mut buf = vec![0u8; OAppBase::TLV_SPACE];
154
+ let mut state = TlvStateMut::unpack(&mut buf[..]).unwrap();
155
+ let len = entry.get_packed_len().unwrap();
156
+ state.alloc::<OAppBase>(len, false).unwrap();
157
+ state.pack_first_variable_len_value(&entry).unwrap();
158
+ buf
159
+ }
160
+
161
+ fn read_entry(buf: &[u8]) -> OAppBase {
162
+ let state = TlvStateBorrowed::unpack(buf).unwrap();
163
+ state.get_first_variable_len_value::<OAppBase>().unwrap()
164
+ }
165
+
166
+ #[derive(
167
+ Clone,
168
+ Debug,
169
+ PartialEq,
170
+ Eq,
171
+ AnchorSerialize,
172
+ AnchorDeserialize,
173
+ InitSpace,
174
+ SplDiscriminate,
175
+ SplBorshVariableLenPack,
176
+ )]
177
+ #[discriminator_hash_input("oapp::tests::variable-info")]
178
+ struct VariableInfo {
179
+ #[max_len(16)]
180
+ bytes: Vec<u8>,
181
+ }
182
+
183
+ impl VariableInfo {
184
+ fn new(bytes: &[u8]) -> Self {
185
+ Self { bytes: bytes.to_vec() }
186
+ }
187
+ }
188
+
189
+ fn test_account_info<'a>(
190
+ key: &'a Pubkey,
191
+ owner: &'a Pubkey,
192
+ lamports: &'a mut u64,
193
+ data: &'a mut [u8],
194
+ ) -> AccountInfo<'a> {
195
+ AccountInfo::new(key, false, true, lamports, data, owner, false)
196
+ }
197
+
198
+ fn read_variable_info(info: &AccountInfo) -> VariableInfo {
199
+ let raw = info.try_borrow_data().unwrap();
200
+ let state = TlvStateBorrowed::unpack(&raw[..]).unwrap();
201
+ state.get_first_variable_len_value::<VariableInfo>().unwrap()
202
+ }
203
+
204
+ #[test]
205
+ fn entry_packed_len_matches_tlv_space() {
206
+ let entry = OAppBase {
207
+ schema_version: OAPP_BASE_SCHEMA_VERSION,
208
+ idl_version: OAPP_BASE_IDL_VERSION,
209
+ app_discriminator: NO_APP_DISCRIMINATOR,
210
+ };
211
+ // get_packed_len and INIT_SPACE both report value bytes only; TLV_SPACE adds framing.
212
+ assert_eq!(entry.get_packed_len().unwrap(), OAppBase::INIT_SPACE);
213
+ assert_eq!(entry.get_packed_len().unwrap() + TLV_HEADER_BYTES, OAppBase::TLV_SPACE);
214
+ }
215
+
216
+ #[test]
217
+ fn tlv_round_trip_default_versions() {
218
+ let entry = OAppBase {
219
+ schema_version: OAPP_BASE_SCHEMA_VERSION,
220
+ idl_version: OAPP_BASE_IDL_VERSION,
221
+ app_discriminator: NO_APP_DISCRIMINATOR,
222
+ };
223
+ let buf = write_entry(entry);
224
+ let out = read_entry(&buf);
225
+
226
+ assert_eq!(out.schema_version, OAPP_BASE_SCHEMA_VERSION);
227
+ assert_eq!(out.idl_version, OAPP_BASE_IDL_VERSION);
228
+ assert_eq!(out.app_discriminator, NO_APP_DISCRIMINATOR);
229
+ }
230
+
231
+ #[test]
232
+ fn tlv_round_trip_with_app_discriminator() {
233
+ let disc = [1, 2, 3, 4, 5, 6, 7, 8];
234
+ let entry = OAppBase {
235
+ schema_version: OAPP_BASE_SCHEMA_VERSION,
236
+ idl_version: OAPP_BASE_IDL_VERSION,
237
+ app_discriminator: disc,
238
+ };
239
+ let buf = write_entry(entry);
240
+ let out = read_entry(&buf);
241
+
242
+ assert_eq!(out.app_discriminator, disc);
243
+ assert_eq!(out.schema_version, OAPP_BASE_SCHEMA_VERSION);
244
+ assert_eq!(out.idl_version, OAPP_BASE_IDL_VERSION);
245
+ }
246
+
247
+ #[test]
248
+ fn update_tlv_entry_rejects_longer_value() {
249
+ let key = Pubkey::new_unique();
250
+ let owner = Pubkey::new_unique();
251
+ let mut lamports = 1;
252
+ let mut data = vec![0u8; VariableInfo::TLV_SPACE];
253
+ let info = test_account_info(&key, &owner, &mut lamports, &mut data);
254
+
255
+ let original = VariableInfo::new(&[1, 2, 3]);
256
+ init_tlv_entry(&info, &original).unwrap();
257
+
258
+ let err = update_tlv_entry(&info, &VariableInfo::new(&[1, 2, 3, 4])).unwrap_err();
259
+ assert_eq!(err, OAppError::TlvLengthMismatch.into());
260
+ assert_eq!(read_variable_info(&info), original);
261
+ }
262
+
263
+ #[test]
264
+ fn update_tlv_entry_rejects_shorter_value() {
265
+ let key = Pubkey::new_unique();
266
+ let owner = Pubkey::new_unique();
267
+ let mut lamports = 1;
268
+ let mut data = vec![0u8; VariableInfo::TLV_SPACE];
269
+ let info = test_account_info(&key, &owner, &mut lamports, &mut data);
270
+
271
+ let original = VariableInfo::new(&[1, 2, 3, 4]);
272
+ init_tlv_entry(&info, &original).unwrap();
273
+
274
+ let err = update_tlv_entry(&info, &VariableInfo::new(&[1, 2, 3])).unwrap_err();
275
+ assert_eq!(err, OAppError::TlvLengthMismatch.into());
276
+ assert_eq!(read_variable_info(&info), original);
277
+ }
278
+ }
package/src/options.rs ADDED
@@ -0,0 +1,147 @@
1
+ use crate::errors::OAppError;
2
+ use anchor_lang::prelude::*;
3
+
4
+ pub const OPTIONS_TYPE_3: u16 = 3;
5
+
6
+ pub fn combine_options(mut enforced_options: Vec<u8>, extra_options: &[u8]) -> Result<Vec<u8>> {
7
+ // No enforced options, pass whatever the caller supplied, even if it's empty or legacy type
8
+ // 1/2 options.
9
+ if enforced_options.is_empty() {
10
+ return Ok(extra_options.to_vec());
11
+ }
12
+
13
+ // No caller options, return enforced
14
+ if extra_options.is_empty() {
15
+ return Ok(enforced_options);
16
+ }
17
+
18
+ // If caller provided extra_options, must be type 3 as it's the ONLY type that can be
19
+ // combined.
20
+ if extra_options.len() >= 2 {
21
+ assert_type_3(extra_options)?;
22
+ // Remove the first 2 bytes containing the type from the extra_options and combine with
23
+ // enforced.
24
+ enforced_options.extend_from_slice(&extra_options[2..]);
25
+ return Ok(enforced_options);
26
+ }
27
+
28
+ // No valid set of options was found.
29
+ Err(OAppError::InvalidOptions.into())
30
+ }
31
+
32
+ pub fn assert_type_3(options: &[u8]) -> anchor_lang::Result<()> {
33
+ require!(options.len() >= 2, OAppError::InvalidOptionsLength);
34
+ let mut option_type_bytes = [0; 2];
35
+ option_type_bytes.copy_from_slice(&options[0..2]);
36
+ require!(u16::from_be_bytes(option_type_bytes) == OPTIONS_TYPE_3, OAppError::InvalidOptions);
37
+ Ok(())
38
+ }
39
+
40
+ #[cfg(test)]
41
+ mod tests {
42
+ use super::*;
43
+
44
+ fn type3_header() -> [u8; 2] {
45
+ OPTIONS_TYPE_3.to_be_bytes()
46
+ }
47
+
48
+ // -----------------------------------------------------------------------
49
+ // assert_type_3
50
+ // -----------------------------------------------------------------------
51
+
52
+ #[test]
53
+ fn assert_type_3_valid() {
54
+ assert!(assert_type_3(&type3_header()).is_ok());
55
+ }
56
+
57
+ #[test]
58
+ fn assert_type_3_empty_returns_invalid_length() {
59
+ let err = assert_type_3(&[]).unwrap_err();
60
+ assert_eq!(err, OAppError::InvalidOptionsLength.into());
61
+ }
62
+
63
+ #[test]
64
+ fn assert_type_3_one_byte_returns_invalid_length() {
65
+ let err = assert_type_3(&[0x00]).unwrap_err();
66
+ assert_eq!(err, OAppError::InvalidOptionsLength.into());
67
+ }
68
+
69
+ #[test]
70
+ fn assert_type_3_wrong_type_returns_invalid_options() {
71
+ let err = assert_type_3(&[0x00, 0x01]).unwrap_err();
72
+ assert_eq!(err, OAppError::InvalidOptions.into());
73
+ }
74
+
75
+ // -----------------------------------------------------------------------
76
+ // combine_options — enforced empty
77
+ // -----------------------------------------------------------------------
78
+
79
+ #[test]
80
+ fn combine_no_enforced_returns_extra_as_is() {
81
+ let result = combine_options(vec![], &[0x01, 0x02, 0x03]).unwrap();
82
+ assert_eq!(result, vec![0x01, 0x02, 0x03]);
83
+ }
84
+
85
+ #[test]
86
+ fn combine_no_enforced_extra_also_empty_returns_empty() {
87
+ let result = combine_options(vec![], &[]).unwrap();
88
+ assert_eq!(result, Vec::<u8>::new());
89
+ }
90
+
91
+ // -----------------------------------------------------------------------
92
+ // combine_options — extra empty
93
+ // -----------------------------------------------------------------------
94
+
95
+ #[test]
96
+ fn combine_enforced_present_extra_empty_returns_enforced() {
97
+ let enforced = vec![0x00, 0x03, 0xFF];
98
+ let result = combine_options(enforced.clone(), &[]).unwrap();
99
+ assert_eq!(result, enforced);
100
+ }
101
+
102
+ // -----------------------------------------------------------------------
103
+ // combine_options — both present
104
+ // -----------------------------------------------------------------------
105
+
106
+ #[test]
107
+ fn combine_both_present_type3_strips_header_and_appends() {
108
+ let enforced = vec![0x00, 0x03, 0xAA, 0xBB];
109
+ let [h0, h1] = type3_header();
110
+ let extra = vec![h0, h1, 0xCC, 0xDD];
111
+
112
+ let result = combine_options(enforced.clone(), &extra).unwrap();
113
+
114
+ let mut expected = enforced;
115
+ expected.extend_from_slice(&[0xCC, 0xDD]);
116
+ assert_eq!(result, expected);
117
+ }
118
+
119
+ #[test]
120
+ fn combine_both_present_type3_only_header_no_payload() {
121
+ let enforced = vec![0xAA];
122
+ let [h0, h1] = type3_header();
123
+ let extra = vec![h0, h1]; // type-3 header, zero extra payload
124
+
125
+ let result = combine_options(enforced.clone(), &extra).unwrap();
126
+ assert_eq!(result, enforced); // nothing appended beyond header
127
+ }
128
+
129
+ #[test]
130
+ fn combine_both_present_non_type3_returns_invalid_options() {
131
+ let enforced = vec![0x00, 0x03, 0xAA];
132
+ let extra = vec![0x00, 0x01, 0xBB]; // type 1 — not allowed when enforced exists
133
+
134
+ let err = combine_options(enforced, &extra).unwrap_err();
135
+ assert_eq!(err, OAppError::InvalidOptions.into());
136
+ }
137
+
138
+ #[test]
139
+ fn combine_both_present_single_byte_extra_returns_invalid_options() {
140
+ // extra_options.len() == 1 → falls through to the final Err
141
+ let enforced = vec![0xAA];
142
+ let extra = vec![0x03]; // only 1 byte
143
+
144
+ let err = combine_options(enforced, &extra).unwrap_err();
145
+ assert_eq!(err, OAppError::InvalidOptions.into());
146
+ }
147
+ }
package/src/types.rs ADDED
@@ -0,0 +1,114 @@
1
+ //! Standard LayerZero OApp types.
2
+ //!
3
+ //! Canonical parameter and return structs for OApp instructions that match
4
+ //! the LayerZero protocol interface. They don't change between implementations.
5
+
6
+ use anchor_lang::prelude::*;
7
+
8
+ /// Params for the `set_peer` instruction.
9
+ ///
10
+ /// Configures a trusted remote peer address for a specific chain, enabling
11
+ /// cross-chain communication.
12
+ #[derive(AnchorSerialize, AnchorDeserialize, Clone)]
13
+ pub struct SetPeerParams {
14
+ /// The endpoint ID of the remote chain (e.g., Ethereum mainnet, Arbitrum).
15
+ pub eid: u32,
16
+ /// The 32-byte address of the trusted peer on the remote chain.
17
+ pub peer: [u8; 32],
18
+ }
19
+
20
+ /// Params for the `set_enforced_options` instruction.
21
+ ///
22
+ /// Sets mandatory execution options (e.g., gas limits)
23
+ /// for a specific message type to a specific destination chain.
24
+ #[derive(AnchorSerialize, AnchorDeserialize, Clone)]
25
+ pub struct SetEnforcedOptionsParams {
26
+ /// The endpoint ID of the destination chain.
27
+ pub eid: u32,
28
+ /// The message type identifier (oapp-defined).
29
+ pub msg_type: u16,
30
+ /// The encoded execution options to enforce for this destination/message type pair.
31
+ pub options: Vec<u8>,
32
+ }
33
+
34
+ /// Params for the `get_enforced_options` instruction (read-only view method).
35
+ ///
36
+ /// Queries the stored enforced options for a given destination chain and
37
+ /// message type.
38
+ #[derive(AnchorSerialize, AnchorDeserialize, Clone)]
39
+ pub struct GetEnforcedOptionsParams {
40
+ /// The endpoint ID of the destination chain to query.
41
+ pub eid: u32,
42
+ /// The message type identifier to query.
43
+ pub msg_type: u16,
44
+ }
45
+
46
+ /// Params for the `lz_receive` instruction.
47
+ ///
48
+ /// Delivered by the LayerZero executor after message verification is complete.
49
+ #[derive(AnchorSerialize, AnchorDeserialize, Clone)]
50
+ pub struct LzReceiveParams {
51
+ /// The endpoint ID of the source chain that sent the message.
52
+ pub src_eid: u32,
53
+ /// The 32-byte address of the sender on the source chain.
54
+ pub sender: [u8; 32],
55
+ /// The message sequence number.
56
+ pub nonce: u64,
57
+ /// The globally unique identifier for this cross-chain message.
58
+ pub guid: [u8; 32],
59
+ /// The encoded message payload (oapp-defined).
60
+ pub message: Vec<u8>,
61
+ /// Additional executor-provided data for the receive transaction.
62
+ pub extra_data: Vec<u8>,
63
+ }
64
+
65
+ /// Params for the `lz_compose` instruction.
66
+ ///
67
+ /// Enables multi-step cross-chain execution (the compose flow).
68
+ #[derive(AnchorSerialize, AnchorDeserialize, Clone)]
69
+ pub struct LzComposeParams {
70
+ /// The OApp receiver address that sent the compose message.
71
+ pub from: Pubkey,
72
+ /// The composer address that will receive the message.
73
+ pub to: Pubkey,
74
+ /// The globally unique identifier of the original cross-chain message.
75
+ pub guid: [u8; 32],
76
+ /// The index of this composed message (supports multiple composes per message).
77
+ pub index: u16,
78
+ /// The encoded composed message payload.
79
+ pub message: Vec<u8>,
80
+ /// Additional executor-provided data for the compose transaction.
81
+ pub extra_data: Vec<u8>,
82
+ }
83
+
84
+ /// Params for the `next_nonce` instruction (read-only view method).
85
+ ///
86
+ /// Queries the next expected nonce for ordered message delivery from a
87
+ /// specific source chain and sender. The default implementation returns 0
88
+ /// (no ordering enforcement); OApps can override to enforce sequencing.
89
+ #[derive(AnchorSerialize, AnchorDeserialize, Clone)]
90
+ pub struct NextNonceParams {
91
+ /// The endpoint ID of the source chain.
92
+ pub src_eid: u32,
93
+ /// The 32-byte address of the remote sender.
94
+ pub sender: [u8; 32],
95
+ }
96
+
97
+ /// Params for the `is_compose_msg_sender` instruction (read-only view method).
98
+ ///
99
+ /// Validates whether an account is authorized to send composed messages for a
100
+ /// given cross-chain message. The default implementation only permits the OApp
101
+ /// itself; OApps can override for custom authorization logic. Returns `bool`.
102
+ #[derive(AnchorSerialize, AnchorDeserialize, Clone)]
103
+ pub struct IsComposeMsgSenderParams {
104
+ /// The endpoint ID of the source chain.
105
+ pub src_eid: u32,
106
+ /// The 32-byte address of the remote sender.
107
+ pub sender: [u8; 32],
108
+ /// The message sequence number.
109
+ pub nonce: u64,
110
+ /// The encoded message payload for context.
111
+ pub message: Vec<u8>,
112
+ /// The account claiming authorization to send the compose message.
113
+ pub compose_sender: Pubkey,
114
+ }