Skip to content

Plugin Development Kit

The @akta/plugin package provides everything you need to build custom plugins for ARC-58 abstract accounts. It includes a base contract class, utility functions, types, and constants — so you can focus on your plugin’s business logic instead of ARC-58 boilerplate.

Terminal window
npm install @akta/plugin

The package depends on @algorandfoundation/puya-ts for compiling Algorand TypeScript to TEAL.

Every plugin extends the Plugin base class and follows three rules:

  1. First parameter must be wallet: Application
  2. Second parameter must be rekeyBack: boolean
  3. The last inner transaction must rekey back to the wallet when rekeyBack is true
import { Application, itxn, uint64 } from '@algorandfoundation/algorand-typescript'
import { Plugin, getSpendingAccount, rekeyAddress } from '@akta/plugin'
export class MyPayPlugin extends Plugin {
pay(wallet: Application, rekeyBack: boolean, receiver: Account, amount: uint64): void {
const sender = getSpendingAccount(wallet)
itxn
.payment({
sender,
receiver,
amount,
rekeyTo: rekeyAddress(rekeyBack, wallet),
})
.submit()
}
}

When a user calls wallet.usePlugin(), the ARC-58 wallet temporarily rekeys the spending account to your plugin contract. Your plugin executes inner transactions as that account, then rekeys control back to the wallet.

User → Wallet (ARC-58) → rekey to Plugin → Plugin executes inner txns → rekey back to Wallet

The rekeyBack flag controls whether your plugin should return control to the wallet or allow chaining to another plugin call. When rekeyBack is true, your last inner transaction must include rekeyTo: wallet.address.

Here’s the actual Pay Plugin from the Akita smart contracts — it handles both ALGO and ASA payments in a single call:

import { Application, Global, itxn, uint64 } from '@algorandfoundation/algorand-typescript'
import { Plugin, getSpendingAccount, rekeyAddress } from '@akta/plugin'
type PayParams = { receiver: Account; asset: uint64; amount: uint64 }
export class PayPlugin extends Plugin {
pay(wallet: Application, rekeyBack: boolean, payments: PayParams[]): void {
const sender = getSpendingAccount(wallet)
for (let i: uint64 = 0; i < payments.length; i++) {
const { receiver, asset, amount } = payments[i]
const isLast = i === payments.length - 1
if (asset === 0) {
itxn
.payment({
sender,
receiver,
amount,
rekeyTo: isLast ? rekeyAddress(rekeyBack, wallet) : Global.zeroAddress,
})
.submit()
} else {
itxn
.assetTransfer({
sender,
assetReceiver: receiver,
assetAmount: amount,
xferAsset: asset,
rekeyTo: isLast ? rekeyAddress(rekeyBack, wallet) : Global.zeroAddress,
})
.submit()
}
}
}
}

Key patterns:

  • Only the last inner transaction includes the rekey — intermediate transactions use Global.zeroAddress
  • Use getSpendingAccount(wallet) to get the account your plugin is authorized to act as
  • Use rekeyAddress(rekeyBack, wallet) to determine whether to rekey back or allow chaining

Returning Control Without an Inner Transaction

Section titled “Returning Control Without an Inner Transaction”

When rekeyBack is true, your plugin must return control of the spending account to the wallet. Normally, you do this on your last inner transaction via rekeyTo: rekeyAddress(rekeyBack, wallet). But if your plugin’s operation doesn’t naturally involve an inner transaction (e.g., it only writes to box storage or updates global state), there’s no transaction to attach the rekey to.

Use rekeyBackIfNecessary() in these cases — it submits a minimal payment to handle the rekey back:

import { Plugin, getSpendingAccount, rekeyBackIfNecessary } from '@akta/plugin'
export class MyPlugin extends Plugin {
doSomething(wallet: Application, rekeyBack: boolean): void {
const sender = getSpendingAccount(wallet)
// ... operations that don't involve inner transactions
this.someBox(key).value = someValue
// Return control to the wallet since there's no inner transaction to do it
rekeyBackIfNecessary(rekeyBack, wallet)
}
}

Plugins can read the ARC-58 wallet’s global state to access account information:

import { getAccounts, getOriginAccount, getReferrerAccount } from '@akta/plugin'
// Get all accounts at once
const { walletAddress, origin, sender, referrer } = getAccounts(wallet)
// Or individually
const origin = getOriginAccount(wallet) // The real user who owns the wallet
const referrer = getReferrerAccount(wallet) // The referrer account (or zero address)

Typically, the main authorization check is implicit: any inner transactions your plugin issues will use the spending account as the sender, which will fail if the plugin doesn’t actually have authorization of that account during execution. This means plugins that submit inner transactions are naturally protected.

However, for operations that don’t involve inner transactions — where there’s no implicit authorization through the rekey mechanism — getSpendingAccount(wallet) alone isn’t trustworthy since wallet is just an argument and a caller could pass any application ID. Use controls() to verify that the spending account’s auth address matches your plugin, confirming the wallet genuinely granted control:

import { controls, getSpendingAccount } from '@akta/plugin'
const sender = getSpendingAccount(wallet)
assert(controls(sender)) // Ensure this plugin was actually given control