@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 +52 -0
- package/LICENSE +23 -0
- package/clippy.toml +6 -0
- package/package.json +55 -0
- package/rust-toolchain.toml +4 -0
- package/rustfmt.toml +15 -0
- package/src/codec.rs +62 -0
- package/src/counter.rs +274 -0
- package/src/errors.rs +9 -0
- package/src/lib.rs +16 -0
- package/src/options.rs +30 -0
- package/src/storage.rs +32 -0
- package/src/tests/mod.rs +24 -0
- package/src/tests/test_codec.rs +64 -0
- package/src/tests/test_counter.rs +387 -0
- package/src/tests/test_u256_ext.rs +48 -0
- package/src/u256_ext.rs +21 -0
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
|
+
}
|
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
package/src/lib.rs
ADDED
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
|
+
}
|
package/src/tests/mod.rs
ADDED
|
@@ -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
|
+
}
|
package/src/u256_ext.rs
ADDED
|
@@ -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
|
+
}
|