Make a Fee Relay Transfer

Fee relay transactions are those where fees are paid by another address. This can be useful when you split wallets that contain fees and payment funds. The process requires the construction of a custom transaction body that needs to undergo several manual checks due to certain features of the OMG Network.

Implementation

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

Requires Node >= 8.11.3 < 13.0.0

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

2. Import dependencies

Make a fee relay transaction on the OMG Network involves using ChildChain omg-js object. Here's an example of how to instantiate it:

import BN from "bn.js";
import { ChildChain, OmgUtil } from "@omisego/omg-js";const childChain = new ChildChain({ watcherUrl });
const ethUtil = require('ethereumjs-util');
const sigUtil = require('eth-sig-util');
  • watcherUrl - the Watcher Info URL for defined environment (personal or from OMG Network).

There are several ways to send a transaction on the OMG Network. We recommend using the first method but you may want to choose another approach for your specific use case.

3. Define constants


const sender = {
  address: '0x8CB0DE6206f459812525F2BA043b14155C2230C0',
  privateKey: 'CD55F2A7C476306B27315C7986BC50BD81DB4130D4B5CFD49E3EAF9ED1EDE4F7'
}const receiver = {
  address: '0xA9cc140410c2bfEB60A7260B3692dcF29665c254'
}const feePayer = {
  address: '0x001ebfBd3C6D6855bF4EF1a2Ec7f2a779B1028C2',
  privateKey: '5444fec49a972a1c6264745610ef3c8107980540ff3d732af4c5ddb87c5f03c2'
}const transactionToken = '0xd92e713d051c37ebb2561803a3b5fbabc4962431';
const plasmaContractAddress = plasmaContractAddress;
const feeCurrency = OmgUtil.transaction.ETH_CURRENCY;
const amount = "29";
  • plasmaContractAddress - CONTRACT_ADDRESS_PLASMA_FRAMEWORK for defined environment.

4. Create helpers

Fee helper

The fee helper retrieves the fee amount you need to pay during a transfer.


const transferAmount = BN.isBN(amount)
  ? amount
  : new BN(amount.toString());
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 a transaction. Currently, the OMG Network has a limitation of only 4 inputs and 4 outputs, thus you can have a maximum of 3 payment inputs and reserve the last one for a fee input. For simplicity purposes, we select UTXOs that have an amount that is greater or equal to the amount requested in a transaction.


const getUsableUtxos = async (address, currency, spendAmount) => {
  const utxos = await childChain.getUtxos(address);
  return 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 amount.gte(spendAmount);
    })
    .map(utxo => {
      const amount = BN.isBN(utxo.amount)
        ? utxo.amount
        : new BN(utxo.amount.toString());
      return {
        ...utxo, amount
      }
    });
}

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 a sender and fee payer.


const getSignatures = (typedData, sender, feePayer) => {
  const toSign = OmgUtil.transaction.getToSignHash(typedData);
  const senderSignature = getSignature(toSign, sender);
  const feePayerSignature = getSignature(toSign, feePayer);
  return [senderSignature, 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);
}

4. Create a transaction body

To construct a relay 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,
  feePayer,
  feeAmount,
  feeUtxo,
  metadata
) => {  
  const encodedMetadata = metadata
    ? OmgUtil.transaction.encodeMetadata(metadata)
    : OmgUtil.transaction.NULL_METADATA;  
  const transactionBody = {
    inputs: [senderUtxo, feeUtxo],
    outputs: [{
      outputType: 1,
      outputGuard: receiver,
      currency: senderUtxo.currency,
      amount: transferAmount
    }],
    metadata: encodedMetadata
  }  
  checkUtxoForChange(sender, senderUtxo, transferAmount, transactionBody);
  checkUtxoForChange(feePayer, feeUtxo, feeAmount, transactionBody);
  return transactionBody;
}

5. Submit a relay transaction

The process of submitting a relay transaction is the same as submitting any other transaction, except you pass a custom transaction body created earlier.


async function transactionRelay() {
  
  const feeAmount = await getFeeAmount(feeCurrency);  
  const feeUtxos = await getUsableUtxos(
    feePayer.address,
    feeCurrency,
    feeAmount
  );  
  const senderUtxos = await getUsableUtxos(
    sender.address,
    transactionToken,
    transferAmount
  );  
  if (senderUtxos.length !== 1) {
    console.log("This transaction only supports one payment input. To proceed with transaction, please merge the selected inputs first.");
  } else {
    
    const transactionBody = createTransactionBody(
      sender.address,
      senderUtxos[0],
      receiver.address,
      feePayer.address,
      feeAmount,
      feeUtxos[0],
      "relay test"
    );    
    const typedData = OmgUtil.transaction.getTypedData(
      transactionBody,
      plasmaContractAddress
    );    
    const signatures = getSignatures(
      typedData,
      sender,
      feePayer
    );    
    const signedTx = childChain.buildSignedTransaction(
      typedData,
      signatures
    );    
    const receipt = await childChain.submitTransaction(signedTx);
    console.log(receipt);
  }
}

NOTE, you can create a custom logic and include up to 3 payment inputs. This sample demonstrates the easiest option where you have only 1 payment input and 1 fee input.

Last updated