@algorandfoundation/algokit-utils 8.1.0-beta.2 → 8.1.0-beta.4

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