@layerzerolabs/oapp-macros-stellar-contracts 0.2.122
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/Cargo.toml +17 -0
- package/LICENSE +23 -0
- package/clippy.toml +6 -0
- package/package.json +37 -0
- package/rust-toolchain.toml +4 -0
- package/rustfmt.toml +15 -0
- package/src/generators.rs +147 -0
- package/src/lib.rs +166 -0
- package/src/tests/mod.rs +2 -0
- package/src/tests/oapp.rs +74 -0
- package/src/tests/parse_custom_impls.rs +90 -0
- package/src/tests/snapshots/oapp_macros__tests__oapp__snapshot_generate_oapp.snap +111 -0
package/Cargo.toml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "oapp-macros"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
publish = false
|
|
6
|
+
|
|
7
|
+
[lib]
|
|
8
|
+
proc-macro = true
|
|
9
|
+
|
|
10
|
+
[dependencies]
|
|
11
|
+
proc-macro2 = "1.0"
|
|
12
|
+
quote = "1.0"
|
|
13
|
+
syn = { version = "2.0", features = ["full", "extra-traits"] }
|
|
14
|
+
|
|
15
|
+
[dev-dependencies]
|
|
16
|
+
insta = "1.44.3"
|
|
17
|
+
prettyplease = "0.2.37"
|
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Copyright (c) 2026 - LayerZero Labs Ltd.
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any
|
|
4
|
+
person obtaining a copy of this software and associated
|
|
5
|
+
documentation files (the "Software"), to deal in the
|
|
6
|
+
Software without restriction, including without
|
|
7
|
+
limitation the rights to use, copy, modify, merge,
|
|
8
|
+
publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software
|
|
10
|
+
is furnished to do so, subject to the following
|
|
11
|
+
conditions:
|
|
12
|
+
The above copyright notice and this permission notice
|
|
13
|
+
shall be included in all copies or substantial portions
|
|
14
|
+
of the Software.
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
|
16
|
+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
|
17
|
+
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
18
|
+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
|
19
|
+
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
20
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
|
22
|
+
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
23
|
+
DEALINGS IN THE SOFTWARE.
|
package/clippy.toml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Clippy configuration for Stellar contracts
|
|
2
|
+
|
|
3
|
+
# Set the too_many_arguments threshold to 11.
|
|
4
|
+
# This aligns with Stellar: contract functions are limited to 10 parameters, plus 1 for 'env'.
|
|
5
|
+
# (Default is 7; raised here to prevent unnecessary clippy warnings on valid contract interfaces.)
|
|
6
|
+
too-many-arguments-threshold = 11
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@layerzerolabs/oapp-macros-stellar-contracts",
|
|
3
|
+
"version": "0.2.122",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Stellar OApp procedural macros",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"files": [
|
|
8
|
+
"LICENSE",
|
|
9
|
+
"src",
|
|
10
|
+
"Cargo.toml",
|
|
11
|
+
"rust-toolchain.toml",
|
|
12
|
+
"rustfmt.toml",
|
|
13
|
+
"clippy.toml"
|
|
14
|
+
],
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@layerzerolabs/vm-tooling-stellar": "0.2.122"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public",
|
|
20
|
+
"registry": "https://registry.npmjs.org/"
|
|
21
|
+
},
|
|
22
|
+
"externalRepoConfig": {
|
|
23
|
+
"targets": [
|
|
24
|
+
"audit-external",
|
|
25
|
+
"console-pen-test",
|
|
26
|
+
"monorepo-external",
|
|
27
|
+
"onesig-client"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "pnpm exec lz-tool --script \"cargo build\" stellar",
|
|
32
|
+
"clean": "rm -rf ./target ./node_modules",
|
|
33
|
+
"format": "pnpm exec lz-tool --script \"cargo fmt\" stellar",
|
|
34
|
+
"lint": "pnpm exec lz-tool --script \"cargo clippy -- -D warnings\" stellar",
|
|
35
|
+
"test": "pnpm exec lz-tool --script \"cargo nextest run\" stellar"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/rustfmt.toml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
format_macro_bodies = true
|
|
2
|
+
max_width = 120
|
|
3
|
+
format_macro_matchers = true
|
|
4
|
+
format_strings = true
|
|
5
|
+
imports_granularity = "Crate"
|
|
6
|
+
match_arm_blocks = false
|
|
7
|
+
reorder_impl_items = true
|
|
8
|
+
group_imports = "StdExternalCrate"
|
|
9
|
+
use_field_init_shorthand = true
|
|
10
|
+
use_small_heuristics = "Max"
|
|
11
|
+
wrap_comments = true
|
|
12
|
+
format_code_in_doc_comments = true
|
|
13
|
+
|
|
14
|
+
# most of these are unstable, so we enable them
|
|
15
|
+
unstable_features = true
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
//! Code generators for OApp trait implementations.
|
|
2
|
+
|
|
3
|
+
use proc_macro2::TokenStream;
|
|
4
|
+
use quote::quote;
|
|
5
|
+
use syn::{
|
|
6
|
+
bracketed,
|
|
7
|
+
parse::{Parse, ParseStream},
|
|
8
|
+
punctuated::Punctuated,
|
|
9
|
+
Error, Ident, ItemStruct, Token,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/// Specifies which OApp trait implementations the user will provide themselves.
|
|
13
|
+
///
|
|
14
|
+
/// Parsed from `#[oapp(custom = [...])]`. When a field is `true`, the macro skips
|
|
15
|
+
/// generating that trait implementation, allowing the user to provide their own.
|
|
16
|
+
#[derive(Debug, Clone, Copy, Default)]
|
|
17
|
+
pub(crate) struct CustomImpls {
|
|
18
|
+
pub core: bool,
|
|
19
|
+
pub sender: bool,
|
|
20
|
+
pub receiver: bool,
|
|
21
|
+
pub options_type3: bool,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
impl Parse for CustomImpls {
|
|
25
|
+
fn parse(input: ParseStream) -> syn::Result<Self> {
|
|
26
|
+
if input.is_empty() {
|
|
27
|
+
return Ok(Self::default());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Consume the `custom` keyword
|
|
31
|
+
let key: Ident = input.parse()?;
|
|
32
|
+
if key != "custom" {
|
|
33
|
+
return Err(Error::new(key.span(), "expected `custom`"));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Consume the `=` in `custom = [...]`
|
|
37
|
+
input.parse::<Token![=]>()?;
|
|
38
|
+
|
|
39
|
+
// Parse the `[...]` brackets and capture the inner tokens into `content`
|
|
40
|
+
let content;
|
|
41
|
+
bracketed!(content in input);
|
|
42
|
+
|
|
43
|
+
// Parse the comma-separated list of identifiers into `idents`
|
|
44
|
+
let idents = Punctuated::<Ident, Token![,]>::parse_terminated(&content)?;
|
|
45
|
+
|
|
46
|
+
// Initialize the custom implementations struct
|
|
47
|
+
let mut custom_impls = Self::default();
|
|
48
|
+
|
|
49
|
+
for ident in idents {
|
|
50
|
+
match ident.to_string().as_str() {
|
|
51
|
+
"core" => custom_impls.core = true,
|
|
52
|
+
"sender" => custom_impls.sender = true,
|
|
53
|
+
"receiver" => custom_impls.receiver = true,
|
|
54
|
+
"options_type3" => custom_impls.options_type3 = true,
|
|
55
|
+
_ => {
|
|
56
|
+
return Err(Error::new(
|
|
57
|
+
ident.span(),
|
|
58
|
+
"expected one of `core`, `sender`, `receiver`, `options_type3`",
|
|
59
|
+
));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Ok(custom_impls)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Generates OApp trait implementations only. No contract-level attributes are applied.
|
|
69
|
+
///
|
|
70
|
+
/// This function creates OAppCore, OAppSenderInternal, OAppReceiver, and OAppOptionsType3 trait implementations.
|
|
71
|
+
///
|
|
72
|
+
/// **The user must apply a contract macro** such as `#[common_macros::lz_contract]` or
|
|
73
|
+
/// `#[soroban_sdk::contract]` to the struct. `#[lz_contract]` provides contract, TTL, and Auth
|
|
74
|
+
/// (via `#[ownable]` or `#[multisig]`) in one place.
|
|
75
|
+
///
|
|
76
|
+
/// The `custom` parameter controls which trait implementations are generated vs.
|
|
77
|
+
/// expected to be provided by the user.
|
|
78
|
+
pub fn generate_oapp(attr: TokenStream, input: TokenStream) -> TokenStream {
|
|
79
|
+
let custom: CustomImpls = syn::parse2(attr).unwrap_or_else(|e| panic!("failed to parse oapp attributes: {}", e));
|
|
80
|
+
let item_struct: ItemStruct = syn::parse2(input).unwrap_or_else(|e| panic!("failed to parse struct: {}", e));
|
|
81
|
+
|
|
82
|
+
let core_impl = (!custom.core).then(|| generate_oapp_core(&item_struct.ident));
|
|
83
|
+
let sender_impl = (!custom.sender).then(|| generate_oapp_sender(&item_struct.ident));
|
|
84
|
+
let receiver_impl = (!custom.receiver).then(|| generate_oapp_receiver(&item_struct.ident));
|
|
85
|
+
let options_type3_impl = (!custom.options_type3).then(|| generate_oapp_options_type3(&item_struct.ident));
|
|
86
|
+
quote! {
|
|
87
|
+
#item_struct
|
|
88
|
+
|
|
89
|
+
#core_impl
|
|
90
|
+
#sender_impl
|
|
91
|
+
#receiver_impl
|
|
92
|
+
#options_type3_impl
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Generates an empty `impl OAppCore` that uses the trait's default implementations
|
|
97
|
+
/// for peer management and endpoint access.
|
|
98
|
+
///
|
|
99
|
+
/// Also generates `impl RoleBasedAccessControl` because OAppCore extends it.
|
|
100
|
+
fn generate_oapp_core(name: &Ident) -> TokenStream {
|
|
101
|
+
quote! {
|
|
102
|
+
use oapp::oapp_core::OAppCore as _;
|
|
103
|
+
use utils::rbac::RoleBasedAccessControl as _;
|
|
104
|
+
|
|
105
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
106
|
+
impl oapp::oapp_core::OAppCore for #name {}
|
|
107
|
+
|
|
108
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
109
|
+
impl utils::rbac::RoleBasedAccessControl for #name {}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Generates the `OAppSenderInternal` trait implementation.
|
|
114
|
+
///
|
|
115
|
+
/// Generates an empty `impl OAppSenderInternal` that uses the trait's default
|
|
116
|
+
/// implementations for `__lz_quote` and `__lz_send`.
|
|
117
|
+
fn generate_oapp_sender(name: &Ident) -> TokenStream {
|
|
118
|
+
quote! {
|
|
119
|
+
use oapp::oapp_sender::OAppSenderInternal as _;
|
|
120
|
+
|
|
121
|
+
impl oapp::oapp_sender::OAppSenderInternal for #name {}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Generates an empty `impl OAppReceiver` that uses the trait's default `lz_receive`
|
|
126
|
+
/// implementation (which calls `clear_payload_and_transfer` then `__lz_receive`).
|
|
127
|
+
///
|
|
128
|
+
/// Users must implement `LzReceiveInternal` themselves to provide the `__lz_receive` method.
|
|
129
|
+
fn generate_oapp_receiver(name: &Ident) -> TokenStream {
|
|
130
|
+
quote! {
|
|
131
|
+
use oapp::oapp_receiver::OAppReceiver as _;
|
|
132
|
+
|
|
133
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
134
|
+
impl oapp::oapp_receiver::OAppReceiver for #name {}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// Generates an empty `impl OAppOptionsType3` that uses the trait's default
|
|
139
|
+
/// implementations for enforced options management.
|
|
140
|
+
fn generate_oapp_options_type3(name: &Ident) -> TokenStream {
|
|
141
|
+
quote! {
|
|
142
|
+
use oapp::oapp_options_type3::OAppOptionsType3 as _;
|
|
143
|
+
|
|
144
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
145
|
+
impl oapp::oapp_options_type3::OAppOptionsType3 for #name {}
|
|
146
|
+
}
|
|
147
|
+
}
|
package/src/lib.rs
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
//! # OApp Procedural Macros
|
|
2
|
+
//!
|
|
3
|
+
//! This crate provides the `#[oapp]` procedural macro for implementing LayerZero OApp
|
|
4
|
+
//! (Omnichain Application) functionality on Soroban smart contracts.
|
|
5
|
+
//!
|
|
6
|
+
//! ## Overview
|
|
7
|
+
//!
|
|
8
|
+
//! The OApp framework provides full bidirectional cross-chain communication:
|
|
9
|
+
//!
|
|
10
|
+
//! - **OAppCore**: Foundation for all OApp functionality (peer management, endpoint access)
|
|
11
|
+
//! - **OAppSenderInternal**: Enables sending cross-chain messages through LayerZero
|
|
12
|
+
//! - **LzReceiveInternal**: Internal trait for message handling logic
|
|
13
|
+
//! - **OAppReceiver**: Handles incoming cross-chain messages (has default `lz_receive` impl)
|
|
14
|
+
//! - **OAppOptionsType3**: Manages enforced options for message execution parameters
|
|
15
|
+
//!
|
|
16
|
+
//! ## Usage
|
|
17
|
+
//!
|
|
18
|
+
//! ### `#[oapp]`
|
|
19
|
+
//! The main macro that provides Core + Sender + Receiver + OptionsType3 functionality.
|
|
20
|
+
//! This is the only macro you need for cross-chain communication.
|
|
21
|
+
//!
|
|
22
|
+
//! ### Custom Implementations
|
|
23
|
+
//!
|
|
24
|
+
//! By default, `#[oapp]` generates default trait implementations. Use `#[oapp(custom = [...])]`
|
|
25
|
+
//! to provide your own custom implementations for specific traits:
|
|
26
|
+
//!
|
|
27
|
+
//! Supported options:
|
|
28
|
+
//! - `core` - Custom implement `OAppCore`
|
|
29
|
+
//! - `sender` - Custom implement `OAppSenderInternal`
|
|
30
|
+
//! - `receiver` - Custom implement `OAppReceiver` (useful for custom `next_nonce`, etc.)
|
|
31
|
+
//! - `options_type3` - Custom implement `OAppOptionsType3`
|
|
32
|
+
//!
|
|
33
|
+
//! ## Examples
|
|
34
|
+
//!
|
|
35
|
+
//! ### Full OApp with Default Implementations
|
|
36
|
+
//!
|
|
37
|
+
//! ```ignore
|
|
38
|
+
//! use oapp::oapp_receiver::LzReceiveInternal;
|
|
39
|
+
//! use oapp_macros::oapp;
|
|
40
|
+
//!
|
|
41
|
+
//! #[oapp]
|
|
42
|
+
//! #[common_macros::lz_contract]
|
|
43
|
+
//! struct MyOApp;
|
|
44
|
+
//!
|
|
45
|
+
//! // Implement LzReceiveInternal to handle incoming messages
|
|
46
|
+
//! impl LzReceiveInternal for MyOApp {
|
|
47
|
+
//! fn __lz_receive(
|
|
48
|
+
//! env: &Env,
|
|
49
|
+
//! origin: &Origin,
|
|
50
|
+
//! guid: &BytesN<32>,
|
|
51
|
+
//! message: &Bytes,
|
|
52
|
+
//! extra_data: &Bytes,
|
|
53
|
+
//! executor: &Address,
|
|
54
|
+
//! value: i128,
|
|
55
|
+
//! ) {
|
|
56
|
+
//! // Your message handling logic (clear_payload_and_transfer already called)
|
|
57
|
+
//! }
|
|
58
|
+
//! }
|
|
59
|
+
//! ```
|
|
60
|
+
//!
|
|
61
|
+
//! ### OApp with Custom Core Version
|
|
62
|
+
//!
|
|
63
|
+
//! ```ignore
|
|
64
|
+
//! use oapp::oapp_receiver::LzReceiveInternal;
|
|
65
|
+
//! use utils::rbac::RoleBasedAccessControl;
|
|
66
|
+
//!
|
|
67
|
+
//! #[oapp(custom = [core])]
|
|
68
|
+
//! #[common_macros::lz_contract]
|
|
69
|
+
//! struct MyOApp;
|
|
70
|
+
//!
|
|
71
|
+
//! // Required: `custom = [core]` skips generating both OAppCore and RoleBasedAccessControl impls
|
|
72
|
+
//! impl RoleBasedAccessControl for MyOApp {}
|
|
73
|
+
//!
|
|
74
|
+
//! #[contractimpl(contracttrait)]
|
|
75
|
+
//! impl OAppCore for MyOApp {
|
|
76
|
+
//! fn oapp_version(_env: &Env) -> (u64, u64) {
|
|
77
|
+
//! (1, 1) // Custom version
|
|
78
|
+
//! }
|
|
79
|
+
//! }
|
|
80
|
+
//!
|
|
81
|
+
//! impl LzReceiveInternal for MyOApp {
|
|
82
|
+
//! fn __lz_receive(...) { /* ... */ }
|
|
83
|
+
//! }
|
|
84
|
+
//! ```
|
|
85
|
+
//!
|
|
86
|
+
//! ### OApp with Custom Receiver (e.g., ordered delivery)
|
|
87
|
+
//!
|
|
88
|
+
//! ```ignore
|
|
89
|
+
//! use oapp::oapp_receiver::{LzReceiveInternal, OAppReceiver};
|
|
90
|
+
//!
|
|
91
|
+
//! #[oapp(custom = [receiver])]
|
|
92
|
+
//! #[common_macros::lz_contract]
|
|
93
|
+
//! struct MyOrderedOApp;
|
|
94
|
+
//!
|
|
95
|
+
//! impl LzReceiveInternal for MyOrderedOApp {
|
|
96
|
+
//! fn __lz_receive(env: &Env, origin: &Origin, guid: &BytesN<32>, ...) {
|
|
97
|
+
//! // Your message handling logic
|
|
98
|
+
//! }
|
|
99
|
+
//! }
|
|
100
|
+
//!
|
|
101
|
+
//! #[contractimpl(contracttrait)]
|
|
102
|
+
//! impl OAppReceiver for MyOrderedOApp {
|
|
103
|
+
//! fn next_nonce(env: &Env, src_eid: u32, sender: &BytesN<32>) -> u64 {
|
|
104
|
+
//! // Custom nonce logic for ordered delivery
|
|
105
|
+
//! Storage::max_received_nonce(env, src_eid, sender) + 1
|
|
106
|
+
//! }
|
|
107
|
+
//! // lz_receive uses default impl which calls clear_payload_and_transfer then __lz_receive
|
|
108
|
+
//! }
|
|
109
|
+
//! ```
|
|
110
|
+
//!
|
|
111
|
+
//! ### OApp with Multiple Custom Implementations
|
|
112
|
+
//!
|
|
113
|
+
//! ```ignore
|
|
114
|
+
//! use oapp::oapp_receiver::LzReceiveInternal;
|
|
115
|
+
//! use utils::rbac::RoleBasedAccessControl;
|
|
116
|
+
//!
|
|
117
|
+
//! #[oapp(custom = [core, sender, options_type3])]
|
|
118
|
+
//! #[common_macros::lz_contract]
|
|
119
|
+
//! struct MyCustomOApp;
|
|
120
|
+
//!
|
|
121
|
+
//! // Required: `custom = [core]` skips generating both OAppCore and RoleBasedAccessControl impls
|
|
122
|
+
//! impl RoleBasedAccessControl for MyCustomOApp {}
|
|
123
|
+
//!
|
|
124
|
+
//! #[contractimpl(contracttrait)]
|
|
125
|
+
//! impl OAppCore for MyCustomOApp { /* ... */ }
|
|
126
|
+
//! impl OAppSenderInternal for MyCustomOApp { /* ... */ }
|
|
127
|
+
//! #[contractimpl(contracttrait)]
|
|
128
|
+
//! impl OAppOptionsType3 for MyCustomOApp { /* ... */ }
|
|
129
|
+
//!
|
|
130
|
+
//! impl LzReceiveInternal for MyCustomOApp {
|
|
131
|
+
//! fn __lz_receive(...) { /* ... */ }
|
|
132
|
+
//! }
|
|
133
|
+
//! ```
|
|
134
|
+
|
|
135
|
+
mod generators;
|
|
136
|
+
|
|
137
|
+
use proc_macro::TokenStream;
|
|
138
|
+
|
|
139
|
+
/// Derives OApp trait implementations. Apply `#[lz_contract]` (or similar) for contract + TTL + Auth.
|
|
140
|
+
///
|
|
141
|
+
/// ## Usage
|
|
142
|
+
///
|
|
143
|
+
/// ```ignore
|
|
144
|
+
/// // Default: generates all trait implementations
|
|
145
|
+
/// #[oapp]
|
|
146
|
+
/// struct MyOApp;
|
|
147
|
+
///
|
|
148
|
+
/// // Provide custom implementations for specific traits
|
|
149
|
+
/// #[oapp(custom = [core, receiver])]
|
|
150
|
+
/// struct MyCustomOApp;
|
|
151
|
+
/// ```
|
|
152
|
+
///
|
|
153
|
+
/// ## Custom Options
|
|
154
|
+
/// - `core` - Manually implement `OAppCore`
|
|
155
|
+
/// - `sender` - Manually implement `OAppSenderInternal`
|
|
156
|
+
/// - `receiver` - Manually implement `OAppReceiver`
|
|
157
|
+
/// - `options_type3` - Manually implement `OAppOptionsType3`
|
|
158
|
+
///
|
|
159
|
+
/// You must implement `LzReceiveInternal` for your struct. See module docs for examples.
|
|
160
|
+
#[proc_macro_attribute]
|
|
161
|
+
pub fn oapp(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
162
|
+
generators::generate_oapp(attr.into(), item.into()).into()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#[cfg(test)]
|
|
166
|
+
mod tests;
|
package/src/tests/mod.rs
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
use quote::quote;
|
|
2
|
+
|
|
3
|
+
// ============================================
|
|
4
|
+
// Snapshot Tests: OApp Code Generation
|
|
5
|
+
// ============================================
|
|
6
|
+
|
|
7
|
+
#[test]
|
|
8
|
+
fn snapshot_generate_oapp() {
|
|
9
|
+
let struct_input = quote! {
|
|
10
|
+
pub struct MyOApp;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let all_defaults = {
|
|
14
|
+
let result = crate::generators::generate_oapp(quote! {}, struct_input.clone());
|
|
15
|
+
prettyplease::unparse(&syn::parse2::<syn::File>(result).expect("failed to parse generated code"))
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let custom_core_only = {
|
|
19
|
+
let result = crate::generators::generate_oapp(quote! { custom = [core] }, struct_input.clone());
|
|
20
|
+
prettyplease::unparse(&syn::parse2::<syn::File>(result).expect("failed to parse generated code"))
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let custom_sender_only = {
|
|
24
|
+
let result = crate::generators::generate_oapp(quote! { custom = [sender] }, struct_input.clone());
|
|
25
|
+
prettyplease::unparse(&syn::parse2::<syn::File>(result).expect("failed to parse generated code"))
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let custom_receiver_only = {
|
|
29
|
+
let result = crate::generators::generate_oapp(quote! { custom = [receiver] }, struct_input.clone());
|
|
30
|
+
prettyplease::unparse(&syn::parse2::<syn::File>(result).expect("failed to parse generated code"))
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
let custom_options_type3_only = {
|
|
34
|
+
let result = crate::generators::generate_oapp(quote! { custom = [options_type3] }, struct_input.clone());
|
|
35
|
+
prettyplease::unparse(&syn::parse2::<syn::File>(result).expect("failed to parse generated code"))
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let custom_all = {
|
|
39
|
+
let result = crate::generators::generate_oapp(
|
|
40
|
+
quote! { custom = [core, sender, receiver, options_type3] },
|
|
41
|
+
struct_input.clone(),
|
|
42
|
+
);
|
|
43
|
+
prettyplease::unparse(&syn::parse2::<syn::File>(result).expect("failed to parse generated code"))
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
let preserves_struct_attributes_and_fields = {
|
|
47
|
+
let fancy_input = quote! {
|
|
48
|
+
#[derive(Clone, Debug)]
|
|
49
|
+
pub struct FancyOApp {
|
|
50
|
+
pub x: u32,
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
let result = crate::generators::generate_oapp(quote! {}, fancy_input);
|
|
54
|
+
prettyplease::unparse(&syn::parse2::<syn::File>(result).expect("failed to parse generated code"))
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
let combined = format!(
|
|
58
|
+
"// === Default (no custom impls) ===\n\n{}\n\n\
|
|
59
|
+
// === custom = [core] ===\n\n{}\n\n\
|
|
60
|
+
// === custom = [sender] ===\n\n{}\n\n\
|
|
61
|
+
// === custom = [receiver] ===\n\n{}\n\n\
|
|
62
|
+
// === custom = [options_type3] ===\n\n{}\n\n\
|
|
63
|
+
// === custom = [core, sender, receiver, options_type3] ===\n\n{}\n\n\
|
|
64
|
+
// === Struct attributes + fields are preserved ===\n\n{}",
|
|
65
|
+
all_defaults,
|
|
66
|
+
custom_core_only,
|
|
67
|
+
custom_sender_only,
|
|
68
|
+
custom_receiver_only,
|
|
69
|
+
custom_options_type3_only,
|
|
70
|
+
custom_all,
|
|
71
|
+
preserves_struct_attributes_and_fields
|
|
72
|
+
);
|
|
73
|
+
insta::assert_snapshot!(combined);
|
|
74
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
use quote::quote;
|
|
2
|
+
|
|
3
|
+
// ============================================
|
|
4
|
+
// Unit Tests: #[oapp(custom = [...])] parsing
|
|
5
|
+
// ============================================
|
|
6
|
+
|
|
7
|
+
#[test]
|
|
8
|
+
fn test_custom_impls_parse_empty_is_default() {
|
|
9
|
+
let parsed: crate::generators::CustomImpls = syn::parse2(quote! {}).expect("failed to parse empty attrs");
|
|
10
|
+
assert!(!parsed.core);
|
|
11
|
+
assert!(!parsed.sender);
|
|
12
|
+
assert!(!parsed.receiver);
|
|
13
|
+
assert!(!parsed.options_type3);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
#[test]
|
|
17
|
+
fn test_custom_impls_parse_valid_all_list() {
|
|
18
|
+
let parsed: crate::generators::CustomImpls =
|
|
19
|
+
syn::parse2(quote! { custom = [core, sender, receiver, options_type3] }).expect("failed to parse attrs");
|
|
20
|
+
assert!(parsed.core);
|
|
21
|
+
assert!(parsed.sender);
|
|
22
|
+
assert!(parsed.receiver);
|
|
23
|
+
assert!(parsed.options_type3);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[test]
|
|
27
|
+
fn test_custom_impls_parse_empty_custom_list_is_default() {
|
|
28
|
+
let parsed: crate::generators::CustomImpls = syn::parse2(quote! { custom = [] }).expect("failed to parse attrs");
|
|
29
|
+
assert!(!parsed.core);
|
|
30
|
+
assert!(!parsed.sender);
|
|
31
|
+
assert!(!parsed.receiver);
|
|
32
|
+
assert!(!parsed.options_type3);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[test]
|
|
36
|
+
fn test_custom_impls_allows_trailing_comma() {
|
|
37
|
+
let parsed: crate::generators::CustomImpls =
|
|
38
|
+
syn::parse2(quote! { custom = [core,] }).expect("failed to parse attrs");
|
|
39
|
+
assert!(parsed.core);
|
|
40
|
+
assert!(!parsed.sender);
|
|
41
|
+
assert!(!parsed.receiver);
|
|
42
|
+
assert!(!parsed.options_type3);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[test]
|
|
46
|
+
fn test_custom_impls_allows_duplicates() {
|
|
47
|
+
// Duplicates are harmless and should not cause parsing to fail.
|
|
48
|
+
let parsed: crate::generators::CustomImpls =
|
|
49
|
+
syn::parse2(quote! { custom = [core, core, receiver, receiver] }).expect("failed to parse attrs");
|
|
50
|
+
assert!(parsed.core);
|
|
51
|
+
assert!(!parsed.sender);
|
|
52
|
+
assert!(parsed.receiver);
|
|
53
|
+
assert!(!parsed.options_type3);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#[test]
|
|
57
|
+
fn test_custom_impls_rejects_wrong_key() {
|
|
58
|
+
let err =
|
|
59
|
+
syn::parse2::<crate::generators::CustomImpls>(quote! { nope = [core] }).expect_err("expected parse failure");
|
|
60
|
+
assert!(err.to_string().contains("expected `custom`"), "unexpected error: {err}");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[test]
|
|
64
|
+
fn test_custom_impls_rejects_unknown_ident() {
|
|
65
|
+
let err = syn::parse2::<crate::generators::CustomImpls>(quote! { custom = [core, not_a_real_option] })
|
|
66
|
+
.expect_err("expected failure");
|
|
67
|
+
assert!(
|
|
68
|
+
err.to_string().contains("expected one of `core`, `sender`, `receiver`, `options_type3`"),
|
|
69
|
+
"unexpected error: {err}"
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#[test]
|
|
74
|
+
fn test_custom_impls_rejects_trailing_tokens_after_list() {
|
|
75
|
+
// `syn::parse2` expects the parser to consume the full stream.
|
|
76
|
+
syn::parse2::<crate::generators::CustomImpls>(quote! { custom = [core] extra })
|
|
77
|
+
.expect_err("expected parse failure due to trailing tokens");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#[test]
|
|
81
|
+
fn test_custom_impls_rejects_missing_equals() {
|
|
82
|
+
syn::parse2::<crate::generators::CustomImpls>(quote! { custom [core] })
|
|
83
|
+
.expect_err("expected parse failure (missing `=`)");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[test]
|
|
87
|
+
fn test_custom_impls_rejects_non_bracketed_list() {
|
|
88
|
+
syn::parse2::<crate::generators::CustomImpls>(quote! { custom = (core) })
|
|
89
|
+
.expect_err("expected parse failure (not `[...]`)");
|
|
90
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
source: contracts/oapps/oapp-macros/src/tests/oapp.rs
|
|
3
|
+
assertion_line: 87
|
|
4
|
+
expression: combined
|
|
5
|
+
---
|
|
6
|
+
// === Default (no custom impls) ===
|
|
7
|
+
|
|
8
|
+
pub struct MyOApp;
|
|
9
|
+
use oapp::oapp_core::OAppCore as _;
|
|
10
|
+
use utils::rbac::RoleBasedAccessControl as _;
|
|
11
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
12
|
+
impl oapp::oapp_core::OAppCore for MyOApp {}
|
|
13
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
14
|
+
impl utils::rbac::RoleBasedAccessControl for MyOApp {}
|
|
15
|
+
use oapp::oapp_sender::OAppSenderInternal as _;
|
|
16
|
+
impl oapp::oapp_sender::OAppSenderInternal for MyOApp {}
|
|
17
|
+
use oapp::oapp_receiver::OAppReceiver as _;
|
|
18
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
19
|
+
impl oapp::oapp_receiver::OAppReceiver for MyOApp {}
|
|
20
|
+
use oapp::oapp_options_type3::OAppOptionsType3 as _;
|
|
21
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
22
|
+
impl oapp::oapp_options_type3::OAppOptionsType3 for MyOApp {}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
// === custom = [core] ===
|
|
26
|
+
|
|
27
|
+
pub struct MyOApp;
|
|
28
|
+
use oapp::oapp_sender::OAppSenderInternal as _;
|
|
29
|
+
impl oapp::oapp_sender::OAppSenderInternal for MyOApp {}
|
|
30
|
+
use oapp::oapp_receiver::OAppReceiver as _;
|
|
31
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
32
|
+
impl oapp::oapp_receiver::OAppReceiver for MyOApp {}
|
|
33
|
+
use oapp::oapp_options_type3::OAppOptionsType3 as _;
|
|
34
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
35
|
+
impl oapp::oapp_options_type3::OAppOptionsType3 for MyOApp {}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
// === custom = [sender] ===
|
|
39
|
+
|
|
40
|
+
pub struct MyOApp;
|
|
41
|
+
use oapp::oapp_core::OAppCore as _;
|
|
42
|
+
use utils::rbac::RoleBasedAccessControl as _;
|
|
43
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
44
|
+
impl oapp::oapp_core::OAppCore for MyOApp {}
|
|
45
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
46
|
+
impl utils::rbac::RoleBasedAccessControl for MyOApp {}
|
|
47
|
+
use oapp::oapp_receiver::OAppReceiver as _;
|
|
48
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
49
|
+
impl oapp::oapp_receiver::OAppReceiver for MyOApp {}
|
|
50
|
+
use oapp::oapp_options_type3::OAppOptionsType3 as _;
|
|
51
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
52
|
+
impl oapp::oapp_options_type3::OAppOptionsType3 for MyOApp {}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
// === custom = [receiver] ===
|
|
56
|
+
|
|
57
|
+
pub struct MyOApp;
|
|
58
|
+
use oapp::oapp_core::OAppCore as _;
|
|
59
|
+
use utils::rbac::RoleBasedAccessControl as _;
|
|
60
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
61
|
+
impl oapp::oapp_core::OAppCore for MyOApp {}
|
|
62
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
63
|
+
impl utils::rbac::RoleBasedAccessControl for MyOApp {}
|
|
64
|
+
use oapp::oapp_sender::OAppSenderInternal as _;
|
|
65
|
+
impl oapp::oapp_sender::OAppSenderInternal for MyOApp {}
|
|
66
|
+
use oapp::oapp_options_type3::OAppOptionsType3 as _;
|
|
67
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
68
|
+
impl oapp::oapp_options_type3::OAppOptionsType3 for MyOApp {}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
// === custom = [options_type3] ===
|
|
72
|
+
|
|
73
|
+
pub struct MyOApp;
|
|
74
|
+
use oapp::oapp_core::OAppCore as _;
|
|
75
|
+
use utils::rbac::RoleBasedAccessControl as _;
|
|
76
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
77
|
+
impl oapp::oapp_core::OAppCore for MyOApp {}
|
|
78
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
79
|
+
impl utils::rbac::RoleBasedAccessControl for MyOApp {}
|
|
80
|
+
use oapp::oapp_sender::OAppSenderInternal as _;
|
|
81
|
+
impl oapp::oapp_sender::OAppSenderInternal for MyOApp {}
|
|
82
|
+
use oapp::oapp_receiver::OAppReceiver as _;
|
|
83
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
84
|
+
impl oapp::oapp_receiver::OAppReceiver for MyOApp {}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
// === custom = [core, sender, receiver, options_type3] ===
|
|
88
|
+
|
|
89
|
+
pub struct MyOApp;
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
// === Struct attributes + fields are preserved ===
|
|
93
|
+
|
|
94
|
+
#[derive(Clone, Debug)]
|
|
95
|
+
pub struct FancyOApp {
|
|
96
|
+
pub x: u32,
|
|
97
|
+
}
|
|
98
|
+
use oapp::oapp_core::OAppCore as _;
|
|
99
|
+
use utils::rbac::RoleBasedAccessControl as _;
|
|
100
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
101
|
+
impl oapp::oapp_core::OAppCore for FancyOApp {}
|
|
102
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
103
|
+
impl utils::rbac::RoleBasedAccessControl for FancyOApp {}
|
|
104
|
+
use oapp::oapp_sender::OAppSenderInternal as _;
|
|
105
|
+
impl oapp::oapp_sender::OAppSenderInternal for FancyOApp {}
|
|
106
|
+
use oapp::oapp_receiver::OAppReceiver as _;
|
|
107
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
108
|
+
impl oapp::oapp_receiver::OAppReceiver for FancyOApp {}
|
|
109
|
+
use oapp::oapp_options_type3::OAppOptionsType3 as _;
|
|
110
|
+
#[soroban_sdk::contractimpl(contracttrait)]
|
|
111
|
+
impl oapp::oapp_options_type3::OAppOptionsType3 for FancyOApp {}
|