#arbitrary tx execution in smart contract

1 messages · Page 1 of 1 (latest)

pure gate
#

create me example for smart contract wallet where one of the methods will be generic tx group submission. the group can be any mix of app calls, payment, or asset transfer transactions. please use algorand typescript smart contract to do it

prisma zenithBOT
#

Hi @pure gate. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

Smart Contract Wallet with Generic Transaction Group Submission

Based on the knowledge sources, here's an example of a smart contract wallet in Algorand TypeScript that includes a method for generic transaction group submission using itxnCompose (which supports dynamic/arbitrary groups) and gtxn for verifying incoming group transactions.

Note: For a dynamic/arbitrary mix of transactions, itxnCompose is the right tool. For reading results from dynamically composed inner transactions, you use op.GITxn ops. [Inner Transactions]

import {
  Contract,
  Global,
  assert,
  gtxn,
  itxn,
  itxnCompose,
  Account,
  Application,
  Asset,
  Uint64,
  TransactionType,
} from '@algorandfoundation/algorand-typescript'

/**
 * A simple smart contract wallet that can:
 * 1. Execute a payment inner transaction
 * 2. Execute an asset transfer inner transaction
 * 3. Execute a generic group of inner transactions (app calls, payments, asset transfers)
 */
export class SmartContractWallet extends Contract {

  /**
   * Execute a single payment from the contract
   */
  executePayment(receiver: Account, amount: uint64): void {
    // Only the owner (creator) can trigger payments
    assert(Txn.sender === Global.creatorAddress, 'Only owner can execute payments')

    itxn
      .payment({
        receiver: receiver,
        amount: amount,
        fee: 0,
      })
      .submit()
  }

  /**
   * Execute a single asset transfer from the contract
   */
  executeAssetTransfer(receiver: Account, assetId: Asset, amount: uint64): void {
    assert(Txn.sender === Global.creatorAddress, 'Only owner can execute asset transfers')
#

itxn
.assetTransfer({
xferAsset: assetId,
assetReceiver: receiver,
assetAmount: amount,
fee: 0,
})
.submit()
}

/**

  • Execute a generic group of inner transactions atomically.
  • Supports any mix of payment, asset transfer, and app call transactions.
  • @param receivers - Array of receiver addresses (for payments/asset transfers)
  • @param amounts - Array of amounts corresponding to each receiver
  • @param assetIds - Array of asset IDs (use 0 for payment transactions)
  • @param appId - Application to call (optional, pass 0 to skip app call)
  • @param count - Total number of transactions in the group
    */
    executeGenericGroup(
    receivers: Address[],
    amounts: uint64[],
    assetIds: uint64[],
    appId: Application,
    count: uint64,
    ): void {
    assert(Txn.sender === Global.creatorAddress, 'Only owner can execute transaction groups')
    assert(count > Uint64(0), 'Must have at least one transaction')
// Build the group dynamically using itxnCompose
for (const i of urange(count)) {
  const receiver = receivers[i]
  const amount = amounts[i]
  const assetId = assetIds[i]

  if (assetId === Uint64(0)) {
    // Payment transaction
    const payFields = {
      type: TransactionType.Payment,
      receiver: receiver,
      amount: amount,
      fee: Uint64(0),
    } satisfies PaymentComposeFields
#

if (i === Uint64(0)) {
itxnCompose.begin(payFields)
} else {
itxnCompose.next(payFields)
}
} else {
// Asset transfer transaction
const xferFields = {
type: TransactionType.AssetTransfer,
xferAsset: assetId,
assetReceiver: receiver,
assetAmount: amount,
fee: Uint64(0),
} satisfies AssetTransferComposeFields

    if (i === Uint64(0)) {
      itxnCompose.begin(xferFields)
    } else {
      itxnCompose.next(xferFields)
    }
  }
}

// Optionally append an app call at the end
if (appId.id !== Uint64(0)) {
  itxnCompose.next(
    itxn.applicationCall({
      appId: appId.id,
      fee: Uint64(0),
    })
  )
}

// Submit all staged transactions atomically
itxnCompose.submit()

}
}


## Key Concepts Used

| Concept | Purpose |
|---|---|
| `itxnCompose.begin()` | Stages the **first** transaction in a new inner group [[Inner Transactions](<https://dev.algorand.co/algokit/languages/typescript/lg-itxns/#itxn-compose-api>)] |
| `itxnCompose.next()` | Stages **subsequent** transactions in the group [[Inner Transactions](<https://dev.algorand.co/algokit/languages/typescript/lg-itxns/#itxn-compose-api>)] |
| `itxnCompose.submit()` | Dispatches all staged transactions atomically [[Inner Transactions](<https://dev.algorand.co/algokit/languages/typescript/lg-itxns/#itxn-compose-api>)] |
| `itxn.payment().submit()` | Submits a single payment inner transaction [[Grouped Inner Transactions](<https://dev.algorand.co/concepts/smart-contracts/inner-txn/#grouped-inner-transactions>)] |
| `itxn.assetTransfer().submit()` | Submits a single asset transfer inner transaction |

## Important Limitations
#
  • itxnCompose does not provide strongly typed return values. To read results from any transaction in the group, use op.GITxn ops (e.g., op.GITxn.lastLog(1)). [Inner Transactions]
  • If the number of transactions is known at compile time, prefer itxn.submitGroup(...) instead, as it gives typed return values. [Fixed-Size Groups]
  • ItxnParams objects cannot be stored in arrays or application state. [Inner Transactions]

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: