Skip to content

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