@layerzerolabs/test-utils-stellar 0.2.122

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +108 -0
  3. package/dist/4UKUSHEU.js +30 -0
  4. package/dist/4UKUSHEU.js.map +1 -0
  5. package/dist/7DZ4SVEO.js +46 -0
  6. package/dist/7DZ4SVEO.js.map +1 -0
  7. package/dist/CYINPPER.js +293 -0
  8. package/dist/CYINPPER.js.map +1 -0
  9. package/dist/DNRE4ENZ.js +56 -0
  10. package/dist/DNRE4ENZ.js.map +1 -0
  11. package/dist/GAGBEOCZ.js +25 -0
  12. package/dist/GAGBEOCZ.js.map +1 -0
  13. package/dist/PMDPSIA2.js +149 -0
  14. package/dist/PMDPSIA2.js.map +1 -0
  15. package/dist/TYAJMKGY.js +183 -0
  16. package/dist/TYAJMKGY.js.map +1 -0
  17. package/dist/VRABOZPX.js +364 -0
  18. package/dist/VRABOZPX.js.map +1 -0
  19. package/dist/VUOMXK5T.js +6 -0
  20. package/dist/VUOMXK5T.js.map +1 -0
  21. package/dist/ZWKXKZVT.js +394 -0
  22. package/dist/ZWKXKZVT.js.map +1 -0
  23. package/dist/client.d.ts +17 -0
  24. package/dist/client.d.ts.map +1 -0
  25. package/dist/client.js +4 -0
  26. package/dist/client.js.map +1 -0
  27. package/dist/deploy.d.ts +43 -0
  28. package/dist/deploy.d.ts.map +1 -0
  29. package/dist/deploy.js +4 -0
  30. package/dist/deploy.js.map +1 -0
  31. package/dist/env.d.ts +45 -0
  32. package/dist/env.d.ts.map +1 -0
  33. package/dist/env.js +5 -0
  34. package/dist/env.js.map +1 -0
  35. package/dist/index.d.ts +10 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +12 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/localnet-global-setup.d.ts +7 -0
  40. package/dist/localnet-global-setup.d.ts.map +1 -0
  41. package/dist/localnet-global-setup.js +6 -0
  42. package/dist/localnet-global-setup.js.map +1 -0
  43. package/dist/localnet.d.ts +37 -0
  44. package/dist/localnet.d.ts.map +1 -0
  45. package/dist/localnet.js +5 -0
  46. package/dist/localnet.js.map +1 -0
  47. package/dist/protocol-global-setup.d.ts +58 -0
  48. package/dist/protocol-global-setup.d.ts.map +1 -0
  49. package/dist/protocol-global-setup.js +7 -0
  50. package/dist/protocol-global-setup.js.map +1 -0
  51. package/dist/scan.d.ts +59 -0
  52. package/dist/scan.d.ts.map +1 -0
  53. package/dist/scan.js +4 -0
  54. package/dist/scan.js.map +1 -0
  55. package/dist/secp256k1.d.ts +23 -0
  56. package/dist/secp256k1.d.ts.map +1 -0
  57. package/dist/secp256k1.js +4 -0
  58. package/dist/secp256k1.js.map +1 -0
  59. package/dist/utils.d.ts +54 -0
  60. package/dist/utils.d.ts.map +1 -0
  61. package/dist/utils.js +5 -0
  62. package/dist/utils.js.map +1 -0
  63. package/package.json +50 -0
  64. package/src/client.ts +37 -0
  65. package/src/deploy.ts +316 -0
  66. package/src/env.ts +104 -0
  67. package/src/index.ts +40 -0
  68. package/src/localnet-global-setup.ts +30 -0
  69. package/src/localnet.ts +406 -0
  70. package/src/protocol-global-setup.ts +747 -0
  71. package/src/scan.ts +255 -0
  72. package/src/secp256k1.ts +59 -0
  73. package/src/utils.ts +642 -0
package/src/utils.ts ADDED
@@ -0,0 +1,642 @@
1
+ import { keccak_256 } from '@noble/hashes/sha3';
2
+ import type { contract, Keypair } from '@stellar/stellar-sdk';
3
+ import {
4
+ Account,
5
+ Address,
6
+ authorizeEntry,
7
+ BASE_FEE,
8
+ Contract,
9
+ hash,
10
+ nativeToScVal,
11
+ scValToNative,
12
+ TransactionBuilder,
13
+ xdr,
14
+ } from '@stellar/stellar-sdk';
15
+ import * as rpc from '@stellar/stellar-sdk/rpc';
16
+
17
+ import type { StellarTestEnv } from './env.js';
18
+ import type { Secp256k1KeyPair } from './secp256k1.js';
19
+
20
+ export { createClient } from './client.js';
21
+
22
+ // ============================================================================
23
+ // Token Balance Helpers
24
+ // ============================================================================
25
+
26
+ /**
27
+ * Helper to get token balance for an address using SAC (Stellar Asset Contract).
28
+ * Works for any token including native XLM.
29
+ *
30
+ * @param tokenAddress - The SAC contract address (defaults to native XLM)
31
+ * @param accountAddress - The account to check balance for
32
+ */
33
+ export async function getTokenBalance(
34
+ env: StellarTestEnv,
35
+ accountAddress: string,
36
+ tokenAddress: string = env.NATIVE_TOKEN_ADDRESS,
37
+ ): Promise<bigint> {
38
+ const server = new rpc.Server(env.RPC_URL, { allowHttp: true });
39
+ const tokenContract = new Contract(tokenAddress);
40
+
41
+ // Build the balance call
42
+ const balanceOp = tokenContract.call(
43
+ 'balance',
44
+ nativeToScVal(Address.fromString(accountAddress), { type: 'address' }),
45
+ );
46
+
47
+ const account = await server.getAccount(env.DEFAULT_DEPLOYER.publicKey());
48
+ const tx = new TransactionBuilder(account, {
49
+ fee: BASE_FEE,
50
+ networkPassphrase: env.NETWORK_PASSPHRASE,
51
+ })
52
+ .addOperation(balanceOp)
53
+ .setTimeout(30)
54
+ .build();
55
+
56
+ const simulated = await server.simulateTransaction(tx);
57
+ if (rpc.Api.isSimulationError(simulated)) {
58
+ throw new Error(`Balance query failed: ${JSON.stringify(simulated)}`);
59
+ }
60
+
61
+ // Extract result from simulation
62
+ const result = (simulated as rpc.Api.SimulateTransactionSuccessResponse).result;
63
+ if (result?.retval) {
64
+ return scValToNative(result.retval) as bigint;
65
+ }
66
+ return 0n;
67
+ }
68
+
69
+ /**
70
+ * Helper to get native token (XLM) balance for an address.
71
+ * Convenience wrapper around getTokenBalance.
72
+ */
73
+ export async function getNativeBalance(
74
+ env: StellarTestEnv,
75
+ accountAddress: string,
76
+ ): Promise<bigint> {
77
+ return getTokenBalance(env, accountAddress, env.NATIVE_TOKEN_ADDRESS);
78
+ }
79
+
80
+ /**
81
+ * Helper to check if an account is authorized on a SAC (Stellar Asset Contract).
82
+ * Calls the SAC's `authorized()` method directly.
83
+ *
84
+ * @param accountAddress - The account to check authorization for
85
+ * @param tokenAddress - The SAC contract address
86
+ */
87
+ export async function getTokenAuthorized(
88
+ env: StellarTestEnv,
89
+ accountAddress: string,
90
+ tokenAddress: string,
91
+ ): Promise<boolean> {
92
+ const server = new rpc.Server(env.RPC_URL, { allowHttp: true });
93
+ const tokenContract = new Contract(tokenAddress);
94
+
95
+ const authorizedOp = tokenContract.call(
96
+ 'authorized',
97
+ nativeToScVal(Address.fromString(accountAddress), { type: 'address' }),
98
+ );
99
+
100
+ const account = await server.getAccount(env.DEFAULT_DEPLOYER.publicKey());
101
+ const tx = new TransactionBuilder(account, {
102
+ fee: BASE_FEE,
103
+ networkPassphrase: env.NETWORK_PASSPHRASE,
104
+ })
105
+ .addOperation(authorizedOp)
106
+ .setTimeout(30)
107
+ .build();
108
+
109
+ const simulated = await server.simulateTransaction(tx);
110
+ if (rpc.Api.isSimulationError(simulated)) {
111
+ throw new Error(`Authorized query failed: ${JSON.stringify(simulated)}`);
112
+ }
113
+
114
+ const result = (simulated as rpc.Api.SimulateTransactionSuccessResponse).result;
115
+ if (result?.retval) {
116
+ return scValToNative(result.retval) as boolean;
117
+ }
118
+ return false;
119
+ }
120
+
121
+ // ============================================================================
122
+ // DVN Abstract Account Auth Signing
123
+ // ============================================================================
124
+
125
+ /**
126
+ * Signs the DVN abstract account's auth entries.
127
+ *
128
+ * The DVN contract implements CustomAccountInterface with Signature = TransactionAuthData.
129
+ * TransactionAuthData contains:
130
+ * - vid: u32 - Verifier ID
131
+ * - expiration: u64 - Ledger timestamp for when auth expires
132
+ * - signatures: Vec<BytesN<65>> - Secp256k1 signatures from multisig signers
133
+ * - sender: Sender - Either None or Admin(public_key, ed25519_signature)
134
+ */
135
+ export async function signDvnAuthEntries<T>(
136
+ dvnAddress: string,
137
+ vid: number,
138
+ adminKeypair: Keypair,
139
+ multisigSigners: Secp256k1KeyPair[],
140
+ assembledTx: contract.AssembledTransaction<T>,
141
+ networkPassphrase: string,
142
+ ): Promise<void> {
143
+ const dvnAddr = Address.fromString(dvnAddress);
144
+
145
+ console.log('\n🔄 Simulating DVN transaction to get the auth entries');
146
+ await assembledTx.simulate();
147
+
148
+ // Print debug info
149
+ let remaining = assembledTx.needsNonInvokerSigningBy({ includeAlreadySigned: false });
150
+ console.log('\n📋 Addresses needing to sign:', remaining);
151
+
152
+ const networkId = hash(Buffer.from(networkPassphrase));
153
+
154
+ // Custom authorizer for DVN abstract account
155
+ const customAuthorizeEntry = async (
156
+ entry: xdr.SorobanAuthorizationEntry,
157
+ _signer: Keypair | ((preimage: xdr.HashIdPreimage) => Promise<unknown>),
158
+ validUntilLedgerSeq: number,
159
+ _passphrase?: string,
160
+ ): Promise<xdr.SorobanAuthorizationEntry> => {
161
+ const credentials = entry.credentials();
162
+ if (credentials.switch() !== xdr.SorobanCredentialsType.sorobanCredentialsAddress()) {
163
+ throw new Error('Expected address credentials');
164
+ }
165
+
166
+ const addressCred = credentials.address();
167
+ const credentialAddress = Address.fromScAddress(addressCred.address());
168
+ const rootInvocation = entry.rootInvocation();
169
+
170
+ if (credentialAddress.toString() !== dvnAddr.toString()) {
171
+ throw new Error('Credential address mismatch');
172
+ }
173
+
174
+ // Log the DVN's auth entry tree
175
+ console.log('\n🌳 DVN Auth Entry Tree:');
176
+ logInvocationTree(rootInvocation, 0);
177
+
178
+ // 1. Compute the signature_payload (soroban authorization hash)
179
+ const preimage = xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
180
+ new xdr.HashIdPreimageSorobanAuthorization({
181
+ networkId,
182
+ nonce: addressCred.nonce(),
183
+ signatureExpirationLedger: validUntilLedgerSeq,
184
+ invocation: rootInvocation,
185
+ }),
186
+ );
187
+ const signaturePayload = hash(preimage.toXDR());
188
+ console.log(
189
+ '\n📝 Signature payload (soroban auth hash):',
190
+ signaturePayload.toString('hex'),
191
+ );
192
+
193
+ // 2. Sign the signature_payload with admin's Ed25519 key
194
+ const adminSignature = adminKeypair.sign(signaturePayload);
195
+ console.log(
196
+ '✍️ Admin Ed25519 signature created:',
197
+ adminSignature.toString('hex').slice(0, 32) + '...',
198
+ );
199
+
200
+ // 3. Extract calls from the auth entry and compute the multisig hash
201
+ // The expiration is the ledger timestamp when the auth expires
202
+ // We use validUntilLedgerSeq * 5 as a rough approximation (5 seconds per ledger)
203
+ const expiration = BigInt(validUntilLedgerSeq) * 5n + BigInt(Math.floor(Date.now() / 1000));
204
+ const rootCall = getRootCall(rootInvocation);
205
+ const isSelfCall = rootCall.to === dvnAddr.toString();
206
+ const calls = isSelfCall ? [rootCall] : collectCallsFromInvocation(rootInvocation);
207
+ const callsXdr = serializeCallsToXdr(calls);
208
+ const callHash = computeCallHash(vid, expiration, callsXdr);
209
+ console.log('📝 Call hash for multisig:', Buffer.from(callHash).toString('hex'));
210
+
211
+ // 4. Sign the call hash with multisig signers
212
+ const signatures: Buffer[] = [];
213
+ for (const signer of multisigSigners) {
214
+ const sig = await signer.sign(callHash);
215
+ signatures.push(sig);
216
+ console.log('✍️ Multisig signature created:', sig.toString('hex').slice(0, 32) + '...');
217
+ }
218
+
219
+ // 5. Sort signatures by recovered signer address (ascending)
220
+ // This is required by the DVN contract's verify_signatures function
221
+ const signaturesWithAddresses = await Promise.all(
222
+ signatures.map(async (sig, i) => ({
223
+ sig,
224
+ address: multisigSigners[i].ethAddress,
225
+ })),
226
+ );
227
+ signaturesWithAddresses.sort((a, b) => a.address.compare(b.address));
228
+ const sortedSignatures = signaturesWithAddresses.map((s) => s.sig);
229
+
230
+ // 6. Build TransactionAuthData as ScVal
231
+ // struct TransactionAuthData { vid: u32, expiration: u64, signatures: Vec<BytesN<65>>, sender: Sender }
232
+ // enum Sender { None, Admin(BytesN<32>, BytesN<64>) }
233
+ const transactionAuthDataScVal = xdr.ScVal.scvMap([
234
+ new xdr.ScMapEntry({
235
+ key: xdr.ScVal.scvSymbol('expiration'),
236
+ val: xdr.ScVal.scvU64(new xdr.Uint64(expiration)),
237
+ }),
238
+ new xdr.ScMapEntry({
239
+ key: xdr.ScVal.scvSymbol('sender'),
240
+ // Sender::Admin(public_key, signature)
241
+ val: xdr.ScVal.scvVec([
242
+ xdr.ScVal.scvSymbol('Admin'),
243
+ xdr.ScVal.scvBytes(adminKeypair.rawPublicKey()),
244
+ xdr.ScVal.scvBytes(adminSignature),
245
+ ]),
246
+ }),
247
+ new xdr.ScMapEntry({
248
+ key: xdr.ScVal.scvSymbol('signatures'),
249
+ val: xdr.ScVal.scvVec(sortedSignatures.map((sig) => xdr.ScVal.scvBytes(sig))),
250
+ }),
251
+ new xdr.ScMapEntry({
252
+ key: xdr.ScVal.scvSymbol('vid'),
253
+ val: xdr.ScVal.scvU32(vid),
254
+ }),
255
+ ]);
256
+
257
+ // Return DVN's auth entry with TransactionAuthData
258
+ const newCred = new xdr.SorobanAddressCredentials({
259
+ address: addressCred.address(),
260
+ nonce: addressCred.nonce(),
261
+ signatureExpirationLedger: validUntilLedgerSeq,
262
+ signature: transactionAuthDataScVal,
263
+ });
264
+
265
+ return new xdr.SorobanAuthorizationEntry({
266
+ credentials: xdr.SorobanCredentials.sorobanCredentialsAddress(newCred),
267
+ rootInvocation,
268
+ });
269
+ };
270
+
271
+ // Check if the DVN needs to sign
272
+ if (remaining.includes(dvnAddr.toString())) {
273
+ await assembledTx.signAuthEntries({
274
+ address: dvnAddr.toString(),
275
+ authorizeEntry: customAuthorizeEntry,
276
+ });
277
+
278
+ console.log('\n🔄 DVN auth signed');
279
+
280
+ remaining = assembledTx.needsNonInvokerSigningBy({
281
+ includeAlreadySigned: false,
282
+ });
283
+ console.log('📋 Remaining signers after DVN:', remaining);
284
+ }
285
+
286
+ // Final re-simulation
287
+ console.log('\n🔄 Final re-simulation with DVN auth entries signed');
288
+ await assembledTx.simulate();
289
+ console.log('✅ Final simulation complete');
290
+ }
291
+
292
+ /**
293
+ * Represents a contract call for multisig authorization.
294
+ */
295
+ interface Call {
296
+ to: string; // Contract address
297
+ func: string; // Function name
298
+ args: xdr.ScVal[]; // Function arguments
299
+ }
300
+
301
+ /**
302
+ * Extracts only the root call from an invocation (no recursion into sub-invocations).
303
+ */
304
+ function getRootCall(invocation: xdr.SorobanAuthorizedInvocation): Call {
305
+ const fn = invocation.function();
306
+ if (
307
+ fn.switch() !== xdr.SorobanAuthorizedFunctionType.sorobanAuthorizedFunctionTypeContractFn()
308
+ ) {
309
+ throw new Error('Root invocation is not a contract function');
310
+ }
311
+ const contractFn = fn.contractFn();
312
+ return {
313
+ to: Address.fromScAddress(contractFn.contractAddress()).toString(),
314
+ func: contractFn.functionName().toString(),
315
+ args: contractFn.args(),
316
+ };
317
+ }
318
+
319
+ /**
320
+ * Collects all contract calls from an invocation tree.
321
+ */
322
+ function collectCallsFromInvocation(invocation: xdr.SorobanAuthorizedInvocation): Call[] {
323
+ const calls: Call[] = [];
324
+ collectCallsRecursive(invocation, calls);
325
+ return calls;
326
+ }
327
+
328
+ function collectCallsRecursive(invocation: xdr.SorobanAuthorizedInvocation, calls: Call[]): void {
329
+ const fn = invocation.function();
330
+
331
+ if (
332
+ fn.switch() === xdr.SorobanAuthorizedFunctionType.sorobanAuthorizedFunctionTypeContractFn()
333
+ ) {
334
+ const contractFn = fn.contractFn();
335
+ const contractAddr = Address.fromScAddress(contractFn.contractAddress());
336
+
337
+ calls.push({
338
+ to: contractAddr.toString(),
339
+ func: contractFn.functionName().toString(),
340
+ args: contractFn.args(),
341
+ });
342
+ }
343
+
344
+ // Process sub-invocations
345
+ for (const sub of invocation.subInvocations()) {
346
+ collectCallsRecursive(sub, calls);
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Serializes calls to XDR format matching Soroban's Vec<Call> serialization.
352
+ *
353
+ * Call struct: { args: Vec<Val>, func: Symbol, to: Address }
354
+ * Serialized as ScMap with keys in alphabetical order.
355
+ */
356
+ function serializeCallsToXdr(calls: Call[]): Buffer {
357
+ const callScVals = calls.map((call) =>
358
+ xdr.ScVal.scvMap([
359
+ new xdr.ScMapEntry({
360
+ key: xdr.ScVal.scvSymbol('args'),
361
+ val: xdr.ScVal.scvVec(call.args),
362
+ }),
363
+ new xdr.ScMapEntry({
364
+ key: xdr.ScVal.scvSymbol('func'),
365
+ val: xdr.ScVal.scvSymbol(call.func),
366
+ }),
367
+ new xdr.ScMapEntry({
368
+ key: xdr.ScVal.scvSymbol('to'),
369
+ val: Address.fromString(call.to).toScVal(),
370
+ }),
371
+ ]),
372
+ );
373
+
374
+ const vecScVal = xdr.ScVal.scvVec(callScVals);
375
+ return Buffer.from(vecScVal.toXDR());
376
+ }
377
+
378
+ /**
379
+ * Computes the call hash for multisig signing.
380
+ * hash = keccak256(vid.to_be_bytes(4) || expiration.to_be_bytes(8) || calls_xdr)
381
+ */
382
+ function computeCallHash(vid: number, expiration: bigint, callsXdr: Buffer): Uint8Array {
383
+ // vid as 4-byte big-endian
384
+ const vidBytes = Buffer.alloc(4);
385
+ vidBytes.writeUInt32BE(vid, 0);
386
+
387
+ // expiration as 8-byte big-endian
388
+ const expirationBytes = Buffer.alloc(8);
389
+ expirationBytes.writeBigUInt64BE(expiration, 0);
390
+
391
+ // Concatenate and hash
392
+ const data = Buffer.concat([vidBytes, expirationBytes, callsXdr]);
393
+ return keccak_256(data);
394
+ }
395
+
396
+ // ============================================================================
397
+ // Executor Abstract Account Auth Signing (with Non-Root Auth Support)
398
+ // ============================================================================
399
+
400
+ /**
401
+ * Signs and sends a transaction with Executor abstract account auth entries.
402
+ *
403
+ * The Executor contract implements CustomAccountInterface with Signature = ExecutorSignature.
404
+ * ExecutorSignature contains:
405
+ * - public_key: BytesN<32> - Admin's Ed25519 public key
406
+ * - signature: BytesN<64> - Ed25519 signature over the signature_payload
407
+ *
408
+ * This function uses `record_allow_nonroot` simulation mode to capture non-root auth entries,
409
+ * which is required because the executor authorizes `lz_receive`/`lz_compose` calls that are
410
+ * invoked by the ExecutorHelper contract (not the root invoker).
411
+ *
412
+ * @returns The transaction result after sending
413
+ */
414
+ export async function signAndSendWithExecutorAuth<T>(
415
+ env: StellarTestEnv,
416
+ executorAddress: string,
417
+ adminKeypair: Keypair,
418
+ assembledTx: contract.AssembledTransaction<T>,
419
+ networkPassphrase: string = env.NETWORK_PASSPHRASE,
420
+ ): Promise<rpc.Api.GetSuccessfulTransactionResponse> {
421
+ const executorAddr = Address.fromString(executorAddress);
422
+ const server = new rpc.Server(env.RPC_URL, { allowHttp: true });
423
+
424
+ console.log('\n🔄 Building transaction for non-root auth...');
425
+
426
+ // 1. Build the raw transaction (don't simulate yet)
427
+ if (!assembledTx.raw) {
428
+ throw new Error(
429
+ 'Failed to build raw transaction: assembledTx.raw is undefined (transaction was not assembled)',
430
+ );
431
+ }
432
+ const rawTx = assembledTx.raw.build();
433
+
434
+ // 2. Simulate with record_allow_nonroot to capture non-root auth entries
435
+ // This is required because the executor's auth is not the root invocation
436
+ console.log('🔄 Simulating with record_allow_nonroot...');
437
+ const sim = await server.simulateTransaction(
438
+ rawTx,
439
+ undefined, // addlResources
440
+ 'record_allow_nonroot', // ← This enables non-root auth recording!
441
+ );
442
+
443
+ if (rpc.Api.isSimulationError(sim)) {
444
+ throw new Error(`Simulation failed: ${JSON.stringify(sim)}`);
445
+ }
446
+
447
+ console.log('✅ Simulation complete');
448
+ console.log(' Auth entries returned:', sim.result?.auth?.length ?? 0);
449
+
450
+ // 3. Sign auth entries
451
+ const latestLedger = sim.latestLedger;
452
+ const validUntilLedger = latestLedger + 100;
453
+ const networkId = hash(Buffer.from(networkPassphrase));
454
+
455
+ if (sim.result && sim.result.auth) {
456
+ sim.result.auth = await Promise.all(
457
+ sim.result.auth.map(async (entry) => {
458
+ const credentials = entry.credentials();
459
+
460
+ // Source account credentials are already signed by tx envelope
461
+ if (
462
+ credentials.switch() ===
463
+ xdr.SorobanCredentialsType.sorobanCredentialsSourceAccount()
464
+ ) {
465
+ console.log(' Skipping source account auth entry');
466
+ return entry;
467
+ }
468
+
469
+ // Address credentials need explicit signature
470
+ const addressCred = credentials.address();
471
+ const addr = Address.fromScAddress(addressCred.address()).toString();
472
+ const rootInvocation = entry.rootInvocation();
473
+
474
+ console.log(' Processing auth entry for address:', addr);
475
+ console.log(' Auth entry tree:');
476
+ logInvocationTree(rootInvocation, 2);
477
+
478
+ // Check if this is the executor's auth entry (Abstract Account)
479
+ if (addr === executorAddr.toString()) {
480
+ console.log(' ✍️ Signing executor auth entry (Abstract Account)...');
481
+
482
+ // Compute the signature_payload hash
483
+ const preimage = xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
484
+ new xdr.HashIdPreimageSorobanAuthorization({
485
+ networkId,
486
+ nonce: addressCred.nonce(),
487
+ signatureExpirationLedger: validUntilLedger,
488
+ invocation: rootInvocation,
489
+ }),
490
+ );
491
+ const signaturePayload = hash(preimage.toXDR());
492
+
493
+ // Sign the signature_payload with admin's Ed25519 key
494
+ const adminSignature = adminKeypair.sign(signaturePayload);
495
+
496
+ // Build ExecutorSignature struct as ScVal
497
+ // struct ExecutorSignature { public_key: BytesN<32>, signature: BytesN<64> }
498
+ const executorSignatureScVal = xdr.ScVal.scvMap([
499
+ new xdr.ScMapEntry({
500
+ key: xdr.ScVal.scvSymbol('public_key'),
501
+ val: xdr.ScVal.scvBytes(adminKeypair.rawPublicKey()),
502
+ }),
503
+ new xdr.ScMapEntry({
504
+ key: xdr.ScVal.scvSymbol('signature'),
505
+ val: xdr.ScVal.scvBytes(adminSignature),
506
+ }),
507
+ ]);
508
+
509
+ // Return executor's auth entry with ExecutorSignature
510
+ const newCred = new xdr.SorobanAddressCredentials({
511
+ address: addressCred.address(),
512
+ nonce: addressCred.nonce(),
513
+ signatureExpirationLedger: validUntilLedger,
514
+ signature: executorSignatureScVal,
515
+ });
516
+
517
+ return new xdr.SorobanAuthorizationEntry({
518
+ credentials: xdr.SorobanCredentials.sorobanCredentialsAddress(newCred),
519
+ rootInvocation,
520
+ });
521
+ }
522
+
523
+ // Check if this is the admin's auth entry (regular Stellar account)
524
+ // This happens when admin needs to authorize token transfers (e.g., native_drop, value transfers)
525
+ if (addr === adminKeypair.publicKey()) {
526
+ console.log(' ✍️ Signing admin auth entry (regular account)...');
527
+ return authorizeEntry(entry, adminKeypair, validUntilLedger, networkPassphrase);
528
+ }
529
+
530
+ throw new Error(`Unexpected auth signer needed: ${addr}`);
531
+ }),
532
+ );
533
+ console.log('✅ Auth entries signed');
534
+ }
535
+
536
+ // 4. Assemble transaction with signed auth entries
537
+ const txWithSignedAuth = rpc.assembleTransaction(rawTx, sim).build();
538
+
539
+ console.log('✅ Transaction assembled with signed auth entries');
540
+
541
+ // 5. Re-simulate to get correct footprint (includes __check_auth storage accesses)
542
+ const finalSim = await server.simulateTransaction(txWithSignedAuth);
543
+ if (rpc.Api.isSimulationError(finalSim)) {
544
+ throw new Error(`Final simulation failed: ${JSON.stringify(finalSim)}`);
545
+ }
546
+
547
+ console.log('✅ Final simulation completed');
548
+
549
+ // 6. Assemble final transaction with accurate footprint
550
+ const assembledFinalTx = rpc.assembleTransaction(txWithSignedAuth, finalSim).build();
551
+
552
+ // 7. Rebuild the transaction with adminKeypair as the source account
553
+ // The original transaction was built with DEFAULT_DEPLOYER as source,
554
+ // but we want EXECUTOR_ADMIN to be the source so they can sign the envelope
555
+ // Fetch the current sequence number for the admin account from the network
556
+ const adminAccountInfo = await server.getAccount(adminKeypair.publicKey());
557
+ const adminAccount = new Account(adminKeypair.publicKey(), adminAccountInfo.sequenceNumber());
558
+ const finalTxBuilder = new TransactionBuilder(adminAccount, {
559
+ fee: assembledFinalTx.fee,
560
+ networkPassphrase,
561
+ });
562
+
563
+ // Get the transaction XDR to extract the operation and soroban data
564
+ const txXdr = assembledFinalTx.toEnvelope().v1().tx();
565
+
566
+ // Copy the Soroban invoke operation (there's only one operation in Soroban transactions)
567
+ const operationXdr = txXdr.operations()[0];
568
+ finalTxBuilder.addOperation(xdr.Operation.fromXDR(operationXdr.toXDR()));
569
+
570
+ // Copy the Soroban transaction data (footprint, resources, etc.)
571
+ const extSwitch = txXdr.ext().switch();
572
+ if (extSwitch === 1) {
573
+ // Has SorobanTransactionData
574
+ finalTxBuilder.setSorobanData(txXdr.ext().sorobanData());
575
+ }
576
+
577
+ // Set timeout to match original
578
+ finalTxBuilder.setTimeout(30);
579
+
580
+ const finalTx = finalTxBuilder.build();
581
+
582
+ // 8. Sign the transaction envelope
583
+ finalTx.sign(adminKeypair);
584
+
585
+ console.log('✅ Transaction envelope signed');
586
+
587
+ // 9. Send and poll
588
+ const sentResult = await server.sendTransaction(finalTx);
589
+
590
+ if (sentResult.status !== 'PENDING') {
591
+ throw new Error(`Transaction failed to send: ${JSON.stringify(sentResult)}`);
592
+ }
593
+
594
+ console.log('✅ Transaction sent, hash:', sentResult.hash);
595
+
596
+ const txResult = await server.pollTransaction(sentResult.hash);
597
+
598
+ if (txResult.status !== 'SUCCESS') {
599
+ throw new Error(`Transaction failed: ${JSON.stringify(txResult)}`);
600
+ }
601
+
602
+ console.log('✅ Transaction completed successfully');
603
+
604
+ return txResult as rpc.Api.GetSuccessfulTransactionResponse;
605
+ }
606
+
607
+ /**
608
+ * Logs the invocation tree for debugging auth entries.
609
+ */
610
+ function logInvocationTree(invocation: xdr.SorobanAuthorizedInvocation, depth: number): void {
611
+ const indent = ' '.repeat(depth);
612
+ const fn = invocation.function();
613
+
614
+ if (
615
+ fn.switch() === xdr.SorobanAuthorizedFunctionType.sorobanAuthorizedFunctionTypeContractFn()
616
+ ) {
617
+ const contractFn = fn.contractFn();
618
+ const contractAddr = Address.fromScAddress(contractFn.contractAddress());
619
+ const fnName = contractFn.functionName().toString();
620
+
621
+ console.log(`${indent}📞 ${contractAddr.toString()}...${fnName}()`);
622
+ } else {
623
+ console.log(`${indent}🔧 CreateContractHostFn`);
624
+ }
625
+
626
+ // Log sub-invocations
627
+ const subInvocations = invocation.subInvocations();
628
+ for (const sub of subInvocations) {
629
+ logInvocationTree(sub, depth + 1);
630
+ }
631
+ }
632
+
633
+ export function assertTransactionSucceeded(
634
+ txResult: rpc.Api.GetTransactionResponse,
635
+ contextLabel: string,
636
+ ): void {
637
+ if (txResult.status !== rpc.Api.GetTransactionStatus.SUCCESS) {
638
+ throw new Error(
639
+ `Transaction ${contextLabel} failed with status ${txResult.status}. Response: ${JSON.stringify(txResult)}`,
640
+ );
641
+ }
642
+ }