Skip to content
Algorand Developer Portal

Atomic Swap

← Back to Transactions

This example demonstrates how to perform an atomic swap of ALGO for ASA between two parties. In an atomic swap:

  • Party A sends ASA to Party B
  • Party B sends ALGO to Party A
  • Each party signs ONLY their own transaction
  • Signatures are combined and submitted together
  • Both transfers succeed or both fail (atomicity) Key difference from regular atomic groups: different parties sign different transactions.
  • LocalNet running (via algokit localnet start)

From the repository root:

Terminal window
cd examples
npm run example transact/08-atomic-swap.ts

View source on GitHub

08-atomic-swap.ts
/**
* Example: Atomic Swap
*
* This example demonstrates how to perform an atomic swap of ALGO for ASA between two parties.
* In an atomic swap:
* - Party A sends ASA to Party B
* - Party B sends ALGO to Party A
* - Each party signs ONLY their own transaction
* - Signatures are combined and submitted together
* - Both transfers succeed or both fail (atomicity)
*
* Key difference from regular atomic groups: different parties sign different transactions.
*
* Prerequisites:
* - LocalNet running (via `algokit localnet start`)
*/
import { AlgorandClient } from '@algorandfoundation/algokit-utils';
import type { PendingTransactionResponse } from '@algorandfoundation/algokit-utils/algod-client';
import {
Transaction,
TransactionType,
assignFee,
groupTransactions,
type AssetConfigTransactionFields,
type AssetTransferTransactionFields,
type PaymentTransactionFields,
} from '@algorandfoundation/algokit-utils/transact';
import {
createAlgodClient,
formatAlgo,
getAccountBalance,
printHeader,
printInfo,
printStep,
printSuccess,
shortenAddress,
waitForConfirmation,
} from '../shared/utils.js';
/**
* Gets a funded account from LocalNet's KMD wallet
*/
async function getLocalNetFundedAccount(algorand: AlgorandClient) {
return await algorand.account.kmd.getLocalNetDispenserAccount();
}
async function main() {
printHeader('Atomic Swap Example (ALGO <-> ASA)');
// Step 1: Initialize clients
printStep(1, 'Initialize Algod Client');
const algod = createAlgodClient();
const algorand = AlgorandClient.defaultLocalNet();
printInfo('Connected to LocalNet Algod');
// Step 2: Get Party A from KMD (asset owner)
printStep(2, 'Setup Party A (Asset Owner)');
const partyA = await getLocalNetFundedAccount(algorand);
const partyABalanceBefore = await getAccountBalance(algorand, partyA.addr.toString());
printInfo(`Party A address: ${shortenAddress(partyA.addr.toString())}`);
printInfo(`Party A ALGO balance: ${formatAlgo(partyABalanceBefore.microAlgo)}`);
// Step 3: Generate and fund Party B using AlgorandClient helper
printStep(3, 'Setup Party B (ALGO holder)');
const partyB = algorand.account.random();
// Fund Party B with ALGO for the swap + fees
const suggestedParams = await algod.suggestedParams();
const partyBFundingAmount = 10_000_000n; // 10 ALGO (will use 5 ALGO for swap)
const fundBTx = new Transaction({
type: TransactionType.Payment,
sender: partyA.addr,
firstValid: suggestedParams.firstValid,
lastValid: suggestedParams.lastValid,
genesisHash: suggestedParams.genesisHash,
genesisId: suggestedParams.genesisId,
payment: {
receiver: partyB.addr,
amount: partyBFundingAmount,
},
});
const fundBTxWithFee = assignFee(fundBTx, {
feePerByte: suggestedParams.fee,
minFee: suggestedParams.minFee,
});
const signedFundBTx = await partyA.signer([fundBTxWithFee], [0]);
await algod.sendRawTransaction(signedFundBTx);
await waitForConfirmation(algod, fundBTxWithFee.txId());
const partyBBalanceBefore = await getAccountBalance(algorand, partyB.addr.toString());
printInfo(`Party B address: ${shortenAddress(partyB.addr.toString())}`);
printInfo(`Party B ALGO balance: ${formatAlgo(partyBBalanceBefore.microAlgo)}`);
// Step 4: Party A creates an asset
printStep(4, 'Party A Creates Asset');
const assetTotal = 1_000_000n; // 1,000,000 units (no decimals for simplicity)
const assetDecimals = 0;
const assetName = 'Swap Token';
const assetUnitName = 'SWAP';
const createParams = await algod.suggestedParams();
const assetConfigFields: AssetConfigTransactionFields = {
assetId: 0n,
total: assetTotal,
decimals: assetDecimals,
defaultFrozen: false,
assetName: assetName,
unitName: assetUnitName,
url: 'https://example.com/swap-token',
manager: partyA.addr,
reserve: partyA.addr,
freeze: partyA.addr,
clawback: partyA.addr,
};
const createAssetTx = new Transaction({
type: TransactionType.AssetConfig,
sender: partyA.addr,
firstValid: createParams.firstValid,
lastValid: createParams.lastValid,
genesisHash: createParams.genesisHash,
genesisId: createParams.genesisId,
assetConfig: assetConfigFields,
});
const createAssetTxWithFee = assignFee(createAssetTx, {
feePerByte: createParams.fee,
minFee: createParams.minFee,
});
const signedCreateTx = await partyA.signer([createAssetTxWithFee], [0]);
await algod.sendRawTransaction(signedCreateTx);
const createPendingInfo = (await waitForConfirmation(
algod,
createAssetTxWithFee.txId(),
)) as PendingTransactionResponse;
const assetId = createPendingInfo.assetId;
if (!assetId) {
throw new Error('Asset ID not found');
}
printInfo(`Created asset: ${assetName} (${assetUnitName})`);
printInfo(`Asset ID: ${assetId}`);
printInfo(`Party A holds: ${assetTotal} ${assetUnitName}`);
// Step 5: Party B opts into the asset
printStep(5, 'Party B Opts Into Asset');
const optInParams = await algod.suggestedParams();
const optInFields: AssetTransferTransactionFields = {
assetId: assetId,
receiver: partyB.addr,
amount: 0n,
};
const optInTx = new Transaction({
type: TransactionType.AssetTransfer,
sender: partyB.addr,
firstValid: optInParams.firstValid,
lastValid: optInParams.lastValid,
genesisHash: optInParams.genesisHash,
genesisId: optInParams.genesisId,
assetTransfer: optInFields,
});
const optInTxWithFee = assignFee(optInTx, {
feePerByte: optInParams.fee,
minFee: optInParams.minFee,
});
const signedOptInTx = await partyB.signer([optInTxWithFee], [0]);
await algod.sendRawTransaction(signedOptInTx);
await waitForConfirmation(algod, optInTxWithFee.txId());
printInfo(`Party B opted into asset ID: ${assetId}`);
printSuccess('Opt-in successful!');
// Step 6: Build the atomic swap transactions
printStep(6, 'Build Atomic Swap Transactions');
const swapAssetAmount = 100n; // Party A sends 100 SWAP to Party B
const swapAlgoAmount = 5_000_000n; // Party B sends 5 ALGO to Party A
printInfo(`Swap terms:`);
printInfo(` - Party A sends: ${swapAssetAmount} ${assetUnitName} -> Party B`);
printInfo(` - Party B sends: ${formatAlgo(swapAlgoAmount)} -> Party A`);
const swapParams = await algod.suggestedParams();
// Transaction 1: Party A sends ASA to Party B
const asaSendFields: AssetTransferTransactionFields = {
assetId: assetId,
receiver: partyB.addr,
amount: swapAssetAmount,
};
const asaSendTx = new Transaction({
type: TransactionType.AssetTransfer,
sender: partyA.addr,
firstValid: swapParams.firstValid,
lastValid: swapParams.lastValid,
genesisHash: swapParams.genesisHash,
genesisId: swapParams.genesisId,
assetTransfer: asaSendFields,
});
const asaSendTxWithFee = assignFee(asaSendTx, {
feePerByte: swapParams.fee,
minFee: swapParams.minFee,
});
// Transaction 2: Party B sends ALGO to Party A
const algoSendFields: PaymentTransactionFields = {
receiver: partyA.addr,
amount: swapAlgoAmount,
};
const algoSendTx = new Transaction({
type: TransactionType.Payment,
sender: partyB.addr,
firstValid: swapParams.firstValid,
lastValid: swapParams.lastValid,
genesisHash: swapParams.genesisHash,
genesisId: swapParams.genesisId,
payment: algoSendFields,
});
const algoSendTxWithFee = assignFee(algoSendTx, {
feePerByte: swapParams.fee,
minFee: swapParams.minFee,
});
printInfo('Transaction 1: Party A sends ASA to Party B');
printInfo('Transaction 2: Party B sends ALGO to Party A');
// Step 7: Group the transactions using groupTransactions()
printStep(7, 'Group Transactions with groupTransactions()');
const groupedTransactions = groupTransactions([asaSendTxWithFee, algoSendTxWithFee]);
const groupId = groupedTransactions[0].group;
printInfo(`Group ID assigned to both transactions`);
printInfo(
`Group ID (base64): ${groupId ? Buffer.from(groupId).toString('base64') : 'undefined'}`,
);
printSuccess('Transactions grouped successfully!');
// Step 8: Each party signs ONLY their transaction
printStep(8, 'Each Party Signs Their Own Transaction');
printInfo('Party A signs transaction 0 (ASA transfer)');
const signedAsaTx = await partyA.signer([groupedTransactions[0]], [0]);
printInfo('Party B signs transaction 1 (ALGO payment)');
const signedAlgoTx = await partyB.signer([groupedTransactions[1]], [0]);
printSuccess('Both parties signed their respective transactions!');
printInfo("Note: Party A cannot see/modify Party B's transaction and vice versa");
printInfo('The atomic group ensures both execute or neither does');
// Step 9: Combine signatures and submit
printStep(9, 'Combine Signatures and Submit Atomic Swap');
// Concatenate signed transactions in group order
const combinedSignedTxns = new Uint8Array([...signedAsaTx[0], ...signedAlgoTx[0]]);
printInfo('Submitting atomic swap to network...');
await algod.sendRawTransaction(combinedSignedTxns);
const swapTxId = groupedTransactions[0].txId();
const pendingInfo = await waitForConfirmation(algod, swapTxId);
printInfo(`Atomic swap confirmed in round: ${pendingInfo.confirmedRound}`);
printSuccess('Atomic swap executed successfully!');
// Step 10: Verify the swap results
printStep(10, 'Verify Swap Results');
// Get Party A's balances after swap
const partyABalanceAfter = await getAccountBalance(algorand, partyA.addr.toString());
const partyAAssetInfo = await algod.accountAssetInformation(partyA.addr.toString(), assetId);
const partyAAssetBalance = partyAAssetInfo.assetHolding?.amount ?? 0n;
// Get Party B's balances after swap
const partyBBalanceAfter = await getAccountBalance(algorand, partyB.addr.toString());
const partyBAssetInfo = await algod.accountAssetInformation(partyB.addr.toString(), assetId);
const partyBAssetBalance = partyBAssetInfo.assetHolding?.amount ?? 0n;
printInfo('Party A (after swap):');
printInfo(` - ALGO: ${formatAlgo(partyABalanceAfter.microAlgo)}`);
printInfo(
` - ${assetUnitName}: ${partyAAssetBalance} (sent ${swapAssetAmount}, remaining: ${assetTotal - swapAssetAmount})`,
);
printInfo('Party B (after swap):');
printInfo(` - ALGO: ${formatAlgo(partyBBalanceAfter.microAlgo)}`);
printInfo(` - ${assetUnitName}: ${partyBAssetBalance} (received ${swapAssetAmount})`);
// Verify Party A received ALGO
// Party A's balance should have increased by swapAlgoAmount minus fees paid for various transactions
// We check that they received roughly the ALGO from the swap
printInfo('');
printInfo('Verification:');
// Verify Party B received ASA
if (partyBAssetBalance !== swapAssetAmount) {
throw new Error(
`Party B ASA balance mismatch: expected ${swapAssetAmount}, got ${partyBAssetBalance}`,
);
}
printSuccess(`Party B received ${swapAssetAmount} ${assetUnitName}`);
// Verify Party A's ASA balance decreased
const expectedPartyAAsaBalance = assetTotal - swapAssetAmount;
if (partyAAssetBalance !== expectedPartyAAsaBalance) {
throw new Error(
`Party A ASA balance mismatch: expected ${expectedPartyAAsaBalance}, got ${partyAAssetBalance}`,
);
}
printSuccess(`Party A ASA balance correctly reduced to ${partyAAssetBalance} ${assetUnitName}`);
// Verify Party B's ALGO balance decreased (sent 5 ALGO + fee)
const partyBAlgoDecrease = partyBBalanceBefore.microAlgo - partyBBalanceAfter.microAlgo;
if (partyBAlgoDecrease < swapAlgoAmount) {
throw new Error(`Party B should have sent at least ${swapAlgoAmount} microALGO`);
}
printSuccess(
`Party B sent ${formatAlgo(swapAlgoAmount)} (plus ${formatAlgo(partyBAlgoDecrease - swapAlgoAmount)} in fees)`,
);
printInfo('');
printInfo('Atomic Swap Summary:');
printInfo(` - Party A gave: ${swapAssetAmount} ${assetUnitName}`);
printInfo(` - Party A received: ${formatAlgo(swapAlgoAmount)}`);
printInfo(` - Party B gave: ${formatAlgo(swapAlgoAmount)}`);
printInfo(` - Party B received: ${swapAssetAmount} ${assetUnitName}`);
printSuccess('Atomic swap example completed successfully!');
}
main().catch(error => {
console.error('Error:', error);
process.exit(1);
});