@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.
- package/Anchor.toml +23 -0
- package/Cargo.toml +26 -0
- package/README.md +181 -0
- package/idls/endpoint.json +3520 -0
- package/macros/Cargo.toml +23 -0
- package/macros/src/composer.rs +43 -0
- package/macros/src/instructions/composer/lz_compose.rs +17 -0
- package/macros/src/instructions/composer/lz_compose_types_info.rs +29 -0
- package/macros/src/instructions/composer/lz_compose_types_v2.rs +17 -0
- package/macros/src/instructions/composer/mod.rs +23 -0
- package/macros/src/instructions/mod.rs +4 -0
- package/macros/src/instructions/oapp/accept_default_admin_transfer.rs +50 -0
- package/macros/src/instructions/oapp/get_enforced_options.rs +57 -0
- package/macros/src/instructions/oapp/get_peer.rs +55 -0
- package/macros/src/instructions/oapp/init_enforced_options.rs +87 -0
- package/macros/src/instructions/oapp/is_compose_msg_sender.rs +44 -0
- package/macros/src/instructions/oapp/lz_receive.rs +33 -0
- package/macros/src/instructions/oapp/lz_receive_types_info.rs +28 -0
- package/macros/src/instructions/oapp/lz_receive_types_v2.rs +17 -0
- package/macros/src/instructions/oapp/mod.rs +41 -0
- package/macros/src/instructions/oapp/next_nonce.rs +52 -0
- package/macros/src/instructions/oapp/set_enforced_options.rs +101 -0
- package/macros/src/instructions/oapp/set_peer.rs +74 -0
- package/macros/src/lib.rs +81 -0
- package/macros/src/oapp.rs +86 -0
- package/macros/src/state_types.rs +79 -0
- package/package.json +61 -0
- package/rust-toolchain.toml +3 -0
- package/rustfmt.toml +23 -0
- package/src/common.rs +156 -0
- package/src/endpoint_cpi.rs +386 -0
- package/src/errors.rs +16 -0
- package/src/events.rs +26 -0
- package/src/lib.rs +82 -0
- package/src/lz_compose_types_v2.rs +88 -0
- package/src/lz_receive_types_v2.rs +147 -0
- package/src/oapp_info.rs +278 -0
- package/src/options.rs +147 -0
- package/src/types.rs +114 -0
- 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
|
+
}
|
package/src/oapp_info.rs
ADDED
|
@@ -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
|
+
}
|