Send Asset Operations
Description
Section titled “Description”This example demonstrates how to perform ASA (Algorand Standard Asset) operations:
- algorand.send.assetCreate() to create a new ASA with all parameters
- algorand.send.assetConfig() to reconfigure an asset
- algorand.send.assetOptIn() for receiver to opt into the asset
- algorand.send.assetTransfer() to transfer assets between accounts
- algorand.send.assetFreeze() to freeze/unfreeze an account’s asset holding
- algorand.send.assetTransfer() with clawbackTarget for clawback operations
- algorand.send.assetOptOut() to opt out and close asset holding
- algorand.send.assetDestroy() to destroy an asset
Prerequisites
Section titled “Prerequisites”- LocalNet running (via
algokit localnet start)
Run This Example
Section titled “Run This Example”From the repository root:
cd examplesnpm run example algorand_client/07-send-asset-ops.ts/** * Example: Send Asset Operations * * This example demonstrates how to perform ASA (Algorand Standard Asset) operations: * - algorand.send.assetCreate() to create a new ASA with all parameters * - algorand.send.assetConfig() to reconfigure an asset * - algorand.send.assetOptIn() for receiver to opt into the asset * - algorand.send.assetTransfer() to transfer assets between accounts * - algorand.send.assetFreeze() to freeze/unfreeze an account's asset holding * - algorand.send.assetTransfer() with clawbackTarget for clawback operations * - algorand.send.assetOptOut() to opt out and close asset holding * - algorand.send.assetDestroy() to destroy an asset * * Prerequisites: * - LocalNet running (via `algokit localnet start`) */
import { AlgorandClient, algo } from '@algorandfoundation/algokit-utils'import { printError, printHeader, printInfo, printStep, printSuccess, shortenAddress } from '../shared/utils.js'
async function main() { printHeader('Send Asset Operations Example')
// Initialize client and verify LocalNet is running const algorand = AlgorandClient.defaultLocalNet()
try { await algorand.client.algod.status() printSuccess('Connected to LocalNet') } catch (error) { printError(`Failed to connect to LocalNet: ${error instanceof Error ? error.message : String(error)}`) printInfo('Make sure LocalNet is running (e.g., algokit localnet start)') return }
// Step 1: Create and fund test accounts printStep(1, 'Create and fund test accounts') printInfo('Creating creator, receiver, and frozenAccount for asset operations')
const creator = algorand.account.random() const receiver = algorand.account.random() const frozenAccount = algorand.account.random()
printInfo(`\nCreated accounts:`) printInfo(` Creator: ${shortenAddress(creator.addr.toString())}`) printInfo(` Receiver: ${shortenAddress(receiver.addr.toString())}`) printInfo(` FrozenAccount: ${shortenAddress(frozenAccount.addr.toString())}`)
// Fund all accounts await algorand.account.ensureFundedFromEnvironment(creator.addr, algo(10)) await algorand.account.ensureFundedFromEnvironment(receiver.addr, algo(5)) await algorand.account.ensureFundedFromEnvironment(frozenAccount.addr, algo(5))
printSuccess('Created and funded test accounts')
// Step 2: Create a new ASA with all parameters printStep(2, 'Create a new ASA with algorand.send.assetCreate()') printInfo('Creating an asset with all configurable parameters')
// Create a metadata hash (32 bytes) const metadataHash = new Uint8Array(32) for (let i = 0; i < 32; i++) { metadataHash[i] = i }
const createResult = await algorand.send.assetCreate({ sender: creator.addr, total: 1_000_000n, // 1 million units (10,000 whole tokens with 2 decimals) decimals: 2, assetName: 'AlgoKit Example Token', unitName: 'AKEX', url: 'https://example.com/asset', metadataHash: metadataHash, defaultFrozen: false, manager: creator.addr, // Can reconfigure the asset reserve: creator.addr, // Holds uncirculated supply freeze: creator.addr, // Can freeze/unfreeze accounts clawback: creator.addr, // Can clawback assets })
const assetId = createResult.assetId printInfo(`\nAsset created:`) printInfo(` Asset ID: ${assetId}`) printInfo(` Transaction ID: ${createResult.txIds[0]}`) printInfo(` Confirmed round: ${createResult.confirmation.confirmedRound}`)
// Retrieve and display asset details const assetInfo = await algorand.asset.getById(assetId) printInfo(`\nAsset details from chain:`) printInfo(` Name: ${assetInfo.assetName}`) printInfo(` Unit: ${assetInfo.unitName}`) printInfo(` Total: ${assetInfo.total} (smallest units)`) printInfo(` Decimals: ${assetInfo.decimals}`) printInfo(` Creator: ${shortenAddress(assetInfo.creator.toString())}`) printInfo(` Manager: ${shortenAddress(assetInfo.manager?.toString() ?? 'none')}`) printInfo(` Reserve: ${shortenAddress(assetInfo.reserve?.toString() ?? 'none')}`) printInfo(` Freeze: ${shortenAddress(assetInfo.freeze?.toString() ?? 'none')}`) printInfo(` Clawback: ${shortenAddress(assetInfo.clawback?.toString() ?? 'none')}`) printInfo(` Default Frozen: ${assetInfo.defaultFrozen}`) printInfo(` URL: ${assetInfo.url}`)
printSuccess('Asset created successfully')
// Step 3: Reconfigure the asset printStep(3, 'Reconfigure the asset with algorand.send.assetConfig()') printInfo('Changing the reserve address to a different account')
// Create a new reserve account const newReserve = algorand.account.random() await algorand.account.ensureFundedFromEnvironment(newReserve.addr, algo(1))
const configResult = await algorand.send.assetConfig({ sender: creator.addr, // Must be the manager assetId: assetId, manager: creator.addr, // Keep manager the same reserve: newReserve.addr, // Change reserve freeze: creator.addr, // Keep freeze the same clawback: creator.addr, // Keep clawback the same })
printInfo(`\nAsset reconfigured:`) printInfo(` Transaction ID: ${configResult.txIds[0]}`) printInfo(` Confirmed round: ${configResult.confirmation.confirmedRound}`)
// Verify the change const updatedAssetInfo = await algorand.asset.getById(assetId) printInfo(` New Reserve: ${shortenAddress(updatedAssetInfo.reserve?.toString() ?? 'none')}`)
printSuccess('Asset reconfigured successfully')
// Step 4: Opt-in receiver to the asset printStep(4, 'Opt-in receiver with algorand.send.assetOptIn()') printInfo('Before receiving assets, an account must opt-in to the asset')
const optInResult = await algorand.send.assetOptIn({ sender: receiver.addr, assetId: assetId, })
printInfo(`\nReceiver opted in:`) printInfo(` Transaction ID: ${optInResult.txIds[0]}`) printInfo(` Confirmed round: ${optInResult.confirmation.confirmedRound}`)
// Verify opt-in const receiverAssetInfo = await algorand.asset.getAccountInformation(receiver.addr, assetId) printInfo(` Receiver balance after opt-in: ${receiverAssetInfo.balance}`) printInfo(` Receiver frozen status: ${receiverAssetInfo.frozen}`)
printSuccess('Receiver opted in successfully')
// Step 5: Transfer assets to receiver printStep(5, 'Transfer assets with algorand.send.assetTransfer()') printInfo('Transferring 100 whole tokens (10000 smallest units) to receiver')
const transferResult = await algorand.send.assetTransfer({ sender: creator.addr, receiver: receiver.addr, assetId: assetId, amount: 10_000n, // 100 whole tokens (100 * 10^2) note: 'Initial token distribution', })
printInfo(`\nTransfer completed:`) printInfo(` Transaction ID: ${transferResult.txIds[0]}`) printInfo(` Confirmed round: ${transferResult.confirmation.confirmedRound}`)
// Check balances const creatorAssetInfo = await algorand.asset.getAccountInformation(creator.addr, assetId) const receiverAfterTransfer = await algorand.asset.getAccountInformation(receiver.addr, assetId) printInfo(` Creator balance: ${creatorAssetInfo.balance} (${Number(creatorAssetInfo.balance) / 100} tokens)`) printInfo(` Receiver balance: ${receiverAfterTransfer.balance} (${Number(receiverAfterTransfer.balance) / 100} tokens)`)
printSuccess('Asset transfer completed successfully')
// Step 6: Freeze an account's asset holding printStep(6, 'Freeze account with algorand.send.assetFreeze()') printInfo('First opt-in frozenAccount, then freeze its asset holding')
// Opt-in frozenAccount await algorand.send.assetOptIn({ sender: frozenAccount.addr, assetId: assetId, })
// Transfer some tokens to frozenAccount await algorand.send.assetTransfer({ sender: creator.addr, receiver: frozenAccount.addr, assetId: assetId, amount: 5_000n, // 50 whole tokens })
// Now freeze the account const freezeResult = await algorand.send.assetFreeze({ sender: creator.addr, // Must be the freeze address assetId: assetId, freezeTarget: frozenAccount.addr, frozen: true, })
printInfo(`\nAccount frozen:`) printInfo(` Transaction ID: ${freezeResult.txIds[0]}`) printInfo(` Confirmed round: ${freezeResult.confirmation.confirmedRound}`)
// Verify frozen status const frozenAccountInfo = await algorand.asset.getAccountInformation(frozenAccount.addr, assetId) printInfo(` Frozen account balance: ${frozenAccountInfo.balance}`) printInfo(` Frozen status: ${frozenAccountInfo.frozen}`)
// Try to transfer from frozen account (should fail) printInfo(`\nAttempting transfer from frozen account (should fail)...`) try { await algorand.send.assetTransfer({ sender: frozenAccount.addr, receiver: receiver.addr, assetId: assetId, amount: 1_000n, }) printError('Transfer should have failed!') } catch { printInfo(` Transfer failed as expected: account is frozen`) }
printSuccess('Freeze operation completed successfully')
// Step 7: Unfreeze and demonstrate clawback printStep(7, 'Unfreeze and demonstrate clawback operation') printInfo('Unfreezing the account, then using clawback to reclaim assets')
// Unfreeze the account const unfreezeResult = await algorand.send.assetFreeze({ sender: creator.addr, assetId: assetId, freezeTarget: frozenAccount.addr, frozen: false, })
printInfo(`\nAccount unfrozen:`) printInfo(` Transaction ID: ${unfreezeResult.txIds[0]}`)
const unfrozenAccountInfo = await algorand.asset.getAccountInformation(frozenAccount.addr, assetId) printInfo(` Frozen status after unfreeze: ${unfrozenAccountInfo.frozen}`)
// Demonstrate clawback - reclaim assets from frozenAccount printInfo(`\nClawback operation: reclaiming assets from frozenAccount to creator`)
const clawbackResult = await algorand.send.assetTransfer({ sender: creator.addr, // Clawback address sends the transaction receiver: creator.addr, // Assets go back to creator assetId: assetId, amount: 2_500n, // Clawback 25 tokens clawbackTarget: frozenAccount.addr, // Account to clawback from note: 'Clawback operation', })
printInfo(`\nClawback completed:`) printInfo(` Transaction ID: ${clawbackResult.txIds[0]}`) printInfo(` Confirmed round: ${clawbackResult.confirmation.confirmedRound}`)
// Check balances after clawback const creatorAfterClawback = await algorand.asset.getAccountInformation(creator.addr, assetId) const frozenAfterClawback = await algorand.asset.getAccountInformation(frozenAccount.addr, assetId) printInfo(` Creator balance after clawback: ${creatorAfterClawback.balance}`) printInfo(` FrozenAccount balance after clawback: ${frozenAfterClawback.balance}`)
printSuccess('Clawback operation completed successfully')
// Step 8: Opt-out of the asset printStep(8, 'Opt-out with algorand.send.assetOptOut()') printInfo('Receiver will opt-out of the asset, returning remaining balance to creator')
// First transfer all assets back to creator so receiver has zero balance const receiverCurrentBalance = await algorand.asset.getAccountInformation(receiver.addr, assetId) if (receiverCurrentBalance.balance > 0n) { await algorand.send.assetTransfer({ sender: receiver.addr, receiver: creator.addr, assetId: assetId, amount: receiverCurrentBalance.balance, }) printInfo(` Transferred ${receiverCurrentBalance.balance} units back to creator`) }
// Now opt-out const optOutResult = await algorand.send.assetOptOut({ sender: receiver.addr, assetId: assetId, creator: creator.addr, ensureZeroBalance: true, // Safety check to ensure zero balance before opt-out })
printInfo(`\nReceiver opted out:`) printInfo(` Transaction ID: ${optOutResult.txIds[0]}`) printInfo(` Confirmed round: ${optOutResult.confirmation.confirmedRound}`)
// Verify opt-out (getAccountInformation will throw if not opted in) try { await algorand.asset.getAccountInformation(receiver.addr, assetId) printError('Receiver should not be opted in!') } catch { printInfo(` Receiver successfully opted out of asset`) }
printSuccess('Opt-out completed successfully')
// Step 9: Destroy the asset printStep(9, 'Destroy the asset with algorand.send.assetDestroy()') printInfo('All assets must be returned to creator before destruction')
// Return assets from frozenAccount const frozenCurrentBalance = await algorand.asset.getAccountInformation(frozenAccount.addr, assetId) if (frozenCurrentBalance.balance > 0n) { await algorand.send.assetTransfer({ sender: frozenAccount.addr, receiver: creator.addr, assetId: assetId, amount: frozenCurrentBalance.balance, }) printInfo(` Transferred ${frozenCurrentBalance.balance} units from frozenAccount to creator`) }
// Opt-out frozenAccount await algorand.send.assetOptOut({ sender: frozenAccount.addr, assetId: assetId, creator: creator.addr, ensureZeroBalance: true, }) printInfo(` FrozenAccount opted out`)
// Verify creator has all assets const creatorFinalBalance = await algorand.asset.getAccountInformation(creator.addr, assetId) printInfo(` Creator final balance: ${creatorFinalBalance.balance} (should be ${assetInfo.total})`)
// Destroy the asset const destroyResult = await algorand.send.assetDestroy({ sender: creator.addr, // Must be the manager assetId: assetId, })
printInfo(`\nAsset destroyed:`) printInfo(` Transaction ID: ${destroyResult.txIds[0]}`) printInfo(` Confirmed round: ${destroyResult.confirmation.confirmedRound}`)
// Verify destruction try { await algorand.asset.getById(assetId) printError('Asset should not exist!') } catch { printInfo(` Asset ${assetId} no longer exists`) }
printSuccess('Asset destroyed successfully')
// Step 10: Summary of asset operations printStep(10, 'Summary - Asset Operations API') printInfo('Asset operations available through algorand.send:') printInfo('') printInfo('assetCreate(params):') printInfo(' sender: Address - Creator of the asset') printInfo(' total: bigint - Total units in smallest divisible unit') printInfo(' decimals?: number - Decimal places (0-19)') printInfo(' assetName?: string - Asset name (max 32 bytes)') printInfo(' unitName?: string - Unit name/ticker (max 8 bytes)') printInfo(' url?: string - URL for asset info (max 96 bytes)') printInfo(' metadataHash?: Uint8Array - 32-byte metadata hash') printInfo(' defaultFrozen?: boolean - Default freeze status') printInfo(' manager?: Address - Can reconfigure/destroy asset') printInfo(' reserve?: Address - Holds uncirculated supply (informational)') printInfo(' freeze?: Address - Can freeze/unfreeze holdings') printInfo(' clawback?: Address - Can clawback from any account') printInfo('') printInfo('assetConfig(params):') printInfo(' sender: Address - Must be current manager') printInfo(' assetId: bigint - Asset to reconfigure') printInfo(' manager, reserve, freeze, clawback: Addresses to update') printInfo('') printInfo('assetOptIn(params):') printInfo(' sender: Address - Account opting in') printInfo(' assetId: bigint - Asset to opt into') printInfo('') printInfo('assetTransfer(params):') printInfo(' sender: Address - Sender (or clawback address)') printInfo(' receiver: Address - Recipient') printInfo(' assetId: bigint - Asset to transfer') printInfo(' amount: bigint - Amount in smallest units') printInfo(' clawbackTarget?: Address - Account to clawback from') printInfo(' closeAssetTo?: Address - Close holding to this address') printInfo('') printInfo('assetFreeze(params):') printInfo(' sender: Address - Must be freeze address') printInfo(' assetId: bigint - Asset ID') printInfo(' freezeTarget: Address - Account to freeze/unfreeze') printInfo(' frozen: boolean - Freeze (true) or unfreeze (false)') printInfo('') printInfo('assetOptOut(params):') printInfo(' sender: Address - Account opting out') printInfo(' assetId: bigint - Asset to opt out of') printInfo(' creator: Address - Asset creator (receives remaining units)') printInfo(' ensureZeroBalance: boolean - Safety check') printInfo('') printInfo('assetDestroy(params):') printInfo(' sender: Address - Must be manager') printInfo(' assetId: bigint - Asset to destroy') printInfo(' Note: All units must be in creator account')
printSuccess('Send Asset Operations example completed!')}
main().catch((error) => { printError(`Unhandled error: ${error instanceof Error ? error.message : String(error)}`) process.exit(1)})Other examples in Algorand Client
Section titled “Other examples in Algorand Client”- Client Instantiation
- AlgoAmount Utility
- Signer Configuration
- Suggested Params Configuration
- Account Manager
- Send Payment
- Send Asset Operations
- Send Application Operations
- Create Transaction (Unsigned Transactions)
- Transaction Composer (Atomic Transaction Groups)
- Asset Manager
- App Manager
- App Deployer
- Client Manager
- Error Transformers
- Transaction Leases