Skip to content
Algorand Developer Portal

Send Asset Operations

← Back to Algorand Client

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
  • LocalNet running (via algokit localnet start)

From the repository root:

Terminal window
cd examples
npm run example algorand_client/07-send-asset-ops.ts

View source on GitHub

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);
});