@layerzerolabs/protocol-stellar-v2 0.2.40 → 0.2.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/.turbo/turbo-build.log +312 -397
  2. package/.turbo/turbo-lint.log +185 -245
  3. package/.turbo/turbo-test.log +1846 -1942
  4. package/Cargo.lock +22 -127
  5. package/Cargo.toml +4 -6
  6. package/contracts/common-macros/src/lib.rs +38 -15
  7. package/contracts/common-macros/src/lz_contract.rs +12 -21
  8. package/contracts/common-macros/src/tests/lz_contract.rs +17 -8
  9. package/contracts/common-macros/src/tests/snapshots/common_macros__tests__lz_contract__snapshot_generated_lz_contract_code.snap +20 -0
  10. package/contracts/common-macros/src/upgradeable.rs +37 -30
  11. package/contracts/endpoint-v2/src/endpoint_v2.rs +4 -3
  12. package/contracts/endpoint-v2/src/errors.rs +2 -2
  13. package/contracts/endpoint-v2/src/messaging_channel.rs +11 -0
  14. package/contracts/endpoint-v2/src/messaging_composer.rs +1 -0
  15. package/contracts/endpoint-v2/src/tests/endpoint_v2/clear.rs +12 -25
  16. package/contracts/endpoint-v2/src/tests/endpoint_v2/initializable.rs +4 -4
  17. package/contracts/endpoint-v2/src/tests/endpoint_v2/verifiable.rs +50 -10
  18. package/contracts/endpoint-v2/src/tests/endpoint_v2/verify.rs +6 -35
  19. package/contracts/endpoint-v2/src/tests/messaging_channel/burn.rs +2 -2
  20. package/contracts/endpoint-v2/src/tests/messaging_channel/clear_payload.rs +50 -1
  21. package/contracts/endpoint-v2/src/tests/messaging_channel/inbound.rs +78 -0
  22. package/contracts/endpoint-v2/src/tests/messaging_channel/insert_and_drain_pending_nonces.rs +272 -0
  23. package/contracts/endpoint-v2/src/tests/messaging_channel/mod.rs +1 -0
  24. package/contracts/endpoint-v2/src/tests/messaging_channel/nilify.rs +10 -5
  25. package/contracts/endpoint-v2/src/tests/messaging_channel/skip.rs +30 -0
  26. package/contracts/macro-integration-tests/tests/runtime/oapp/mod.rs +25 -6
  27. package/contracts/macro-integration-tests/tests/runtime/oapp/oapp_core.rs +13 -11
  28. package/contracts/macro-integration-tests/tests/runtime/oapp/options_type3.rs +13 -10
  29. package/contracts/macro-integration-tests/tests/runtime/oapp/receiver.rs +15 -11
  30. package/contracts/macro-integration-tests/tests/runtime/oapp/sender.rs +5 -3
  31. package/contracts/macro-integration-tests/tests/runtime/ownable/mod.rs +1 -1
  32. package/contracts/macro-integration-tests/tests/runtime/ownable/two_step_transfer.rs +14 -12
  33. package/contracts/macro-integration-tests/tests/runtime/upgradeable/migrate_guard_and_state.rs +3 -9
  34. package/contracts/macro-integration-tests/tests/ui/lz_contract/fail/upgradeable_invalid_inner_option.stderr +24 -1
  35. package/contracts/macro-integration-tests/tests/ui/lz_contract/fail/upgradeable_missing_internal.stderr +3 -3
  36. package/contracts/macro-integration-tests/tests/ui/lz_contract/pass/upgradeable_rbac.rs +44 -0
  37. package/contracts/macro-integration-tests/tests/ui/oapp/fail/missing_auth_trait.rs +28 -0
  38. package/contracts/macro-integration-tests/tests/ui/oapp/fail/missing_auth_trait.stderr +397 -0
  39. package/contracts/macro-integration-tests/tests/ui/oapp/fail/missing_lz_receive_internal.rs +1 -0
  40. package/contracts/macro-integration-tests/tests/ui/oapp/fail/missing_lz_receive_internal.stderr +10 -10
  41. package/contracts/macro-integration-tests/tests/ui/oapp/pass/custom_all.rs +4 -0
  42. package/contracts/macro-integration-tests/tests/ui/oapp/pass/custom_single_trait.rs +7 -0
  43. package/contracts/macro-integration-tests/tests/ui/oapp/pass/minimal_contract.rs +5 -4
  44. package/contracts/macro-integration-tests/tests/ui/oapp/pass/struct_with_fields.rs +2 -0
  45. package/contracts/macro-integration-tests/tests/ui/ownable/pass/basic.rs +1 -1
  46. package/contracts/macro-integration-tests/tests/ui/upgradeable/fail/attr_args.stderr +1 -1
  47. package/contracts/macro-integration-tests/tests/ui/upgradeable/fail/missing_auth_trait.stderr +2 -2
  48. package/contracts/macro-integration-tests/tests/ui/upgradeable/fail/missing_upgradeable_internal.stderr +2 -2
  49. package/contracts/macro-integration-tests/tests/ui/upgradeable/pass/rbac.rs +44 -0
  50. package/contracts/oapps/counter/integration_tests/utils.rs +5 -3
  51. package/contracts/oapps/counter/src/counter.rs +4 -3
  52. package/contracts/oapps/counter/src/tests/mod.rs +16 -1
  53. package/contracts/oapps/counter/src/tests/test_counter.rs +5 -2
  54. package/contracts/oapps/oapp/src/oapp_core.rs +22 -8
  55. package/contracts/oapps/oapp/src/oapp_options_type3.rs +7 -5
  56. package/contracts/oapps/oapp/src/tests/mod.rs +21 -0
  57. package/contracts/oapps/oapp/src/tests/oapp_core.rs +14 -11
  58. package/contracts/oapps/oapp/src/tests/oapp_options_type3.rs +17 -10
  59. package/contracts/oapps/oapp/src/tests/oapp_receiver.rs +6 -3
  60. package/contracts/oapps/oapp/src/tests/oapp_sender.rs +5 -3
  61. package/contracts/oapps/oapp/src/tests/test_macros.rs +25 -0
  62. package/contracts/oapps/oapp-macros/src/generators.rs +12 -9
  63. package/contracts/oapps/oapp-macros/src/lib.rs +1 -1
  64. package/contracts/oapps/oapp-macros/src/tests/snapshots/oapp_macros__tests__oapp__snapshot_generate_oapp.snap +15 -7
  65. package/contracts/oapps/oft/integration-tests/setup.rs +22 -4
  66. package/contracts/oapps/oft/integration-tests/utils.rs +94 -13
  67. package/contracts/oapps/oft/src/extensions/oft_fee.rs +23 -10
  68. package/contracts/oapps/oft/src/extensions/pausable.rs +31 -10
  69. package/contracts/oapps/oft/src/extensions/rate_limiter.rs +9 -4
  70. package/contracts/oapps/oft/src/oft.rs +3 -3
  71. package/contracts/oapps/oft/src/tests/extensions/oft_fee.rs +39 -27
  72. package/contracts/oapps/oft/src/tests/extensions/pausable.rs +38 -24
  73. package/contracts/oapps/oft/src/tests/extensions/rate_limiter.rs +87 -69
  74. package/contracts/oapps/oft/src/tests/oft_types/lock_unlock.rs +1 -0
  75. package/contracts/oapps/oft-core/integration-tests/setup.rs +28 -3
  76. package/contracts/oapps/oft-core/src/oft_core.rs +11 -6
  77. package/contracts/oapps/oft-core/src/tests/test_msg_inspector.rs +20 -20
  78. package/contracts/oapps/oft-core/src/tests/test_utils.rs +33 -3
  79. package/contracts/upgrader/src/lib.rs +67 -30
  80. package/contracts/upgrader/src/tests/test_data/test_upgradeable_contract3.wasm +0 -0
  81. package/contracts/upgrader/src/tests/test_data/test_upgradeable_contract4.wasm +0 -0
  82. package/contracts/upgrader/src/tests/test_upgrader.rs +50 -4
  83. package/contracts/utils/src/ownable.rs +16 -5
  84. package/contracts/utils/src/tests/ownable.rs +39 -39
  85. package/contracts/utils/src/upgradeable.rs +60 -17
  86. package/docs/oapp-guide.md +18 -13
  87. package/package.json +5 -5
  88. package/sdk/.turbo/turbo-test.log +359 -348
  89. package/sdk/dist/generated/bml.d.ts +4 -4
  90. package/sdk/dist/generated/bml.js +6 -6
  91. package/sdk/dist/generated/counter.d.ts +269 -123
  92. package/sdk/dist/generated/counter.js +45 -25
  93. package/sdk/dist/generated/dvn.d.ts +4 -6
  94. package/sdk/dist/generated/dvn.js +8 -8
  95. package/sdk/dist/generated/dvn_fee_lib.d.ts +8 -10
  96. package/sdk/dist/generated/dvn_fee_lib.js +8 -8
  97. package/sdk/dist/generated/endpoint.d.ts +9 -9
  98. package/sdk/dist/generated/endpoint.js +9 -9
  99. package/sdk/dist/generated/executor.d.ts +9 -11
  100. package/sdk/dist/generated/executor.js +11 -11
  101. package/sdk/dist/generated/executor_fee_lib.d.ts +9 -11
  102. package/sdk/dist/generated/executor_fee_lib.js +11 -11
  103. package/sdk/dist/generated/executor_helper.d.ts +4 -4
  104. package/sdk/dist/generated/executor_helper.js +6 -6
  105. package/sdk/dist/generated/layerzero_view.d.ts +9 -11
  106. package/sdk/dist/generated/layerzero_view.js +11 -11
  107. package/sdk/dist/generated/oft.d.ts +323 -156
  108. package/sdk/dist/generated/oft.js +65 -43
  109. package/sdk/dist/generated/price_feed.d.ts +8 -10
  110. package/sdk/dist/generated/price_feed.js +8 -8
  111. package/sdk/dist/generated/sac_manager.d.ts +8 -8
  112. package/sdk/dist/generated/sac_manager.js +6 -6
  113. package/sdk/dist/generated/sml.d.ts +9 -9
  114. package/sdk/dist/generated/sml.js +9 -9
  115. package/sdk/dist/generated/treasury.d.ts +9 -9
  116. package/sdk/dist/generated/treasury.js +9 -9
  117. package/sdk/dist/generated/uln302.d.ts +9 -9
  118. package/sdk/dist/generated/uln302.js +9 -9
  119. package/sdk/dist/generated/upgrader.d.ts +25 -16
  120. package/sdk/dist/generated/upgrader.js +5 -5
  121. package/sdk/package.json +1 -1
  122. package/sdk/test/counter-sml.test.ts +20 -0
  123. package/sdk/test/counter-uln.test.ts +20 -0
  124. package/sdk/test/oft-sml.test.ts +22 -0
  125. package/sdk/test/upgrader.test.ts +1 -0
  126. package/ts-bindings-gen.toml +67 -0
  127. package/turbo.json +1 -8
  128. package/tools/ts-bindings-gen/Cargo.toml +0 -16
  129. package/tools/ts-bindings-gen/src/main.rs +0 -214
@@ -4,7 +4,7 @@ use proc_macro2::TokenStream;
4
4
  use quote::quote;
5
5
  use syn::{
6
6
  parse::{Parse, ParseStream},
7
- Ident, ItemStruct,
7
+ Ident, ItemStruct, Token,
8
8
  };
9
9
 
10
10
  /// Configuration options for the `#[upgradeable]` macro.
@@ -13,51 +13,52 @@ pub struct UpgradeableConfig {
13
13
  /// If true, generates a default no-op `UpgradeableInternal` implementation.
14
14
  /// Use this for initial deployments when no migration logic is needed yet.
15
15
  pub no_migration: bool,
16
+ /// If true, uses `UpgradeableRbac` (Auth + RoleBased) instead of `Upgradeable` (Auth only).
17
+ pub rbac: bool,
16
18
  }
17
19
 
18
20
  impl Parse for UpgradeableConfig {
19
21
  fn parse(input: ParseStream) -> syn::Result<Self> {
22
+ let mut config = Self::default();
20
23
  if input.is_empty() {
21
- return Ok(Self::default());
24
+ return Ok(config);
22
25
  }
23
26
 
24
- let ident: Ident = input.parse()?;
25
- if ident == "no_migration" {
26
- Ok(Self { no_migration: true })
27
- } else {
28
- Err(syn::Error::new(ident.span(), "expected `no_migration`"))
27
+ while !input.is_empty() {
28
+ let ident: Ident = input.parse()?;
29
+ match ident.to_string().as_str() {
30
+ "no_migration" => config.no_migration = true,
31
+ "rbac" => config.rbac = true,
32
+ _ => return Err(syn::Error::new(ident.span(), "expected `no_migration` or `rbac`")),
33
+ }
34
+
35
+ // Consume optional trailing comma
36
+ if input.peek(Token![,]) {
37
+ input.parse::<Token![,]>()?;
38
+ }
29
39
  }
40
+ Ok(config)
30
41
  }
31
42
  }
32
43
 
33
44
  /// Generates the upgradeable implementation from the `#[upgradeable]` attribute macro.
34
45
  ///
35
- /// This function generates the implementation of the `Upgradeable` trait for a
36
- /// given contract type, enabling the contract to be upgraded by replacing its
37
- /// WASM bytecode with migration support.
46
+ /// Generates an impl of `Upgradeable` or `UpgradeableRbac` for a contract type,
47
+ /// enabling upgrades by replacing WASM bytecode with migration support.
38
48
  ///
39
49
  /// # Behavior
40
50
  ///
41
- /// - Implements the `Upgradeable` trait using its default methods (which include auth).
51
+ /// - By default implements `Upgradeable` (Auth-based, `#[only_auth]`). With `rbac`,
52
+ /// implements `UpgradeableRbac` (Auth + RoleBased, `UPGRADER_ROLE`).
42
53
  /// - Sets the contract crate version as `"binver"` metadata using
43
- /// `soroban_sdk::contractmeta!`. Gets the crate version via the env variable
44
- /// `CARGO_PKG_VERSION` which corresponds to the "version" attribute in
45
- /// Cargo.toml. If no such attribute or if it is "0.0.0", skips this step.
46
- /// - By default, requires the contract to implement `UpgradeableInternal` trait.
47
- /// - With `no_migration` flag, generates a default no-op `UpgradeableInternal` impl.
48
- ///
49
- /// # Example
50
- /// ```ignore
51
- /// // Requires manual UpgradeableInternal implementation (default, safety first)
52
- /// #[ownable]
53
- /// #[upgradeable]
54
- /// pub struct MyContract;
54
+ /// `soroban_sdk::contractmeta!`. Uses `CARGO_PKG_VERSION` (from Cargo.toml
55
+ /// `[package]` version). Skips if missing or `"0.0.0"`.
56
+ /// - By default, requires the contract to implement `UpgradeableInternal`.
57
+ /// - With `no_migration`, generates a no-op `UpgradeableInternal` impl.
58
+ /// - With `rbac`, uses `UpgradeableRbac` (requires `RoleBasedAccessControl`, which
59
+ /// extends `Auth`) instead of `Upgradeable` (requires `Auth`).
55
60
  ///
56
- /// // Auto-generates no-op UpgradeableInternal (for initial deployment)
57
- /// #[ownable]
58
- /// #[upgradeable(no_migration)]
59
- /// pub struct MyContract;
60
- /// ```
61
+ /// See the `#[upgradeable]` macro documentation for full examples.
61
62
  pub fn generate_upgradeable_impl(attr: TokenStream, input: TokenStream) -> TokenStream {
62
63
  let config: UpgradeableConfig =
63
64
  syn::parse2(attr).unwrap_or_else(|e| panic!("failed to parse upgradeable config: {}", e));
@@ -78,17 +79,23 @@ pub fn generate_upgradeable_impl(attr: TokenStream, input: TokenStream) -> Token
78
79
  quote! {}
79
80
  };
80
81
 
82
+ let trait_path = if config.rbac {
83
+ quote! { utils::upgradeable::UpgradeableRbac }
84
+ } else {
85
+ quote! { utils::upgradeable::Upgradeable }
86
+ };
87
+
81
88
  quote! {
82
89
  #item_struct
83
90
 
84
- use utils::upgradeable::Upgradeable as _;
91
+ use #trait_path as _;
85
92
 
86
93
  #binver
87
94
 
88
95
  #default_internal_impl
89
96
 
90
97
  #[common_macros::contract_impl(contracttrait)]
91
- impl utils::upgradeable::Upgradeable for #name {}
98
+ impl #trait_path for #name {}
92
99
  }
93
100
  }
94
101
 
@@ -52,7 +52,7 @@ impl ILayerZeroEndpointV2 for EndpointV2 {
52
52
  let packet = Self::build_outbound_packet(env, sender, *dst_eid, receiver, message, nonce);
53
53
 
54
54
  let fee = SendLibClient::new(env, &send_lib).quote(&packet, options, pay_in_zro);
55
- assert_with_error!(env, fee.native_fee >= 0 && fee.zro_fee >= 0, EndpointError::InvalidFeeAmount);
55
+ assert_with_error!(env, fee.native_fee >= 0 && fee.zro_fee >= 0, EndpointError::InvalidAmount);
56
56
 
57
57
  fee
58
58
  }
@@ -127,6 +127,7 @@ impl ILayerZeroEndpointV2 for EndpointV2 {
127
127
  reason: &Bytes,
128
128
  ) {
129
129
  executor.require_auth();
130
+ assert_with_error!(env, gas >= 0 && value >= 0, EndpointError::InvalidAmount);
130
131
  LzReceiveAlert {
131
132
  receiver: receiver.clone(),
132
133
  executor: executor.clone(),
@@ -257,7 +258,7 @@ impl EndpointV2 {
257
258
  // Fee amounts are modeled as non-negative values. The field type is i128 for
258
259
  // compatibility with token APIs, but negative fees are always invalid and rejected
259
260
  // here, while zero amounts are treated as a no-op (skipped by the check below).
260
- assert_with_error!(env, r.amount >= 0, EndpointError::InvalidFeeAmount);
261
+ assert_with_error!(env, r.amount >= 0, EndpointError::InvalidAmount);
261
262
  if r.amount > 0 {
262
263
  assert_with_error!(env, native_fee_supplied >= r.amount, EndpointError::InsufficientNativeFee);
263
264
  native_fee_supplied -= r.amount;
@@ -286,7 +287,7 @@ impl EndpointV2 {
286
287
  // Fee amounts are modeled as non-negative values. The field type is i128 for
287
288
  // compatibility with token APIs, but negative fees are always invalid and rejected
288
289
  // here, while zero amounts are treated as a no-op (skipped by the check below).
289
- assert_with_error!(env, r.amount >= 0, EndpointError::InvalidFeeAmount);
290
+ assert_with_error!(env, r.amount >= 0, EndpointError::InvalidAmount);
290
291
  if r.amount > 0 {
291
292
  assert_with_error!(env, zro_fee_supplied >= r.amount, EndpointError::InsufficientZroFee);
292
293
  zro_fee_supplied -= r.amount;
@@ -18,8 +18,8 @@ pub enum EndpointError {
18
18
  InsufficientZroFee,
19
19
  /// Timeout expiry is invalid (already expired)
20
20
  InvalidExpiry,
21
- /// Fee amount is invalid (negative)
22
- InvalidFeeAmount,
21
+ /// Amount is invalid (negative)
22
+ InvalidAmount,
23
23
  /// Compose index exceeds maximum allowed value
24
24
  InvalidIndex,
25
25
  /// Nonce is invalid for the requested operation
@@ -322,5 +322,16 @@ mod test {
322
322
  ) {
323
323
  Self::clear_payload(env, receiver, src_eid, sender, nonce, payload)
324
324
  }
325
+
326
+ /// Test-only wrapper for insert_and_drain_pending_nonces.
327
+ pub fn insert_and_drain_pending_nonces_for_test(
328
+ env: &Env,
329
+ receiver: &Address,
330
+ src_eid: u32,
331
+ sender: &BytesN<32>,
332
+ new_nonce: u64,
333
+ ) {
334
+ Self::insert_and_drain_pending_nonces(env, receiver, src_eid, sender, new_nonce)
335
+ }
325
336
  }
326
337
  }
@@ -73,6 +73,7 @@ impl IMessagingComposer for EndpointV2 {
73
73
  reason: &Bytes,
74
74
  ) {
75
75
  executor.require_auth();
76
+ assert_with_error!(env, gas >= 0 && value >= 0, EndpointError::InvalidAmount);
76
77
  assert_compose_index(env, index);
77
78
  LzComposeAlert {
78
79
  executor: executor.clone(),
@@ -92,33 +92,18 @@ fn test_clear_removes_inbound_payload_hash() {
92
92
  // Now clear the payload
93
93
  clear_packet_with_auth(&context, &receiver, &origin, &receiver, &guid, &message);
94
94
 
95
+ // Verify PacketDelivered event was emitted.
96
+ assert_eq_event(
97
+ env,
98
+ &endpoint_client.address,
99
+ PacketDelivered { origin: origin.clone(), receiver: receiver.clone() },
100
+ );
101
+
95
102
  // Verify payload hash was removed via public interface
96
103
  let stored_hash = endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &nonce);
97
104
  assert_eq!(stored_hash, None);
98
105
  }
99
106
 
100
- // Event emission
101
- #[test]
102
- fn test_clear_emits_packet_delivered_event() {
103
- let context = setup();
104
- let env = &context.env;
105
- let endpoint_client = &context.endpoint_client;
106
-
107
- let src_eid = 2u32;
108
- let sender = BytesN::from_array(env, &[1u8; 32]);
109
- let receiver = env.register(MockReceiver, ());
110
- let nonce = 1u64;
111
-
112
- let message = Bytes::from_array(env, &[1, 2, 3, 4]);
113
- let guid = BytesN::from_array(env, &[5u8; 32]);
114
- let (_receive_lib, origin, _payload_hash) =
115
- arrange_verified_packet_with_auth(&context, src_eid, &sender, &receiver, nonce, &guid, &message);
116
-
117
- clear_packet_with_auth(&context, &receiver, &origin, &receiver, &guid, &message);
118
-
119
- assert_eq_event(env, &endpoint_client.address, PacketDelivered { origin: origin.clone(), receiver: receiver.clone() });
120
- }
121
-
122
107
  // Inbound nonce is advanced during verify, not during clear
123
108
  #[test]
124
109
  fn test_clear_does_not_change_inbound_nonce() {
@@ -147,7 +132,7 @@ fn test_clear_does_not_change_inbound_nonce() {
147
132
 
148
133
  // Sequential nonce behavior
149
134
  #[test]
150
- fn test_clear_success_sequential_nonces_update_lazy_nonce_to_latest() {
135
+ fn test_clear_success_sequential_nonces_keep_inbound_nonce_at_latest_verified() {
151
136
  let context = setup();
152
137
  let env = &context.env;
153
138
  let endpoint_client = &context.endpoint_client;
@@ -175,8 +160,10 @@ fn test_clear_success_sequential_nonces_update_lazy_nonce_to_latest() {
175
160
  let origin2 = Origin { src_eid, sender: sender.clone(), nonce: 2 };
176
161
 
177
162
  verify_packet_with_auth(&context, &receive_lib, &origin2, &receiver, &payload_hash2);
178
- clear_packet_with_auth(&context, &receiver, &origin2, &receiver, &guid2, &message2);
179
163
 
164
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
165
+
166
+ clear_packet_with_auth(&context, &receiver, &origin2, &receiver, &guid2, &message2);
180
167
  // Verify advanced inbound nonce.
181
168
  assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
182
169
  }
@@ -382,7 +369,7 @@ fn test_clear_failure_missing_intermediate_nonce() {
382
369
  }
383
370
 
384
371
  #[test]
385
- fn test_clear_does_not_advance_lazy_nonce_when_clearing_older_nonce() {
372
+ fn test_clear_does_not_advance_inbound_nonce_when_clearing_older_nonce() {
386
373
  let context = setup();
387
374
  let env = &context.env;
388
375
  let endpoint_client = &context.endpoint_client;
@@ -20,7 +20,7 @@ fn test_initializable_new_path_receiver_allows() {
20
20
  let sender = BytesN::from_array(&context.env, &[1u8; 32]);
21
21
  let origin = Origin { src_eid, sender, nonce: 1 };
22
22
 
23
- // For a new path (lazy nonce is 0), initializable depends on receiver contract.
23
+ // For a new path (inbound_nonce is 0), initializable depends on receiver contract.
24
24
  let result = endpoint_client.initializable(&origin, &receiver);
25
25
  assert!(result);
26
26
  }
@@ -36,12 +36,12 @@ fn test_initializable_new_path_receiver_rejects() {
36
36
  let sender = BytesN::from_array(&context.env, &[1u8; 32]);
37
37
  let origin = Origin { src_eid, sender, nonce: 1 };
38
38
 
39
- // For a new path (lazy nonce is 0), initializable depends on receiver contract.
39
+ // For a new path (inbound_nonce is 0), initializable depends on receiver contract.
40
40
  let result = endpoint_client.initializable(&origin, &receiver);
41
41
  assert!(!result);
42
42
  }
43
43
 
44
- // Established paths always return true (lazy nonce > 0)
44
+ // Established paths always return true (inbound_nonce > 0)
45
45
  #[test]
46
46
  fn test_initializable_established_path_always_true() {
47
47
  let context = setup();
@@ -58,7 +58,7 @@ fn test_initializable_established_path_always_true() {
58
58
  context.mock_auth(&receiver, "skip", (&receiver, &receiver, &src_eid, &sender, &nonce));
59
59
  context.endpoint_client.skip(&receiver, &receiver, &src_eid, &sender, &nonce);
60
60
 
61
- // Now the path is established (lazy_nonce > 0), it should return true regardless of receiver.
61
+ // Now the path is established (inbound_nonce > 0), it should return true regardless of receiver.
62
62
  let origin2 = Origin { src_eid, sender, nonce: 2 };
63
63
  let result = endpoint_client.initializable(&origin2, &receiver);
64
64
  assert!(result);
@@ -37,7 +37,7 @@ fn test_verifiable_new_path_nonce_1_true() {
37
37
 
38
38
  // Established path => verifiable when origin.nonce is in (inbound_nonce, inbound_nonce + 256]
39
39
  #[test]
40
- fn test_verifiable_after_skip_nonce_gt_lazy_true() {
40
+ fn test_verifiable_after_skip_nonce_gt_inbound_nonce_true() {
41
41
  let context = setup();
42
42
  let env = &context.env;
43
43
  let endpoint_client = &context.endpoint_client;
@@ -54,9 +54,9 @@ fn test_verifiable_after_skip_nonce_gt_lazy_true() {
54
54
  assert!(result);
55
55
  }
56
56
 
57
- // Nonce <= lazy is still verifiable when inbound payload hash exists
57
+ // Nonce <= inbound_nonce is still verifiable when an inbound payload hash exists
58
58
  #[test]
59
- fn test_verifiable_true_when_nonce_leq_lazy_but_payload_hash_exists() {
59
+ fn test_verifiable_true_when_nonce_leq_inbound_nonce_but_payload_hash_exists() {
60
60
  let context = setup();
61
61
  let env = &context.env;
62
62
  let endpoint_client = &context.endpoint_client;
@@ -68,28 +68,28 @@ fn test_verifiable_true_when_nonce_leq_lazy_but_payload_hash_exists() {
68
68
  // Setup receive library (needed to store a payload hash via verify).
69
69
  let receive_lib = context.setup_default_receive_lib(src_eid, 0);
70
70
 
71
- // Establish the path by skipping nonce 1 (lazy becomes 1).
71
+ // Establish the path by skipping nonce 1 (inbound_nonce becomes 1).
72
72
  skip_with_auth(&context, &receiver, src_eid, &sender, 1);
73
73
 
74
- // Verify nonce 2 (allowed because 2 > lazy 1) which stores inbound payload hash for nonce 2.
74
+ // Verify nonce 2 (allowed because 2 > inbound_nonce 1) which stores inbound payload hash for nonce 2.
75
75
  let origin2 = Origin { src_eid, sender: sender.clone(), nonce: 2u64 };
76
76
  let payload_hash2 = BytesN::from_array(env, &[0x33u8; 32]);
77
77
  verify_with_auth(&context, &receive_lib, &origin2, &receiver, &payload_hash2);
78
78
 
79
- // Advance lazy nonce to 3 while keeping payload hash for 2 (skip sets lazy nonce only).
79
+ // Advance inbound_nonce to 3 while keeping payload hash for 2 (skip doesn't clear payload hashes).
80
80
  skip_with_auth(&context, &receiver, src_eid, &sender, 3);
81
81
 
82
82
  // Sanity: inbound nonce was advanced, and the payload hash for nonce 2 still exists.
83
83
  assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 3);
84
84
  assert!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &2u64).is_some());
85
85
 
86
- // Now nonce 2 <= lazy 3, but payload hash exists -> verifiable should be true.
86
+ // Now nonce 2 <= inbound_nonce 3, but payload hash exists -> verifiable should be true.
87
87
  assert!(endpoint_client.verifiable(&origin2, &receiver));
88
88
  }
89
89
 
90
- // Boundary case (nonce == lazy)
90
+ // Boundary case (nonce == inbound_nonce)
91
91
  #[test]
92
- fn test_verifiable_nonce_eq_lazy_false_without_payload_hash() {
92
+ fn test_verifiable_nonce_eq_inbound_nonce_false_without_payload_hash() {
93
93
  let context = setup();
94
94
  let env = &context.env;
95
95
  let endpoint_client = &context.endpoint_client;
@@ -103,7 +103,7 @@ fn test_verifiable_nonce_eq_lazy_false_without_payload_hash() {
103
103
  skip_with_auth(&context, &receiver, src_eid, &sender, 2);
104
104
  assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
105
105
 
106
- // nonce == lazy and payload hash missing -> verifiable should be false.
106
+ // nonce == inbound_nonce and payload hash missing -> verifiable should be false.
107
107
  let origin2 = Origin { src_eid, sender: sender.clone(), nonce: 2u64 };
108
108
  assert!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &2u64).is_none());
109
109
  assert!(!endpoint_client.verifiable(&origin2, &receiver));
@@ -145,3 +145,43 @@ fn test_verifiable_upper_bound_is_enforced_when_inbound_nonce_nonzero() {
145
145
  assert!(endpoint_client.verifiable(&ok, &receiver));
146
146
  assert!(!endpoint_client.verifiable(&too_far, &receiver));
147
147
  }
148
+
149
+
150
+ #[test]
151
+ fn test_verifiable_true_when_payload_hash_exists_even_if_nonce_outside_window() {
152
+ let context = setup();
153
+ let env = &context.env;
154
+ let endpoint_client = &context.endpoint_client;
155
+
156
+ let src_eid = 2u32;
157
+ let sender = BytesN::from_array(env, &[1u8; 32]);
158
+ let receiver = soroban_sdk::Address::generate(env);
159
+
160
+ // NOTE: This state should be unreachable in normal execution flows.
161
+ //
162
+ // In production, payload hashes are written by `verify()` -> `inbound()`. For *new* nonces
163
+ // (`nonce > inbound_nonce`), `inbound()` calls `insert_and_drain_pending_nonces()` which enforces
164
+ // the pending window bound (`nonce <= inbound_nonce + 256`).
165
+ //
166
+ // Therefore, with `inbound_nonce == 0`, a payload hash at a "far" nonce (e.g. 999) cannot be
167
+ // created through valid contract calls (it would fail `verifiable()` and/or the pending window
168
+ // bound on insertion).
169
+ //
170
+ // We write storage directly here to simulate corrupted/invalid state and to ensure `verifiable()`
171
+ // preserves its OR semantics: if a payload hash exists for a nonce, `verifiable()` must return
172
+ // true even when the nonce is outside the pending window.
173
+
174
+ // Choose a nonce that is clearly outside the pending window when inbound_nonce == 0.
175
+ let far_nonce = 999u64;
176
+
177
+ // Write payload hash directly to storage to exercise the OR branch:
178
+ // `EndpointStorage::has_inbound_payload_hash(...) == true` should make verifiable() return true,
179
+ // even if the nonce is outside (inbound_nonce, inbound_nonce + 256].
180
+ let payload_hash = BytesN::from_array(env, &[0x42u8; 32]);
181
+ env.as_contract(&endpoint_client.address, || {
182
+ storage::EndpointStorage::set_inbound_payload_hash(env, &receiver, src_eid, &sender, far_nonce, &payload_hash)
183
+ });
184
+
185
+ let origin_far = Origin { src_eid, sender, nonce: far_nonce };
186
+ assert!(endpoint_client.verifiable(&origin_far, &receiver));
187
+ }
@@ -63,35 +63,6 @@ fn test_verify_success() {
63
63
  }
64
64
 
65
65
  // Storage & Nonce Behavior
66
- #[test]
67
- fn test_verify_stores_payload_hash() {
68
- let context = setup();
69
- let env = &context.env;
70
- let endpoint_client = &context.endpoint_client;
71
-
72
- let src_eid = 2u32;
73
- let sender = BytesN::from_array(env, &[1u8; 32]);
74
- let receiver = env.register(MockReceiver, ());
75
- let nonce = 1u64;
76
-
77
- // Setup receive library
78
- let receive_lib = context.setup_default_receive_lib(src_eid, 0);
79
-
80
- // Create payload hash
81
- let payload_hash = default_payload_hash(env);
82
-
83
- let origin = Origin { src_eid, sender: sender.clone(), nonce };
84
-
85
- // Mock auth for receive_lib
86
- context.mock_auth(&receive_lib, "verify", (&receive_lib, &origin, &receiver, &payload_hash));
87
-
88
- endpoint_client.verify(&receive_lib, &origin, &receiver, &payload_hash);
89
-
90
- // Verify inbound payload hash was stored
91
- let stored_hash = endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &nonce);
92
- assert_eq!(stored_hash, Some(payload_hash));
93
- }
94
-
95
66
  #[test]
96
67
  fn test_verify_overwrites_payload_hash_for_same_nonce() {
97
68
  let context = setup();
@@ -262,7 +233,7 @@ fn test_verify_path_not_initializable() {
262
233
  context.mock_auth(&receive_lib, "verify", (&receive_lib, &origin, &receiver, &payload_hash));
263
234
 
264
235
  // Library validation passes (receive_lib matches default)
265
- // initializable checks: lazy_inbound_nonce == 0, and receiver.allow_initialize_path(origin) == false
236
+ // initializable checks: inbound_nonce == 0, and receiver.allow_initialize_path(origin) == false
266
237
  // So initializable returns false, should panic with PathNotInitializable error
267
238
  let result = endpoint_client.try_verify(&receive_lib, &origin, &receiver, &payload_hash);
268
239
  assert_eq!(result.err().unwrap().ok().unwrap(), EndpointError::PathNotInitializable.into());
@@ -281,7 +252,7 @@ fn test_verify_path_not_verifiable() {
281
252
  // Setup receive library and set as default (so library validation passes)
282
253
  let receive_lib = context.setup_default_receive_lib(src_eid, 0);
283
254
 
284
- // Skip nonce 1 to set lazy_inbound_nonce to 1 (so initializable passes: lazy_nonce > 0)
255
+ // Skip nonce 1 to advance inbound_nonce to 1 (so initializable passes: inbound_nonce > 0)
285
256
  context.mock_auth(&receiver, "skip", (&receiver, &receiver, &src_eid, &sender, &1u64));
286
257
  endpoint_client.skip(&receiver, &receiver, &src_eid, &sender, &1);
287
258
 
@@ -291,15 +262,15 @@ fn test_verify_path_not_verifiable() {
291
262
  let payload = build_payload(env, &guid, &message);
292
263
  let payload_hash = keccak256(env, &payload);
293
264
 
294
- // Try to verify nonce 1, but lazy_nonce is already 1, so nonce 1 is not verifiable
295
- // verifiable checks: nonce > lazy_inbound_nonce (1 > 1 is false) OR has payload hash (false)
265
+ // Try to verify nonce 1, but inbound_nonce is already 1, so nonce 1 is not verifiable
266
+ // verifiable checks: nonce > inbound_nonce (1 > 1 is false) OR has payload hash (false)
296
267
  let origin = Origin { src_eid, sender, nonce: 1 };
297
268
 
298
269
  // Mock auth for receive_lib
299
270
  context.mock_auth(&receive_lib, "verify", (&receive_lib, &origin, &receiver, &payload_hash));
300
271
 
301
- // Library validation passes, initializable passes (lazy_nonce > 0)
302
- // verifiable fails (nonce <= lazy_nonce and no payload hash), should panic with PathNotVerifiable error
272
+ // Library validation passes, initializable passes (inbound_nonce > 0)
273
+ // verifiable fails (nonce <= inbound_nonce and no payload hash), should panic with PathNotVerifiable error
303
274
  let result = endpoint_client.try_verify(&receive_lib, &origin, &receiver, &payload_hash);
304
275
  assert_eq!(result.err().unwrap().ok().unwrap(), EndpointError::PathNotVerifiable.into());
305
276
  }
@@ -258,9 +258,9 @@ fn test_burn_payload_hash_not_found_when_storage_none() {
258
258
  assert_eq!(result.err().unwrap().ok().unwrap(), EndpointError::PayloadHashNotFound.into());
259
259
  }
260
260
 
261
- // Failure when nonce is greater than lazy nonce
261
+ // Failure when nonce is greater than `inbound_nonce`
262
262
  #[test]
263
- fn test_burn_invalid_nonce_when_greater_than_lazy_nonce() {
263
+ fn test_burn_invalid_nonce_when_greater_than_inbound_nonce() {
264
264
  let context = setup();
265
265
  let env = &context.env;
266
266
 
@@ -52,6 +52,8 @@ fn test_clear_payload_success() {
52
52
  let payload_hash = inbound_as_verified_from_payload(&context, &receiver, src_eid, &sender, nonce, &payload);
53
53
  assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &nonce), Some(payload_hash.clone()));
54
54
 
55
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), nonce);
56
+
55
57
  clear_payload(&context, &receiver, src_eid, &sender, nonce, &payload);
56
58
 
57
59
  assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &nonce), None);
@@ -88,6 +90,42 @@ fn test_clear_payload_keeps_other_payload_hashes_intact() {
88
90
  let _ = hash3; // hash3 is only used to ensure it was computed and stored for nonce 3.
89
91
  }
90
92
 
93
+ #[test]
94
+ fn test_clear_payload_does_not_change_pending_inbound_nonces() {
95
+ let context = setup();
96
+ let env = &context.env;
97
+ let endpoint_client = &context.endpoint_client;
98
+
99
+ let receiver = Address::generate(env);
100
+ let src_eid = 2;
101
+ let sender = BytesN::from_array(env, &[1u8; 32]);
102
+
103
+ // Verify 1 and 2 consecutively, then verify 4 out-of-order.
104
+ // This produces: inbound_nonce = 2, pending_inbound_nonces = [4].
105
+ let payload1 = Bytes::from_array(env, &[0x01]);
106
+ let payload2 = Bytes::from_array(env, &[0x02]);
107
+ let payload4 = Bytes::from_array(env, &[0x04]);
108
+
109
+ let hash1 = inbound_as_verified_from_payload(&context, &receiver, src_eid, &sender, 1, &payload1);
110
+ let hash2 = inbound_as_verified_from_payload(&context, &receiver, src_eid, &sender, 2, &payload2);
111
+ let hash4 = inbound_as_verified_from_payload(&context, &receiver, src_eid, &sender, 4, &payload4);
112
+
113
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
114
+ assert_eq!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender), soroban_sdk::vec![env, 4u64]);
115
+
116
+ // Clear nonce 2. This must not affect pending nonces (which are > inbound_nonce).
117
+ clear_payload(&context, &receiver, src_eid, &sender, 2, &payload2);
118
+
119
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
120
+ assert_eq!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender), soroban_sdk::vec![env, 4u64]);
121
+
122
+ // Only nonce 2 is cleared; others remain.
123
+ assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &2), None);
124
+ assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &1), Some(hash1));
125
+ assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &4), Some(hash4));
126
+ let _ = hash2; // hash2 is only used to ensure it was computed and stored for nonce 2.
127
+ }
128
+
91
129
  // Clearing a nonce <= inbound nonce does not update inbound nonce
92
130
  #[test]
93
131
  fn test_clear_payload_does_not_update_inbound_nonce_when_nonce_is_not_greater() {
@@ -132,7 +170,7 @@ fn test_clear_payload_payload_hash_not_found_when_nonce_is_checkpointed_but_miss
132
170
  let src_eid = 2;
133
171
  let sender = BytesN::from_array(env, &[1u8; 32]);
134
172
 
135
- // nonce <= lazy_nonce, so clear_payload will NOT run the "has_payload for all intermediate nonces" check.
173
+ // nonce <= inbound_nonce, so clear_payload will NOT run the "has_payload for all intermediate nonces" check.
136
174
  // It should fail at the payload hash check instead.
137
175
  env.as_contract(&endpoint_client.address, || {
138
176
  storage::EndpointStorage::set_inbound_nonce(env, &receiver, src_eid, &sender, &5u64)
@@ -186,6 +224,7 @@ fn test_clear_payload_not_stored() {
186
224
  fn test_clear_payload_missing_intermediate_nonce() {
187
225
  let context = setup();
188
226
  let env = &context.env;
227
+ let endpoint_client = &context.endpoint_client;
189
228
 
190
229
  let receiver = Address::generate(env);
191
230
  let src_eid = 2;
@@ -198,9 +237,19 @@ fn test_clear_payload_missing_intermediate_nonce() {
198
237
  let _ = inbound_as_verified_from_payload(&context, &receiver, src_eid, &sender, 1, &payload1);
199
238
  let _ = inbound_as_verified_from_payload(&context, &receiver, src_eid, &sender, 3, &payload3);
200
239
 
240
+ // inbound_nonce should be 1 (nonce 2 is missing), and nonce 3 should be pending.
241
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 1);
242
+ assert_eq!(
243
+ endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender),
244
+ soroban_sdk::vec![env, 3u64]
245
+ );
246
+
201
247
  // Clearing nonce 1 succeeds.
202
248
  clear_payload(&context, &receiver, src_eid, &sender, 1, &payload1);
203
249
 
250
+ // clear_payload does not advance inbound_nonce; it remains 1.
251
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 1);
252
+
204
253
  // Try to clear nonce 3 - should panic because inbound_nonce is still 1 (nonce 3 is out-of-order).
205
254
  clear_payload(&context, &receiver, src_eid, &sender, 3, &payload3);
206
255
  }