@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,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
+ &params.eid.to_be_bytes(),
71
+ &params.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(&params.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(), &params.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(), &params.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
+ }
@@ -0,0 +1,3 @@
1
+ [toolchain]
2
+ channel = "1.92.0"
3
+ components = ["rustfmt", "clippy"]
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
+ }