Make an Atomic Swap

Atomic Swap transactions represent an exchange between two different tokens on the OMG Network. A standard exchange rate is 1:1 but you can set your own rate as well. Atomic swaps can be useful during trading operations, claiming community tokens, etc.

To access network features from your application, use our official libraries:

Requires Node >= 8.11.3 < 13.0.0

npm install @omisego/omg-js web3 bn.jsnpm install @omisego/omg-js ethereumjs-util eth-sig-util bn.js bignumber.js

2. Import dependencies, define constants

import BN from "bn.js";
import BigNumber from "bignumber.js";
import { ChildChain, OmgUtil } from "@omisego/omg-js";
const ethUtil = require('ethereumjs-util');
const sigUtil = require('eth-sig-util');const childChain = new ChildChain({
  watcherUrl: watcherUrl,
  watcherProxyUrl: '',
  plasmaContractAddress: plasmaContractAddress
});
const sender = {
  address: '0x8CB0DE6206f459812525F2BA043b14155C2230C0',
  privateKey: 'CD55F2A7C476306B27315C7986BC50BD81DB4130D4B5CFD49E3EAF9ED1EDE4F7',
  currency: OmgUtil.hexPrefix("0xd92e713d051c37ebb2561803a3b5fbabc4962431")
}const receiver = {
  address: '0xA9cc140410c2bfEB60A7260B3692dcF29665c254',
  privateKey: 'E4F82A4822A2E6A28A6E8CE44490190B15000E58C7CBF62B4729A3FDC9515FD2',
  currency: OmgUtil.hexPrefix("0x2d453e2a14a00f4a26714a82abfc235c2b2094d5")
}const feePayer = {
  address: '0x001ebfBd3C6D6855bF4EF1a2Ec7f2a779B1028C2',
  privateKey: '5444fec49a972a1c6264745610ef3c8107980540ff3d732af4c5ddb87c5f03c2',
  currency: OmgUtil.transaction.ETH_CURRENCY
}const plasmaContractAddress = plasmaContractAddress;
  • watcherUrl - the Watcher Info URL for defined environment (personal or from OMG Network).

  • plasmaContractAddress - CONTRACT_ADDRESS_PLASMA_FRAMEWORK for defined environment.

The above constants are defined for the Rinkeby environment. If you want to work on the Mainnet, check the Environments page.

3. Create helpers

BigNumber helper

BigNumber helper converts a given amount into a BigNumber. It helps when you want to do an atomic swap between ERC20 tokens that have different decimals. Most tokens have 18 decimals by default, however, some tokens have 6 (e.g. USDT), or even 0 decimals.



const amountToBN = (amount, decimals = 18) => {
  const multiplier = new BigNumber(10).pow(decimals);
  const subunit = new BigNumber(amount).times(multiplier).toFixed();
  return new BN(subunit);
}

Fee helper

The fee helper retrieves the fee amount you need to pay during an atomic swap.


const getFeeAmount = async (currency) => {
  const fees = await childChain.getFees();
  const selectedFee = fees['1'].find(fee => fee.currency === currency);
  return BN.isBN(selectedFee.amount)
    ? selectedFee.amount
    : new BN(selectedFee.amount.toString());
}

UTXO helper

UTXO helper filters UTXOs that can be used during an atomic swap. Currently, the OMG Network has a limitation of only 4 inputs and 4 outputs, thus you can have a maximum of 3 payment inputs (sender, receiver, fee payer) and 1 fee input. For simplicity, we select UTXOs that are equal to the amount requested in a swap operation.


const getUsableUtxos = async (address, currency, spendAmount, filterEqual) => {
  const utxos = await childChain.getUtxos(address);
  const filteredUtxos = utxos
    .filter(utxo => {
      return utxo.currency.toLowerCase() === currency.toLowerCase();
    })
    .filter(utxo => {
      const amount = BN.isBN(utxo.amount)
        ? utxo.amount
        : new BN(utxo.amount.toString());
      return filterEqual ? amount.eq(spendAmount) : amount.gte(spendAmount);
    })
    .map(utxo => {
      const amount = BN.isBN(utxo.amount)
        ? utxo.amount
        : new BN(utxo.amount.toString());
      return {
        ...utxo, amount
      }
    });
  if (!filteredUtxos.length) {
    console.log(`There are no utxos that can cover a payment for ${spendAmount} '${currency}'`);
  }
  return filteredUtxos;
}

UTXO change helper

UTXO change helper checks if the provided UTXO (payment or fee) needs a change. If the change is needed, the helper creates and pushes an additional output to the existing array of transaction outputs.


const checkUtxoForChange = (address, utxo, amount, transactionBody) => {
  if (!utxo || utxo.length === 0) {
    throw new Error(`No UTXO provided for ${address}`);
  }  if (transactionBody.outputs.length > 4) {
    throw new Error(`The provided transaction body has 4 outputs. You need to have at least 1 spare output to proceed.`);
  }  if (utxo.amount.gt(amount)) {
    const changeAmount = utxo.amount.sub(amount);
    const changeOutput = {
      outputType: 1,
      outputGuard: address,
      currency: utxo.currency,
      amount: changeAmount
    }
    transactionBody.outputs.push(changeOutput);
  }
}

Signature helper

Signature helper signs and returns the inputs by the sender, receiver, and fee payer.


const getSignatures = (typedData, sender, receiver, feePayer) => {
  const toSign = OmgUtil.transaction.getToSignHash(typedData);
  const senderSignature = getSignature(toSign, sender);
  const receiverSignature = getSignature(toSign, receiver);
  const feePayerSignature = getSignature(toSign, feePayer);
  return [senderSignature, receiverSignature, feePayerSignature];
}
const getSignature = (toSign, signer) => {
  const signature = ethUtil.ecsign(
    toSign,
    Buffer(signer.privateKey.replace('0x', ''), 'hex')
  );
  return sigUtil.concatSig(signature.v, signature.r, signature.s);
}

3. Create a transaction body

To construct an atomic swap transaction, you need to create a custom transaction body. It should contain details about the sender, receiver, fee, fee payer, additional metadata.


const createTransactionBody = (
  sender,
  senderUtxo,
  receiver,
  receiverUtxo,
  feePayer,
  feeAmount,
  feeUtxo,
  metadata
) => {  
  const encodedMetadata = metadata
    ? OmgUtil.transaction.encodeMetadata(metadata)
    : OmgUtil.transaction.NULL_METADATA;  
  const transactionBody = {
    inputs: [senderUtxo, receiverUtxo, feeUtxo],
    outputs: [
      {
        outputType: 1,
        outputGuard: receiver.address,
        currency: sender.currency,
        amount: senderUtxo.amount
      },
      {
        outputType: 1,
        outputGuard: sender.address,
        currency: receiver.currency,
        amount: receiverUtxo.amount
      }
    ],
    metadata: encodedMetadata
  }  
  checkUtxoForChange(feePayer.address, feeUtxo, feeAmount, transactionBody);
  return transactionBody;
}

4. Submit an atomic swap

The process of submitting an atomic swap transaction is the same as submitting any other transaction, except you pass a custom transaction body created earlier. The transaction described below creates an atomic swap of 400 TUSDT (with 6 decimals) into 100 OMG (18 decimals) with an exchange rate of 4:1.


async function atomicSwap() {
  
  const feeAmount = await getFeeAmount(feePayer.currency);  
  const amount = "400";
  const swapAmountSender = amountToBN(amount, 6);  
  const feeUtxo = await getUsableUtxos(
    feePayer.address,
    feePayer.currency,
    feeAmount,
    false
  );  
  const senderUtxo = await getUsableUtxos(
    sender.address,
    sender.currency,
    swapAmountSender,
    true
  );  
  
  const exchangeRate = amountToBN(4, 6);  
  const tempSwapAmountReceiver = swapAmountSender.div(exchangeRate);  
  const swapAmountReceiver = amountToBN(tempSwapAmountReceiver, 18);  
  const receiverUtxo = await getUsableUtxos(
    receiver.address,
    receiver.currency,
    swapAmountReceiver,
    true
  );  
  if (senderUtxo.length > 1) {
    console.log("You can have only 1 sender UTXO to cover the atomic swap. To proceed with a transaction, please merge the selected UTXO first.");
  }
  if (receiverUtxo.length > 1) {
    console.log("You can have only 1 receiver UTXO to cover the atomic swap. To proceed with a transaction, please merge the selected UTXO first.");
  }
  if (feeUtxo.length > 2) {
    console.log("You can have only 2 fees UTXO to cover the atomic swap fee. To proceed with a transaction, please merge the selected UTXO first.");
  }
  else {
    
    const atomicSwapBody = createTransactionBody(
      sender,
      senderUtxo[0],
      receiver,
      receiverUtxo[0],
      feePayer,
      feeAmount,
      feeUtxo[0],
      "atomic swap"
    );    
    const typedData = OmgUtil.transaction.getTypedData(
      atomicSwapBody,
      plasmaContractAddress
    );    
    const signatures = getSignatures(
      typedData,
      sender,
      receiver,
      feePayer
    );    
    const signedTx = childChain.buildSignedTransaction(
      typedData,
      signatures
    );    
    const receipt = await childChain.submitTransaction(signedTx);
    console.log(receipt);
  }
}

Last updated