Skip to content

Commit 6b94454

Browse files
committed
feat: use algod's resoure population
1 parent 55a2e3e commit 6b94454

File tree

1 file changed

+23
-271
lines changed

1 file changed

+23
-271
lines changed

src/transaction/transaction.ts

+23-271
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import algosdk, { Address, ApplicationTransactionFields, TransactionBoxReference, TransactionType, stringifyJSON } from 'algosdk'
1+
import algosdk, { ApplicationTransactionFields, TransactionType } from 'algosdk'
22
import { Buffer } from 'buffer'
33
import { Config } from '../config'
44
import { AlgoAmount } from '../types/amount'
@@ -233,7 +233,6 @@ export const sendTransaction = async function (
233233
const populateAppCallResources = sendParams?.populateAppCallResources ?? Config.populateAppCallResources
234234

235235
// Populate resources if the transaction is an appcall and populateAppCallResources wasn't explicitly set to false
236-
// NOTE: Temporary false by default until this algod bug is fixed: https://github.com/algorand/go-algorand/issues/5914
237236
if (txnToSend.type === algosdk.TransactionType.appl && populateAppCallResources) {
238237
const newAtc = new AtomicTransactionComposer()
239238
newAtc.addTransaction({ txn: txnToSend, signer: getSenderTransactionSigner(from) })
@@ -279,6 +278,7 @@ async function getGroupExecutionInfo(
279278
allowUnnamedResources: true,
280279
allowEmptySignatures: true,
281280
fixSigners: true,
281+
populateResources: sendParams.populateAppCallResources,
282282
})
283283

284284
const nullSigner = algosdk.makeEmptyTransactionSigner()
@@ -325,6 +325,10 @@ async function getGroupExecutionInfo(
325325
}
326326

327327
return {
328+
populatedResourceArrays: sendParams.populateAppCallResources
329+
? groupResponse.txnResults.map((t) => t.populatedResourceArrays)
330+
: undefined,
331+
extraResourceArrays: sendParams.populateAppCallResources ? groupResponse.extraResourceArrays : undefined,
328332
groupUnnamedResourcesAccessed: sendParams.populateAppCallResources ? groupResponse.unnamedResourcesAccessed : undefined,
329333
txns: groupResponse.txnResults.map((txn, i) => {
330334
const originalTxn = atc['transactions'][i].txn as algosdk.Transaction
@@ -460,33 +464,7 @@ export async function prepareGroupForSending(
460464
)
461465
: [0n, new Map<number, bigint>()]
462466

463-
executionInfo.txns.forEach(({ unnamedResourcesAccessed: r }, i) => {
464-
// Populate Transaction App Call Resources
465-
if (sendParams.populateAppCallResources && r !== undefined && group[i].txn.type === TransactionType.appl) {
466-
if (r.boxes || r.extraBoxRefs) throw Error('Unexpected boxes at the transaction level')
467-
if (r.appLocals) throw Error('Unexpected app local at the transaction level')
468-
if (r.assetHoldings)
469-
throw Error('Unexpected asset holding at the transaction level')
470-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
471-
;(group[i].txn as any)['applicationCall'] = {
472-
...group[i].txn.applicationCall,
473-
accounts: [...(group[i].txn?.applicationCall?.accounts ?? []), ...(r.accounts ?? [])],
474-
foreignApps: [...(group[i].txn?.applicationCall?.foreignApps ?? []), ...(r.apps ?? [])],
475-
foreignAssets: [...(group[i].txn?.applicationCall?.foreignAssets ?? []), ...(r.assets ?? [])],
476-
boxes: [...(group[i].txn?.applicationCall?.boxes ?? []), ...(r.boxes ?? [])],
477-
} satisfies Partial<ApplicationTransactionFields>
478-
479-
const accounts = group[i].txn.applicationCall?.accounts?.length ?? 0
480-
if (accounts > MAX_APP_CALL_ACCOUNT_REFERENCES)
481-
throw Error(`Account reference limit of ${MAX_APP_CALL_ACCOUNT_REFERENCES} exceeded in transaction ${i}`)
482-
const assets = group[i].txn.applicationCall?.foreignAssets?.length ?? 0
483-
const apps = group[i].txn.applicationCall?.foreignApps?.length ?? 0
484-
const boxes = group[i].txn.applicationCall?.boxes?.length ?? 0
485-
if (accounts + assets + apps + boxes > MAX_APP_CALL_FOREIGN_REFERENCES) {
486-
throw Error(`Resource reference limit of ${MAX_APP_CALL_FOREIGN_REFERENCES} exceeded in transaction ${i}`)
487-
}
488-
}
489-
467+
executionInfo.txns.forEach((_, i) => {
490468
// Cover App Call Inner Transaction Fees
491469
if (sendParams.coverAppCallInnerTransactionFees) {
492470
const additionalTransactionFee = additionalTransactionFees.get(i)
@@ -508,253 +486,27 @@ export async function prepareGroupForSending(
508486
})
509487

510488
// Populate Group App Call Resources
511-
if (sendParams.populateAppCallResources) {
512-
const populateGroupResource = (
513-
txns: algosdk.TransactionWithSigner[],
514-
reference:
515-
| string
516-
| algosdk.modelsv2.BoxReference
517-
| algosdk.modelsv2.ApplicationLocalReference
518-
| algosdk.modelsv2.AssetHoldingReference
519-
| bigint
520-
| number
521-
| Address,
522-
type: 'account' | 'assetHolding' | 'appLocal' | 'app' | 'box' | 'asset',
523-
): void => {
524-
const isApplBelowLimit = (t: algosdk.TransactionWithSigner) => {
525-
if (t.txn.type !== algosdk.TransactionType.appl) return false
526-
527-
const accounts = t.txn.applicationCall?.accounts?.length ?? 0
528-
const assets = t.txn.applicationCall?.foreignAssets?.length ?? 0
529-
const apps = t.txn.applicationCall?.foreignApps?.length ?? 0
530-
const boxes = t.txn.applicationCall?.boxes?.length ?? 0
531-
532-
return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES
533-
}
534-
535-
// If this is a asset holding or app local, first try to find a transaction that already has the account available
536-
if (type === 'assetHolding' || type === 'appLocal') {
537-
const { account } = reference as algosdk.modelsv2.ApplicationLocalReference | algosdk.modelsv2.AssetHoldingReference
538-
539-
let txnIndex = txns.findIndex((t) => {
540-
if (!isApplBelowLimit(t)) return false
541-
542-
return (
543-
// account is in the foreign accounts array
544-
t.txn.applicationCall?.accounts?.map((a) => a.toString()).includes(account.toString()) ||
545-
// account is available as an app account
546-
t.txn.applicationCall?.foreignApps?.map((a) => algosdk.getApplicationAddress(a).toString()).includes(account.toString()) ||
547-
// account is available since it's in one of the fields
548-
Object.values(t.txn).some((f) =>
549-
stringifyJSON(f, (_, v) => (v instanceof Address ? v.toString() : v))?.includes(account.toString()),
550-
)
551-
)
552-
})
553-
554-
if (txnIndex > -1) {
555-
if (type === 'assetHolding') {
556-
const { asset } = reference as algosdk.modelsv2.AssetHoldingReference
557-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
558-
;(txns[txnIndex].txn as any)['applicationCall'] = {
559-
...txns[txnIndex].txn.applicationCall,
560-
foreignAssets: [...(txns[txnIndex].txn?.applicationCall?.foreignAssets ?? []), ...[asset]],
561-
} satisfies Partial<ApplicationTransactionFields>
562-
} else {
563-
const { app } = reference as algosdk.modelsv2.ApplicationLocalReference
564-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
565-
;(txns[txnIndex].txn as any)['applicationCall'] = {
566-
...txns[txnIndex].txn.applicationCall,
567-
foreignApps: [...(txns[txnIndex].txn?.applicationCall?.foreignApps ?? []), ...[app]],
568-
} satisfies Partial<ApplicationTransactionFields>
569-
}
570-
return
571-
}
572-
573-
// Now try to find a txn that already has that app or asset available
574-
txnIndex = txns.findIndex((t) => {
575-
if (!isApplBelowLimit(t)) return false
576-
577-
// check if there is space in the accounts array
578-
if ((t.txn.applicationCall?.accounts?.length ?? 0) >= MAX_APP_CALL_ACCOUNT_REFERENCES) return false
579-
580-
if (type === 'assetHolding') {
581-
const { asset } = reference as algosdk.modelsv2.AssetHoldingReference
582-
return t.txn.applicationCall?.foreignAssets?.includes(asset)
583-
} else {
584-
const { app } = reference as algosdk.modelsv2.ApplicationLocalReference
585-
return t.txn.applicationCall?.foreignApps?.includes(app) || t.txn.applicationCall?.appIndex === app
586-
}
587-
})
588-
589-
if (txnIndex > -1) {
590-
const { account } = reference as algosdk.modelsv2.AssetHoldingReference | algosdk.modelsv2.ApplicationLocalReference
591-
592-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
593-
;(txns[txnIndex].txn as any)['applicationCall'] = {
594-
...txns[txnIndex].txn.applicationCall,
595-
accounts: [...(txns[txnIndex].txn?.applicationCall?.accounts ?? []), ...[account]],
596-
} satisfies Partial<ApplicationTransactionFields>
597-
598-
return
599-
}
600-
}
601-
602-
// If this is a box, first try to find a transaction that already has the app available
603-
if (type === 'box') {
604-
const { app, name } = reference as algosdk.modelsv2.BoxReference
605-
606-
const txnIndex = txns.findIndex((t) => {
607-
if (!isApplBelowLimit(t)) return false
608-
609-
// If the app is in the foreign array OR the app being called, then we know it's available
610-
return t.txn.applicationCall?.foreignApps?.includes(app) || t.txn.applicationCall?.appIndex === app
489+
if (executionInfo.populatedResourceArrays) {
490+
executionInfo.populatedResourceArrays.forEach((r, i) => {
491+
const txn = group[i].txn.applicationCall
492+
if (r === undefined || txn === undefined) return
493+
494+
if (r.boxes) {
495+
// @ts-expect-error boxes is readonly
496+
txn.boxes = r.boxes.map((b) => {
497+
return { appIndex: BigInt(b.app), name: b.name }
611498
})
612-
613-
if (txnIndex > -1) {
614-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
615-
;(txns[txnIndex].txn as any)['applicationCall'] = {
616-
...txns[txnIndex].txn.applicationCall,
617-
boxes: [...(txns[txnIndex].txn?.applicationCall?.boxes ?? []), ...[{ appIndex: app, name } satisfies TransactionBoxReference]],
618-
} satisfies Partial<ApplicationTransactionFields>
619-
620-
return
621-
}
622499
}
623500

624-
// Find the txn index to put the reference(s)
625-
const txnIndex = txns.findIndex((t) => {
626-
if (t.txn.type !== algosdk.TransactionType.appl) return false
627-
628-
const accounts = t.txn.applicationCall?.accounts?.length ?? 0
629-
if (type === 'account') return accounts < MAX_APP_CALL_ACCOUNT_REFERENCES
501+
// @ts-expect-error accounts is readonly
502+
if (r.accounts) txn.accounts = r.accounts
630503

631-
const assets = t.txn.applicationCall?.foreignAssets?.length ?? 0
632-
const apps = t.txn.applicationCall?.foreignApps?.length ?? 0
633-
const boxes = t.txn.applicationCall?.boxes?.length ?? 0
504+
// @ts-expect-error apps is readonly
505+
if (r.apps) txn.foreignApps = r.apps
634506

635-
// If we're adding local state or asset holding, we need space for the acocunt and the other reference
636-
if (type === 'assetHolding' || type === 'appLocal') {
637-
return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES - 1 && accounts < MAX_APP_CALL_ACCOUNT_REFERENCES
638-
}
639-
640-
// If we're adding a box, we need space for both the box ref and the app ref
641-
if (type === 'box' && BigInt((reference as algosdk.modelsv2.BoxReference).app) !== BigInt(0)) {
642-
return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES - 1
643-
}
644-
645-
return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES
646-
})
647-
648-
if (txnIndex === -1) {
649-
throw Error('No more transactions below reference limit. Add another app call to the group.')
650-
}
651-
652-
if (type === 'account') {
653-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
654-
;(txns[txnIndex].txn as any)['applicationCall'] = {
655-
...txns[txnIndex].txn.applicationCall,
656-
accounts: [...(txns[txnIndex].txn?.applicationCall?.accounts ?? []), ...[reference as Address]],
657-
} satisfies Partial<ApplicationTransactionFields>
658-
} else if (type === 'app') {
659-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
660-
;(txns[txnIndex].txn as any)['applicationCall'] = {
661-
...txns[txnIndex].txn.applicationCall,
662-
foreignApps: [
663-
...(txns[txnIndex].txn?.applicationCall?.foreignApps ?? []),
664-
...[typeof reference === 'bigint' ? reference : BigInt(reference as number)],
665-
],
666-
} satisfies Partial<ApplicationTransactionFields>
667-
} else if (type === 'box') {
668-
const { app, name } = reference as algosdk.modelsv2.BoxReference
669-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
670-
;(txns[txnIndex].txn as any)['applicationCall'] = {
671-
...txns[txnIndex].txn.applicationCall,
672-
boxes: [...(txns[txnIndex].txn?.applicationCall?.boxes ?? []), ...[{ appIndex: app, name } satisfies TransactionBoxReference]],
673-
} satisfies Partial<ApplicationTransactionFields>
674-
675-
if (app.toString() !== '0') {
676-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
677-
;(txns[txnIndex].txn as any)['applicationCall'] = {
678-
...txns[txnIndex].txn.applicationCall,
679-
foreignApps: [...(txns[txnIndex].txn?.applicationCall?.foreignApps ?? []), ...[app]],
680-
} satisfies Partial<ApplicationTransactionFields>
681-
}
682-
} else if (type === 'assetHolding') {
683-
const { asset, account } = reference as algosdk.modelsv2.AssetHoldingReference
684-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
685-
;(txns[txnIndex].txn as any)['applicationCall'] = {
686-
...txns[txnIndex].txn.applicationCall,
687-
foreignAssets: [...(txns[txnIndex].txn?.applicationCall?.foreignAssets ?? []), ...[asset]],
688-
accounts: [...(txns[txnIndex].txn?.applicationCall?.accounts ?? []), ...[account]],
689-
} satisfies Partial<ApplicationTransactionFields>
690-
} else if (type === 'appLocal') {
691-
const { app, account } = reference as algosdk.modelsv2.ApplicationLocalReference
692-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
693-
;(txns[txnIndex].txn as any)['applicationCall'] = {
694-
...txns[txnIndex].txn.applicationCall,
695-
foreignApps: [...(txns[txnIndex].txn?.applicationCall?.foreignApps ?? []), ...[app]],
696-
accounts: [...(txns[txnIndex].txn?.applicationCall?.accounts ?? []), ...[account]],
697-
} satisfies Partial<ApplicationTransactionFields>
698-
} else if (type === 'asset') {
699-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
700-
;(txns[txnIndex].txn as any)['applicationCall'] = {
701-
...txns[txnIndex].txn.applicationCall,
702-
foreignAssets: [
703-
...(txns[txnIndex].txn?.applicationCall?.foreignAssets ?? []),
704-
...[typeof reference === 'bigint' ? reference : BigInt(reference as number)],
705-
],
706-
} satisfies Partial<ApplicationTransactionFields>
707-
}
708-
}
709-
710-
const g = executionInfo.groupUnnamedResourcesAccessed
711-
712-
if (g) {
713-
// Do cross-reference resources first because they are the most restrictive in terms
714-
// of which transactions can be used
715-
g.appLocals?.forEach((a) => {
716-
populateGroupResource(group, a, 'appLocal')
717-
718-
// Remove resources from the group if we're adding them here
719-
g.accounts = g.accounts?.filter((acc) => acc !== a.account)
720-
g.apps = g.apps?.filter((app) => BigInt(app) !== BigInt(a.app))
721-
})
722-
723-
g.assetHoldings?.forEach((a) => {
724-
populateGroupResource(group, a, 'assetHolding')
725-
726-
// Remove resources from the group if we're adding them here
727-
g.accounts = g.accounts?.filter((acc) => acc !== a.account)
728-
g.assets = g.assets?.filter((asset) => BigInt(asset) !== BigInt(a.asset))
729-
})
730-
731-
// Do accounts next because the account limit is 4
732-
g.accounts?.forEach((a) => {
733-
populateGroupResource(group, a, 'account')
734-
})
735-
736-
g.boxes?.forEach((b) => {
737-
populateGroupResource(group, b, 'box')
738-
739-
// Remove apps as resource from the group if we're adding it here
740-
g.apps = g.apps?.filter((app) => BigInt(app) !== BigInt(b.app))
741-
})
742-
743-
g.assets?.forEach((a) => {
744-
populateGroupResource(group, a, 'asset')
745-
})
746-
747-
g.apps?.forEach((a) => {
748-
populateGroupResource(group, a, 'app')
749-
})
750-
751-
if (g.extraBoxRefs) {
752-
for (let i = 0; i < g.extraBoxRefs; i += 1) {
753-
const ref = new algosdk.modelsv2.BoxReference({ app: 0, name: new Uint8Array(0) })
754-
populateGroupResource(group, ref, 'box')
755-
}
756-
}
757-
}
507+
// @ts-expect-error assets is readonly
508+
if (r.assets) txn.foreignAssets = r.assets
509+
})
758510
}
759511

760512
const newAtc = new algosdk.AtomicTransactionComposer()

0 commit comments

Comments
 (0)