@algorandfoundation/algokit-utils 8.1.0-beta.1 → 8.1.0-beta.3

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.
@@ -189,14 +189,14 @@ const sendTransaction = async function (send, algod) {
189
189
  return { transaction };
190
190
  }
191
191
  let txnToSend = transaction;
192
- const populateResources = sendParams?.populateAppCallResources ?? Config.populateAppCallResources;
193
- // Populate resources if the transaction is an appcall and populateAppCallResources wasn't explicitly set to false
192
+ const populateAppCallResources = sendParams?.populateAppCallResources ?? Config.populateAppCallResources;
193
+ // Populate resources if the transaction is an appcall and populateAppCallResources wasn't explicitly set to false
194
194
  // NOTE: Temporary false by default until this algod bug is fixed: https://github.com/algorand/go-algorand/issues/5914
195
- if (txnToSend.type === algosdk.TransactionType.appl && populateResources) {
195
+ if (txnToSend.type === algosdk.TransactionType.appl && populateAppCallResources) {
196
196
  const newAtc = new AtomicTransactionComposer();
197
197
  newAtc.addTransaction({ txn: txnToSend, signer: getSenderTransactionSigner(from) });
198
- const packed = await populateAppCallResources(newAtc, algod);
199
- txnToSend = packed.buildGroup()[0].txn;
198
+ const atc = await prepareGroupForSending(newAtc, algod, { ...sendParams, populateAppCallResources });
199
+ txnToSend = atc.buildGroup()[0].txn;
200
200
  }
201
201
  const signedTransaction = await signTransaction(txnToSend, from);
202
202
  await algod.sendRawTransaction(signedTransaction).do();
@@ -208,13 +208,19 @@ const sendTransaction = async function (send, algod) {
208
208
  return { transaction: txnToSend, confirmation };
209
209
  };
210
210
  /**
211
- * Get all of the unamed resources used by the group in the given ATC
211
+ * Get the execution info of a transaction group for the given ATC
212
+ * The function uses the simulate endpoint and depending on the sendParams can return the following:
213
+ * - The unnamed resources accessed by the group
214
+ * - The unnamed resources accessed by each transaction in the group
215
+ * - The required fee delta for each transaction in the group. A positive value indicates a fee deficit, a negative value indicates a surplus.
212
216
  *
213
- * @param algod The algod client to use for the simulation
214
217
  * @param atc The ATC containing the txn group
215
- * @returns The unnamed resources accessed by the group and by each transaction in the group
218
+ * @param algod The algod client to use for the simulation
219
+ * @param sendParams The send params for the transaction group
220
+ * @param additionalAtcContext Additional ATC context used to determine how best to alter transactions in the group
221
+ * @returns The execution info for the group
216
222
  */
217
- async function getUnnamedAppCallResourcesAccessed(atc, algod) {
223
+ async function getGroupExecutionInfo(atc, algod, sendParams, additionalAtcContext) {
218
224
  const simulateRequest = new algosdk.modelsv2.SimulateRequest({
219
225
  txnGroups: [],
220
226
  allowUnnamedResources: true,
@@ -223,28 +229,77 @@ async function getUnnamedAppCallResourcesAccessed(atc, algod) {
223
229
  });
224
230
  const nullSigner = algosdk.makeEmptyTransactionSigner();
225
231
  const emptySignerAtc = atc.clone();
226
- emptySignerAtc['transactions'].forEach((t) => {
232
+ const appCallIndexesWithoutMaxFees = [];
233
+ emptySignerAtc['transactions'].forEach((t, i) => {
227
234
  t.signer = nullSigner;
235
+ if (sendParams.coverAppCallInnerTransactionFees && t.txn.type === TransactionType.appl) {
236
+ if (!additionalAtcContext?.suggestedParams) {
237
+ throw Error(`Please provide additionalAtcContext.suggestedParams when coverAppCallInnerTransactionFees is enabled`);
238
+ }
239
+ const maxFee = additionalAtcContext?.maxFees?.get(i)?.microAlgo;
240
+ if (maxFee === undefined) {
241
+ appCallIndexesWithoutMaxFees.push(i);
242
+ }
243
+ else {
244
+ t.txn.fee = maxFee;
245
+ }
246
+ }
228
247
  });
248
+ if (sendParams.coverAppCallInnerTransactionFees && appCallIndexesWithoutMaxFees.length > 0) {
249
+ throw Error(`Please provide a maxFee for each app call transaction when coverAppCallInnerTransactionFees is enabled. Required for transaction ${appCallIndexesWithoutMaxFees.join(', ')}`);
250
+ }
251
+ const perByteTxnFee = BigInt(additionalAtcContext?.suggestedParams.fee ?? 0n);
252
+ const minTxnFee = BigInt(additionalAtcContext?.suggestedParams.minFee ?? 1000n);
229
253
  const result = await emptySignerAtc.simulate(algod, simulateRequest);
230
254
  const groupResponse = result.simulateResponse.txnGroups[0];
231
255
  if (groupResponse.failureMessage) {
232
- throw Error(`Error during resource population simulation in transaction ${groupResponse.failedAt}: ${groupResponse.failureMessage}`);
256
+ if (sendParams.coverAppCallInnerTransactionFees && groupResponse.failureMessage.match(/fee too small/)) {
257
+ throw Error(`Fees were too small to resolve execution info via simulate. You may need to increase an app call transaction maxFee.`);
258
+ }
259
+ throw Error(`Error resolving execution info via simulate in transaction ${groupResponse.failedAt}: ${groupResponse.failureMessage}`);
233
260
  }
234
261
  return {
235
- group: groupResponse.unnamedResourcesAccessed,
236
- txns: groupResponse.txnResults.map(
237
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
238
- (t) => t.unnamedResourcesAccessed),
262
+ groupUnnamedResourcesAccessed: sendParams.populateAppCallResources ? groupResponse.unnamedResourcesAccessed : undefined,
263
+ txns: groupResponse.txnResults.map((txn, i) => {
264
+ const originalTxn = atc['transactions'][i].txn;
265
+ let requiredFeeDelta = 0n;
266
+ if (sendParams.coverAppCallInnerTransactionFees) {
267
+ // Min fee calc is lifted from algosdk https://github.com/algorand/js-algorand-sdk/blob/6973ff583b243ddb0632e91f4c0383021430a789/src/transaction.ts#L710
268
+ // 75 is the number of bytes added to a txn after signing it
269
+ const parentPerByteFee = perByteTxnFee * BigInt(originalTxn.toByte().length + 75);
270
+ const parentMinFee = parentPerByteFee < minTxnFee ? minTxnFee : parentPerByteFee;
271
+ const parentFeeDelta = parentMinFee - originalTxn.fee;
272
+ if (originalTxn.type === TransactionType.appl) {
273
+ const calculateInnerFeeDelta = (itxns, acc = 0n) => {
274
+ // Surplus inner transaction fees do not pool up to the parent transaction.
275
+ // Additionally surplus inner transaction fees only pool from sibling transactions that are sent prior to a given inner transaction, hence why we iterate in reverse order.
276
+ return itxns.reverse().reduce((acc, itxn) => {
277
+ const currentFeeDelta = (itxn.innerTxns && itxn.innerTxns.length > 0 ? calculateInnerFeeDelta(itxn.innerTxns, acc) : acc) +
278
+ (minTxnFee - itxn.txn.txn.fee); // Inner transactions don't require per byte fees
279
+ return currentFeeDelta < 0n ? 0n : currentFeeDelta;
280
+ }, acc);
281
+ };
282
+ const innerFeeDelta = calculateInnerFeeDelta(txn.txnResult.innerTxns ?? []);
283
+ requiredFeeDelta = innerFeeDelta + parentFeeDelta;
284
+ }
285
+ else {
286
+ requiredFeeDelta = parentFeeDelta;
287
+ }
288
+ }
289
+ return {
290
+ unnamedResourcesAccessed: sendParams.populateAppCallResources ? txn.unnamedResourcesAccessed : undefined,
291
+ requiredFeeDelta,
292
+ };
293
+ }),
239
294
  };
240
295
  }
241
296
  /**
242
297
  * Take an existing Atomic Transaction Composer and return a new one with the required
243
- * app call resources packed into it
298
+ * app call resources populated into it
244
299
  *
245
300
  * @param algod The algod client to use for the simulation
246
301
  * @param atc The ATC containing the txn group
247
- * @returns A new ATC with the resources packed into the transactions
302
+ * @returns A new ATC with the resources populated into the transactions
248
303
  *
249
304
  * @privateRemarks
250
305
  *
@@ -256,229 +311,312 @@ async function getUnnamedAppCallResourcesAccessed(atc, algod) {
256
311
  *
257
312
  */
258
313
  async function populateAppCallResources(atc, algod) {
259
- const unnamedResourcesAccessed = await getUnnamedAppCallResourcesAccessed(atc, algod);
314
+ return await prepareGroupForSending(atc, algod, { populateAppCallResources: true });
315
+ }
316
+ /**
317
+ * Take an existing Atomic Transaction Composer and return a new one with changes applied to the transactions
318
+ * based on the supplied sendParams to ensure the transaction group is ready for sending.
319
+ *
320
+ * @param algod The algod client to use for the simulation
321
+ * @param atc The ATC containing the txn group
322
+ * @param sendParams The send params for the transaction group
323
+ * @param additionalAtcContext Additional ATC context used to determine how best to change the transactions in the group
324
+ * @returns A new ATC with the changes applied
325
+ *
326
+ * @privateRemarks
327
+ * Parts of this function will eventually be implemented in algod. Namely:
328
+ * - Simulate will return information on how to populate reference arrays, see https://github.com/algorand/go-algorand/pull/6015
329
+ */
330
+ async function prepareGroupForSending(atc, algod, sendParams, additionalAtcContext) {
331
+ const executionInfo = await getGroupExecutionInfo(atc, algod, sendParams, additionalAtcContext);
260
332
  const group = atc.buildGroup();
261
- unnamedResourcesAccessed.txns.forEach((r, i) => {
262
- if (r === undefined || group[i].txn.type !== TransactionType.appl)
263
- return;
264
- if (r.boxes || r.extraBoxRefs)
265
- throw Error('Unexpected boxes at the transaction level');
266
- if (r.appLocals)
267
- throw Error('Unexpected app local at the transaction level');
268
- if (r.assetHoldings)
269
- throw Error('Unexpected asset holding at the transaction level');
270
- group[i].txn['applicationCall'] = {
271
- ...group[i].txn.applicationCall,
272
- accounts: [...(group[i].txn?.applicationCall?.accounts ?? []), ...(r.accounts ?? [])],
273
- foreignApps: [...(group[i].txn?.applicationCall?.foreignApps ?? []), ...(r.apps ?? [])],
274
- foreignAssets: [...(group[i].txn?.applicationCall?.foreignAssets ?? []), ...(r.assets ?? [])],
275
- boxes: [...(group[i].txn?.applicationCall?.boxes ?? []), ...(r.boxes ?? [])],
276
- };
277
- const accounts = group[i].txn.applicationCall?.accounts?.length ?? 0;
278
- if (accounts > MAX_APP_CALL_ACCOUNT_REFERENCES)
279
- throw Error(`Account reference limit of ${MAX_APP_CALL_ACCOUNT_REFERENCES} exceeded in transaction ${i}`);
280
- const assets = group[i].txn.applicationCall?.foreignAssets?.length ?? 0;
281
- const apps = group[i].txn.applicationCall?.foreignApps?.length ?? 0;
282
- const boxes = group[i].txn.applicationCall?.boxes?.length ?? 0;
283
- if (accounts + assets + apps + boxes > MAX_APP_CALL_FOREIGN_REFERENCES) {
284
- throw Error(`Resource reference limit of ${MAX_APP_CALL_FOREIGN_REFERENCES} exceeded in transaction ${i}`);
333
+ const [_, additionalTransactionFees] = sendParams.coverAppCallInnerTransactionFees
334
+ ? executionInfo.txns
335
+ .map((txn, i) => {
336
+ const groupIndex = i;
337
+ const txnInGroup = group[groupIndex].txn;
338
+ const maxFee = additionalAtcContext?.maxFees?.get(i)?.microAlgo;
339
+ const immutableFee = maxFee !== undefined && maxFee === txnInGroup.fee;
340
+ // Because we don't alter non app call transaction, they take priority
341
+ const priorityMultiplier = txn.requiredFeeDelta > 0n && (immutableFee || txnInGroup.type !== algosdk.TransactionType.appl) ? 1000n : 1n;
342
+ return {
343
+ ...txn,
344
+ groupIndex,
345
+ // Measures the priority level of covering the transaction fee using the surplus group fees. The higher the number, the higher the priority.
346
+ surplusFeePriorityLevel: txn.requiredFeeDelta > 0n ? txn.requiredFeeDelta * priorityMultiplier : -1n,
347
+ };
348
+ })
349
+ .sort((a, b) => {
350
+ return a.surplusFeePriorityLevel > b.surplusFeePriorityLevel ? -1 : a.surplusFeePriorityLevel < b.surplusFeePriorityLevel ? 1 : 0;
351
+ })
352
+ .reduce((acc, { groupIndex, requiredFeeDelta }) => {
353
+ if (requiredFeeDelta > 0n) {
354
+ // There is a fee deficit on the transaction
355
+ let surplusGroupFees = acc[0];
356
+ const additionalTransactionFees = acc[1];
357
+ const additionalFeeDelta = requiredFeeDelta - surplusGroupFees;
358
+ if (additionalFeeDelta <= 0n) {
359
+ // The surplus group fees fully cover the required fee delta
360
+ surplusGroupFees = -additionalFeeDelta;
361
+ }
362
+ else {
363
+ // The surplus group fees do not fully cover the required fee delta, use what is available
364
+ additionalTransactionFees.set(groupIndex, additionalFeeDelta);
365
+ surplusGroupFees = 0n;
366
+ }
367
+ return [surplusGroupFees, additionalTransactionFees];
368
+ }
369
+ return acc;
370
+ }, [
371
+ executionInfo.txns.reduce((acc, { requiredFeeDelta }) => {
372
+ if (requiredFeeDelta < 0n) {
373
+ return acc + -requiredFeeDelta;
374
+ }
375
+ return acc;
376
+ }, 0n),
377
+ new Map(),
378
+ ])
379
+ : [0n, new Map()];
380
+ executionInfo.txns.forEach(({ unnamedResourcesAccessed: r }, i) => {
381
+ // Populate Transaction App Call Resources
382
+ if (sendParams.populateAppCallResources && r !== undefined && group[i].txn.type === TransactionType.appl) {
383
+ if (r.boxes || r.extraBoxRefs)
384
+ throw Error('Unexpected boxes at the transaction level');
385
+ if (r.appLocals)
386
+ throw Error('Unexpected app local at the transaction level');
387
+ if (r.assetHoldings)
388
+ throw Error('Unexpected asset holding at the transaction level');
389
+ group[i].txn['applicationCall'] = {
390
+ ...group[i].txn.applicationCall,
391
+ accounts: [...(group[i].txn?.applicationCall?.accounts ?? []), ...(r.accounts ?? [])],
392
+ foreignApps: [...(group[i].txn?.applicationCall?.foreignApps ?? []), ...(r.apps ?? [])],
393
+ foreignAssets: [...(group[i].txn?.applicationCall?.foreignAssets ?? []), ...(r.assets ?? [])],
394
+ boxes: [...(group[i].txn?.applicationCall?.boxes ?? []), ...(r.boxes ?? [])],
395
+ };
396
+ const accounts = group[i].txn.applicationCall?.accounts?.length ?? 0;
397
+ if (accounts > MAX_APP_CALL_ACCOUNT_REFERENCES)
398
+ throw Error(`Account reference limit of ${MAX_APP_CALL_ACCOUNT_REFERENCES} exceeded in transaction ${i}`);
399
+ const assets = group[i].txn.applicationCall?.foreignAssets?.length ?? 0;
400
+ const apps = group[i].txn.applicationCall?.foreignApps?.length ?? 0;
401
+ const boxes = group[i].txn.applicationCall?.boxes?.length ?? 0;
402
+ if (accounts + assets + apps + boxes > MAX_APP_CALL_FOREIGN_REFERENCES) {
403
+ throw Error(`Resource reference limit of ${MAX_APP_CALL_FOREIGN_REFERENCES} exceeded in transaction ${i}`);
404
+ }
405
+ }
406
+ // Cover App Call Inner Transaction Fees
407
+ if (sendParams.coverAppCallInnerTransactionFees) {
408
+ const additionalTransactionFee = additionalTransactionFees.get(i);
409
+ if (additionalTransactionFee !== undefined) {
410
+ if (group[i].txn.type !== algosdk.TransactionType.appl) {
411
+ throw Error(`An additional fee of ${additionalTransactionFee} µALGO is required for non app call transaction ${i}`);
412
+ }
413
+ const transactionFee = group[i].txn.fee + additionalTransactionFee;
414
+ const maxFee = additionalAtcContext?.maxFees?.get(i)?.microAlgo;
415
+ if (maxFee === undefined || transactionFee > maxFee) {
416
+ throw Error(`Calculated transaction fee ${transactionFee} µALGO is greater than max of ${maxFee ?? 'undefined'} for transaction ${i}`);
417
+ }
418
+ group[i].txn.fee = transactionFee;
419
+ }
285
420
  }
286
421
  });
287
- const populateGroupResource = (txns, reference, type) => {
288
- const isApplBelowLimit = (t) => {
289
- if (t.txn.type !== algosdk.TransactionType.appl)
290
- return false;
291
- const accounts = t.txn.applicationCall?.accounts?.length ?? 0;
292
- const assets = t.txn.applicationCall?.foreignAssets?.length ?? 0;
293
- const apps = t.txn.applicationCall?.foreignApps?.length ?? 0;
294
- const boxes = t.txn.applicationCall?.boxes?.length ?? 0;
295
- return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES;
296
- };
297
- // If this is a asset holding or app local, first try to find a transaction that already has the account available
298
- if (type === 'assetHolding' || type === 'appLocal') {
299
- const { account } = reference;
300
- let txnIndex = txns.findIndex((t) => {
301
- if (!isApplBelowLimit(t))
422
+ // Populate Group App Call Resources
423
+ if (sendParams.populateAppCallResources) {
424
+ const populateGroupResource = (txns, reference, type) => {
425
+ const isApplBelowLimit = (t) => {
426
+ if (t.txn.type !== algosdk.TransactionType.appl)
302
427
  return false;
303
- return (
304
- // account is in the foreign accounts array
305
- t.txn.applicationCall?.accounts?.map((a) => a.toString()).includes(account.toString()) ||
306
- // account is available as an app account
307
- t.txn.applicationCall?.foreignApps?.map((a) => algosdk.getApplicationAddress(a).toString()).includes(account.toString()) ||
308
- // account is available since it's in one of the fields
309
- Object.values(t.txn).some((f) => stringifyJSON(f, (_, v) => (v instanceof Address ? v.toString() : v))?.includes(account.toString())));
310
- });
311
- if (txnIndex > -1) {
312
- if (type === 'assetHolding') {
313
- const { asset } = reference;
428
+ const accounts = t.txn.applicationCall?.accounts?.length ?? 0;
429
+ const assets = t.txn.applicationCall?.foreignAssets?.length ?? 0;
430
+ const apps = t.txn.applicationCall?.foreignApps?.length ?? 0;
431
+ const boxes = t.txn.applicationCall?.boxes?.length ?? 0;
432
+ return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES;
433
+ };
434
+ // If this is a asset holding or app local, first try to find a transaction that already has the account available
435
+ if (type === 'assetHolding' || type === 'appLocal') {
436
+ const { account } = reference;
437
+ let txnIndex = txns.findIndex((t) => {
438
+ if (!isApplBelowLimit(t))
439
+ return false;
440
+ return (
441
+ // account is in the foreign accounts array
442
+ t.txn.applicationCall?.accounts?.map((a) => a.toString()).includes(account.toString()) ||
443
+ // account is available as an app account
444
+ t.txn.applicationCall?.foreignApps?.map((a) => algosdk.getApplicationAddress(a).toString()).includes(account.toString()) ||
445
+ // account is available since it's in one of the fields
446
+ Object.values(t.txn).some((f) => stringifyJSON(f, (_, v) => (v instanceof Address ? v.toString() : v))?.includes(account.toString())));
447
+ });
448
+ if (txnIndex > -1) {
449
+ if (type === 'assetHolding') {
450
+ const { asset } = reference;
451
+ txns[txnIndex].txn['applicationCall'] = {
452
+ ...txns[txnIndex].txn.applicationCall,
453
+ foreignAssets: [...(txns[txnIndex].txn?.applicationCall?.foreignAssets ?? []), ...[asset]],
454
+ };
455
+ }
456
+ else {
457
+ const { app } = reference;
458
+ txns[txnIndex].txn['applicationCall'] = {
459
+ ...txns[txnIndex].txn.applicationCall,
460
+ foreignApps: [...(txns[txnIndex].txn?.applicationCall?.foreignApps ?? []), ...[app]],
461
+ };
462
+ }
463
+ return;
464
+ }
465
+ // Now try to find a txn that already has that app or asset available
466
+ txnIndex = txns.findIndex((t) => {
467
+ if (!isApplBelowLimit(t))
468
+ return false;
469
+ // check if there is space in the accounts array
470
+ if ((t.txn.applicationCall?.accounts?.length ?? 0) >= MAX_APP_CALL_ACCOUNT_REFERENCES)
471
+ return false;
472
+ if (type === 'assetHolding') {
473
+ const { asset } = reference;
474
+ return t.txn.applicationCall?.foreignAssets?.includes(asset);
475
+ }
476
+ else {
477
+ const { app } = reference;
478
+ return t.txn.applicationCall?.foreignApps?.includes(app) || t.txn.applicationCall?.appIndex === app;
479
+ }
480
+ });
481
+ if (txnIndex > -1) {
482
+ const { account } = reference;
314
483
  txns[txnIndex].txn['applicationCall'] = {
315
484
  ...txns[txnIndex].txn.applicationCall,
316
- foreignAssets: [...(txns[txnIndex].txn?.applicationCall?.foreignAssets ?? []), ...[asset]],
485
+ accounts: [...(txns[txnIndex].txn?.applicationCall?.accounts ?? []), ...[account]],
317
486
  };
487
+ return;
318
488
  }
319
- else {
320
- const { app } = reference;
489
+ }
490
+ // If this is a box, first try to find a transaction that already has the app available
491
+ if (type === 'box') {
492
+ const { app, name } = reference;
493
+ const txnIndex = txns.findIndex((t) => {
494
+ if (!isApplBelowLimit(t))
495
+ return false;
496
+ // If the app is in the foreign array OR the app being called, then we know it's available
497
+ return t.txn.applicationCall?.foreignApps?.includes(app) || t.txn.applicationCall?.appIndex === app;
498
+ });
499
+ if (txnIndex > -1) {
321
500
  txns[txnIndex].txn['applicationCall'] = {
322
501
  ...txns[txnIndex].txn.applicationCall,
323
- foreignApps: [...(txns[txnIndex].txn?.applicationCall?.foreignApps ?? []), ...[app]],
502
+ boxes: [...(txns[txnIndex].txn?.applicationCall?.boxes ?? []), ...[{ appIndex: app, name }]],
324
503
  };
504
+ return;
325
505
  }
326
- return;
327
506
  }
328
- // Now try to find a txn that already has that app or asset available
329
- txnIndex = txns.findIndex((t) => {
330
- if (!isApplBelowLimit(t))
331
- return false;
332
- // check if there is space in the accounts array
333
- if ((t.txn.applicationCall?.accounts?.length ?? 0) >= MAX_APP_CALL_ACCOUNT_REFERENCES)
507
+ // Find the txn index to put the reference(s)
508
+ const txnIndex = txns.findIndex((t) => {
509
+ if (t.txn.type !== algosdk.TransactionType.appl)
334
510
  return false;
335
- if (type === 'assetHolding') {
336
- const { asset } = reference;
337
- return t.txn.applicationCall?.foreignAssets?.includes(asset);
511
+ const accounts = t.txn.applicationCall?.accounts?.length ?? 0;
512
+ if (type === 'account')
513
+ return accounts < MAX_APP_CALL_ACCOUNT_REFERENCES;
514
+ const assets = t.txn.applicationCall?.foreignAssets?.length ?? 0;
515
+ const apps = t.txn.applicationCall?.foreignApps?.length ?? 0;
516
+ const boxes = t.txn.applicationCall?.boxes?.length ?? 0;
517
+ // If we're adding local state or asset holding, we need space for the acocunt and the other reference
518
+ if (type === 'assetHolding' || type === 'appLocal') {
519
+ return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES - 1 && accounts < MAX_APP_CALL_ACCOUNT_REFERENCES;
338
520
  }
339
- else {
340
- const { app } = reference;
341
- return t.txn.applicationCall?.foreignApps?.includes(app) || t.txn.applicationCall?.appIndex === app;
521
+ // If we're adding a box, we need space for both the box ref and the app ref
522
+ if (type === 'box' && BigInt(reference.app) !== BigInt(0)) {
523
+ return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES - 1;
342
524
  }
525
+ return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES;
343
526
  });
344
- if (txnIndex > -1) {
345
- const { account } = reference;
527
+ if (txnIndex === -1) {
528
+ throw Error('No more transactions below reference limit. Add another app call to the group.');
529
+ }
530
+ if (type === 'account') {
346
531
  txns[txnIndex].txn['applicationCall'] = {
347
532
  ...txns[txnIndex].txn.applicationCall,
348
- accounts: [...(txns[txnIndex].txn?.applicationCall?.accounts ?? []), ...[account]],
533
+ accounts: [...(txns[txnIndex].txn?.applicationCall?.accounts ?? []), ...[reference]],
349
534
  };
350
- return;
351
535
  }
352
- }
353
- // If this is a box, first try to find a transaction that already has the app available
354
- if (type === 'box') {
355
- const { app, name } = reference;
356
- const txnIndex = txns.findIndex((t) => {
357
- if (!isApplBelowLimit(t))
358
- return false;
359
- // If the app is in the foreign array OR the app being called, then we know it's available
360
- return t.txn.applicationCall?.foreignApps?.includes(app) || t.txn.applicationCall?.appIndex === app;
361
- });
362
- if (txnIndex > -1) {
536
+ else if (type === 'app') {
363
537
  txns[txnIndex].txn['applicationCall'] = {
364
538
  ...txns[txnIndex].txn.applicationCall,
365
- boxes: [...(txns[txnIndex].txn?.applicationCall?.boxes ?? []), ...[{ appIndex: app, name }]],
539
+ foreignApps: [
540
+ ...(txns[txnIndex].txn?.applicationCall?.foreignApps ?? []),
541
+ ...[typeof reference === 'bigint' ? reference : BigInt(reference)],
542
+ ],
366
543
  };
367
- return;
368
544
  }
369
- }
370
- // Find the txn index to put the reference(s)
371
- const txnIndex = txns.findIndex((t) => {
372
- if (t.txn.type !== algosdk.TransactionType.appl)
373
- return false;
374
- const accounts = t.txn.applicationCall?.accounts?.length ?? 0;
375
- if (type === 'account')
376
- return accounts < MAX_APP_CALL_ACCOUNT_REFERENCES;
377
- const assets = t.txn.applicationCall?.foreignAssets?.length ?? 0;
378
- const apps = t.txn.applicationCall?.foreignApps?.length ?? 0;
379
- const boxes = t.txn.applicationCall?.boxes?.length ?? 0;
380
- // If we're adding local state or asset holding, we need space for the acocunt and the other reference
381
- if (type === 'assetHolding' || type === 'appLocal') {
382
- return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES - 1 && accounts < MAX_APP_CALL_ACCOUNT_REFERENCES;
545
+ else if (type === 'box') {
546
+ const { app, name } = reference;
547
+ txns[txnIndex].txn['applicationCall'] = {
548
+ ...txns[txnIndex].txn.applicationCall,
549
+ boxes: [...(txns[txnIndex].txn?.applicationCall?.boxes ?? []), ...[{ appIndex: app, name }]],
550
+ };
551
+ if (app.toString() !== '0') {
552
+ txns[txnIndex].txn['applicationCall'] = {
553
+ ...txns[txnIndex].txn.applicationCall,
554
+ foreignApps: [...(txns[txnIndex].txn?.applicationCall?.foreignApps ?? []), ...[app]],
555
+ };
556
+ }
383
557
  }
384
- // If we're adding a box, we need space for both the box ref and the app ref
385
- if (type === 'box' && BigInt(reference.app) !== BigInt(0)) {
386
- return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES - 1;
558
+ else if (type === 'assetHolding') {
559
+ const { asset, account } = reference;
560
+ txns[txnIndex].txn['applicationCall'] = {
561
+ ...txns[txnIndex].txn.applicationCall,
562
+ foreignAssets: [...(txns[txnIndex].txn?.applicationCall?.foreignAssets ?? []), ...[asset]],
563
+ accounts: [...(txns[txnIndex].txn?.applicationCall?.accounts ?? []), ...[account]],
564
+ };
387
565
  }
388
- return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES;
389
- });
390
- if (txnIndex === -1) {
391
- throw Error('No more transactions below reference limit. Add another app call to the group.');
392
- }
393
- if (type === 'account') {
394
- txns[txnIndex].txn['applicationCall'] = {
395
- ...txns[txnIndex].txn.applicationCall,
396
- accounts: [...(txns[txnIndex].txn?.applicationCall?.accounts ?? []), ...[reference]],
397
- };
398
- }
399
- else if (type === 'app') {
400
- txns[txnIndex].txn['applicationCall'] = {
401
- ...txns[txnIndex].txn.applicationCall,
402
- foreignApps: [
403
- ...(txns[txnIndex].txn?.applicationCall?.foreignApps ?? []),
404
- ...[typeof reference === 'bigint' ? reference : BigInt(reference)],
405
- ],
406
- };
407
- }
408
- else if (type === 'box') {
409
- const { app, name } = reference;
410
- txns[txnIndex].txn['applicationCall'] = {
411
- ...txns[txnIndex].txn.applicationCall,
412
- boxes: [...(txns[txnIndex].txn?.applicationCall?.boxes ?? []), ...[{ appIndex: app, name }]],
413
- };
414
- if (app.toString() !== '0') {
566
+ else if (type === 'appLocal') {
567
+ const { app, account } = reference;
415
568
  txns[txnIndex].txn['applicationCall'] = {
416
569
  ...txns[txnIndex].txn.applicationCall,
417
570
  foreignApps: [...(txns[txnIndex].txn?.applicationCall?.foreignApps ?? []), ...[app]],
571
+ accounts: [...(txns[txnIndex].txn?.applicationCall?.accounts ?? []), ...[account]],
418
572
  };
419
573
  }
420
- }
421
- else if (type === 'assetHolding') {
422
- const { asset, account } = reference;
423
- txns[txnIndex].txn['applicationCall'] = {
424
- ...txns[txnIndex].txn.applicationCall,
425
- foreignAssets: [...(txns[txnIndex].txn?.applicationCall?.foreignAssets ?? []), ...[asset]],
426
- accounts: [...(txns[txnIndex].txn?.applicationCall?.accounts ?? []), ...[account]],
427
- };
428
- }
429
- else if (type === 'appLocal') {
430
- const { app, account } = reference;
431
- txns[txnIndex].txn['applicationCall'] = {
432
- ...txns[txnIndex].txn.applicationCall,
433
- foreignApps: [...(txns[txnIndex].txn?.applicationCall?.foreignApps ?? []), ...[app]],
434
- accounts: [...(txns[txnIndex].txn?.applicationCall?.accounts ?? []), ...[account]],
435
- };
436
- }
437
- else if (type === 'asset') {
438
- txns[txnIndex].txn['applicationCall'] = {
439
- ...txns[txnIndex].txn.applicationCall,
440
- foreignAssets: [
441
- ...(txns[txnIndex].txn?.applicationCall?.foreignAssets ?? []),
442
- ...[typeof reference === 'bigint' ? reference : BigInt(reference)],
443
- ],
444
- };
445
- }
446
- };
447
- const g = unnamedResourcesAccessed.group;
448
- if (g) {
449
- // Do cross-reference resources first because they are the most restrictive in terms
450
- // of which transactions can be used
451
- g.appLocals?.forEach((a) => {
452
- populateGroupResource(group, a, 'appLocal');
453
- // Remove resources from the group if we're adding them here
454
- g.accounts = g.accounts?.filter((acc) => acc !== a.account);
455
- g.apps = g.apps?.filter((app) => BigInt(app) !== BigInt(a.app));
456
- });
457
- g.assetHoldings?.forEach((a) => {
458
- populateGroupResource(group, a, 'assetHolding');
459
- // Remove resources from the group if we're adding them here
460
- g.accounts = g.accounts?.filter((acc) => acc !== a.account);
461
- g.assets = g.assets?.filter((asset) => BigInt(asset) !== BigInt(a.asset));
462
- });
463
- // Do accounts next because the account limit is 4
464
- g.accounts?.forEach((a) => {
465
- populateGroupResource(group, a, 'account');
466
- });
467
- g.boxes?.forEach((b) => {
468
- populateGroupResource(group, b, 'box');
469
- // Remove apps as resource from the group if we're adding it here
470
- g.apps = g.apps?.filter((app) => BigInt(app) !== BigInt(b.app));
471
- });
472
- g.assets?.forEach((a) => {
473
- populateGroupResource(group, a, 'asset');
474
- });
475
- g.apps?.forEach((a) => {
476
- populateGroupResource(group, a, 'app');
477
- });
478
- if (g.extraBoxRefs) {
479
- for (let i = 0; i < g.extraBoxRefs; i += 1) {
480
- const ref = new algosdk.modelsv2.BoxReference({ app: 0, name: new Uint8Array(0) });
481
- populateGroupResource(group, ref, 'box');
574
+ else if (type === 'asset') {
575
+ txns[txnIndex].txn['applicationCall'] = {
576
+ ...txns[txnIndex].txn.applicationCall,
577
+ foreignAssets: [
578
+ ...(txns[txnIndex].txn?.applicationCall?.foreignAssets ?? []),
579
+ ...[typeof reference === 'bigint' ? reference : BigInt(reference)],
580
+ ],
581
+ };
582
+ }
583
+ };
584
+ const g = executionInfo.groupUnnamedResourcesAccessed;
585
+ if (g) {
586
+ // Do cross-reference resources first because they are the most restrictive in terms
587
+ // of which transactions can be used
588
+ g.appLocals?.forEach((a) => {
589
+ populateGroupResource(group, a, 'appLocal');
590
+ // Remove resources from the group if we're adding them here
591
+ g.accounts = g.accounts?.filter((acc) => acc !== a.account);
592
+ g.apps = g.apps?.filter((app) => BigInt(app) !== BigInt(a.app));
593
+ });
594
+ g.assetHoldings?.forEach((a) => {
595
+ populateGroupResource(group, a, 'assetHolding');
596
+ // Remove resources from the group if we're adding them here
597
+ g.accounts = g.accounts?.filter((acc) => acc !== a.account);
598
+ g.assets = g.assets?.filter((asset) => BigInt(asset) !== BigInt(a.asset));
599
+ });
600
+ // Do accounts next because the account limit is 4
601
+ g.accounts?.forEach((a) => {
602
+ populateGroupResource(group, a, 'account');
603
+ });
604
+ g.boxes?.forEach((b) => {
605
+ populateGroupResource(group, b, 'box');
606
+ // Remove apps as resource from the group if we're adding it here
607
+ g.apps = g.apps?.filter((app) => BigInt(app) !== BigInt(b.app));
608
+ });
609
+ g.assets?.forEach((a) => {
610
+ populateGroupResource(group, a, 'asset');
611
+ });
612
+ g.apps?.forEach((a) => {
613
+ populateGroupResource(group, a, 'app');
614
+ });
615
+ if (g.extraBoxRefs) {
616
+ for (let i = 0; i < g.extraBoxRefs; i += 1) {
617
+ const ref = new algosdk.modelsv2.BoxReference({ app: 0, name: new Uint8Array(0) });
618
+ populateGroupResource(group, ref, 'box');
619
+ }
482
620
  }
483
621
  }
484
622
  }
@@ -497,15 +635,17 @@ async function populateAppCallResources(atc, algod) {
497
635
  * @returns An object with transaction IDs, transactions, group transaction ID (`groupTransactionId`) if more than 1 transaction sent, and (if `skipWaiting` is `false` or unset) confirmation (`confirmation`)
498
636
  */
499
637
  const sendAtomicTransactionComposer = async function (atcSend, algod) {
500
- const { atc: givenAtc, sendParams, ...executeParams } = atcSend;
638
+ const { atc: givenAtc, sendParams, additionalAtcContext, ...executeParams } = atcSend;
501
639
  let atc;
502
640
  atc = givenAtc;
503
641
  try {
504
642
  const transactionsWithSigner = atc.buildGroup();
505
643
  // If populateAppCallResources is true OR if populateAppCallResources is undefined and there are app calls, then populate resources
506
- const populateResources = executeParams?.populateAppCallResources ?? sendParams?.populateAppCallResources ?? Config.populateAppCallResources;
507
- if (populateResources && transactionsWithSigner.map((t) => t.txn.type).includes(algosdk.TransactionType.appl)) {
508
- atc = await populateAppCallResources(givenAtc, algod);
644
+ const populateAppCallResources = executeParams?.populateAppCallResources ?? sendParams?.populateAppCallResources ?? Config.populateAppCallResources;
645
+ const coverAppCallInnerTransactionFees = executeParams?.coverAppCallInnerTransactionFees;
646
+ if ((populateAppCallResources || coverAppCallInnerTransactionFees) &&
647
+ transactionsWithSigner.map((t) => t.txn.type).includes(algosdk.TransactionType.appl)) {
648
+ atc = await prepareGroupForSending(givenAtc, algod, { ...executeParams, populateAppCallResources, coverAppCallInnerTransactionFees }, additionalAtcContext);
509
649
  }
510
650
  const transactionsToSend = transactionsWithSigner.map((t) => {
511
651
  return t.txn;
@@ -519,7 +659,7 @@ const sendAtomicTransactionComposer = async function (atcSend, algod) {
519
659
  Config.getLogger(executeParams?.suppressLog ?? sendParams?.suppressLog).debug(`Transaction IDs (${groupId})`, transactionsToSend.map((t) => t.txID()));
520
660
  }
521
661
  if (Config.debug && Config.traceAll) {
522
- // Dump the traces to a file for use with AlgoKit AVM debugger
662
+ // Emit the simulate response for use with AlgoKit AVM debugger
523
663
  const simulateResponse = await performAtomicTransactionComposerSimulate(atc, algod);
524
664
  await Config.events.emitAsync(EventType.TxnGroupSimulated, {
525
665
  simulateResponse,
@@ -561,7 +701,7 @@ const sendAtomicTransactionComposer = async function (atcSend, algod) {
561
701
  }
562
702
  if (Config.debug && typeof e === 'object') {
563
703
  err.traces = [];
564
- Config.logger.error('Received error executing Atomic Transaction Composer and debug flag enabled; attempting simulation to get more information', err);
704
+ Config.getLogger(executeParams?.suppressLog ?? sendParams?.suppressLog).error('Received error executing Atomic Transaction Composer and debug flag enabled; attempting simulation to get more information', err);
565
705
  const simulate = await performAtomicTransactionComposerSimulate(atc, algod);
566
706
  if (Config.debug && !Config.traceAll) {
567
707
  // Emit the event only if traceAll: false, as it should have already been emitted above
@@ -582,7 +722,7 @@ const sendAtomicTransactionComposer = async function (atcSend, algod) {
582
722
  }
583
723
  }
584
724
  else {
585
- Config.logger.error('Received error executing Atomic Transaction Composer, for more information enable the debug flag', err);
725
+ Config.getLogger(executeParams?.suppressLog ?? sendParams?.suppressLog).error('Received error executing Atomic Transaction Composer, for more information enable the debug flag', err);
586
726
  }
587
727
  throw err;
588
728
  }
@@ -796,5 +936,5 @@ function getAtomicTransactionComposerTransactions(atc) {
796
936
  }
797
937
  }
798
938
 
799
- export { MAX_APP_CALL_ACCOUNT_REFERENCES, MAX_APP_CALL_FOREIGN_REFERENCES, MAX_TRANSACTION_GROUP_SIZE, capTransactionFee, controlFees, encodeLease, encodeTransactionNote, getABIReturnValue, getAtomicTransactionComposerTransactions, getSenderAddress, getSenderTransactionSigner, getTransactionParams, getTransactionWithSigner, populateAppCallResources, sendAtomicTransactionComposer, sendGroupOfTransactions, sendTransaction, signTransaction, waitForConfirmation };
939
+ export { MAX_APP_CALL_ACCOUNT_REFERENCES, MAX_APP_CALL_FOREIGN_REFERENCES, MAX_TRANSACTION_GROUP_SIZE, capTransactionFee, controlFees, encodeLease, encodeTransactionNote, getABIReturnValue, getAtomicTransactionComposerTransactions, getSenderAddress, getSenderTransactionSigner, getTransactionParams, getTransactionWithSigner, populateAppCallResources, prepareGroupForSending, sendAtomicTransactionComposer, sendGroupOfTransactions, sendTransaction, signTransaction, waitForConfirmation };
800
940
  //# sourceMappingURL=transaction.mjs.map