@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
@@ -123,6 +123,36 @@ fn test_inbound_out_of_order_populates_pending_and_drains_when_gap_closed() {
123
123
  assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &2u64), Some(hash_2));
124
124
  }
125
125
 
126
+ #[test]
127
+ fn test_inbound_when_nonce_already_pending_does_not_advance_inbound_nonce() {
128
+ let context = setup();
129
+ let env = &context.env;
130
+ let endpoint_client = &context.endpoint_client;
131
+
132
+ let receiver = Address::generate(env);
133
+ let src_eid = 2;
134
+ let sender = BytesN::from_array(env, &[1u8; 32]);
135
+
136
+ // First inbound at nonce 2 adds it to pending (gap at nonce 1).
137
+ let hash_2_a = BytesN::from_array(env, &[0x22u8; 32]);
138
+ env.as_contract(&endpoint_client.address, || {
139
+ EndpointV2::inbound_for_test(env, &receiver, src_eid, &sender, 2u64, &hash_2_a)
140
+ });
141
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 0);
142
+ assert_eq!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender), soroban_sdk::vec![env, 2u64]);
143
+ assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &2u64), Some(hash_2_a.clone()));
144
+
145
+ // Re-inbound at the same nonce should overwrite the payload hash, but should NOT duplicate pending
146
+ // entries nor advance inbound_nonce (gap at nonce 1 still exists).
147
+ let hash_2_b = BytesN::from_array(env, &[0x23u8; 32]);
148
+ env.as_contract(&endpoint_client.address, || {
149
+ EndpointV2::inbound_for_test(env, &receiver, src_eid, &sender, 2u64, &hash_2_b)
150
+ });
151
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 0);
152
+ assert_eq!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender), soroban_sdk::vec![env, 2u64]);
153
+ assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &2u64), Some(hash_2_b));
154
+ }
155
+
126
156
  #[test]
127
157
  #[should_panic(expected = "Error(Contract, #11)")] // EndpointError::InvalidNonce
128
158
  fn test_inbound_rejects_nonce_beyond_pending_window() {
@@ -141,6 +171,52 @@ fn test_inbound_rejects_nonce_beyond_pending_window() {
141
171
  });
142
172
  }
143
173
 
174
+ #[test]
175
+ fn test_inbound_accepts_upper_bound_when_inbound_nonce_nonzero() {
176
+ let context = setup();
177
+ let env = &context.env;
178
+ let endpoint_client = &context.endpoint_client;
179
+
180
+ let receiver = Address::generate(env);
181
+ let src_eid = 2;
182
+ let sender = BytesN::from_array(env, &[1u8; 32]);
183
+
184
+ // Set inbound_nonce to 100 so the upper bound is 356 (= 100 + 256).
185
+ context.set_inbound_nonce(&receiver, src_eid, &sender, 100);
186
+
187
+ let hash = BytesN::from_array(env, &[0xabu8; 32]);
188
+ env.as_contract(&endpoint_client.address, || {
189
+ EndpointV2::inbound_for_test(env, &receiver, src_eid, &sender, 356u64, &hash)
190
+ });
191
+
192
+ // Nonce 356 is not consecutive to 100, so it stays pending and inbound_nonce does not advance.
193
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 100);
194
+ assert_eq!(
195
+ endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender),
196
+ soroban_sdk::vec![env, 356u64]
197
+ );
198
+ assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &356u64), Some(hash));
199
+ }
200
+
201
+ #[test]
202
+ #[should_panic(expected = "Error(Contract, #11)")] // EndpointError::InvalidNonce
203
+ fn test_inbound_rejects_beyond_upper_bound_when_inbound_nonce_nonzero() {
204
+ let context = setup();
205
+ let env = &context.env;
206
+ let endpoint_client = &context.endpoint_client;
207
+
208
+ let receiver = Address::generate(env);
209
+ let src_eid = 2;
210
+ let sender = BytesN::from_array(env, &[1u8; 32]);
211
+
212
+ context.set_inbound_nonce(&receiver, src_eid, &sender, 100);
213
+
214
+ let hash = BytesN::from_array(env, &[0xabu8; 32]);
215
+ env.as_contract(&endpoint_client.address, || {
216
+ EndpointV2::inbound_for_test(env, &receiver, src_eid, &sender, 357u64, &hash)
217
+ });
218
+ }
219
+
144
220
  #[test]
145
221
  #[should_panic(expected = "Error(Contract, #11)")] // EndpointError::InvalidNonce
146
222
  fn test_inbound_rejects_reverify_when_nonce_leq_inbound_and_payload_missing() {
@@ -184,4 +260,6 @@ fn test_inbound_allows_reverify_when_nonce_leq_inbound_and_payload_exists() {
184
260
  EndpointV2::inbound_for_test(env, &receiver, src_eid, &sender, 1u64, &new_hash)
185
261
  });
186
262
  assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &1u64), Some(new_hash));
263
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 1);
264
+ assert!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender).is_empty());
187
265
  }
@@ -0,0 +1,272 @@
1
+ use soroban_sdk::{testutils::Address as _, Address, BytesN};
2
+
3
+ use crate::{endpoint_v2::EndpointV2, tests::endpoint_setup::setup};
4
+
5
+ fn insert_and_drain(
6
+ context: &crate::tests::endpoint_setup::TestSetup,
7
+ receiver: &Address,
8
+ src_eid: u32,
9
+ sender: &BytesN<32>,
10
+ nonce: u64,
11
+ ) {
12
+ let env = &context.env;
13
+ let endpoint_client = &context.endpoint_client;
14
+ env.as_contract(&endpoint_client.address, || {
15
+ EndpointV2::insert_and_drain_pending_nonces_for_test(env, receiver, src_eid, sender, nonce)
16
+ });
17
+ }
18
+
19
+ #[test]
20
+ fn test_insert_and_drain_inserts_sorted_and_dedupes() {
21
+ let context = setup();
22
+ let env = &context.env;
23
+ let endpoint_client = &context.endpoint_client;
24
+
25
+ let receiver = Address::generate(env);
26
+ let src_eid = 2u32;
27
+ let sender = BytesN::from_array(env, &[1u8; 32]);
28
+
29
+ // Start with inbound_nonce = 0.
30
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 0);
31
+ assert!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender).is_empty());
32
+
33
+ // Insert out of order: 3 then 2. Pending should remain sorted.
34
+ insert_and_drain(&context, &receiver, src_eid, &sender, 3);
35
+ insert_and_drain(&context, &receiver, src_eid, &sender, 2);
36
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 0);
37
+ assert_eq!(
38
+ endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender),
39
+ soroban_sdk::vec![env, 2u64, 3u64]
40
+ );
41
+
42
+ // Re-inserting an already pending nonce should be a no-op (no duplicates, no drain).
43
+ insert_and_drain(&context, &receiver, src_eid, &sender, 2);
44
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 0);
45
+ assert_eq!(
46
+ endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender),
47
+ soroban_sdk::vec![env, 2u64, 3u64]
48
+ );
49
+
50
+ insert_and_drain(&context, &receiver, src_eid, &sender, 1);
51
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 3);
52
+ assert!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender).is_empty());
53
+ }
54
+
55
+ #[test]
56
+ fn test_insert_and_drain_drains_consecutive_sequence() {
57
+ let context = setup();
58
+ let env = &context.env;
59
+ let endpoint_client = &context.endpoint_client;
60
+
61
+ let receiver = Address::generate(env);
62
+ let src_eid = 2u32;
63
+ let sender = BytesN::from_array(env, &[1u8; 32]);
64
+
65
+ // Insert 2 and 3, then insert 1. Inserting 1 should drain 1,2,3 and advance inbound_nonce to 3.
66
+ insert_and_drain(&context, &receiver, src_eid, &sender, 2);
67
+ insert_and_drain(&context, &receiver, src_eid, &sender, 3);
68
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 0);
69
+ assert_eq!(
70
+ endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender),
71
+ soroban_sdk::vec![env, 2u64, 3u64]
72
+ );
73
+
74
+ insert_and_drain(&context, &receiver, src_eid, &sender, 1);
75
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 3);
76
+ assert!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender).is_empty());
77
+ }
78
+
79
+ #[test]
80
+ fn test_insert_and_drain_leaves_nonconsecutive_tail_pending() {
81
+ let context = setup();
82
+ let env = &context.env;
83
+ let endpoint_client = &context.endpoint_client;
84
+
85
+ let receiver = Address::generate(env);
86
+ let src_eid = 2u32;
87
+ let sender = BytesN::from_array(env, &[1u8; 32]);
88
+
89
+ // Insert 2 and 4, then insert 1. Drain should advance to 2 but leave 4 pending (gap at 3).
90
+ insert_and_drain(&context, &receiver, src_eid, &sender, 2);
91
+ insert_and_drain(&context, &receiver, src_eid, &sender, 4);
92
+ insert_and_drain(&context, &receiver, src_eid, &sender, 1);
93
+
94
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
95
+ assert_eq!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender), soroban_sdk::vec![env, 4u64]);
96
+ }
97
+
98
+ #[test]
99
+ fn test_insert_and_drain_accepts_upper_bound_when_inbound_nonce_zero() {
100
+ let context = setup();
101
+ let env = &context.env;
102
+ let endpoint_client = &context.endpoint_client;
103
+
104
+ let receiver = Address::generate(env);
105
+ let src_eid = 2u32;
106
+ let sender = BytesN::from_array(env, &[1u8; 32]);
107
+
108
+ // inbound_nonce == 0: 256 is allowed.
109
+ insert_and_drain(&context, &receiver, src_eid, &sender, 256);
110
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 0);
111
+ assert_eq!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender), soroban_sdk::vec![env, 256u64]);
112
+ }
113
+
114
+ #[test]
115
+ #[should_panic(expected = "Error(Contract, #11)")] // EndpointError::InvalidNonce
116
+ fn test_insert_and_drain_rejects_beyond_upper_bound_when_inbound_nonce_zero() {
117
+ let context = setup();
118
+ let env = &context.env;
119
+
120
+ let receiver = Address::generate(env);
121
+ let src_eid = 2u32;
122
+ let sender = BytesN::from_array(env, &[1u8; 32]);
123
+
124
+ // inbound_nonce == 0: 257 is out of range (max is 256).
125
+ insert_and_drain(&context, &receiver, src_eid, &sender, 257);
126
+ }
127
+
128
+ #[test]
129
+ #[should_panic(expected = "Error(Contract, #11)")] // EndpointError::InvalidNonce
130
+ fn test_insert_and_drain_rejects_nonce_leq_inbound_nonce() {
131
+ let context = setup();
132
+ let env = &context.env;
133
+ let endpoint_client = &context.endpoint_client;
134
+
135
+ let receiver = Address::generate(env);
136
+ let src_eid = 2u32;
137
+ let sender = BytesN::from_array(env, &[1u8; 32]);
138
+
139
+ // Advance inbound_nonce to 5 via storage utility.
140
+ context.set_inbound_nonce(&receiver, src_eid, &sender, 5);
141
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 5);
142
+
143
+ // Calling with new_nonce == inbound_nonce should panic InvalidNonce.
144
+ insert_and_drain(&context, &receiver, src_eid, &sender, 5);
145
+ }
146
+
147
+ #[test]
148
+ fn test_insert_and_drain_accepts_upper_bound_when_inbound_nonce_nonzero() {
149
+ let context = setup();
150
+ let env = &context.env;
151
+ let endpoint_client = &context.endpoint_client;
152
+
153
+ let receiver = Address::generate(env);
154
+ let src_eid = 2u32;
155
+ let sender = BytesN::from_array(env, &[1u8; 32]);
156
+
157
+ // inbound_nonce == 100: upper bound is 356 (= 100 + 256).
158
+ context.set_inbound_nonce(&receiver, src_eid, &sender, 100);
159
+ insert_and_drain(&context, &receiver, src_eid, &sender, 356);
160
+
161
+ // Not consecutive to 100, so it remains pending and inbound_nonce does not advance.
162
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 100);
163
+ assert_eq!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender), soroban_sdk::vec![env, 356u64]);
164
+ }
165
+
166
+ #[test]
167
+ #[should_panic(expected = "Error(Contract, #11)")] // EndpointError::InvalidNonce
168
+ fn test_insert_and_drain_rejects_beyond_upper_bound_when_inbound_nonce_nonzero() {
169
+ let context = setup();
170
+ let env = &context.env;
171
+
172
+ let receiver = Address::generate(env);
173
+ let src_eid = 2u32;
174
+ let sender = BytesN::from_array(env, &[1u8; 32]);
175
+
176
+ context.set_inbound_nonce(&receiver, src_eid, &sender, 100);
177
+
178
+ // inbound_nonce == 100: 357 is out of range (max is 356).
179
+ insert_and_drain(&context, &receiver, src_eid, &sender, 357);
180
+ }
181
+
182
+ #[test]
183
+ fn test_insert_and_drain_drains_across_existing_pending_tail_when_inbound_nonce_nonzero() {
184
+ let context = setup();
185
+ let env = &context.env;
186
+ let endpoint_client = &context.endpoint_client;
187
+
188
+ let receiver = Address::generate(env);
189
+ let src_eid = 2u32;
190
+ let sender = BytesN::from_array(env, &[1u8; 32]);
191
+
192
+ // Set inbound_nonce to 2.
193
+ context.set_inbound_nonce(&receiver, src_eid, &sender, 2);
194
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
195
+
196
+ // Insert 4 first -> pending [4], inbound_nonce remains 2 (missing 3).
197
+ insert_and_drain(&context, &receiver, src_eid, &sender, 4);
198
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
199
+ assert_eq!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender), soroban_sdk::vec![env, 4u64]);
200
+
201
+ // Insert 3 -> should drain 3 then 4 and advance inbound_nonce to 4.
202
+ insert_and_drain(&context, &receiver, src_eid, &sender, 3);
203
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 4);
204
+ assert!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender).is_empty());
205
+ }
206
+
207
+ #[test]
208
+ fn test_insert_and_drain_drains_single_next_nonce_when_no_pending() {
209
+ let context = setup();
210
+ let env = &context.env;
211
+ let endpoint_client = &context.endpoint_client;
212
+
213
+ let receiver = Address::generate(env);
214
+ let src_eid = 2u32;
215
+ let sender = BytesN::from_array(env, &[1u8; 32]);
216
+
217
+ context.set_inbound_nonce(&receiver, src_eid, &sender, 5);
218
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 5);
219
+ assert!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender).is_empty());
220
+
221
+ // Insert exactly inbound_nonce + 1. This should drain immediately and leave pending empty.
222
+ insert_and_drain(&context, &receiver, src_eid, &sender, 6);
223
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 6);
224
+ assert!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender).is_empty());
225
+ }
226
+
227
+ #[test]
228
+ fn test_insert_and_drain_window_holds_255_when_inbound_plus_one_missing_then_drains() {
229
+ let context = setup();
230
+ let env = &context.env;
231
+ let endpoint_client = &context.endpoint_client;
232
+
233
+ let receiver = Address::generate(env);
234
+ let src_eid = 2u32;
235
+ let sender = BytesN::from_array(env, &[1u8; 32]);
236
+
237
+ // Fill the pending window with 2..=256 while nonce 1 is still missing.
238
+ // This creates 255 pending nonces and inbound_nonce remains 0.
239
+ for nonce in 2u64..=256u64 {
240
+ insert_and_drain(&context, &receiver, src_eid, &sender, nonce);
241
+ }
242
+
243
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 0);
244
+ let pending = endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender);
245
+ assert_eq!(pending.len(), 255);
246
+ assert_eq!(pending.get(0).unwrap(), 2u64);
247
+ assert_eq!(pending.get(254).unwrap(), 256u64);
248
+
249
+ // Inserting nonce 1 closes the gap and drains the entire window.
250
+ insert_and_drain(&context, &receiver, src_eid, &sender, 1);
251
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 256);
252
+ assert!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender).is_empty());
253
+ }
254
+
255
+ #[test]
256
+ #[should_panic(expected = "Error(Contract, #11)")] // EndpointError::InvalidNonce
257
+ fn test_insert_and_drain_rejects_257_when_missing_inbound_plus_one() {
258
+ let context = setup();
259
+ let env = &context.env;
260
+
261
+ let receiver = Address::generate(env);
262
+ let src_eid = 2u32;
263
+ let sender = BytesN::from_array(env, &[1u8; 32]);
264
+
265
+ // Keep inbound_nonce at 0 and fill 2..=256 into pending.
266
+ for nonce in 2u64..=256u64 {
267
+ insert_and_drain(&context, &receiver, src_eid, &sender, nonce);
268
+ }
269
+
270
+ // 257 is outside the allowed range (0, 256] while inbound_nonce is still 0.
271
+ insert_and_drain(&context, &receiver, src_eid, &sender, 257);
272
+ }
@@ -3,6 +3,7 @@ mod clear_payload;
3
3
  mod inbound;
4
4
  mod inbound_nonce;
5
5
  mod inbound_payload_hash;
6
+ mod insert_and_drain_pending_nonces;
6
7
  mod pending_inbound_nonces;
7
8
  mod next_guid;
8
9
  mod nilify;
@@ -92,6 +92,7 @@ fn test_nilify_success_with_stored_payload() {
92
92
 
93
93
  // Verify payload hash is stored.
94
94
  assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &nonce), Some(payload_hash.clone()));
95
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 1);
95
96
 
96
97
  // Nilify the payload.
97
98
  let payload_hash_opt = Some(payload_hash.clone());
@@ -114,9 +115,10 @@ fn test_nilify_success_with_stored_payload() {
114
115
  let nilified_hash = endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &nonce);
115
116
  let expected_nil_hash = nil_hash(&context);
116
117
  assert_eq!(nilified_hash, Some(expected_nil_hash));
118
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 1);
117
119
  }
118
120
 
119
- // Successful nilify with None (non-verified nonce) when nonce > lazy nonce
121
+ // Successful nilify with None (no existing payload hash) when nonce > inbound_nonce
120
122
  #[test]
121
123
  fn test_nilify_success_with_empty_payload() {
122
124
  let context = setup();
@@ -150,9 +152,9 @@ fn test_nilify_success_with_empty_payload() {
150
152
  assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
151
153
  }
152
154
 
153
- // Nilify with None counts as "verified" for inbound_nonce (but does not change lazy nonce)
155
+ // Nilify with None counts toward `inbound_nonce` advancement (via pending-nonce draining).
154
156
  #[test]
155
- fn test_nilify_with_none_advances_inbound_nonce_without_changing_lazy_nonce() {
157
+ fn test_nilify_with_none_advances_inbound_nonce_without_changing_payload_hashes() {
156
158
  let context = setup();
157
159
  let env = &context.env;
158
160
  let endpoint_client = &context.endpoint_client;
@@ -211,7 +213,7 @@ fn test_nilify_closes_gap_and_drains_pending_nonces() {
211
213
  );
212
214
  }
213
215
 
214
- // Nilify is allowed when nonce <= lazy nonce if a payload hash exists
216
+ // Nilify is allowed when nonce <= inbound_nonce if an `inbound_payload_hash` exists
215
217
  #[test]
216
218
  fn test_nilify_allows_when_nonce_is_checkpointed_if_payload_exists() {
217
219
  let context = setup();
@@ -232,7 +234,7 @@ fn test_nilify_allows_when_nonce_is_checkpointed_if_payload_exists() {
232
234
  assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 10);
233
235
  assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &nonce), Some(payload_hash.clone()));
234
236
 
235
- // Even though nonce <= lazy_nonce, this should succeed because a payload hash exists.
237
+ // Even though nonce <= inbound_nonce, this should succeed because a payload hash exists.
236
238
  let payload_hash_opt = Some(payload_hash.clone());
237
239
  nilify_with_auth(&context, &receiver, &receiver, src_eid, &sender, nonce, &payload_hash_opt);
238
240
  assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &nonce), Some(nil_hash(&context)));
@@ -253,16 +255,19 @@ fn test_nilify_allows_repeated_call_when_already_nilified() {
253
255
 
254
256
  // First nilify: verified payload hash -> nil hash.
255
257
  context.inbound_as_verified(&receiver, src_eid, &sender, nonce, &payload_hash);
258
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 1);
256
259
  let payload_hash_opt = Some(payload_hash);
257
260
  nilify_with_auth(&context, &receiver, &receiver, src_eid, &sender, nonce, &payload_hash_opt);
258
261
 
259
262
  let nil = nil_hash(&context);
260
263
  assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &nonce), Some(nil.clone()));
264
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 1);
261
265
 
262
266
  // Second nilify: passing the current stored nil hash should succeed.
263
267
  let nil_opt = Some(nil.clone());
264
268
  nilify_with_auth(&context, &receiver, &receiver, src_eid, &sender, nonce, &nil_opt);
265
269
  assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &nonce), Some(nil));
270
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 1);
266
271
  }
267
272
 
268
273
  // Delegate authorization (delegate(receiver) is allowed)
@@ -248,6 +248,36 @@ fn test_skip_closes_gap_and_advances_inbound_nonce() {
248
248
  assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &2), Some(payload_hash_2));
249
249
  }
250
250
 
251
+ // Skipping a missing nonce can close *part* of the gap while leaving later gaps pending.
252
+ #[test]
253
+ fn test_skip_closes_gap_but_pending_inbound_nonces_not_empty() {
254
+ let context = setup();
255
+ let env = &context.env;
256
+ let endpoint_client = &context.endpoint_client;
257
+
258
+ let receiver = Address::generate(env);
259
+ let src_eid = 2;
260
+ let sender = BytesN::from_array(env, &[1u8; 32]);
261
+
262
+ // Verify nonce 2 and 4 out-of-order. inbound_nonce stays 0 because nonce 1 is missing, and
263
+ // pending list contains [2, 4] (gap at 1 and 3).
264
+ let payload_hash_2 = BytesN::from_array(env, &[0x11u8; 32]);
265
+ let payload_hash_4 = BytesN::from_array(env, &[0x22u8; 32]);
266
+ context.inbound_as_verified(&receiver, src_eid, &sender, 2, &payload_hash_2);
267
+ context.inbound_as_verified(&receiver, src_eid, &sender, 4, &payload_hash_4);
268
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 0);
269
+
270
+ // Skip nonce 1 closes the first gap and drains consecutive pending nonces up to 2,
271
+ // but nonce 4 remains pending because nonce 3 is still missing.
272
+ skip_with_auth(&context, &receiver, &receiver, src_eid, &sender, 1);
273
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
274
+ assert_eq!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender), soroban_sdk::vec![env, 4u64]);
275
+
276
+ // Payload hashes remain intact.
277
+ assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &2), Some(payload_hash_2));
278
+ assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &4), Some(payload_hash_4));
279
+ }
280
+
251
281
  // Repeated skip of the same nonce is rejected
252
282
  #[test]
253
283
  fn test_skip_rejects_repeated_same_nonce() {
@@ -1,7 +1,12 @@
1
1
  use endpoint_v2::Origin;
2
+ use oapp::oapp_core::OAPP_ADMIN_ROLE;
2
3
  use oapp::oapp_receiver::LzReceiveInternal;
3
4
  use oapp_macros::oapp;
4
- use soroban_sdk::{contractimpl, symbol_short, Address, Bytes, BytesN, Env};
5
+ use soroban_sdk::{
6
+ contractimpl, symbol_short,
7
+ testutils::{MockAuth, MockAuthInvoke},
8
+ Address, Bytes, BytesN, Env, IntoVal, Symbol,
9
+ };
5
10
 
6
11
  mod oapp_core;
7
12
  mod options_type3;
@@ -11,12 +16,10 @@ mod sender;
11
16
  /// Shared contract used by oapp-macros runtime tests.
12
17
  ///
13
18
  /// Notes:
14
- /// - `#[oapp]` expands to `#[common_macros::lz_contract]` + default trait impls for:
15
- /// - `oapp::oapp_core::OAppCore` (contract trait)
16
- /// - `oapp::oapp_receiver::OAppReceiver` (contract trait)
17
- /// - `oapp::oapp_options_type3::OAppOptionsType3` (contract trait)
18
- /// - `oapp::oapp_sender::OAppSenderInternal` (internal trait)
19
+ /// - `#[oapp]` generates only trait impls. User must apply `#[lz_contract]` (or similar) for contract + TTL + Auth.
20
+ /// - Default trait impls: `OAppCore`, `OAppReceiver`, `OAppOptionsType3`, `OAppSenderInternal`
19
21
  #[oapp]
22
+ #[common_macros::lz_contract]
20
23
  pub struct TestOApp;
21
24
 
22
25
  impl LzReceiveInternal for TestOApp {
@@ -46,3 +49,19 @@ impl TestOApp {
46
49
  env.storage().instance().get(&symbol_short!("lzr_c")).unwrap_or(false)
47
50
  }
48
51
  }
52
+
53
+ /// Grants `OAPP_ADMIN_ROLE` to `owner` via the public `grant_role` interface.
54
+ /// Call after `init()` in tests that exercise RBAC-protected entrypoints.
55
+ pub fn grant_oapp_admin(env: &Env, contract: &Address, owner: &Address) {
56
+ let role = Symbol::new(env, OAPP_ADMIN_ROLE);
57
+ env.mock_auths(&[MockAuth {
58
+ address: owner,
59
+ invoke: &MockAuthInvoke {
60
+ contract,
61
+ fn_name: "grant_role",
62
+ args: (owner, &role, owner).into_val(env),
63
+ sub_invokes: &[],
64
+ },
65
+ }]);
66
+ utils::rbac::RoleBasedAccessControlClient::new(env, contract).grant_role(owner, &role, owner);
67
+ }
@@ -1,6 +1,6 @@
1
1
  // Runtime tests: OAppCore behavior generated by `#[oapp]`.
2
2
 
3
- use super::{TestOApp, TestOAppClient};
3
+ use super::{grant_oapp_admin, TestOApp, TestOAppClient};
4
4
  use oapp::oapp_core::{OAppCoreStorage, PeerSet};
5
5
  use soroban_sdk::{
6
6
  contract, contractimpl,
@@ -64,6 +64,7 @@ fn set_peer_requires_auth_and_updates_storage() {
64
64
  let owner = Address::generate(&env);
65
65
  let endpoint = Address::generate(&env);
66
66
  client.init(&owner, &endpoint);
67
+ grant_oapp_admin(&env, &contract_id, &owner);
67
68
 
68
69
  let eid: u32 = 101;
69
70
  let peer = Some(BytesN::<32>::from_array(&env, &[7u8; 32]));
@@ -72,7 +73,7 @@ fn set_peer_requires_auth_and_updates_storage() {
72
73
  assert_eq!(client.peer(&eid), None);
73
74
 
74
75
  // Unauthorized set should fail.
75
- let unauthorized = client.try_set_peer(&eid, &peer);
76
+ let unauthorized = client.try_set_peer(&eid, &peer, &owner);
76
77
  assert_eq!(
77
78
  unauthorized.unwrap_err().unwrap(),
78
79
  Error::from_type_and_code(ScErrorType::Context, ScErrorCode::InvalidAction)
@@ -85,11 +86,11 @@ fn set_peer_requires_auth_and_updates_storage() {
85
86
  invoke: &MockAuthInvoke {
86
87
  contract: &contract_id,
87
88
  fn_name: "set_peer",
88
- args: (&eid, &peer).into_val(&env),
89
+ args: (&eid, &peer, &owner).into_val(&env),
89
90
  sub_invokes: &[],
90
91
  },
91
92
  }])
92
- .set_peer(&eid, &peer);
93
+ .set_peer(&eid, &peer, &owner);
93
94
 
94
95
  // Assert event before any other contract call (Soroban events are per-call).
95
96
  assert_contains_event(&env, &contract_id, PeerSet { eid, peer: peer.clone() });
@@ -106,11 +107,11 @@ fn set_peer_requires_auth_and_updates_storage() {
106
107
  invoke: &MockAuthInvoke {
107
108
  contract: &contract_id,
108
109
  fn_name: "set_peer",
109
- args: (&eid, &none).into_val(&env),
110
+ args: (&eid, &none, &owner).into_val(&env),
110
111
  sub_invokes: &[],
111
112
  },
112
113
  }]);
113
- client.set_peer(&eid, &none);
114
+ client.set_peer(&eid, &none, &owner);
114
115
  assert_contains_event(&env, &contract_id, PeerSet { eid, peer: None });
115
116
  env.as_contract(&contract_id, || {
116
117
  assert_eq!(OAppCoreStorage::peer(&env, eid), None);
@@ -128,10 +129,11 @@ fn set_delegate_requires_auth_and_updates_endpoint() {
128
129
 
129
130
  let owner = Address::generate(&env);
130
131
  client.init(&owner, &endpoint_id);
132
+ grant_oapp_admin(&env, &contract_id, &owner);
131
133
 
132
134
  // Unauthorized set should fail.
133
135
  let delegate = Some(Address::generate(&env));
134
- let unauthorized = client.try_set_delegate(&delegate);
136
+ let unauthorized = client.try_set_delegate(&delegate, &owner);
135
137
  assert_eq!(
136
138
  unauthorized.unwrap_err().unwrap(),
137
139
  Error::from_type_and_code(ScErrorType::Context, ScErrorCode::InvalidAction)
@@ -144,11 +146,11 @@ fn set_delegate_requires_auth_and_updates_endpoint() {
144
146
  invoke: &MockAuthInvoke {
145
147
  contract: &contract_id,
146
148
  fn_name: "set_delegate",
147
- args: (&delegate,).into_val(&env),
149
+ args: (&delegate, &owner).into_val(&env),
148
150
  sub_invokes: &[],
149
151
  },
150
152
  }])
151
- .set_delegate(&delegate);
153
+ .set_delegate(&delegate, &owner);
152
154
 
153
155
  assert_eq!(endpoint_client.delegate(&contract_id), delegate);
154
156
 
@@ -160,11 +162,11 @@ fn set_delegate_requires_auth_and_updates_endpoint() {
160
162
  invoke: &MockAuthInvoke {
161
163
  contract: &contract_id,
162
164
  fn_name: "set_delegate",
163
- args: (&none,).into_val(&env),
165
+ args: (&none, &owner).into_val(&env),
164
166
  sub_invokes: &[],
165
167
  },
166
168
  }])
167
- .set_delegate(&none);
169
+ .set_delegate(&none, &owner);
168
170
 
169
171
  assert_eq!(endpoint_client.delegate(&contract_id), None);
170
172
  }