@layerzerolabs/protocol-stellar-v2 0.2.11 → 0.2.13

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 (74) hide show
  1. package/.turbo/turbo-build.log +202 -194
  2. package/.turbo/turbo-lint.log +38 -38
  3. package/.turbo/turbo-test.log +891 -891
  4. package/Cargo.lock +1 -1
  5. package/contracts/common-macros/src/lib.rs +3 -36
  6. package/contracts/endpoint-v2/src/endpoint_v2.rs +4 -4
  7. package/contracts/endpoint-v2/src/events.rs +40 -22
  8. package/contracts/endpoint-v2/src/interfaces/message_lib.rs +2 -2
  9. package/contracts/endpoint-v2/src/interfaces/message_lib_manager.rs +2 -2
  10. package/contracts/endpoint-v2/src/interfaces/messaging_channel.rs +2 -2
  11. package/contracts/endpoint-v2/src/interfaces/messaging_composer.rs +2 -2
  12. package/contracts/endpoint-v2/src/interfaces/send_lib.rs +2 -2
  13. package/contracts/endpoint-v2/src/message_lib_manager.rs +3 -3
  14. package/contracts/endpoint-v2/src/messaging_channel.rs +1 -1
  15. package/contracts/endpoint-v2/src/messaging_composer.rs +1 -1
  16. package/contracts/endpoint-v2/src/tests/message_lib_manager/set_default_receive_lib_timeout.rs +4 -8
  17. package/contracts/endpoint-v2/src/tests/message_lib_manager/set_default_receive_library.rs +3 -7
  18. package/contracts/message-libs/{block-message-lib → blocked-message-lib}/Cargo.toml +1 -1
  19. package/contracts/message-libs/treasury/src/events.rs +9 -6
  20. package/contracts/message-libs/uln-302/src/events.rs +19 -11
  21. package/contracts/message-libs/uln-302/src/interfaces/receive_uln.rs +2 -2
  22. package/contracts/message-libs/uln-302/src/interfaces/send_uln.rs +2 -2
  23. package/contracts/message-libs/uln-302/src/receive_uln.rs +2 -2
  24. package/contracts/message-libs/uln-302/src/send_uln.rs +3 -3
  25. package/contracts/message-libs/uln-302/src/tests/receive_uln302/set_default_receive_uln_configs.rs +5 -5
  26. package/contracts/message-libs/uln-302/src/tests/send_uln302/set_default_send_uln_configs.rs +5 -5
  27. package/contracts/message-libs/uln-302/src/tests/setup.rs +3 -3
  28. package/contracts/message-libs/uln-302/src/types.rs +24 -24
  29. package/contracts/message-libs/uln-302/src/uln302.rs +1 -1
  30. package/contracts/oapps/counter/integration_tests/utils.rs +1 -1
  31. package/contracts/oapps/oapp/src/oapp_core.rs +4 -3
  32. package/contracts/oapps/oapp/src/oapp_options_type3.rs +4 -3
  33. package/contracts/oapps/oft/integration-tests/utils.rs +1 -1
  34. package/contracts/oapps/oft/src/events.rs +5 -4
  35. package/contracts/oapps/oft/src/extensions/oft_fee.rs +10 -6
  36. package/contracts/oapps/oft/src/extensions/pausable.rs +4 -4
  37. package/contracts/oapps/oft/src/extensions/rate_limiter.rs +8 -6
  38. package/contracts/utils/src/ownable.rs +6 -4
  39. package/contracts/utils/src/tests/testing_utils.rs +7 -5
  40. package/contracts/utils/src/ttl.rs +5 -4
  41. package/contracts/workers/dvn/src/auth.rs +59 -45
  42. package/contracts/workers/dvn/src/dvn.rs +84 -16
  43. package/contracts/workers/dvn/src/errors.rs +10 -13
  44. package/contracts/workers/dvn/src/events.rs +7 -5
  45. package/contracts/workers/dvn/src/interfaces/dvn.rs +29 -1
  46. package/contracts/workers/dvn/src/multisig.rs +94 -71
  47. package/contracts/workers/dvn/src/storage.rs +9 -12
  48. package/contracts/workers/dvn/src/tests/auth.rs +56 -26
  49. package/contracts/workers/dvn/src/tests/dvn.rs +37 -37
  50. package/contracts/workers/dvn/src/tests/multisig/set_signer.rs +8 -8
  51. package/contracts/workers/dvn/src/tests/multisig/set_threshold.rs +9 -9
  52. package/contracts/workers/dvn/src/tests/multisig/verify_signatures.rs +6 -6
  53. package/contracts/workers/dvn/src/tests/setup.rs +5 -5
  54. package/contracts/workers/executor/src/auth.rs +93 -0
  55. package/contracts/workers/executor/src/events.rs +5 -4
  56. package/contracts/workers/executor/src/{lz_executor.rs → executor.rs} +25 -98
  57. package/contracts/workers/executor/src/interfaces/mod.rs +1 -1
  58. package/contracts/workers/executor/src/lib.rs +6 -5
  59. package/contracts/workers/worker/src/events.rs +23 -13
  60. package/contracts/workers/worker/src/worker.rs +32 -21
  61. package/package.json +3 -3
  62. package/sdk/dist/generated/bml.js +23 -23
  63. package/sdk/dist/generated/counter.js +25 -25
  64. package/sdk/dist/generated/endpoint.js +23 -23
  65. package/sdk/dist/generated/sml.js +23 -23
  66. package/sdk/dist/generated/uln302.d.ts +1 -1
  67. package/sdk/dist/generated/uln302.js +33 -33
  68. package/sdk/package.json +1 -1
  69. package/sdk/test/index.test.ts +1 -1
  70. package/sdk/test/oft.test.ts +847 -0
  71. package/sdk/test/suites/scan.ts +20 -4
  72. package/tools/ts-bindings-gen/src/main.rs +2 -1
  73. package/contracts/common-macros/src/event.rs +0 -16
  74. /package/contracts/message-libs/{block-message-lib → blocked-message-lib}/src/lib.rs +0 -0
@@ -0,0 +1,847 @@
1
+ import {
2
+ Address,
3
+ Asset,
4
+ BASE_FEE,
5
+ Contract,
6
+ Keypair,
7
+ nativeToScVal,
8
+ Operation,
9
+ rpc,
10
+ scValToNative,
11
+ StrKey,
12
+ TransactionBuilder,
13
+ } from '@stellar/stellar-sdk';
14
+ import path from 'path';
15
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
16
+ import { $ } from 'zx';
17
+
18
+ import { getFullyQualifiedRepoRootPath } from '@layerzerolabs/common-node-utils';
19
+ import { PacketSerializer, PacketV1Codec } from '@layerzerolabs/lz-v2-utilities';
20
+
21
+ import { Client as EndpointClient } from '../src/generated/endpoint';
22
+ import { Client as ExecutorClient } from '../src/generated/executor';
23
+ import { Client as ExecutorHelperClient } from '../src/generated/executor_helper';
24
+ import { Client as OFTStdClient, SendParam } from '../src/generated/oft_std';
25
+ import { Client as SMLClient } from '../src/generated/sml';
26
+ import {
27
+ DEFAULT_DEPLOYER,
28
+ EID,
29
+ NATIVE_TOKEN_ADDRESS,
30
+ NETWORK_PASSPHRASE,
31
+ RPC_URL,
32
+ ZRO_TOKEN_ADDRESS,
33
+ } from './suites/constants';
34
+ import { deployAssetSac, deployContract } from './suites/deploy';
35
+ import { fundAccount, startStellarLocalnet, stopStellarLocalnet } from './suites/localnet';
36
+ import { PacketSentEvent, scanPacketSentEvents } from './suites/scan';
37
+ import { assertTransactionsSucceeded, signExecutorAuthEntries } from './utils';
38
+
39
+ /**
40
+ * Helper to get SAC token balance for an address
41
+ */
42
+ async function getSacBalance(tokenAddress: string, accountAddress: string): Promise<bigint> {
43
+ const server = new rpc.Server(RPC_URL, { allowHttp: true });
44
+ const tokenContract = new Contract(tokenAddress);
45
+
46
+ // Build the balance call
47
+ const balanceOp = tokenContract.call(
48
+ 'balance',
49
+ nativeToScVal(Address.fromString(accountAddress), { type: 'address' }),
50
+ );
51
+
52
+ const account = await server.getAccount(DEFAULT_DEPLOYER.publicKey());
53
+ const tx = new TransactionBuilder(account, {
54
+ fee: BASE_FEE,
55
+ networkPassphrase: NETWORK_PASSPHRASE,
56
+ })
57
+ .addOperation(balanceOp)
58
+ .setTimeout(30)
59
+ .build();
60
+
61
+ const simulated = await server.simulateTransaction(tx);
62
+ if (rpc.Api.isSimulationError(simulated)) {
63
+ throw new Error(`Balance query failed: ${JSON.stringify(simulated)}`);
64
+ }
65
+
66
+ // Extract result from simulation
67
+ const result = (simulated as rpc.Api.SimulateTransactionSuccessResponse).result;
68
+ if (result?.retval) {
69
+ return scValToNative(result.retval) as bigint;
70
+ }
71
+ return 0n;
72
+ }
73
+
74
+ $.verbose = true;
75
+ $.stdio = ['inherit', 'pipe', process.stderr];
76
+
77
+ // Contract addresses
78
+ const CONTRACT_ADDRESSES = {
79
+ endpointV2: '',
80
+ sml: '',
81
+ executor: '',
82
+ executorHelper: '',
83
+ oftToken: '', // The SAC for OFT token
84
+ lockUnlockOft: '',
85
+ mintBurnOft: '',
86
+ };
87
+
88
+ // Clients
89
+ let endpointClient: EndpointClient;
90
+ let smlClient: SMLClient;
91
+ let executorClient: ExecutorClient;
92
+ let executorHelperClient: ExecutorHelperClient;
93
+ let lockUnlockOftClient: OFTStdClient;
94
+ let mintBurnOftClient: OFTStdClient;
95
+
96
+ // Test accounts
97
+ const EXECUTOR_ADMIN = Keypair.random();
98
+ const TOKEN_ISSUER = Keypair.random();
99
+ // Use DEFAULT_DEPLOYER as SENDER (same pattern as Counter test)
100
+ // The deployer has proper signing setup with the contract clients
101
+
102
+ // Recipients for each direction
103
+ const RECIPIENT_A = Keypair.random(); // Receives tokens in Lock/Unlock -> Mint/Burn direction
104
+ const RECIPIENT_B = Keypair.random(); // Receives tokens in Mint/Burn -> Lock/Unlock direction
105
+
106
+ // OFT Token asset (custom token for testing)
107
+ const OFT_TOKEN_CODE = 'OFT';
108
+ let OFT_ASSET: Asset;
109
+
110
+ // Constants
111
+ const SHARED_DECIMALS = 6;
112
+ const INITIAL_TOKEN_AMOUNT = '1000'; // 1000 tokens
113
+ const SEND_AMOUNT = 100_0000000n; // 100 tokens in local decimals (7 decimals)
114
+
115
+ // NOTE: run `stellar contract build` before running the test
116
+
117
+ describe('OFT E2E Testing with SAC', async () => {
118
+ const repoRoot = await getFullyQualifiedRepoRootPath();
119
+ const wasmDir = path.join(
120
+ repoRoot,
121
+ 'contracts',
122
+ 'protocol',
123
+ 'stellar',
124
+ 'target',
125
+ 'wasm32v1-none',
126
+ 'release',
127
+ );
128
+
129
+ const ENDPOINT_WASM_PATH = path.join(wasmDir, 'endpoint_v2.wasm');
130
+ const SML_WASM_PATH = path.join(wasmDir, 'simple_message_lib.wasm');
131
+ const EXECUTOR_WASM_PATH = path.join(wasmDir, 'executor.wasm');
132
+ const EXECUTOR_HELPER_WASM_PATH = path.join(wasmDir, 'executor_helper.wasm');
133
+ const OFT_STD_WASM_PATH = path.join(wasmDir, 'oft_std.wasm');
134
+
135
+ beforeAll(async () => {
136
+ await startStellarLocalnet();
137
+ // Fund test accounts
138
+ await fundAccount(EXECUTOR_ADMIN.publicKey());
139
+ await fundAccount(TOKEN_ISSUER.publicKey());
140
+ await fundAccount(RECIPIENT_A.publicKey());
141
+ await fundAccount(RECIPIENT_B.publicKey());
142
+
143
+ // Create the OFT asset (TOKEN_ISSUER is the issuer)
144
+ OFT_ASSET = new Asset(OFT_TOKEN_CODE, TOKEN_ISSUER.publicKey());
145
+ }, 120000); // 2 minute timeout for setup
146
+
147
+ afterAll(async () => {
148
+ await stopStellarLocalnet();
149
+ });
150
+
151
+ describe('Contract Deployments', () => {
152
+ it('Deploy Endpoint', async () => {
153
+ endpointClient = await deployContract<EndpointClient>(
154
+ EndpointClient,
155
+ ENDPOINT_WASM_PATH,
156
+ {
157
+ eid: EID,
158
+ owner: DEFAULT_DEPLOYER.publicKey(),
159
+ native_token: NATIVE_TOKEN_ADDRESS,
160
+ },
161
+ DEFAULT_DEPLOYER,
162
+ );
163
+
164
+ CONTRACT_ADDRESSES.endpointV2 = endpointClient.options.contractId;
165
+ console.log('✅ Endpoint deployed:', CONTRACT_ADDRESSES.endpointV2);
166
+ });
167
+
168
+ it('Deploy SimpleMessageLib', async () => {
169
+ smlClient = await deployContract<SMLClient>(
170
+ SMLClient,
171
+ SML_WASM_PATH,
172
+ {
173
+ owner: DEFAULT_DEPLOYER.publicKey(),
174
+ endpoint: CONTRACT_ADDRESSES.endpointV2,
175
+ fee_recipient: DEFAULT_DEPLOYER.publicKey(),
176
+ },
177
+ DEFAULT_DEPLOYER,
178
+ );
179
+
180
+ CONTRACT_ADDRESSES.sml = smlClient.options.contractId;
181
+ console.log('✅ SimpleMessageLib deployed:', CONTRACT_ADDRESSES.sml);
182
+ });
183
+
184
+ it('Deploy Executor Helper', async () => {
185
+ executorHelperClient = await deployContract<ExecutorHelperClient>(
186
+ ExecutorHelperClient,
187
+ EXECUTOR_HELPER_WASM_PATH,
188
+ undefined,
189
+ DEFAULT_DEPLOYER,
190
+ );
191
+ CONTRACT_ADDRESSES.executorHelper = executorHelperClient.options.contractId;
192
+ console.log('✅ Executor Helper deployed:', CONTRACT_ADDRESSES.executorHelper);
193
+ });
194
+
195
+ it('Deploy Executor', async () => {
196
+ const whitelist = [
197
+ { contract: CONTRACT_ADDRESSES.executorHelper, fn_name: 'native_drop_and_execute' },
198
+ { contract: CONTRACT_ADDRESSES.executorHelper, fn_name: 'execute' },
199
+ { contract: CONTRACT_ADDRESSES.executorHelper, fn_name: 'compose' },
200
+ { contract: CONTRACT_ADDRESSES.executorHelper, fn_name: 'native_drop' },
201
+ ];
202
+
203
+ executorClient = await deployContract<ExecutorClient>(
204
+ ExecutorClient,
205
+ EXECUTOR_WASM_PATH,
206
+ {
207
+ owner: DEFAULT_DEPLOYER.publicKey(),
208
+ endpoint: CONTRACT_ADDRESSES.endpointV2,
209
+ whitelist,
210
+ admins: [EXECUTOR_ADMIN.publicKey()],
211
+ message_libs: [CONTRACT_ADDRESSES.sml],
212
+ // FIXME: Add price feed
213
+ price_feed: CONTRACT_ADDRESSES.endpointV2,
214
+ default_multiplier_bps: 10000,
215
+ },
216
+ DEFAULT_DEPLOYER,
217
+ );
218
+ CONTRACT_ADDRESSES.executor = executorClient.options.contractId;
219
+ console.log('✅ Executor deployed:', CONTRACT_ADDRESSES.executor);
220
+ });
221
+
222
+ it('Deploy OFT Token SAC', async () => {
223
+ const server = new rpc.Server(RPC_URL, { allowHttp: true });
224
+
225
+ // Step 1: Issue the OFT token to DEFAULT_DEPLOYER and set up trustlines for recipients
226
+ const issuerAccount = await server.getAccount(TOKEN_ISSUER.publicKey());
227
+ const issueTx = new TransactionBuilder(issuerAccount, {
228
+ fee: BASE_FEE,
229
+ networkPassphrase: NETWORK_PASSPHRASE,
230
+ })
231
+ // Trustline for DEFAULT_DEPLOYER (sender)
232
+ .addOperation(
233
+ Operation.changeTrust({
234
+ asset: OFT_ASSET,
235
+ source: DEFAULT_DEPLOYER.publicKey(),
236
+ }),
237
+ )
238
+ // Trustline for RECIPIENT_A (receives minted tokens)
239
+ .addOperation(
240
+ Operation.changeTrust({
241
+ asset: OFT_ASSET,
242
+ source: RECIPIENT_A.publicKey(),
243
+ }),
244
+ )
245
+ // Trustline for RECIPIENT_B (receives unlocked tokens)
246
+ .addOperation(
247
+ Operation.changeTrust({
248
+ asset: OFT_ASSET,
249
+ source: RECIPIENT_B.publicKey(),
250
+ }),
251
+ )
252
+ // Issue tokens to DEFAULT_DEPLOYER
253
+ .addOperation(
254
+ Operation.payment({
255
+ asset: OFT_ASSET,
256
+ amount: INITIAL_TOKEN_AMOUNT,
257
+ destination: DEFAULT_DEPLOYER.publicKey(),
258
+ }),
259
+ )
260
+ .setTimeout(30)
261
+ .build();
262
+
263
+ issueTx.sign(TOKEN_ISSUER, DEFAULT_DEPLOYER, RECIPIENT_A, RECIPIENT_B);
264
+ const sendResult = await server.sendTransaction(issueTx);
265
+ if (sendResult.status !== 'PENDING') {
266
+ throw new Error(`Failed to issue OFT token: ${JSON.stringify(sendResult)}`);
267
+ }
268
+ const txResult = await server.pollTransaction(sendResult.hash);
269
+ if (txResult.status !== 'SUCCESS') {
270
+ throw new Error(`Failed to issue OFT token: ${JSON.stringify(txResult)}`);
271
+ }
272
+ console.log('✅ OFT token issued to DEFAULT_DEPLOYER');
273
+
274
+ // Step 2: Deploy the SAC for the OFT token
275
+ CONTRACT_ADDRESSES.oftToken = await deployAssetSac(OFT_ASSET);
276
+ console.log('✅ OFT Token SAC deployed:', CONTRACT_ADDRESSES.oftToken);
277
+ });
278
+
279
+ it('Deploy Lock/Unlock OFT', async () => {
280
+ lockUnlockOftClient = await deployContract<OFTStdClient>(
281
+ OFTStdClient,
282
+ OFT_STD_WASM_PATH,
283
+ {
284
+ token: CONTRACT_ADDRESSES.oftToken,
285
+ owner: DEFAULT_DEPLOYER.publicKey(),
286
+ endpoint: CONTRACT_ADDRESSES.endpointV2,
287
+ delegate: DEFAULT_DEPLOYER.publicKey(),
288
+ shared_decimals: SHARED_DECIMALS,
289
+ is_lock_unlock: true, // Lock/Unlock mode
290
+ },
291
+ DEFAULT_DEPLOYER,
292
+ );
293
+
294
+ CONTRACT_ADDRESSES.lockUnlockOft = lockUnlockOftClient.options.contractId;
295
+ console.log('✅ Lock/Unlock OFT deployed:', CONTRACT_ADDRESSES.lockUnlockOft);
296
+
297
+ // Verify it's in lock/unlock mode
298
+ const { result: isLockUnlock } = await lockUnlockOftClient.is_lock_unlock();
299
+ expect(isLockUnlock).toBe(true);
300
+ });
301
+
302
+ it('Deploy Mint/Burn OFT', async () => {
303
+ mintBurnOftClient = await deployContract<OFTStdClient>(
304
+ OFTStdClient,
305
+ OFT_STD_WASM_PATH,
306
+ {
307
+ token: CONTRACT_ADDRESSES.oftToken,
308
+ owner: DEFAULT_DEPLOYER.publicKey(),
309
+ endpoint: CONTRACT_ADDRESSES.endpointV2,
310
+ delegate: DEFAULT_DEPLOYER.publicKey(),
311
+ shared_decimals: SHARED_DECIMALS,
312
+ is_lock_unlock: false, // Mint/Burn mode
313
+ },
314
+ DEFAULT_DEPLOYER,
315
+ );
316
+
317
+ CONTRACT_ADDRESSES.mintBurnOft = mintBurnOftClient.options.contractId;
318
+ console.log('✅ Mint/Burn OFT deployed:', CONTRACT_ADDRESSES.mintBurnOft);
319
+
320
+ // Verify it's in mint/burn mode
321
+ const { result: isLockUnlock } = await mintBurnOftClient.is_lock_unlock();
322
+ expect(isLockUnlock).toBe(false);
323
+ });
324
+
325
+ it('Verify all contracts deployed', () => {
326
+ console.log('\n📋 All deployed contracts:');
327
+ console.log(' Endpoint:', CONTRACT_ADDRESSES.endpointV2);
328
+ console.log(' SimpleMessageLib:', CONTRACT_ADDRESSES.sml);
329
+ console.log(' Executor:', CONTRACT_ADDRESSES.executor);
330
+ console.log(' Executor Helper:', CONTRACT_ADDRESSES.executorHelper);
331
+ console.log(' OFT Token SAC:', CONTRACT_ADDRESSES.oftToken);
332
+ console.log(' Lock/Unlock OFT:', CONTRACT_ADDRESSES.lockUnlockOft);
333
+ console.log(' Mint/Burn OFT:', CONTRACT_ADDRESSES.mintBurnOft);
334
+ });
335
+ });
336
+
337
+ describe('Wire Contracts', () => {
338
+ it('Register Library', async () => {
339
+ const assembledTx = await endpointClient.register_library({
340
+ new_lib: CONTRACT_ADDRESSES.sml,
341
+ });
342
+ await assembledTx.signAndSend();
343
+ const { result: libs } = await endpointClient.get_registered_libraries({
344
+ start: 0,
345
+ max_count: 100,
346
+ });
347
+ expect(libs.length).toBe(1);
348
+ expect(libs[0]).toBe(CONTRACT_ADDRESSES.sml);
349
+ console.log('✅ Library registered');
350
+ });
351
+
352
+ it('Set Default Send Library', async () => {
353
+ const assembledTx = await endpointClient.set_default_send_library({
354
+ dst_eid: EID,
355
+ new_lib: CONTRACT_ADDRESSES.sml,
356
+ });
357
+ await assembledTx.signAndSend();
358
+ const { result: defaultSendLib } = await endpointClient.default_send_library({
359
+ dst_eid: EID,
360
+ });
361
+ expect(defaultSendLib).toBe(CONTRACT_ADDRESSES.sml);
362
+ console.log('✅ Default send library set');
363
+ });
364
+
365
+ it('Set Default Receive Library', async () => {
366
+ const assembledTx = await endpointClient.set_default_receive_library({
367
+ src_eid: EID,
368
+ new_lib: CONTRACT_ADDRESSES.sml,
369
+ grace_period: 0n,
370
+ });
371
+ await assembledTx.signAndSend();
372
+ const { result: defaultReceiveLib } = await endpointClient.default_receive_library({
373
+ src_eid: EID,
374
+ });
375
+ expect(defaultReceiveLib).toBe(CONTRACT_ADDRESSES.sml);
376
+ console.log('✅ Default receive library set');
377
+ });
378
+
379
+ it('Set ZRO Token', async () => {
380
+ const setZroTx = await endpointClient.set_zro({
381
+ zro: ZRO_TOKEN_ADDRESS,
382
+ });
383
+ await setZroTx.signAndSend();
384
+
385
+ const { result: newZroToken } = await endpointClient.zro();
386
+ expect(newZroToken).toBe(ZRO_TOKEN_ADDRESS);
387
+ console.log('✅ ZRO token set:', ZRO_TOKEN_ADDRESS);
388
+ });
389
+
390
+ it('Set Lock/Unlock OFT Peer (to Mint/Burn OFT)', async () => {
391
+ const mintBurnPeerBytes = StrKey.decodeContract(CONTRACT_ADDRESSES.mintBurnOft);
392
+
393
+ const assembledTx = await lockUnlockOftClient.set_peer({
394
+ eid: EID,
395
+ peer: Buffer.from(mintBurnPeerBytes),
396
+ });
397
+ await assembledTx.signAndSend();
398
+
399
+ const { result: peer } = await lockUnlockOftClient.peer({
400
+ eid: EID,
401
+ });
402
+ expect(peer?.toString()).toBe(Buffer.from(mintBurnPeerBytes).toString());
403
+ console.log('✅ Lock/Unlock OFT peer set to Mint/Burn OFT');
404
+ });
405
+
406
+ it('Set Mint/Burn OFT Peer (to Lock/Unlock OFT)', async () => {
407
+ const lockUnlockPeerBytes = StrKey.decodeContract(CONTRACT_ADDRESSES.lockUnlockOft);
408
+
409
+ const assembledTx = await mintBurnOftClient.set_peer({
410
+ eid: EID,
411
+ peer: Buffer.from(lockUnlockPeerBytes),
412
+ });
413
+ await assembledTx.signAndSend();
414
+
415
+ const { result: peer } = await mintBurnOftClient.peer({
416
+ eid: EID,
417
+ });
418
+ expect(peer?.toString()).toBe(Buffer.from(lockUnlockPeerBytes).toString());
419
+ console.log('✅ Mint/Burn OFT peer set to Lock/Unlock OFT');
420
+ });
421
+
422
+ it('Set SAC Admin to Mint/Burn OFT (for minting)', async () => {
423
+ // The Mint/Burn OFT needs to be the admin of the SAC to mint tokens
424
+ // Use the SAC's set_admin function
425
+ const server = new rpc.Server(RPC_URL, { allowHttp: true });
426
+
427
+ // Build set_admin transaction
428
+ // SAC's set_admin function requires the current admin (TOKEN_ISSUER) to sign
429
+ const account = await server.getAccount(TOKEN_ISSUER.publicKey());
430
+
431
+ // Build the invokeContract operation for SAC's set_admin
432
+ const setAdminTx = new TransactionBuilder(account, {
433
+ fee: BASE_FEE,
434
+ networkPassphrase: NETWORK_PASSPHRASE,
435
+ })
436
+ .addOperation(
437
+ Operation.invokeContractFunction({
438
+ contract: CONTRACT_ADDRESSES.oftToken,
439
+ function: 'set_admin',
440
+ args: [
441
+ // new_admin: Address
442
+ Address.fromString(CONTRACT_ADDRESSES.mintBurnOft).toScVal(),
443
+ ],
444
+ }),
445
+ )
446
+ .setTimeout(30)
447
+ .build();
448
+
449
+ const simulated = await server.simulateTransaction(setAdminTx);
450
+ if (rpc.Api.isSimulationError(simulated)) {
451
+ throw new Error(`Simulation failed: ${JSON.stringify(simulated)}`);
452
+ }
453
+ const preparedTx = rpc.assembleTransaction(setAdminTx, simulated).build();
454
+ preparedTx.sign(TOKEN_ISSUER);
455
+
456
+ const sendResult = await server.sendTransaction(preparedTx);
457
+ if (sendResult.status !== 'PENDING') {
458
+ throw new Error(`Failed to set admin: ${JSON.stringify(sendResult)}`);
459
+ }
460
+ const txResult = await server.pollTransaction(sendResult.hash);
461
+ if (txResult.status !== 'SUCCESS') {
462
+ throw new Error(`Failed to set admin: ${JSON.stringify(txResult)}`);
463
+ }
464
+
465
+ console.log('✅ SAC admin set to Mint/Burn OFT');
466
+ });
467
+ });
468
+
469
+ describe('Send: Lock/Unlock -> Mint/Burn', () => {
470
+ let sendLedger = 0;
471
+ let packetSentEvent: PacketSentEvent;
472
+ let guid: Buffer;
473
+ let message: Buffer;
474
+
475
+ it('Verify initial balances', async () => {
476
+ // Check sender balance (DEFAULT_DEPLOYER has 1000 OFT tokens)
477
+ const senderBalance = await getSacBalance(
478
+ CONTRACT_ADDRESSES.oftToken,
479
+ DEFAULT_DEPLOYER.publicKey(),
480
+ );
481
+ console.log('📊 Initial Balances:');
482
+ console.log(` - Sender (DEFAULT_DEPLOYER): ${senderBalance} (expected: 10000000000)`);
483
+
484
+ // RECIPIENT_A should have 0 tokens
485
+ const recipientABalance = await getSacBalance(
486
+ CONTRACT_ADDRESSES.oftToken,
487
+ RECIPIENT_A.publicKey(),
488
+ );
489
+ console.log(` - RECIPIENT_A: ${recipientABalance} (expected: 0)`);
490
+
491
+ expect(senderBalance).toBe(10000000000n); // 1000 tokens with 7 decimals
492
+ expect(recipientABalance).toBe(0n);
493
+ });
494
+
495
+ it('Quote OFT send', async () => {
496
+ // Build SendParam - send to RECIPIENT_A
497
+ const receiverBytes = StrKey.decodeEd25519PublicKey(RECIPIENT_A.publicKey());
498
+ const sendParam: SendParam = {
499
+ dst_eid: EID,
500
+ to: Buffer.from(receiverBytes),
501
+ amount_ld: SEND_AMOUNT,
502
+ min_amount_ld: SEND_AMOUNT, // No slippage for test
503
+ extra_options: Buffer.from([]),
504
+ compose_msg: Buffer.from([]),
505
+ oft_cmd: Buffer.from([]),
506
+ };
507
+
508
+ // Quote OFT
509
+ const { result: quoteResult } = await lockUnlockOftClient.quote_oft({
510
+ send_param: sendParam,
511
+ });
512
+ const [limit, feeDetails, receipt] = quoteResult;
513
+ console.log('📊 OFT Quote:');
514
+ console.log(' Limit:', limit);
515
+ console.log(' Fee Details:', feeDetails);
516
+ console.log(' Receipt:', receipt);
517
+
518
+ expect(receipt.amount_sent_ld).toBe(SEND_AMOUNT);
519
+ });
520
+
521
+ it('Send tokens (Lock/Unlock -> Mint/Burn)', async () => {
522
+ // Build SendParam - send to RECIPIENT_A
523
+ const receiverBytes = StrKey.decodeEd25519PublicKey(RECIPIENT_A.publicKey());
524
+ const sendParam: SendParam = {
525
+ dst_eid: EID,
526
+ to: Buffer.from(receiverBytes),
527
+ amount_ld: SEND_AMOUNT,
528
+ min_amount_ld: SEND_AMOUNT,
529
+ extra_options: Buffer.from([]),
530
+ compose_msg: Buffer.from([]),
531
+ oft_cmd: Buffer.from([]),
532
+ };
533
+
534
+ // Quote send fee (pay in ZRO like Counter test does)
535
+ const { result: fee } = await lockUnlockOftClient.quote_send({
536
+ sender: DEFAULT_DEPLOYER.publicKey(),
537
+ send_param: sendParam,
538
+ pay_in_zro: true,
539
+ });
540
+ console.log('📊 Messaging Fee:', fee);
541
+
542
+ // Send tokens
543
+ const assembledTx = await lockUnlockOftClient.send({
544
+ sender: DEFAULT_DEPLOYER.publicKey(),
545
+ send_param: sendParam,
546
+ fee: fee,
547
+ refund_address: DEFAULT_DEPLOYER.publicKey(),
548
+ });
549
+
550
+ // Sign and send
551
+ const sentTx = await assembledTx.signAndSend();
552
+
553
+ // Extract ledger number
554
+ const txResponse = sentTx.getTransactionResponse;
555
+ if (txResponse && 'ledger' in txResponse) {
556
+ sendLedger = txResponse.ledger;
557
+ }
558
+
559
+ assertTransactionsSucceeded(sentTx, 'OFT Send');
560
+ console.log('✅ Tokens sent, ledger:', sendLedger);
561
+ });
562
+
563
+ it('Scan PacketSent events', async () => {
564
+ const packetSentEvents = await scanPacketSentEvents(
565
+ CONTRACT_ADDRESSES.endpointV2,
566
+ sendLedger,
567
+ );
568
+ expect(packetSentEvents.length).toBeGreaterThan(0);
569
+ packetSentEvent = packetSentEvents[0];
570
+ console.log(`✅ PacketSent events scanned. Found ${packetSentEvents.length} events`);
571
+ });
572
+
573
+ it('Validate packet via SML', async () => {
574
+ const packet = PacketSerializer.deserialize(packetSentEvent.encoded_packet);
575
+ guid = Buffer.from(packet.guid.replace('0x', ''), 'hex');
576
+ message = Buffer.from(packet.message.replace('0x', ''), 'hex');
577
+ const codec = PacketV1Codec.from(packet);
578
+ const packetHeader = codec.header();
579
+ const payloadHash = codec.payloadHash();
580
+
581
+ const assembledTx = await smlClient.validate_packet({
582
+ header_bytes: Buffer.from(packetHeader.replace('0x', ''), 'hex'),
583
+ payload_hash: Buffer.from(payloadHash.replace('0x', ''), 'hex'),
584
+ });
585
+ await assembledTx.signAndSend();
586
+ console.log('✅ Packet validated via SML');
587
+ });
588
+
589
+ it('Receive tokens (mint on Mint/Burn OFT)', async () => {
590
+ const lockUnlockPeerBytes = StrKey.decodeContract(CONTRACT_ADDRESSES.lockUnlockOft);
591
+ const origin = {
592
+ nonce: 1n,
593
+ sender: Buffer.from(lockUnlockPeerBytes),
594
+ src_eid: EID,
595
+ };
596
+
597
+ // Use native_drop_and_execute with empty native_drop_params
598
+ const assembledTx = await executorHelperClient.native_drop_and_execute({
599
+ executor: CONTRACT_ADDRESSES.executor,
600
+ admin: EXECUTOR_ADMIN.publicKey(),
601
+ origin,
602
+ dst_eid: EID,
603
+ oapp: CONTRACT_ADDRESSES.mintBurnOft,
604
+ native_drop_params: [],
605
+ execute_params: {
606
+ extra_data: Buffer.from([]),
607
+ gas_limit: 0n,
608
+ guid,
609
+ message,
610
+ origin,
611
+ receiver: CONTRACT_ADDRESSES.mintBurnOft,
612
+ value: 0n,
613
+ },
614
+ });
615
+
616
+ // Sign the Executor's auth entries
617
+ await signExecutorAuthEntries(
618
+ CONTRACT_ADDRESSES.executor,
619
+ EXECUTOR_ADMIN,
620
+ assembledTx,
621
+ NETWORK_PASSPHRASE,
622
+ );
623
+
624
+ // Sign and send
625
+ const sentTx = await assembledTx.signAndSend();
626
+ assertTransactionsSucceeded(sentTx, 'LzReceive (Mint)');
627
+
628
+ console.log('✅ Tokens received and minted on Mint/Burn OFT');
629
+ });
630
+
631
+ it('Verify balances after forward send', async () => {
632
+ // Balance changes after forward send:
633
+ // - Sender (DEFAULT_DEPLOYER): 1000 - 100 = 900 tokens
634
+ // - Lock/Unlock OFT contract: holds 100 locked tokens
635
+ // - RECIPIENT_A: received 100 minted tokens
636
+
637
+ const senderBalance = await getSacBalance(
638
+ CONTRACT_ADDRESSES.oftToken,
639
+ DEFAULT_DEPLOYER.publicKey(),
640
+ );
641
+ const lockUnlockOftBalance = await getSacBalance(
642
+ CONTRACT_ADDRESSES.oftToken,
643
+ CONTRACT_ADDRESSES.lockUnlockOft,
644
+ );
645
+ const recipientABalance = await getSacBalance(
646
+ CONTRACT_ADDRESSES.oftToken,
647
+ RECIPIENT_A.publicKey(),
648
+ );
649
+
650
+ console.log('📊 Balances after forward send:');
651
+ console.log(` - Sender (DEFAULT_DEPLOYER): ${senderBalance} (expected: 9000000000)`);
652
+ console.log(
653
+ ` - Lock/Unlock OFT (locked): ${lockUnlockOftBalance} (expected: 1000000000)`,
654
+ );
655
+ console.log(` - RECIPIENT_A (minted): ${recipientABalance} (expected: 1000000000)`);
656
+
657
+ expect(senderBalance).toBe(9000000000n); // 900 tokens
658
+ expect(lockUnlockOftBalance).toBe(1000000000n); // 100 tokens locked
659
+ expect(recipientABalance).toBe(1000000000n); // 100 tokens minted
660
+ });
661
+ });
662
+
663
+ describe('Send: Mint/Burn -> Lock/Unlock (Reverse)', () => {
664
+ let sendLedger = 0;
665
+ let packetSentEvent: PacketSentEvent;
666
+ let guid: Buffer;
667
+ let message: Buffer;
668
+ const REVERSE_SEND_AMOUNT = 50_0000000n; // 50 tokens
669
+
670
+ it('Quote OFT send (reverse)', async () => {
671
+ // Send to RECIPIENT_B
672
+ const receiverBytes = StrKey.decodeEd25519PublicKey(RECIPIENT_B.publicKey());
673
+ const sendParam: SendParam = {
674
+ dst_eid: EID,
675
+ to: Buffer.from(receiverBytes),
676
+ amount_ld: REVERSE_SEND_AMOUNT,
677
+ min_amount_ld: REVERSE_SEND_AMOUNT,
678
+ extra_options: Buffer.from([]),
679
+ compose_msg: Buffer.from([]),
680
+ oft_cmd: Buffer.from([]),
681
+ };
682
+
683
+ const { result: quoteResult } = await mintBurnOftClient.quote_oft({
684
+ send_param: sendParam,
685
+ });
686
+ const [limit, feeDetails, receipt] = quoteResult;
687
+ console.log('📊 Reverse OFT Quote:');
688
+ console.log(' Limit:', limit);
689
+ console.log(' Fee Details:', feeDetails);
690
+ console.log(' Receipt:', receipt);
691
+
692
+ expect(receipt.amount_sent_ld).toBe(REVERSE_SEND_AMOUNT);
693
+ });
694
+
695
+ it('Send tokens (Mint/Burn -> Lock/Unlock)', async () => {
696
+ // DEFAULT_DEPLOYER sends via Mint/Burn OFT to RECIPIENT_B
697
+ const receiverBytes = StrKey.decodeEd25519PublicKey(RECIPIENT_B.publicKey());
698
+ const sendParam: SendParam = {
699
+ dst_eid: EID,
700
+ to: Buffer.from(receiverBytes),
701
+ amount_ld: REVERSE_SEND_AMOUNT,
702
+ min_amount_ld: REVERSE_SEND_AMOUNT,
703
+ extra_options: Buffer.from([]),
704
+ compose_msg: Buffer.from([]),
705
+ oft_cmd: Buffer.from([]),
706
+ };
707
+
708
+ // Quote send fee (pay in ZRO like Counter test does)
709
+ const { result: fee } = await mintBurnOftClient.quote_send({
710
+ sender: DEFAULT_DEPLOYER.publicKey(),
711
+ send_param: sendParam,
712
+ pay_in_zro: true,
713
+ });
714
+ console.log('📊 Reverse Messaging Fee:', fee);
715
+
716
+ // Send tokens
717
+ const assembledTx = await mintBurnOftClient.send({
718
+ sender: DEFAULT_DEPLOYER.publicKey(),
719
+ send_param: sendParam,
720
+ fee: fee,
721
+ refund_address: DEFAULT_DEPLOYER.publicKey(),
722
+ });
723
+
724
+ // Sign and send
725
+ const sentTx = await assembledTx.signAndSend();
726
+
727
+ // Extract ledger number
728
+ const txResponse = sentTx.getTransactionResponse;
729
+ if (txResponse && 'ledger' in txResponse) {
730
+ sendLedger = txResponse.ledger;
731
+ }
732
+
733
+ assertTransactionsSucceeded(sentTx, 'OFT Reverse Send');
734
+ console.log('✅ Tokens sent (reverse), ledger:', sendLedger);
735
+ });
736
+
737
+ it('Scan PacketSent events (reverse)', async () => {
738
+ const packetSentEvents = await scanPacketSentEvents(
739
+ CONTRACT_ADDRESSES.endpointV2,
740
+ sendLedger,
741
+ );
742
+ expect(packetSentEvents.length).toBeGreaterThan(0);
743
+ packetSentEvent = packetSentEvents[0];
744
+ console.log(
745
+ `✅ PacketSent events scanned (reverse). Found ${packetSentEvents.length} events`,
746
+ );
747
+ });
748
+
749
+ it('Validate packet via SML (reverse)', async () => {
750
+ const packet = PacketSerializer.deserialize(packetSentEvent.encoded_packet);
751
+ guid = Buffer.from(packet.guid.replace('0x', ''), 'hex');
752
+ message = Buffer.from(packet.message.replace('0x', ''), 'hex');
753
+ const codec = PacketV1Codec.from(packet);
754
+ const packetHeader = codec.header();
755
+ const payloadHash = codec.payloadHash();
756
+
757
+ const assembledTx = await smlClient.validate_packet({
758
+ header_bytes: Buffer.from(packetHeader.replace('0x', ''), 'hex'),
759
+ payload_hash: Buffer.from(payloadHash.replace('0x', ''), 'hex'),
760
+ });
761
+ await assembledTx.signAndSend();
762
+ console.log('✅ Packet validated via SML (reverse)');
763
+ });
764
+
765
+ it('Receive tokens (unlock on Lock/Unlock OFT)', async () => {
766
+ const mintBurnPeerBytes = StrKey.decodeContract(CONTRACT_ADDRESSES.mintBurnOft);
767
+ const origin = {
768
+ nonce: 1n,
769
+ sender: Buffer.from(mintBurnPeerBytes),
770
+ src_eid: EID,
771
+ };
772
+
773
+ // Use native_drop_and_execute with empty native_drop_params
774
+ const assembledTx = await executorHelperClient.native_drop_and_execute({
775
+ executor: CONTRACT_ADDRESSES.executor,
776
+ admin: EXECUTOR_ADMIN.publicKey(),
777
+ origin,
778
+ dst_eid: EID,
779
+ oapp: CONTRACT_ADDRESSES.lockUnlockOft,
780
+ native_drop_params: [],
781
+ execute_params: {
782
+ extra_data: Buffer.from([]),
783
+ gas_limit: 0n,
784
+ guid,
785
+ message,
786
+ origin,
787
+ receiver: CONTRACT_ADDRESSES.lockUnlockOft,
788
+ value: 0n,
789
+ },
790
+ });
791
+
792
+ // Sign the Executor's auth entries
793
+ await signExecutorAuthEntries(
794
+ CONTRACT_ADDRESSES.executor,
795
+ EXECUTOR_ADMIN,
796
+ assembledTx,
797
+ NETWORK_PASSPHRASE,
798
+ );
799
+
800
+ // Sign and send
801
+ const sentTx = await assembledTx.signAndSend();
802
+ assertTransactionsSucceeded(sentTx, 'LzReceive (Unlock)');
803
+
804
+ console.log('✅ Tokens received and unlocked on Lock/Unlock OFT');
805
+ });
806
+
807
+ it('Verify final balances', async () => {
808
+ // Final balance summary:
809
+ // - DEFAULT_DEPLOYER: started with 1000, sent 100 (locked), sent 50 (burned) = 850
810
+ // - Lock/Unlock OFT: locked 100, unlocked 50 = 50 held
811
+ // - RECIPIENT_A: received 100 (minted)
812
+ // - RECIPIENT_B: received 50 (unlocked)
813
+
814
+ const senderBalance = await getSacBalance(
815
+ CONTRACT_ADDRESSES.oftToken,
816
+ DEFAULT_DEPLOYER.publicKey(),
817
+ );
818
+ const lockUnlockOftBalance = await getSacBalance(
819
+ CONTRACT_ADDRESSES.oftToken,
820
+ CONTRACT_ADDRESSES.lockUnlockOft,
821
+ );
822
+ const recipientABalance = await getSacBalance(
823
+ CONTRACT_ADDRESSES.oftToken,
824
+ RECIPIENT_A.publicKey(),
825
+ );
826
+ const recipientBBalance = await getSacBalance(
827
+ CONTRACT_ADDRESSES.oftToken,
828
+ RECIPIENT_B.publicKey(),
829
+ );
830
+
831
+ console.log('\n📊 Final Balance Summary:');
832
+ console.log(` - Sender (DEFAULT_DEPLOYER): ${senderBalance} (expected: 8500000000)`);
833
+ console.log(
834
+ ` - Lock/Unlock OFT (locked): ${lockUnlockOftBalance} (expected: 500000000)`,
835
+ );
836
+ console.log(` - RECIPIENT_A (minted): ${recipientABalance} (expected: 1000000000)`);
837
+ console.log(` - RECIPIENT_B (unlocked): ${recipientBBalance} (expected: 500000000)`);
838
+
839
+ expect(senderBalance).toBe(8500000000n); // 850 tokens (1000 - 100 - 50)
840
+ expect(lockUnlockOftBalance).toBe(500000000n); // 50 tokens (100 - 50)
841
+ expect(recipientABalance).toBe(1000000000n); // 100 tokens minted
842
+ expect(recipientBBalance).toBe(500000000n); // 50 tokens unlocked
843
+
844
+ console.log('✅ OFT E2E test completed successfully!');
845
+ });
846
+ });
847
+ });