@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,101 @@
|
|
|
1
|
+
//! Code generator for the `set_enforced_options` instruction.
|
|
2
|
+
//!
|
|
3
|
+
//! Updates an existing EnforcedOptions PDA, resizing the account if the new
|
|
4
|
+
//! options buffer differs in length. The account must already exist (created
|
|
5
|
+
//! by `init_enforced_options`).
|
|
6
|
+
//!
|
|
7
|
+
//! # Rent refund behaviour
|
|
8
|
+
//!
|
|
9
|
+
//! The generated `StdSetEnforcedOptions` struct exposes a separate `payer` field
|
|
10
|
+
//! (distinct from `authority`). When the options buffer shrinks, Anchor's
|
|
11
|
+
//! `realloc::payer` directive returns the freed lamports to the **current
|
|
12
|
+
//! transaction's `payer`**, not to the account that originally funded the PDA
|
|
13
|
+
//! in `init_enforced_options`.
|
|
14
|
+
//!
|
|
15
|
+
//! This is intentional: the instruction is gated by
|
|
16
|
+
//! `#[only_role(default_admin_role)]`, so an authorised admin always controls
|
|
17
|
+
//! which account is supplied as `payer`. Integrators should be aware that rent
|
|
18
|
+
//! refunds on buffer shrink flow to the call-time `payer`, not the original
|
|
19
|
+
//! funder.
|
|
20
|
+
|
|
21
|
+
use super::CodegenContext;
|
|
22
|
+
use anchor_trait::{DefaultAccounts, DefaultImpl, InstructionSpec};
|
|
23
|
+
use quote::{format_ident, quote};
|
|
24
|
+
use syn::parse_quote;
|
|
25
|
+
|
|
26
|
+
pub(super) fn spec() -> InstructionSpec<CodegenContext> {
|
|
27
|
+
InstructionSpec {
|
|
28
|
+
params_type: Some(quote!(::oapp::types::SetEnforcedOptionsParams)),
|
|
29
|
+
return_type: quote!(()),
|
|
30
|
+
is_view: false,
|
|
31
|
+
default_impl: Some(DefaultImpl {
|
|
32
|
+
handler_ident: format_ident!("apply"),
|
|
33
|
+
generator: generate_default_impl,
|
|
34
|
+
}),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn generate_default_impl(
|
|
39
|
+
ctx: &CodegenContext,
|
|
40
|
+
spec: &InstructionSpec<CodegenContext>,
|
|
41
|
+
) -> DefaultAccounts {
|
|
42
|
+
let handler_ident = &spec.default_impl.as_ref().unwrap().handler_ident;
|
|
43
|
+
let params_type = spec.params_type.as_ref().unwrap();
|
|
44
|
+
let return_type = &spec.return_type;
|
|
45
|
+
let oapp_type = &ctx.oapp_type;
|
|
46
|
+
let role_type = &ctx.role_type;
|
|
47
|
+
|
|
48
|
+
DefaultAccounts {
|
|
49
|
+
struct_type: parse_quote! {
|
|
50
|
+
#[::rbac::only_role(role = #role_type::default_admin_role(), state = oapp, authority = authority)]
|
|
51
|
+
#[event_cpi]
|
|
52
|
+
#[derive(Accounts)]
|
|
53
|
+
#[instruction(params: #params_type)]
|
|
54
|
+
pub struct StdSetEnforcedOptions<'info> {
|
|
55
|
+
pub authority: Signer<'info>,
|
|
56
|
+
|
|
57
|
+
#[account(mut)]
|
|
58
|
+
pub payer: Signer<'info>,
|
|
59
|
+
|
|
60
|
+
pub oapp: Account<'info, #oapp_type>,
|
|
61
|
+
|
|
62
|
+
#[account(
|
|
63
|
+
mut,
|
|
64
|
+
realloc = EnforcedOptions::space(params.options.len()),
|
|
65
|
+
realloc::payer = payer,
|
|
66
|
+
realloc::zero = true,
|
|
67
|
+
seeds = [
|
|
68
|
+
::oapp::ENFORCED_OPTIONS_SEED,
|
|
69
|
+
oapp.key().as_ref(),
|
|
70
|
+
¶ms.eid.to_be_bytes(),
|
|
71
|
+
¶ms.msg_type.to_be_bytes(),
|
|
72
|
+
],
|
|
73
|
+
bump = enforced_options.bump,
|
|
74
|
+
)]
|
|
75
|
+
pub enforced_options: Account<'info, EnforcedOptions>,
|
|
76
|
+
|
|
77
|
+
pub system_program: Program<'info, System>,
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
impl_block: parse_quote! {
|
|
81
|
+
impl StdSetEnforcedOptions<'_> {
|
|
82
|
+
pub fn #handler_ident(ctx: &mut Context<Self>, params: &#params_type) -> Result<#return_type> {
|
|
83
|
+
if !params.options.is_empty() {
|
|
84
|
+
::oapp::options::assert_type_3(¶ms.options)?;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
ctx.accounts.enforced_options.options = params.options.clone();
|
|
88
|
+
|
|
89
|
+
emit_cpi!(::oapp::events::EnforcedOptionsSet {
|
|
90
|
+
oapp: ctx.accounts.oapp.key(),
|
|
91
|
+
eid: params.eid,
|
|
92
|
+
msg_type: params.msg_type,
|
|
93
|
+
options: params.options.clone(),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
Ok(())
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
//! Code generator for the `set_peer` instruction.
|
|
2
|
+
|
|
3
|
+
use super::CodegenContext;
|
|
4
|
+
use anchor_trait::{DefaultAccounts, DefaultImpl, InstructionSpec};
|
|
5
|
+
use quote::{format_ident, quote};
|
|
6
|
+
use syn::parse_quote;
|
|
7
|
+
|
|
8
|
+
pub(super) fn spec() -> InstructionSpec<CodegenContext> {
|
|
9
|
+
InstructionSpec {
|
|
10
|
+
params_type: Some(quote!(::oapp::types::SetPeerParams)),
|
|
11
|
+
return_type: quote!(()),
|
|
12
|
+
is_view: false,
|
|
13
|
+
default_impl: Some(DefaultImpl {
|
|
14
|
+
handler_ident: format_ident!("apply"),
|
|
15
|
+
generator: generate_default_impl,
|
|
16
|
+
}),
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
fn generate_default_impl(
|
|
21
|
+
ctx: &CodegenContext,
|
|
22
|
+
spec: &InstructionSpec<CodegenContext>,
|
|
23
|
+
) -> DefaultAccounts {
|
|
24
|
+
let handler_ident = &spec.default_impl.as_ref().unwrap().handler_ident;
|
|
25
|
+
let params_type = spec.params_type.as_ref().unwrap();
|
|
26
|
+
let return_type = &spec.return_type;
|
|
27
|
+
let oapp_type = &ctx.oapp_type;
|
|
28
|
+
let role_type = &ctx.role_type;
|
|
29
|
+
|
|
30
|
+
DefaultAccounts {
|
|
31
|
+
struct_type: parse_quote! {
|
|
32
|
+
#[::rbac::only_role(role = #role_type::default_admin_role(), state = oapp, authority = authority)]
|
|
33
|
+
#[event_cpi]
|
|
34
|
+
#[derive(Accounts)]
|
|
35
|
+
#[instruction(params: #params_type)]
|
|
36
|
+
pub struct StdSetPeer<'info> {
|
|
37
|
+
pub authority: Signer<'info>,
|
|
38
|
+
|
|
39
|
+
#[account(mut)]
|
|
40
|
+
pub payer: Signer<'info>,
|
|
41
|
+
|
|
42
|
+
pub oapp: Account<'info, #oapp_type>,
|
|
43
|
+
|
|
44
|
+
#[account(
|
|
45
|
+
init_if_needed,
|
|
46
|
+
payer = payer,
|
|
47
|
+
space = 8 + OAppPeer::INIT_SPACE,
|
|
48
|
+
seeds = [::oapp::PEER_SEED, oapp.key().as_ref(), ¶ms.eid.to_be_bytes()],
|
|
49
|
+
bump
|
|
50
|
+
)]
|
|
51
|
+
pub peer: Account<'info, OAppPeer>,
|
|
52
|
+
|
|
53
|
+
pub system_program: Program<'info, System>,
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
impl_block: parse_quote! {
|
|
57
|
+
impl StdSetPeer<'_> {
|
|
58
|
+
pub fn #handler_ident(ctx: &mut Context<Self>, params: &#params_type) -> Result<#return_type> {
|
|
59
|
+
let peer = &mut ctx.accounts.peer;
|
|
60
|
+
peer.address = params.peer;
|
|
61
|
+
peer.bump = ctx.bumps.peer;
|
|
62
|
+
|
|
63
|
+
emit_cpi!(::oapp::events::PeerSet {
|
|
64
|
+
oapp: ctx.accounts.oapp.key(),
|
|
65
|
+
eid: params.eid,
|
|
66
|
+
peer: params.peer,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
Ok(())
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
use proc_macro::TokenStream;
|
|
2
|
+
|
|
3
|
+
mod composer;
|
|
4
|
+
mod instructions;
|
|
5
|
+
mod oapp;
|
|
6
|
+
mod state_types;
|
|
7
|
+
|
|
8
|
+
/// Attribute macro for generating OApp program entrypoints.
|
|
9
|
+
///
|
|
10
|
+
/// Uses the `anchor-trait` collect → generate → assemble pipeline to emit all
|
|
11
|
+
/// standard OApp instructions. Instructions with a framework-provided default
|
|
12
|
+
/// (e.g. `set_peer`, `set_enforced_options`) are generated automatically; instructions
|
|
13
|
+
/// without a default (e.g. `lz_receive`) must be supplied by the implementor
|
|
14
|
+
/// via `#[oapp_instruction]`. The `#[program]` attribute is always emitted by
|
|
15
|
+
/// the assembly layer — callers never need to add it manually.
|
|
16
|
+
#[proc_macro_attribute]
|
|
17
|
+
pub fn oapp(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
18
|
+
oapp::expand(attr.into(), item.into())
|
|
19
|
+
.unwrap_or_else(|err| err.to_compile_error())
|
|
20
|
+
.into()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Marker attribute for custom OApp instruction handlers.
|
|
24
|
+
///
|
|
25
|
+
/// Place this on a function inside the `#[oapp]` module to supply a custom
|
|
26
|
+
/// handler for the named instruction. The handler's `ctx` parameter must be
|
|
27
|
+
/// taken **by reference** (`&Context<T>`).
|
|
28
|
+
///
|
|
29
|
+
/// The body of this stub is unreachable: `#[oapp]` consumes the enclosing
|
|
30
|
+
/// module token stream and strips this marker before rustc sees it.
|
|
31
|
+
/// Override logic lives in `oapp::expand`.
|
|
32
|
+
///
|
|
33
|
+
/// # Security: `lz_receive`
|
|
34
|
+
///
|
|
35
|
+
/// When overriding `lz_receive`, the handler MUST verify that
|
|
36
|
+
/// `(params.src_eid, params.sender)` corresponds to a registered peer — the
|
|
37
|
+
/// Endpoint does NOT perform this check. The recommended pattern binds the
|
|
38
|
+
/// `OAppPeer` PDA to `params.src_eid` via `seeds` and enforces
|
|
39
|
+
/// `peer.address == params.sender` declaratively with `constraint`:
|
|
40
|
+
///
|
|
41
|
+
/// ```ignore
|
|
42
|
+
/// #[account(
|
|
43
|
+
/// seeds = [::oapp::PEER_SEED, oapp.key().as_ref(), ¶ms.src_eid.to_be_bytes()],
|
|
44
|
+
/// bump = peer.bump,
|
|
45
|
+
/// constraint = peer.address == params.sender,
|
|
46
|
+
/// )]
|
|
47
|
+
/// pub peer: Account<'info, OAppPeer>,
|
|
48
|
+
/// ```
|
|
49
|
+
#[proc_macro_attribute]
|
|
50
|
+
pub fn oapp_instruction(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
51
|
+
item
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Attribute macro for generating Composer program entrypoints.
|
|
55
|
+
///
|
|
56
|
+
/// This macro generates `lz_compose`, `lz_compose_types_v2`, and
|
|
57
|
+
/// `lz_compose_types_info` instruction entrypoints for programs that implement
|
|
58
|
+
/// the LayerZero composer interface. All three instructions are required —
|
|
59
|
+
/// the implementor must provide custom handlers for each via
|
|
60
|
+
/// `#[composer_instruction]`. The `#[program]` attribute is emitted
|
|
61
|
+
/// automatically by the assembly layer.
|
|
62
|
+
#[proc_macro_attribute]
|
|
63
|
+
pub fn composer(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
64
|
+
composer::expand(attr.into(), item.into())
|
|
65
|
+
.unwrap_or_else(|err| err.to_compile_error())
|
|
66
|
+
.into()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Marker attribute for required Composer instruction handlers.
|
|
70
|
+
///
|
|
71
|
+
/// All three instructions (`lz_compose`, `lz_compose_types_v2`, `lz_compose_types_info`)
|
|
72
|
+
/// must be provided — there are no framework-supplied defaults. Each handler's
|
|
73
|
+
/// `ctx` parameter must be taken **by reference** (`&Context<T>`).
|
|
74
|
+
///
|
|
75
|
+
/// The body of this stub is unreachable: `#[composer]` consumes the enclosing
|
|
76
|
+
/// module token stream and strips this marker before rustc sees it.
|
|
77
|
+
/// Override logic lives in `composer::expand`.
|
|
78
|
+
#[proc_macro_attribute]
|
|
79
|
+
pub fn composer_instruction(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
80
|
+
item
|
|
81
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
//! Top-level `#[oapp]` attribute macro implementation.
|
|
2
|
+
//!
|
|
3
|
+
//! Flow: parse inputs and resolve domain types, then collect → generate → assemble.
|
|
4
|
+
|
|
5
|
+
use crate::{
|
|
6
|
+
instructions::oapp::{accept_default_admin_transfer, CodegenContext, OAppInstructionSet},
|
|
7
|
+
state_types,
|
|
8
|
+
};
|
|
9
|
+
use anchor_trait::{generate_instructions, Assembly, InstructionOverrides, OverrideConfig};
|
|
10
|
+
use darling::{ast::NestedMeta, FromMeta};
|
|
11
|
+
use proc_macro2::TokenStream;
|
|
12
|
+
use quote::{format_ident, quote};
|
|
13
|
+
use syn::ItemMod;
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Attribute parsing
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
#[derive(FromMeta)]
|
|
20
|
+
struct OAppAttr {
|
|
21
|
+
state: syn::Path,
|
|
22
|
+
role_type: syn::Path,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const OVERRIDE_CONFIG: OverrideConfig = OverrideConfig {
|
|
26
|
+
attr_name: "oapp_instruction",
|
|
27
|
+
domain: "OApp",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Entry point
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
pub fn expand(attr: TokenStream, item: TokenStream) -> syn::Result<TokenStream> {
|
|
35
|
+
// Parse inputs
|
|
36
|
+
let attr_args = NestedMeta::parse_meta_list(attr)?;
|
|
37
|
+
let attrs = OAppAttr::from_list(&attr_args)?;
|
|
38
|
+
let module = syn::parse2::<ItemMod>(item)?;
|
|
39
|
+
|
|
40
|
+
// Resolve domain-specific types and token streams
|
|
41
|
+
let state = &attrs.state;
|
|
42
|
+
let role = &attrs.role_type;
|
|
43
|
+
|
|
44
|
+
// 1. Collect overrides
|
|
45
|
+
let overrides = InstructionOverrides::collect(&module, OVERRIDE_CONFIG)?;
|
|
46
|
+
|
|
47
|
+
// 2. Generate instructions
|
|
48
|
+
let codegen_ctx = CodegenContext {
|
|
49
|
+
oapp_type: quote!(#state),
|
|
50
|
+
role_type: quote!(#role),
|
|
51
|
+
};
|
|
52
|
+
let mut instructions = generate_instructions::<OAppInstructionSet>(&codegen_ctx, &overrides)?;
|
|
53
|
+
|
|
54
|
+
// Always inject RBAC accept_default_admin_transfer handler override so the endpoint
|
|
55
|
+
// delegate is automatically synced with the new admin (EVM parity:
|
|
56
|
+
// OAppCoreRBACUpgradeable). Reuses RBAC's StdAcceptDefaultAdminTransfer accounts.
|
|
57
|
+
// This is unconditional — programs cannot override this instruction.
|
|
58
|
+
instructions
|
|
59
|
+
.entrypoints
|
|
60
|
+
.extend(accept_default_admin_transfer::generate());
|
|
61
|
+
|
|
62
|
+
// 3. Assemble output
|
|
63
|
+
let peer_prelude = state_types::generate_peer();
|
|
64
|
+
let enforced_options_prelude = state_types::generate_enforced_options();
|
|
65
|
+
let prelude = {
|
|
66
|
+
quote! {
|
|
67
|
+
#peer_prelude
|
|
68
|
+
#enforced_options_prelude
|
|
69
|
+
|
|
70
|
+
use ::oapp::OAppState as _;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
let extra_mod_attrs = quote! {
|
|
74
|
+
#[::rbac::rbac(state = #state, role_type = #role)]
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
Ok(Assembly {
|
|
78
|
+
prelude,
|
|
79
|
+
overrides,
|
|
80
|
+
instructions,
|
|
81
|
+
extra_mod_attrs,
|
|
82
|
+
source_mod: module,
|
|
83
|
+
extend_macro_name: Some(format_ident!("oapp_extend_accounts")),
|
|
84
|
+
}
|
|
85
|
+
.assemble())
|
|
86
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//! State struct generators for the `#[oapp]` macro.
|
|
2
|
+
//!
|
|
3
|
+
//! Contains token-stream generators for OApp state account types:
|
|
4
|
+
//! - `OAppPeer` — the peer account definition.
|
|
5
|
+
//! - `EnforcedOptions` — the enforced-options account definition.
|
|
6
|
+
|
|
7
|
+
use proc_macro2::TokenStream;
|
|
8
|
+
use quote::quote;
|
|
9
|
+
|
|
10
|
+
/// Generates the `OAppPeer` struct with inherent accessor methods.
|
|
11
|
+
pub fn generate_peer() -> TokenStream {
|
|
12
|
+
quote! {
|
|
13
|
+
#[account]
|
|
14
|
+
#[derive(Default, InitSpace)]
|
|
15
|
+
pub struct OAppPeer {
|
|
16
|
+
pub address: [u8; 32],
|
|
17
|
+
pub bump: u8,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
impl OAppPeer {
|
|
21
|
+
/// Returns the configured peer address, or fails if it is the zero address.
|
|
22
|
+
pub fn get_address(&self) -> ::anchor_lang::prelude::Result<[u8; 32]> {
|
|
23
|
+
::anchor_lang::prelude::require!(
|
|
24
|
+
self.address != [0u8; 32],
|
|
25
|
+
::oapp::OAppError::NoPeer
|
|
26
|
+
);
|
|
27
|
+
Ok(self.address)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// PDA seeds for the per-EID peer.
|
|
31
|
+
/// Seed shape: [PEER_SEED, oapp_key, eid_be4]
|
|
32
|
+
pub fn seeds(oapp: &Pubkey, eid: u32) -> &'static [&'static [u8]] {
|
|
33
|
+
let key_bytes: &'static [u8; 32] = Box::leak(Box::new(oapp.to_bytes()));
|
|
34
|
+
let eid_bytes: &'static [u8; 4] = Box::leak(Box::new(eid.to_be_bytes()));
|
|
35
|
+
Box::leak(Box::new([
|
|
36
|
+
::oapp::PEER_SEED,
|
|
37
|
+
key_bytes.as_slice(),
|
|
38
|
+
eid_bytes.as_slice(),
|
|
39
|
+
]))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Generates the default `EnforcedOptions` account struct.
|
|
46
|
+
pub fn generate_enforced_options() -> TokenStream {
|
|
47
|
+
quote! {
|
|
48
|
+
#[account]
|
|
49
|
+
pub struct EnforcedOptions {
|
|
50
|
+
pub options: Vec<u8>,
|
|
51
|
+
pub bump: u8,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
impl EnforcedOptions {
|
|
55
|
+
/// Returns the total account space required for the given options length.
|
|
56
|
+
///
|
|
57
|
+
/// Borsh serializes fields in declaration order, so the on-disk layout is:
|
|
58
|
+
/// discriminator (8) + vec_len_prefix (4) + options_data + bump (1)
|
|
59
|
+
pub fn space(options_len: usize) -> usize {
|
|
60
|
+
8 + 4 + options_len + 1
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// PDA seeds for the per-EID, per-msg-type enforced-options account.
|
|
64
|
+
/// Seed shape: [ENFORCED_OPTIONS_SEED, oapp_key, eid_be4, msg_type_be2]
|
|
65
|
+
pub fn seeds(oapp: &Pubkey, eid: u32, msg_type: u16) -> &'static [&'static [u8]] {
|
|
66
|
+
let key_bytes: &'static [u8; 32] = Box::leak(Box::new(oapp.to_bytes()));
|
|
67
|
+
let eid_bytes: &'static [u8; 4] = Box::leak(Box::new(eid.to_be_bytes()));
|
|
68
|
+
let msg_type_bytes: &'static [u8; 2] =
|
|
69
|
+
Box::leak(Box::new(msg_type.to_be_bytes()));
|
|
70
|
+
Box::leak(Box::new([
|
|
71
|
+
::oapp::ENFORCED_OPTIONS_SEED,
|
|
72
|
+
key_bytes.as_slice(),
|
|
73
|
+
eid_bytes.as_slice(),
|
|
74
|
+
msg_type_bytes.as_slice(),
|
|
75
|
+
]))
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@layerzerolabs/oapp-solana-contracts",
|
|
3
|
+
"version": "1.2.11",
|
|
4
|
+
"private": false,
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"src",
|
|
9
|
+
"idls",
|
|
10
|
+
"macros/src",
|
|
11
|
+
"macros/Cargo.toml",
|
|
12
|
+
"Cargo.toml",
|
|
13
|
+
"Anchor.toml",
|
|
14
|
+
"rust-toolchain.toml",
|
|
15
|
+
"rustfmt.toml"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@layerzerolabs/framework-solana-anchor-trait": "1.2.11",
|
|
19
|
+
"@layerzerolabs/utils-solana-rbac": "1.2.11"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@layerzerolabs/tsup-configuration": "1.2.11",
|
|
23
|
+
"@layerzerolabs/vm-tooling-solana": "1.2.11",
|
|
24
|
+
"@layerzerolabs/typescript-configuration": "1.2.11",
|
|
25
|
+
"@layerzerolabs/build-utils-rust": "1.2.11"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "restricted",
|
|
29
|
+
"registry": "https://registry.npmjs.org/"
|
|
30
|
+
},
|
|
31
|
+
"externalRepoConfig": {
|
|
32
|
+
"targets": [
|
|
33
|
+
"audit-external",
|
|
34
|
+
"usdai-internal"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"implicitDependencies": {
|
|
38
|
+
"@layerzerolabs/framework-solana-anchor-trait": "workspace:*",
|
|
39
|
+
"@layerzerolabs/utils-solana-rbac": "workspace:*"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"allow-read-write": "[ -z \"$CI\" ] || sudo chmod -R a+rwx ./target ./macros/examples/counter/target",
|
|
43
|
+
"build": "pnpm resolve-dependencies && pnpm build:lib && pnpm build:example && pnpm allow-read-write",
|
|
44
|
+
"build:example": "pnpm exec lz-tool --script 'anchor build --no-idl --ignore-keys' anchor",
|
|
45
|
+
"build:example:native": "pnpm exec lz-tool --docker-platform native --script 'anchor build --no-idl --ignore-keys' anchor",
|
|
46
|
+
"build:lib": "pnpm exec lz-tool --script 'cargo build' anchor",
|
|
47
|
+
"build:lib:native": "pnpm exec lz-tool --docker-platform native --script 'cargo build' anchor",
|
|
48
|
+
"build:native": "pnpm resolve-dependencies && pnpm build:lib:native && pnpm build:example:native && pnpm allow-read-write",
|
|
49
|
+
"check": "pnpm exec lz-tool --script 'cargo check' anchor",
|
|
50
|
+
"clean": "rm -rf dependencies target",
|
|
51
|
+
"format": "pnpm exec lz-tool --script 'cargo +nightly-2025-06-01 fmt' anchor",
|
|
52
|
+
"format:check": "pnpm exec lz-tool --script 'cargo +nightly-2025-06-01 fmt -- --check' anchor",
|
|
53
|
+
"lint": "pnpm resolve-dependencies && pnpm exec lz-tool --script 'cargo +nightly-2025-06-01 fmt -- --check && cargo clippy --all-targets' anchor",
|
|
54
|
+
"resolve-dependencies": "pnpm exec build-utils-rust resolve",
|
|
55
|
+
"test": "pnpm test:rust && pnpm test:example",
|
|
56
|
+
"test:example": "pnpm resolve-dependencies && pnpm exec lz-tool --script 'cargo test --manifest-path macros/examples/counter/Cargo.toml --lib --tests' anchor",
|
|
57
|
+
"test:rust": "pnpm resolve-dependencies && pnpm exec lz-tool --script 'cargo test -p oapp --lib --tests' anchor",
|
|
58
|
+
"test:snapshot:check": "pnpm exec turbo-snapshot check",
|
|
59
|
+
"test:snapshot:update": "pnpm exec turbo-snapshot update"
|
|
60
|
+
}
|
|
61
|
+
}
|
package/rustfmt.toml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Basic
|
|
2
|
+
hard_tabs = false
|
|
3
|
+
max_width = 100
|
|
4
|
+
use_small_heuristics = "Max"
|
|
5
|
+
# Imports
|
|
6
|
+
imports_granularity = "Crate"
|
|
7
|
+
reorder_imports = true
|
|
8
|
+
# Consistency
|
|
9
|
+
newline_style = "Unix"
|
|
10
|
+
# Format comments
|
|
11
|
+
comment_width = 100
|
|
12
|
+
wrap_comments = true
|
|
13
|
+
# Misc
|
|
14
|
+
chain_width = 80
|
|
15
|
+
spaces_around_ranges = false
|
|
16
|
+
binop_separator = "Front"
|
|
17
|
+
reorder_impl_items = false
|
|
18
|
+
match_arm_leading_pipes = "Preserve"
|
|
19
|
+
match_arm_blocks = true
|
|
20
|
+
match_block_trailing_comma = true
|
|
21
|
+
trailing_comma = "Vertical"
|
|
22
|
+
trailing_semicolon = true
|
|
23
|
+
use_field_init_shorthand = true
|
package/src/common.rs
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
use std::collections::HashMap;
|
|
2
|
+
|
|
3
|
+
use anchor_lang::{prelude::*, Discriminator};
|
|
4
|
+
use solana_address_lookup_table_interface::state::AddressLookupTable;
|
|
5
|
+
|
|
6
|
+
use crate::errors::OAppError;
|
|
7
|
+
|
|
8
|
+
pub const EXECUTION_CONTEXT_SEED: &[u8] = b"ExecutionContext";
|
|
9
|
+
pub const EXECUTION_CONTEXT_VERSION_1: u8 = 1;
|
|
10
|
+
|
|
11
|
+
/// Execution context data structure that contains metadata for cross-chain execution
|
|
12
|
+
/// This provides execution limits and tracking for LayerZero operations
|
|
13
|
+
#[derive(InitSpace, AnchorSerialize, AnchorDeserialize, Clone)]
|
|
14
|
+
pub struct ExecutionContextV1 {
|
|
15
|
+
pub initial_payer_balance: u64,
|
|
16
|
+
/// The maximum total lamports allowed to be used by all instructions in this execution.
|
|
17
|
+
/// This is a hard cap for the sum of lamports consumed by all instructions in the execution
|
|
18
|
+
/// batch.
|
|
19
|
+
pub fee_limit: u64,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
impl anchor_lang::Discriminator for ExecutionContextV1 {
|
|
23
|
+
// let discriminator_preimage = "account:ExecutionContextV1";
|
|
24
|
+
// let hash = anchor_lang::solana_program::hash::hash(discriminator_preimage.as_bytes());
|
|
25
|
+
const DISCRIMINATOR: &'static [u8] = &[132, 92, 176, 59, 141, 186, 141, 137];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
impl anchor_lang::AccountDeserialize for ExecutionContextV1 {
|
|
29
|
+
fn try_deserialize(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
|
|
30
|
+
if buf.len() < Self::DISCRIMINATOR.len() {
|
|
31
|
+
return Err(anchor_lang::error::ErrorCode::AccountDiscriminatorNotFound.into());
|
|
32
|
+
}
|
|
33
|
+
let given_disc = &buf[..8];
|
|
34
|
+
if Self::DISCRIMINATOR != given_disc {
|
|
35
|
+
return Err(anchor_lang::error!(
|
|
36
|
+
anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch
|
|
37
|
+
)
|
|
38
|
+
.with_account_name("ExecutionContextV1"));
|
|
39
|
+
}
|
|
40
|
+
Self::try_deserialize_unchecked(buf)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
|
|
44
|
+
let mut data: &[u8] = &buf[8..];
|
|
45
|
+
anchor_lang::AnchorDeserialize::deserialize(&mut data)
|
|
46
|
+
.map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotDeserialize.into())
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// A generic account locator used in LZ execution planning for V2.
|
|
51
|
+
/// Can reference the address directly, via ALT, or as a placeholder.
|
|
52
|
+
///
|
|
53
|
+
/// This enum enables the compact account referencing design of V2, supporting:
|
|
54
|
+
/// - OApps to request multiple signer accounts, not just a single Executor EOA
|
|
55
|
+
/// - Dynamic creation of writable EOA-based data accounts
|
|
56
|
+
/// - Efficient encoding of addresses via ALTs, reducing account list size
|
|
57
|
+
///
|
|
58
|
+
/// The legacy is_signer flag is removed. Instead, signer roles are explicitly
|
|
59
|
+
/// declared through Payer and indexed Signer(u8) variants.
|
|
60
|
+
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
|
|
61
|
+
pub enum AddressLocator {
|
|
62
|
+
/// Directly supplied public key - standard address reference
|
|
63
|
+
Address(Pubkey),
|
|
64
|
+
/// Indexed address from a specific Address Lookup Table (ALT)
|
|
65
|
+
/// Format: (ALT list index, address index within ALT)
|
|
66
|
+
/// Enables efficient account list compression via Solana's ALT mechanism
|
|
67
|
+
AltIndex(u8, u8),
|
|
68
|
+
/// Executor's fee payer - substituted by the Executor's EOA
|
|
69
|
+
/// This is the primary signer and fee payer for the transaction
|
|
70
|
+
Payer,
|
|
71
|
+
/// Additional signer accounts - substituted by EOAs provided by the Executor
|
|
72
|
+
/// The u8 index identifies each signer's position in the signer list,
|
|
73
|
+
/// allowing the OApp to reference multiple distinct signers for dynamic account creation
|
|
74
|
+
Signer(u8),
|
|
75
|
+
/// A context account provided by the Executor containing execution
|
|
76
|
+
/// metadata, such as SOL spend limits.
|
|
77
|
+
Context,
|
|
78
|
+
// Append more address placeholders in the future.
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
impl From<Pubkey> for AddressLocator {
|
|
82
|
+
fn from(pubkey: Pubkey) -> Self {
|
|
83
|
+
AddressLocator::Address(pubkey)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Account metadata for V2 execution planning.
|
|
88
|
+
/// Used by the Executor to construct the final transaction.
|
|
89
|
+
///
|
|
90
|
+
/// V2 removes the legacy is_signer flag from V1's AccountMeta.
|
|
91
|
+
/// Instead, signer roles are explicitly declared through AddressLocator variants.
|
|
92
|
+
/// This provides clearer semantics and enables multiple signer support.
|
|
93
|
+
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
|
|
94
|
+
pub struct AccountMetaRef {
|
|
95
|
+
/// The account address locator - supports multiple resolution strategies
|
|
96
|
+
pub pubkey: AddressLocator,
|
|
97
|
+
/// Whether the account should be writable in the final transaction
|
|
98
|
+
pub is_writable: bool,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
pub fn compact_accounts_with_alts(
|
|
102
|
+
alt_accounts: &[AccountInfo],
|
|
103
|
+
instruction_accounts: Vec<AccountMetaRef>,
|
|
104
|
+
) -> Result<Vec<AccountMetaRef>> {
|
|
105
|
+
// Build address lookup table mapping from remaining_accounts
|
|
106
|
+
// This enables efficient account referencing via ALT indices
|
|
107
|
+
let address_to_alt_index_map = build_address_to_alt_index_map(alt_accounts)?;
|
|
108
|
+
|
|
109
|
+
// Convert accounts to use ALT indices where possible
|
|
110
|
+
let compacted_accounts = instruction_accounts
|
|
111
|
+
.into_iter()
|
|
112
|
+
.map(|mut account_meta| {
|
|
113
|
+
if let AddressLocator::Address(pubkey) = account_meta.pubkey {
|
|
114
|
+
account_meta.pubkey = to_address_locator(&address_to_alt_index_map, pubkey);
|
|
115
|
+
}
|
|
116
|
+
account_meta
|
|
117
|
+
})
|
|
118
|
+
.collect();
|
|
119
|
+
|
|
120
|
+
Ok(compacted_accounts)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
pub fn to_address_locator(
|
|
124
|
+
address_to_alt_index_map: &HashMap<Pubkey, (u8, u8)>,
|
|
125
|
+
key: Pubkey,
|
|
126
|
+
) -> AddressLocator {
|
|
127
|
+
address_to_alt_index_map
|
|
128
|
+
.get(&key)
|
|
129
|
+
.map(|alt_index| AddressLocator::AltIndex(alt_index.0, alt_index.1))
|
|
130
|
+
.unwrap_or(AddressLocator::Address(key))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Helper function to deserialize AddressLookupTable data
|
|
134
|
+
pub fn deserialize_alt(alt: &AccountInfo) -> Result<Vec<Pubkey>> {
|
|
135
|
+
let data = alt
|
|
136
|
+
.try_borrow_data()
|
|
137
|
+
.map_err(|_| error!(OAppError::InvalidAddressLookupTable))?;
|
|
138
|
+
AddressLookupTable::deserialize(*data)
|
|
139
|
+
.map(|alt| alt.addresses.to_vec())
|
|
140
|
+
.map_err(|_| error!(OAppError::InvalidAddressLookupTable))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Helper function to build a map of addresses to their ALT indices
|
|
144
|
+
pub fn build_address_to_alt_index_map(
|
|
145
|
+
alt_accounts: &[AccountInfo],
|
|
146
|
+
) -> Result<HashMap<Pubkey, (u8, u8)>> {
|
|
147
|
+
let mut address_to_alt_index_map: HashMap<Pubkey, (u8, u8)> = HashMap::new();
|
|
148
|
+
for (alt_index, alt) in alt_accounts.iter().enumerate() {
|
|
149
|
+
let addresses_in_alt = deserialize_alt(alt)?;
|
|
150
|
+
for (address_index_in_alt, address_in_alt) in addresses_in_alt.iter().enumerate() {
|
|
151
|
+
address_to_alt_index_map
|
|
152
|
+
.insert(*address_in_alt, (alt_index as u8, address_index_in_alt as u8));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
Ok(address_to_alt_index_map)
|
|
156
|
+
}
|