Atomic Swap
Description
Section titled “Description”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
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 transact/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);});