@layerzerolabs/omni-counter-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 ADDED
@@ -0,0 +1,52 @@
1
+ [package]
2
+ name = "counter"
3
+ version = "0.0.1"
4
+ edition = "2021"
5
+ publish = false
6
+
7
+ [lib]
8
+ crate-type = ["cdylib"]
9
+ doctest = false
10
+
11
+ [dependencies]
12
+ soroban-sdk = { version = "25.1.1", features = ["hazmat-address", "hazmat-crypto"] }
13
+ endpoint-v2 = { path = "dependencies/protocol-stellar-v2/endpoint-v2", default-features = false, features = ["library"] }
14
+ utils = { path = "dependencies/common-utils-stellar-contracts" }
15
+ common-macros = { path = "dependencies/common-utils-macros-stellar-contracts" }
16
+ oapp = { path = "dependencies/oapp-stellar-contracts" }
17
+ oapp-macros = { path = "dependencies/oapp-macros-stellar-contracts" }
18
+
19
+ [profile.release]
20
+ opt-level = "z"
21
+ overflow-checks = true
22
+ debug = 0
23
+ strip = "symbols"
24
+ debug-assertions = false
25
+ panic = "abort"
26
+ codegen-units = 1
27
+ lto = true
28
+
29
+ [profile.release-with-logs]
30
+ inherits = "release"
31
+ debug-assertions = true
32
+
33
+ [dev-dependencies]
34
+ soroban-sdk = { version = "25.1.1", features = ["hazmat-address", "hazmat-crypto", "testutils"] }
35
+ utils = { path = "dependencies/common-utils-stellar-contracts", features = ["testutils"] }
36
+ simple-message-lib = { path = "dependencies/protocol-stellar-v2/message-libs/simple-message-lib", features = ["testutils"] }
37
+ blocked-message-lib = { path = "dependencies/protocol-stellar-v2/message-libs/blocked-message-lib" }
38
+ message-lib-common = { path = "dependencies/protocol-stellar-v2/message-libs/message-lib-common", features = ["testutils"] }
39
+ endpoint-v2 = { path = "dependencies/protocol-stellar-v2/endpoint-v2", features = ["testutils"] }
40
+ executor = { path = "dependencies/protocol-stellar-v2/workers/executor", features = ["testutils"] }
41
+ uln302 = { path = "dependencies/protocol-stellar-v2/message-libs/uln-302", features = ["testutils"] }
42
+ dvn = { path = "dependencies/protocol-stellar-v2/workers/dvn", features = ["testutils"] }
43
+ dvn-fee-lib = { path = "dependencies/protocol-stellar-v2/workers/dvn-fee-lib", features = ["testutils"] }
44
+ executor-fee-lib = { path = "dependencies/protocol-stellar-v2/workers/executor-fee-lib", features = ["testutils"] }
45
+ treasury = { path = "dependencies/protocol-stellar-v2/message-libs/treasury", features = ["testutils"] }
46
+ price-feed = { path = "dependencies/protocol-stellar-v2/workers/price-feed", features = ["testutils"] }
47
+ fee-lib-interfaces = { path = "dependencies/protocol-stellar-v2/workers/fee-lib-interfaces" }
48
+ executor-helper = { path = "dependencies/protocol-stellar-v2/workers/executor-helper" }
49
+ k256 = "0.13"
50
+ sha3 = "0.10"
51
+ rand = "0.8"
52
+ ed25519-dalek = "2"
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,55 @@
1
+ {
2
+ "name": "@layerzerolabs/omni-counter-stellar-contracts",
3
+ "version": "0.2.122",
4
+ "private": false,
5
+ "license": "MIT",
6
+ "files": [
7
+ "src",
8
+ "Cargo.toml",
9
+ "rust-toolchain.toml",
10
+ "rustfmt.toml",
11
+ "clippy.toml"
12
+ ],
13
+ "dependencies": {
14
+ "@layerzerolabs/common-utils-macros-stellar-contracts": "0.2.122",
15
+ "@layerzerolabs/common-utils-stellar-contracts": "0.2.122",
16
+ "@layerzerolabs/oapp-stellar-contracts": "0.2.122",
17
+ "@layerzerolabs/protocol-stellar-v2": "0.2.122",
18
+ "@layerzerolabs/oapp-macros-stellar-contracts": "0.2.122"
19
+ },
20
+ "devDependencies": {
21
+ "@layerzerolabs/build-utils-rust": "0.2.122",
22
+ "@layerzerolabs/vm-tooling-stellar": "0.2.122"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public",
26
+ "registry": "https://registry.npmjs.org/"
27
+ },
28
+ "externalRepoConfig": {
29
+ "targets": [
30
+ "audit-external",
31
+ "console-pen-test",
32
+ "monorepo-external",
33
+ "onesig-client"
34
+ ]
35
+ },
36
+ "implicitDependencies": {
37
+ "@layerzerolabs/oapp-stellar-contracts": "workspace:*",
38
+ "@layerzerolabs/protocol-stellar-v2": "workspace:*",
39
+ "@layerzerolabs/common-utils-stellar-contracts": "workspace:*",
40
+ "@layerzerolabs/common-utils-macros-stellar-contracts": "workspace:*",
41
+ "@layerzerolabs/oapp-macros-stellar-contracts": "workspace:*"
42
+ },
43
+ "scripts": {
44
+ "build": "pnpm resolve-dependencies && pnpm exec lz-tool stellar contract build --package counter",
45
+ "build:native": "pnpm resolve-dependencies && pnpm exec lz-tool --docker-platform native stellar contract build --package counter",
46
+ "clean": "rm -rf ./dependencies ./target ./node_modules",
47
+ "format": "pnpm exec lz-tool --script \"cargo fmt\" stellar",
48
+ "lint": "pnpm resolve-dependencies && pnpm exec lz-tool --script \"cargo clippy -- -D warnings\" stellar",
49
+ "lint:fix": "pnpm resolve-dependencies && pnpm exec lz-tool --script \"cargo clippy --fix --allow-dirty --allow-staged && cargo fmt\" stellar",
50
+ "resolve-dependencies": "pnpm exec build-utils-rust resolve",
51
+ "test": "pnpm resolve-dependencies && pnpm exec lz-tool --script \"cargo nextest run\" stellar",
52
+ "test:snapshot:check": "pnpm exec turbo-snapshot check",
53
+ "test:snapshot:update": "pnpm exec turbo-snapshot update"
54
+ }
55
+ }
@@ -0,0 +1,4 @@
1
+ [toolchain]
2
+ channel = "1.90.0"
3
+ targets = ["wasm32v1-none"]
4
+ components = ["rustfmt", "clippy"]
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
package/src/codec.rs ADDED
@@ -0,0 +1,62 @@
1
+ use soroban_sdk::{Bytes, Env, U256};
2
+ use utils::option_ext::OptionExt;
3
+
4
+ use crate::{errors::CounterError, u256_ext::U256Ext};
5
+
6
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
7
+ #[repr(u8)]
8
+ pub enum MsgType {
9
+ Vanilla = 1,
10
+ Composed = 2,
11
+ #[allow(clippy::upper_case_acronyms)]
12
+ ABA = 3,
13
+ ComposedABA = 4,
14
+ }
15
+
16
+ impl From<u8> for MsgType {
17
+ fn from(value: u8) -> Self {
18
+ match value {
19
+ 1 => MsgType::Vanilla,
20
+ 2 => MsgType::Composed,
21
+ 3 => MsgType::ABA,
22
+ 4 => MsgType::ComposedABA,
23
+ _ => panic!("invalid msg type"),
24
+ }
25
+ }
26
+ }
27
+
28
+ pub const _MSG_TYPE_OFFSET: u8 = 0;
29
+ pub const SRC_EID_OFFSET: u8 = 1;
30
+ pub const VALUE_OFFSET: u8 = 5;
31
+
32
+ pub fn encode(env: &Env, msg_type: MsgType, src_eid: u32) -> Bytes {
33
+ let msg_type: u8 = msg_type as u8;
34
+
35
+ let mut data = Bytes::new(env);
36
+ data.extend_from_array(&msg_type.to_be_bytes());
37
+ data.extend_from_array(&src_eid.to_be_bytes());
38
+
39
+ data
40
+ }
41
+
42
+ pub fn encode_with_value(env: &Env, msg_type: MsgType, src_eid: u32, value: u32) -> Bytes {
43
+ let mut data = encode(env, msg_type, src_eid);
44
+ data.append(&U256::from_u32(env, value).to_be_bytes());
45
+ data
46
+ }
47
+
48
+ pub fn msg_type(data: &Bytes) -> MsgType {
49
+ data.get(0).unwrap_or_else(|| panic!("cannot get msg type")).into()
50
+ }
51
+
52
+ pub fn src_eid(data: &Bytes) -> u32 {
53
+ let mut src_eid_bytes = [0u8; 4];
54
+ data.slice((SRC_EID_OFFSET as u32)..(VALUE_OFFSET as u32)).copy_into_slice(&mut src_eid_bytes);
55
+ u32::from_be_bytes(src_eid_bytes)
56
+ }
57
+
58
+ pub fn value(env: &Env, data: &Bytes) -> i128 {
59
+ let slice = data.slice((VALUE_OFFSET as u32)..data.len());
60
+ let value = if slice.is_empty() { U256::from_u32(env, 0) } else { U256::from_be_bytes(env, &slice) };
61
+ value.to_i128().unwrap_or_panic(env, CounterError::InvalidMsgValue)
62
+ }
package/src/counter.rs ADDED
@@ -0,0 +1,274 @@
1
+ use crate::{
2
+ codec::{self, MsgType},
3
+ errors::CounterError,
4
+ options,
5
+ storage::CounterStorage,
6
+ };
7
+ use common_macros::{contract_impl, lz_contract, only_auth};
8
+ use endpoint_v2::{
9
+ ILayerZeroComposer, LayerZeroEndpointV2Client, MessagingChannelClient, MessagingComposerClient, MessagingFee,
10
+ Origin,
11
+ };
12
+ use oapp::{
13
+ oapp_core::{init_ownable_oapp, OAppCore},
14
+ oapp_receiver::{LzReceiveInternal, OAppReceiver},
15
+ oapp_sender::{FeePayer, OAppSenderInternal},
16
+ };
17
+ use oapp_macros::oapp;
18
+ use soroban_sdk::{assert_with_error, panic_with_error, token::TokenClient, Address, Bytes, BytesN, Env};
19
+
20
+ #[lz_contract]
21
+ #[oapp(custom = [receiver])]
22
+ pub struct Counter;
23
+
24
+ #[contract_impl]
25
+ impl Counter {
26
+ pub fn __constructor(env: &Env, owner: &Address, endpoint: &Address, delegate: &Address) {
27
+ init_ownable_oapp::<Self>(env, owner, endpoint, delegate);
28
+ let endpoint_client = LayerZeroEndpointV2Client::new(env, endpoint);
29
+ CounterStorage::set_eid(env, &endpoint_client.eid());
30
+ }
31
+
32
+ pub fn quote(env: &Env, dst_eid: u32, msg_type: u32, options: &Bytes, pay_in_zro: bool) -> MessagingFee {
33
+ Self::__quote(env, dst_eid, &codec::encode(env, (msg_type as u8).into(), Self::eid(env)), options, pay_in_zro)
34
+ }
35
+
36
+ pub fn increment(env: &Env, caller: &Address, dst_eid: u32, msg_type: u32, options: &Bytes, fee: &MessagingFee) {
37
+ caller.require_auth();
38
+
39
+ // Increment the outbound count
40
+ let outbound_count = Self::outbound_count(env, dst_eid);
41
+ CounterStorage::set_outbound_count(env, dst_eid, &(outbound_count + 1));
42
+
43
+ // Send the message — caller already authorized via require_auth() above
44
+ let message = codec::encode(env, (msg_type as u8).into(), Self::eid(env));
45
+ Self::__lz_send(env, dst_eid, &message, options, &FeePayer::Verified(caller.clone()), fee, caller);
46
+ }
47
+
48
+ // ============================================================================================
49
+ // Owner functions
50
+ // ============================================================================================
51
+
52
+ #[only_auth]
53
+ pub fn set_ordered_nonce(env: &Env, ordered_nonce: bool) {
54
+ CounterStorage::set_ordered_nonce(env, &ordered_nonce);
55
+ }
56
+
57
+ #[only_auth]
58
+ pub fn skip_inbound_nonce(env: &Env, src_eid: u32, sender: &BytesN<32>, nonce: u64) {
59
+ let contract_address = env.current_contract_address();
60
+
61
+ let endpoint_address = Self::endpoint(env);
62
+ let endpoint = MessagingChannelClient::new(env, &endpoint_address);
63
+ endpoint.skip(&contract_address, &contract_address, &src_eid, sender, &nonce);
64
+
65
+ if CounterStorage::ordered_nonce(env) {
66
+ let max_received_nonce = CounterStorage::max_received_nonce(env, src_eid, sender);
67
+ CounterStorage::set_max_received_nonce(env, src_eid, sender, &(max_received_nonce + 1));
68
+ }
69
+ }
70
+
71
+ #[only_auth]
72
+ pub fn withdraw(env: &Env, to: &Address, amount: i128) {
73
+ let native_token = LayerZeroEndpointV2Client::new(env, &Self::endpoint(env)).native_token();
74
+ TokenClient::new(env, &native_token).transfer(&env.current_contract_address(), to, &amount);
75
+ }
76
+
77
+ // ============================================================================================
78
+ // View functions
79
+ // ============================================================================================
80
+
81
+ pub fn eid(env: &Env) -> u32 {
82
+ CounterStorage::eid(env).unwrap()
83
+ }
84
+
85
+ pub fn count(env: &Env) -> u64 {
86
+ CounterStorage::count(env)
87
+ }
88
+
89
+ pub fn composed_count(env: &Env) -> u64 {
90
+ CounterStorage::composed_count(env)
91
+ }
92
+
93
+ pub fn inbound_count(env: &Env, eid: u32) -> u64 {
94
+ CounterStorage::inbound_count(env, eid)
95
+ }
96
+
97
+ pub fn outbound_count(env: &Env, eid: u32) -> u64 {
98
+ CounterStorage::outbound_count(env, eid)
99
+ }
100
+
101
+ // ============================================================================================
102
+ // Internal Functions
103
+ // ============================================================================================
104
+
105
+ fn __accept_nonce(env: &Env, src_eid: u32, sender: &BytesN<32>, nonce: u64) {
106
+ let current_nonce = CounterStorage::max_received_nonce(env, src_eid, sender);
107
+ if CounterStorage::ordered_nonce(env) {
108
+ assert_with_error!(env, nonce == current_nonce + 1, CounterError::OAppInvalidNonce);
109
+ }
110
+ // Update the max nonce anyway. once the ordered mode is turned on, missing early nonces will be rejected
111
+ if nonce > current_nonce {
112
+ CounterStorage::set_max_received_nonce(env, src_eid, sender, &nonce);
113
+ }
114
+ }
115
+ }
116
+
117
+ // ============================================================================================
118
+ // LzReceiveInternal implementation
119
+ // ============================================================================================
120
+
121
+ impl LzReceiveInternal for Counter {
122
+ fn __lz_receive(
123
+ env: &Env,
124
+ origin: &Origin,
125
+ guid: &BytesN<32>,
126
+ message: &Bytes,
127
+ _extra_data: &Bytes,
128
+ _executor: &Address,
129
+ value: i128,
130
+ ) {
131
+ // handle the message
132
+ Self::__accept_nonce(env, origin.src_eid, &origin.sender, origin.nonce);
133
+
134
+ let contract_address = env.current_contract_address();
135
+
136
+ // Increment the count and inbound count
137
+ let count = Self::count(env);
138
+ let inbound_count = Self::inbound_count(env, origin.src_eid);
139
+ CounterStorage::set_count(env, &(count + 1));
140
+ CounterStorage::set_inbound_count(env, origin.src_eid, &(inbound_count + 1));
141
+
142
+ let assert_received_value = || {
143
+ let expected_msg_value = codec::value(env, message);
144
+ assert_with_error!(env, value >= expected_msg_value, CounterError::InsufficientValue);
145
+ };
146
+
147
+ match codec::msg_type(message) {
148
+ MsgType::Vanilla => {
149
+ assert_received_value();
150
+ }
151
+ MsgType::Composed | MsgType::ComposedABA => {
152
+ let endpoint: MessagingComposerClient<'_> = MessagingComposerClient::new(env, &Self::endpoint(env));
153
+ endpoint.send_compose(&contract_address, &contract_address, guid, &0, message);
154
+ }
155
+ MsgType::ABA => {
156
+ assert_received_value();
157
+
158
+ // Increment the outbound count
159
+ let outbound_count = Self::outbound_count(env, origin.src_eid);
160
+ CounterStorage::set_outbound_count(env, origin.src_eid, &(outbound_count + 1));
161
+
162
+ // Send the response message — contract is the fee payer (auto-authorized)
163
+ let options = options::executor_lz_receive_option(env, 200000, 10);
164
+ Self::__lz_send(
165
+ env,
166
+ origin.src_eid,
167
+ &codec::encode_with_value(env, MsgType::Vanilla, Self::eid(env), 10),
168
+ &options,
169
+ &FeePayer::Verified(contract_address.clone()),
170
+ &MessagingFee { native_fee: value, zro_fee: 0 },
171
+ &contract_address,
172
+ );
173
+ }
174
+ }
175
+ }
176
+ }
177
+
178
+ // ============================================================================================
179
+ // Custom OAppReceiver implementation
180
+ // ============================================================================================
181
+
182
+ #[contract_impl(contracttrait)]
183
+ impl OAppReceiver for Counter {
184
+ fn next_nonce(env: &Env, src_eid: u32, sender: &BytesN<32>) -> u64 {
185
+ if CounterStorage::ordered_nonce(env) {
186
+ CounterStorage::max_received_nonce(env, src_eid, sender) + 1
187
+ } else {
188
+ 0
189
+ }
190
+ }
191
+ }
192
+
193
+ // ============================================================================================
194
+ // ILayerZeroComposer implementation
195
+ // ============================================================================================
196
+
197
+ #[contract_impl]
198
+ impl ILayerZeroComposer for Counter {
199
+ fn lz_compose(
200
+ env: &Env,
201
+ executor: &Address,
202
+ from: &Address,
203
+ guid: &BytesN<32>,
204
+ index: u32,
205
+ message: &Bytes,
206
+ _extra_data: &Bytes,
207
+ value: i128,
208
+ ) {
209
+ // Clear compose message, transfer value, and require executor auth
210
+ clear_compose_and_transfer::<Self>(env, executor, from, guid, index, message, value);
211
+
212
+ let msg_type = codec::msg_type(message);
213
+ if msg_type != MsgType::Composed && msg_type != MsgType::ComposedABA {
214
+ panic_with_error!(env, CounterError::InvalidMsgType);
215
+ }
216
+
217
+ // Assert the value is sufficient
218
+ let expected_msg_value = codec::value(env, message);
219
+ assert_with_error!(env, value >= expected_msg_value, CounterError::InsufficientValue);
220
+
221
+ // Increment the composed count
222
+ CounterStorage::set_composed_count(env, &(Self::composed_count(env) + 1));
223
+
224
+ // Handle ComposedABA: send response message back to source
225
+ if msg_type == MsgType::ComposedABA {
226
+ let src_eid = codec::src_eid(message);
227
+
228
+ // Increment the outbound count
229
+ CounterStorage::set_outbound_count(env, src_eid, &(Self::outbound_count(env, src_eid) + 1));
230
+
231
+ // Send the response message — contract is the fee payer (auto-authorized)
232
+ let curr_address = env.current_contract_address();
233
+ Self::__lz_send(
234
+ env,
235
+ src_eid,
236
+ &codec::encode(env, MsgType::Vanilla, Self::eid(env)),
237
+ &options::executor_lz_receive_option(env, 200000, 0),
238
+ &FeePayer::Verified(curr_address.clone()),
239
+ &MessagingFee { native_fee: value, zro_fee: 0 },
240
+ &curr_address,
241
+ );
242
+ }
243
+ }
244
+ }
245
+
246
+ /// Clears a compose message and transfers native token value from executor to the OApp.
247
+ ///
248
+ /// This helper handles the common pattern in `lz_compose` implementations:
249
+ /// 1. Requires executor authorization
250
+ /// 2. Clears the compose message from the endpoint
251
+ /// 3. Transfers native token from executor to OApp if value > 0
252
+ pub fn clear_compose_and_transfer<T: OAppCore>(
253
+ env: &Env,
254
+ executor: &Address,
255
+ from: &Address,
256
+ guid: &BytesN<32>,
257
+ index: u32,
258
+ message: &Bytes,
259
+ value: i128,
260
+ ) {
261
+ executor.require_auth();
262
+
263
+ let curr_address = env.current_contract_address();
264
+ let endpoint = T::endpoint(env);
265
+
266
+ // Clear the compose message
267
+ MessagingComposerClient::new(env, &endpoint).clear_compose(&curr_address, from, guid, &index, message);
268
+
269
+ // Transfer value from executor to current contract
270
+ if value > 0 {
271
+ let native_token = LayerZeroEndpointV2Client::new(env, &endpoint).native_token();
272
+ TokenClient::new(env, &native_token).transfer(executor, &curr_address, &value);
273
+ }
274
+ }
package/src/errors.rs ADDED
@@ -0,0 +1,9 @@
1
+ use common_macros::contract_error;
2
+
3
+ #[contract_error]
4
+ pub enum CounterError {
5
+ OAppInvalidNonce,
6
+ InvalidMsgValue,
7
+ InsufficientValue,
8
+ InvalidMsgType,
9
+ }
package/src/lib.rs ADDED
@@ -0,0 +1,16 @@
1
+ #![no_std]
2
+
3
+ mod codec;
4
+ pub mod errors;
5
+ mod options;
6
+ mod storage;
7
+ mod u256_ext;
8
+
9
+ pub mod counter;
10
+
11
+ #[cfg(test)]
12
+ #[path = "../integration_tests/mod.rs"]
13
+ mod integration_tests;
14
+
15
+ #[cfg(test)]
16
+ mod tests;
package/src/options.rs ADDED
@@ -0,0 +1,30 @@
1
+ use soroban_sdk::{Bytes, Env};
2
+
3
+ pub const EXECUTOR_ID: u8 = 1; // WORKER_ID
4
+
5
+ pub const LZ_RECEIVE_TYPE: u8 = 1; // OPTION_TYPE
6
+
7
+ // copied from solana's counter options.rs
8
+ pub fn executor_lz_receive_option(env: &Env, gas_limit: u128, value: u128) -> Bytes {
9
+ let mut options = Bytes::new(env);
10
+
11
+ options.extend_from_slice(&3u16.to_be_bytes());
12
+
13
+ options.push_back(EXECUTOR_ID);
14
+
15
+ let gas_limit_bytes = gas_limit.to_be_bytes();
16
+ let value_bytes = value.to_be_bytes();
17
+
18
+ if value == 0 {
19
+ options.extend_from_slice(&(gas_limit_bytes.len() as u16 + 1).to_be_bytes());
20
+ options.push_back(LZ_RECEIVE_TYPE);
21
+ options.extend_from_slice(&gas_limit_bytes);
22
+ } else {
23
+ options.extend_from_slice(&((gas_limit_bytes.len() + value_bytes.len()) as u16 + 1).to_be_bytes());
24
+ options.push_back(LZ_RECEIVE_TYPE);
25
+ options.extend_from_slice(&gas_limit_bytes);
26
+ options.extend_from_slice(&value_bytes);
27
+ }
28
+
29
+ options
30
+ }
package/src/storage.rs ADDED
@@ -0,0 +1,32 @@
1
+ use common_macros::storage;
2
+ use soroban_sdk::BytesN;
3
+
4
+ #[storage]
5
+ pub enum CounterStorage {
6
+ #[instance(u32)]
7
+ EID,
8
+
9
+ #[persistent(u64)]
10
+ #[default(0)]
11
+ MaxReceivedNonce { eid: u32, sender: BytesN<32> }, // (eid, sender) => nonce
12
+
13
+ #[instance(bool)]
14
+ #[default(false)]
15
+ OrderedNonce,
16
+
17
+ #[instance(u64)]
18
+ #[default(0)]
19
+ Count,
20
+
21
+ #[instance(u64)]
22
+ #[default(0)]
23
+ ComposedCount,
24
+
25
+ #[persistent(u64)]
26
+ #[default(0)]
27
+ InboundCount { eid: u32 },
28
+
29
+ #[persistent(u64)]
30
+ #[default(0)]
31
+ OutboundCount { eid: u32 },
32
+ }
@@ -0,0 +1,24 @@
1
+ use soroban_sdk::{
2
+ testutils::{MockAuth, MockAuthInvoke},
3
+ token::StellarAssetClient,
4
+ Address, Env, IntoVal,
5
+ };
6
+
7
+ pub mod test_codec;
8
+ pub mod test_counter;
9
+ pub mod test_u256_ext;
10
+
11
+ pub fn mint_to(env: &Env, owner: &Address, native_token: &Address, to: &Address, amount: i128) {
12
+ env.mock_auths(&[MockAuth {
13
+ address: owner,
14
+ invoke: &MockAuthInvoke {
15
+ contract: native_token,
16
+ fn_name: "mint",
17
+ args: (to, amount).into_val(env),
18
+ sub_invokes: &[],
19
+ },
20
+ }]);
21
+
22
+ let sac = StellarAssetClient::new(env, native_token);
23
+ sac.mint(to, &amount);
24
+ }
@@ -0,0 +1,64 @@
1
+ use crate::codec::*;
2
+ use soroban_sdk::{Bytes, Env};
3
+
4
+ #[test]
5
+ fn test_encode_and_decode() {
6
+ let env = Env::default();
7
+ let _msg_type = 1;
8
+ let _src_eid = 1;
9
+ let data = encode(&env, _msg_type.into(), _src_eid);
10
+ assert_eq!(msg_type(&data), MsgType::from(_msg_type));
11
+ assert_eq!(src_eid(&data), _src_eid);
12
+ }
13
+
14
+ #[test]
15
+ fn test_encode_and_decode_with_value() {
16
+ let env = Env::default();
17
+ let _msg_type = 1;
18
+ let _src_eid = 1;
19
+ let _value = 100u32;
20
+ let data = encode_with_value(&env, _msg_type.into(), _src_eid, _value);
21
+ assert_eq!(msg_type(&data), MsgType::from(_msg_type));
22
+ assert_eq!(src_eid(&data), _src_eid);
23
+ assert_eq!(value(&env, &data), _value as i128);
24
+ }
25
+
26
+ #[test]
27
+ fn test_msg_type() {
28
+ let env = Env::default();
29
+ let _msg_type = 1;
30
+ let data = encode(&env, _msg_type.into(), 1);
31
+ assert_eq!(msg_type(&data), MsgType::from(_msg_type));
32
+ }
33
+
34
+ #[test]
35
+ fn test_zero_value_when_not_provided() {
36
+ let env = Env::default();
37
+ let data = encode(&env, 1.into(), 1);
38
+ assert_eq!(value(&env, &data), 0i128);
39
+ }
40
+
41
+ #[test]
42
+ #[should_panic(expected = "cannot get msg type")]
43
+ fn test_msg_type_panic_on_invalid_data() {
44
+ let env = Env::default();
45
+ let data = Bytes::new(&env);
46
+ msg_type(&data);
47
+ }
48
+
49
+ #[test]
50
+ #[should_panic(expected = "object index out of bounds")]
51
+ fn test_src_eid_panic_on_invalid_data() {
52
+ let env = Env::default();
53
+ let data = Bytes::new(&env);
54
+ src_eid(&data);
55
+ }
56
+
57
+ #[test]
58
+ #[should_panic(expected = "expected fixed-length bytes slice, got slice with different size")]
59
+ fn test_value_panic_on_short_value() {
60
+ let env = Env::default();
61
+ let mut data = encode(&env, 1.into(), 1);
62
+ data.extend_from_array(&[0u8; 3]);
63
+ value(&env, &data);
64
+ }
@@ -0,0 +1,387 @@
1
+ extern crate std;
2
+
3
+ use endpoint_v2::{MessagingFee, MessagingParams, MessagingReceipt, Origin};
4
+ use soroban_sdk::{
5
+ contract, contractimpl, log, symbol_short,
6
+ testutils::{Address as _, MockAuth, MockAuthInvoke},
7
+ Address, Bytes, BytesN, Env, IntoVal,
8
+ };
9
+
10
+ use crate::{
11
+ codec::{self, MsgType},
12
+ counter::{Counter, CounterClient},
13
+ errors::CounterError,
14
+ tests::mint_to,
15
+ };
16
+
17
+ struct TestSetup<'a> {
18
+ env: Env,
19
+ owner: Address,
20
+ counter: CounterClient<'a>,
21
+ endpoint: Address,
22
+ native_token: Address,
23
+ }
24
+
25
+ #[contract]
26
+ pub struct DummyEndpoint;
27
+
28
+ #[contractimpl]
29
+ impl DummyEndpoint {
30
+ pub fn __constructor(env: Env, native_token: Address) {
31
+ env.storage().instance().set(&symbol_short!("ntk"), &native_token);
32
+ }
33
+
34
+ pub fn set_delegate(_env: Env, _oapp: Address, _delegate: Address) {
35
+ // do nothing
36
+ }
37
+
38
+ pub fn eid(_env: Env) -> u32 {
39
+ 100
40
+ }
41
+
42
+ pub fn native_token(env: Env) -> Address {
43
+ env.storage().instance().get(&symbol_short!("ntk")).unwrap()
44
+ }
45
+
46
+ pub fn send(env: Env, _sender: Address, _params: MessagingParams, _refund_address: Address) -> MessagingReceipt {
47
+ MessagingReceipt {
48
+ guid: BytesN::from_array(&env, &[1u8; 32]),
49
+ nonce: 1,
50
+ fee: MessagingFee { native_fee: 100, zro_fee: 0 },
51
+ }
52
+ }
53
+
54
+ pub fn skip(_env: Env, _caller: Address, _receiver: Address, _src_eid: u32, _sender: BytesN<32>, _nonce: u64) {
55
+ // do nothing
56
+ }
57
+
58
+ pub fn clear(_env: Env, _caller: Address, _origin: Origin, _receiver: Address, _guid: BytesN<32>, _message: Bytes) {
59
+ // do nothing
60
+ }
61
+
62
+ pub fn clear_compose(
63
+ _env: Env,
64
+ _composer: Address,
65
+ _from: Address,
66
+ _guid: BytesN<32>,
67
+ _index: u32,
68
+ _message: Bytes,
69
+ ) {
70
+ // do nothing
71
+ }
72
+ }
73
+
74
+ fn setup<'a>() -> TestSetup<'a> {
75
+ let env = Env::default();
76
+ let owner = Address::generate(&env);
77
+ let sac = env.register_stellar_asset_contract_v2(owner.clone());
78
+
79
+ let endpoint = env.register(DummyEndpoint, (&sac.address(),));
80
+ let counter = env.register(Counter, (&owner, &endpoint, &owner));
81
+ let counter_client = CounterClient::new(&env, &counter);
82
+
83
+ log!(&env, "native_token: {}", sac.address());
84
+ log!(&env, "endpoint: {}", endpoint);
85
+ log!(&env, "counter: {}", counter);
86
+
87
+ TestSetup { env, owner, counter: counter_client, endpoint, native_token: sac.address() }
88
+ }
89
+
90
+ fn setup_mock_peer(env: &Env, owner: &Address, counter: &CounterClient<'_>, dst_eid: u32) -> BytesN<32> {
91
+ let peer = BytesN::from_array(env, &[1u8; 32]);
92
+ let peer_option = Some(peer.clone());
93
+ env.mock_auths(&[MockAuth {
94
+ address: owner,
95
+ invoke: &MockAuthInvoke {
96
+ contract: &counter.address,
97
+ fn_name: "set_peer",
98
+ args: (&dst_eid, &peer_option, owner).into_val(env),
99
+ sub_invokes: &[],
100
+ },
101
+ }]);
102
+ counter.set_peer(&dst_eid, &peer_option, owner);
103
+ peer
104
+ }
105
+
106
+ fn set_ordered_nonce(env: &Env, owner: &Address, counter: &CounterClient<'_>, ordered_nonce: bool) {
107
+ env.mock_auths(&[MockAuth {
108
+ address: owner,
109
+ invoke: &MockAuthInvoke {
110
+ contract: &counter.address,
111
+ fn_name: "set_ordered_nonce",
112
+ args: (&ordered_nonce,).into_val(env),
113
+ sub_invokes: &[],
114
+ },
115
+ }]);
116
+
117
+ counter.set_ordered_nonce(&ordered_nonce);
118
+ }
119
+
120
+ // tests for major entry functions
121
+
122
+ #[test]
123
+ fn test_increment() {
124
+ let TestSetup { env, owner, counter, endpoint, native_token, .. } = setup();
125
+
126
+ let sender = Address::generate(&env);
127
+ let dst_eid = 101;
128
+ let msg_type = MsgType::Vanilla;
129
+ let fee = MessagingFee { native_fee: 100, zro_fee: 0 };
130
+
131
+ setup_mock_peer(&env, &owner, &counter, dst_eid);
132
+ mint_to(&env, &owner, &native_token, &sender, fee.native_fee);
133
+
134
+ env.mock_auths(&[MockAuth {
135
+ address: &sender,
136
+ invoke: &MockAuthInvoke {
137
+ contract: &counter.address,
138
+ fn_name: "increment",
139
+ args: (&sender, &dst_eid, &(msg_type as u32), &Bytes::new(&env), &fee).into_val(&env),
140
+ sub_invokes: &[MockAuthInvoke {
141
+ contract: &native_token,
142
+ fn_name: "transfer",
143
+ args: (&sender, &endpoint, &fee.native_fee).into_val(&env),
144
+ sub_invokes: &[],
145
+ }],
146
+ },
147
+ }]);
148
+ counter.increment(&sender, &dst_eid, &(msg_type as u32), &Bytes::new(&env), &fee);
149
+
150
+ assert_eq!(counter.outbound_count(&dst_eid), 1);
151
+ }
152
+
153
+ #[test]
154
+ fn test_lz_receive_vanilla() {
155
+ let TestSetup { env, owner, counter, native_token, .. } = setup();
156
+
157
+ let origin = Origin { src_eid: 101, sender: BytesN::from_array(&env, &[1u8; 32]), nonce: 1 };
158
+ let guid = BytesN::from_array(&env, &[2u8; 32]);
159
+ let value = 100;
160
+ let message = codec::encode_with_value(&env, MsgType::Vanilla, origin.src_eid, value);
161
+ let executor = Address::generate(&env);
162
+ let extra_data = Bytes::new(&env);
163
+
164
+ setup_mock_peer(&env, &owner, &counter, origin.src_eid);
165
+
166
+ mint_to(&env, &owner, &native_token, &executor, value as i128);
167
+
168
+ let sub_invokes_with_transfer = MockAuthInvoke {
169
+ contract: &native_token,
170
+ fn_name: "transfer",
171
+ args: (&executor, &counter.address, &(value as i128)).into_val(&env),
172
+ sub_invokes: &[],
173
+ };
174
+ env.mock_auths(&[MockAuth {
175
+ address: &executor,
176
+ invoke: &MockAuthInvoke {
177
+ contract: &counter.address,
178
+ fn_name: "lz_receive",
179
+ args: (&executor, &origin, &guid, &message, &extra_data, &(value as i128)).into_val(&env),
180
+ sub_invokes: &[sub_invokes_with_transfer],
181
+ },
182
+ }]);
183
+
184
+ counter.lz_receive(&executor, &origin, &guid, &message, &extra_data, &(value as i128));
185
+
186
+ assert_eq!(counter.count(), 1);
187
+ assert_eq!(counter.inbound_count(&origin.src_eid), 1);
188
+ }
189
+
190
+ #[test]
191
+ #[should_panic(expected = "HostError: Error(Auth, InvalidAction)")]
192
+ fn test_lz_receive_not_from_executor() {
193
+ let TestSetup { env, owner, counter, .. } = setup();
194
+
195
+ let origin = Origin { src_eid: 101, sender: BytesN::from_array(&env, &[1u8; 32]), nonce: 1 };
196
+ let guid = BytesN::from_array(&env, &[2u8; 32]);
197
+ let value = 100;
198
+ let message = codec::encode_with_value(&env, MsgType::Vanilla, origin.src_eid, value);
199
+ let executor = Address::generate(&env);
200
+ let extra_data = Bytes::new(&env);
201
+
202
+ setup_mock_peer(&env, &owner, &counter, origin.src_eid);
203
+ counter.lz_receive(&executor, &origin, &guid, &message, &extra_data, &(value as i128));
204
+ }
205
+
206
+ #[test]
207
+ fn test_lz_receive_vanilla_ordered_nonce_wrong_nonce() {
208
+ let TestSetup { env, owner, counter, native_token, .. } = setup();
209
+
210
+ // next nonce should be 1 instead of 999
211
+ let origin = Origin { src_eid: 101, sender: BytesN::from_array(&env, &[1u8; 32]), nonce: 999 };
212
+ let guid = BytesN::from_array(&env, &[2u8; 32]);
213
+ let value = 100;
214
+ let message = codec::encode_with_value(&env, MsgType::Vanilla, origin.src_eid, value);
215
+ let executor = Address::generate(&env);
216
+ let extra_data = Bytes::new(&env);
217
+
218
+ setup_mock_peer(&env, &owner, &counter, origin.src_eid);
219
+ mint_to(&env, &owner, &native_token, &executor, value as i128);
220
+
221
+ let sub_invokes_with_transfer = MockAuthInvoke {
222
+ contract: &native_token,
223
+ fn_name: "transfer",
224
+ args: (&executor, &counter.address, &(value as i128)).into_val(&env),
225
+ sub_invokes: &[],
226
+ };
227
+ set_ordered_nonce(&env, &owner, &counter, true);
228
+
229
+ env.mock_auths(&[MockAuth {
230
+ address: &executor,
231
+ invoke: &MockAuthInvoke {
232
+ contract: &counter.address,
233
+ fn_name: "lz_receive",
234
+ args: (&executor, &origin, &guid, &message, &extra_data, &(value as i128)).into_val(&env),
235
+ sub_invokes: &[sub_invokes_with_transfer],
236
+ },
237
+ }]);
238
+
239
+ let result = counter.try_lz_receive(&executor, &origin, &guid, &message, &extra_data, &(value as i128));
240
+ assert_eq!(result.err().unwrap().ok().unwrap(), CounterError::OAppInvalidNonce.into());
241
+ }
242
+
243
+ #[test]
244
+ fn test_lz_compose() {
245
+ let TestSetup { env, owner, counter, native_token, .. } = setup();
246
+
247
+ let from = Address::generate(&env);
248
+ let origin = Origin { src_eid: 101, sender: BytesN::from_array(&env, &[1u8; 32]), nonce: 1 };
249
+ let guid = BytesN::from_array(&env, &[2u8; 32]);
250
+ let value = 100;
251
+ let message = codec::encode_with_value(&env, MsgType::Composed, origin.src_eid, value);
252
+ let executor = Address::generate(&env);
253
+ let extra_data = Bytes::new(&env);
254
+
255
+ mint_to(&env, &owner, &native_token, &executor, value as i128);
256
+ let sub_invokes_with_transfer = MockAuthInvoke {
257
+ contract: &native_token,
258
+ fn_name: "transfer",
259
+ args: (&executor, &counter.address, &(value as i128)).into_val(&env),
260
+ sub_invokes: &[],
261
+ };
262
+ env.mock_auths(&[MockAuth {
263
+ address: &executor,
264
+ invoke: &MockAuthInvoke {
265
+ contract: &counter.address,
266
+ fn_name: "lz_compose",
267
+ args: (&executor, &from, &guid, &0_u32, &message, &extra_data, &(value as i128)).into_val(&env),
268
+ sub_invokes: &[sub_invokes_with_transfer],
269
+ },
270
+ }]);
271
+
272
+ counter.lz_compose(&executor, &from, &guid, &0, &message, &extra_data, &(value as i128));
273
+
274
+ assert_eq!(counter.composed_count(), 1);
275
+ }
276
+
277
+ #[test]
278
+ #[should_panic(expected = "HostError: Error(Auth, InvalidAction)")]
279
+ fn test_lz_compose_not_from_executor() {
280
+ let TestSetup { env, counter, .. } = setup();
281
+
282
+ let from = Address::generate(&env);
283
+ let origin = Origin { src_eid: 101, sender: BytesN::from_array(&env, &[1u8; 32]), nonce: 1 };
284
+ let guid = BytesN::from_array(&env, &[2u8; 32]);
285
+ let value = 100;
286
+ let message = codec::encode_with_value(&env, MsgType::Composed, origin.src_eid, value);
287
+ let executor = Address::generate(&env);
288
+ let extra_data = Bytes::new(&env);
289
+
290
+ counter.lz_compose(&executor, &from, &guid, &0, &message, &extra_data, &(value as i128));
291
+ }
292
+
293
+ #[test]
294
+ fn test_lz_compose_aba() {
295
+ let TestSetup { env, owner, counter, native_token, .. } = setup();
296
+
297
+ let from = Address::generate(&env);
298
+ let origin = Origin { src_eid: 101, sender: BytesN::from_array(&env, &[1u8; 32]), nonce: 1 };
299
+ let guid = BytesN::from_array(&env, &[2u8; 32]);
300
+ let value = 100;
301
+ let message = codec::encode_with_value(&env, MsgType::ComposedABA, origin.src_eid, value);
302
+ let executor = Address::generate(&env);
303
+ let extra_data = Bytes::new(&env);
304
+
305
+ setup_mock_peer(&env, &owner, &counter, origin.src_eid);
306
+ mint_to(&env, &owner, &native_token, &executor, value as i128);
307
+ let sub_invokes_with_transfer = MockAuthInvoke {
308
+ contract: &native_token,
309
+ fn_name: "transfer",
310
+ args: (&executor, &counter.address, &(value as i128)).into_val(&env),
311
+ sub_invokes: &[],
312
+ };
313
+ env.mock_auths(&[MockAuth {
314
+ address: &executor,
315
+ invoke: &MockAuthInvoke {
316
+ contract: &counter.address,
317
+ fn_name: "lz_compose",
318
+ args: (&executor, &from, &guid, &0_u32, &message, &extra_data, &(value as i128)).into_val(&env),
319
+ sub_invokes: &[sub_invokes_with_transfer],
320
+ },
321
+ }]);
322
+
323
+ counter.lz_compose(&executor, &from, &guid, &0_u32, &message, &extra_data, &(value as i128));
324
+
325
+ assert_eq!(counter.composed_count(), 1);
326
+ assert_eq!(counter.outbound_count(&origin.src_eid), 1);
327
+ }
328
+
329
+ // tests for one step functions
330
+
331
+ #[test]
332
+ fn test_next_nonce() {
333
+ let TestSetup { env, owner, counter, .. } = setup();
334
+
335
+ let src_eid = 101;
336
+ let sender = BytesN::from_array(&env, &[1u8; 32]);
337
+ let nonce = counter.next_nonce(&src_eid, &sender);
338
+ assert_eq!(nonce, 0);
339
+
340
+ set_ordered_nonce(&env, &owner, &counter, true);
341
+ let nonce = counter.next_nonce(&src_eid, &sender);
342
+ assert_eq!(nonce, 1);
343
+ }
344
+
345
+ #[test]
346
+ fn test_skip_inbound_nonce() {
347
+ let TestSetup { env, owner, counter, .. } = setup();
348
+
349
+ let src_eid = 101;
350
+ let sender = BytesN::from_array(&env, &[1u8; 32]);
351
+ let nonce = 1;
352
+
353
+ env.mock_auths(&[MockAuth {
354
+ address: &owner,
355
+ invoke: &MockAuthInvoke {
356
+ contract: &counter.address,
357
+ fn_name: "skip_inbound_nonce",
358
+ args: (&src_eid, &sender, &nonce).into_val(&env),
359
+ sub_invokes: &[],
360
+ },
361
+ }]);
362
+
363
+ counter.skip_inbound_nonce(&src_eid, &sender, &nonce);
364
+ }
365
+
366
+ #[test]
367
+ #[should_panic(expected = "HostError: Error(Auth, InvalidAction)")]
368
+ fn test_skip_inbound_nonce_no_owner() {
369
+ let TestSetup { env, counter, .. } = setup();
370
+
371
+ let src_eid = 101;
372
+ let sender = BytesN::from_array(&env, &[1u8; 32]);
373
+ let nonce = 1;
374
+
375
+ let non_owner = Address::generate(&env);
376
+ env.mock_auths(&[MockAuth {
377
+ address: &non_owner,
378
+ invoke: &MockAuthInvoke {
379
+ contract: &counter.address,
380
+ fn_name: "skip_inbound_nonce",
381
+ args: (&src_eid, &sender, &nonce).into_val(&env),
382
+ sub_invokes: &[],
383
+ },
384
+ }]);
385
+
386
+ counter.skip_inbound_nonce(&src_eid, &sender, &nonce);
387
+ }
@@ -0,0 +1,48 @@
1
+ use crate::u256_ext::U256Ext;
2
+ use soroban_sdk::{Env, U256};
3
+
4
+ #[test]
5
+ fn test_to_i128_with_small_value() {
6
+ let env = Env::default();
7
+ let val = U256::from_u128(&env, 100);
8
+ assert_eq!(val.to_i128(), Some(100i128));
9
+ }
10
+
11
+ #[test]
12
+ fn test_to_i128_with_zero() {
13
+ let env = Env::default();
14
+ let val = U256::from_u128(&env, 0);
15
+ assert_eq!(val.to_i128(), Some(0i128));
16
+ }
17
+
18
+ #[test]
19
+ fn test_to_i128_with_i128_max() {
20
+ let env = Env::default();
21
+ let val = U256::from_u128(&env, i128::MAX as u128);
22
+ assert_eq!(val.to_i128(), Some(i128::MAX));
23
+ }
24
+
25
+ #[test]
26
+ fn test_to_i128_with_value_larger_than_i128_max_returns_none() {
27
+ let env = Env::default();
28
+ // i128::MAX + 1 should not fit in i128
29
+ let val = U256::from_u128(&env, (i128::MAX as u128) + 1);
30
+ assert_eq!(val.to_i128(), None);
31
+ }
32
+
33
+ #[test]
34
+ fn test_to_i128_with_u128_max_returns_none() {
35
+ let env = Env::default();
36
+ let val = U256::from_u128(&env, u128::MAX);
37
+ assert_eq!(val.to_i128(), None);
38
+ }
39
+
40
+ #[test]
41
+ fn test_to_i128_with_high_bits_set_returns_none() {
42
+ let env = Env::default();
43
+ // Create a U256 with high 128 bits set (value > u128::MAX)
44
+ // from_parts takes (hi_hi, hi_lo, lo_hi, lo_lo) where each is u64
45
+ // Setting hi_hi or hi_lo to non-zero makes it > u128::MAX
46
+ let val = U256::from_parts(&env, 1, 0, 0, 0); // Sets bits 192-255
47
+ assert_eq!(val.to_i128(), None);
48
+ }
@@ -0,0 +1,21 @@
1
+ use soroban_sdk::U256;
2
+
3
+ pub trait U256Ext {
4
+ /// Converts U256 to i128 if the value fits.
5
+ /// Returns None if the value is too large to fit in an i128.
6
+ fn to_i128(&self) -> Option<i128>;
7
+ }
8
+
9
+ impl U256Ext for U256 {
10
+ fn to_i128(&self) -> Option<i128> {
11
+ // First try to convert to u128 (checks if high 128 bits are zero)
12
+ let u128_val = self.to_u128()?;
13
+
14
+ // Check if the u128 value fits within i128::MAX
15
+ if u128_val <= i128::MAX as u128 {
16
+ Some(u128_val as i128)
17
+ } else {
18
+ None
19
+ }
20
+ }
21
+ }