This guide is nearly identical to the Quickstart guide while using USDT rather than USDC. The most important differences are the contract addresses, and the sourceToken parameter in the estimate request.

This example uses the following:

The target contract in our example is a simple WrappedUSDT contract we deployed specially for this occasion. It contains a mint function, which, when called, takes the specified amount of USDT from the caller and sends them the equivalent amount of a WrappedUSDT token.

Step 1. Prepare data

import axios from "axios";
import { Interface, Wallet, parseUnits } from "ethers";
import { config } from "dotenv";
config();

const SRC_CHAIN_ID = 137; // Polygon
const DST_CHAIN_ID = 137; // Polygon
const USDT_TO_WRAP = "0.01"; // 0.01 USDT

// USDT and WUSDT addresses on Polygon
const srcAddresses = {
  USDT: "0xc2132d05d31c914a87c6611c10748aeb04b58e8f",
  WUSDT: "0x421B9eE6D55B67818dB11A3196316AD1Fb4E1a4B",
};
const dstAddresses = srcAddresses; // for our single-chain example

// Initialize contract interfaces for USDT and WUSDT (to encode tx data)
const usdtTokenInterface = new Interface(["function approve(address,uint256)"]);
const wUsdtTokenInterface = new Interface(["function mint(uint256)"]);

// Initialize the wallet to sign and fund the tx with USDT
const wallet = new Wallet(process.env.WALLET_PRIVATE_KEY!); // set it in .env

// Initialize the axios client
const axiosClient = axios.create({
  baseURL: "https://api.peaze.com/api",
  headers: {
    "Content-Type": "application/json",
    "X-Api-Key": process.env.PEAZE_API_KEY, // for public demo key, see Quickstart
  },
});

const endpointPrefix = "/v1/single-chain"; // '/v1/cross-chain' for cross-chain txs

Step 2: Estimate transaction

Call the Estimate Transaction endpoint:

async function estimateTransaction() {
  // Convert the USDT amount to *destination* chain decimals (6 on supported chains)
  const tokenAmount = parseUnits(USDT_TO_WRAP, 6);

  // Encode a tx to approve the WUSDT contract to transfer USDT from the caller
  const approvalTx = {
    to: dstAddresses.USDT,
    data: usdtTokenInterface.encodeFunctionData("approve", [
      dstAddresses.WUSDT,
      tokenAmount,
    ]),
  };

  // Encode a tx to mint WUSDT tokens
  const mintTx = {
    to: dstAddresses.WUSDT,
    data: wUsdtTokenInterface.encodeFunctionData("mint", [tokenAmount]),
    value: 0n.toString(), // can be omitted here, since this transaction has value 0
  };

  // Specify that we expect to receive WUSDT tokens as a result of the tx
  const expectedERC20Tokens = [dstAddresses.WUSDT];

  // Send the request to the estimate endpoint
  const { data } = await axiosClient.post(`${endpointPrefix}/estimate`, {
    sourceChain: SRC_CHAIN_ID,
    destinationChain: DST_CHAIN_ID,
    sourceToken: srcAddresses.USDT, // THIS ACTIVATES THE NON-USDC FUNDING FEATURE
    userAddress: wallet.address, // address signing and funding the tx with USDT
    tokenAmount: tokenAmount.toString(), // USDT amount to fund the tx with (in dst decimals)
    transactions: [approvalTx, mintTx], // array of individual txs to execute in order
    expectedERC20Tokens, // array of ERC-20 tokens we expect to receive as a result of the tx
  });

  return data;
}

const estimateTxData = await estimateTransaction();

You will receive a response that looks like this:

{
    "quote": {
        "general": {...},
        "peazeTypedData": {...},
        "permitTypedData": {...}
    },
    "costSummary": {
        "totalAmount": 1.53,
        "baseAmount": 1.0,
        "gasCost": 0.43,
        "peazeFee": 0.1,
        [...]
    }
}

quote is an official signed Peaze quote, containing the full details of the requested transaction and its cost. It includes two typed data objects that the user must sign to authorize the transaction.

costSummary is a simpler object that contains a more user-friendly cost breakdown for the transaction. It shows the total amount of USDT the user will be charged, how much of it pays for gas, how much of it is the Peaze protocol fee, etc.

For more details, please check out the references for single-chain and cross-chain transactions.

Step 3: Sign and execute transaction

Once we get the typed data signed to authorize the transaction, we simply pass the quote and signatures to the Single-chain Execute Transaction endpoint.

// Extract typed data to sign and generate signatures
const { peazeTypedData, permitTypedData } = quote;

const signatures = {
  peazeSignature: await wallet.signTypedData(
    peazeTypedData.domain,
    peazeTypedData.types,
    peazeTypedData.message
  ),
  permitSignature: await wallet.signTypedData(
    permitTypedData.domain,
    permitTypedData.types,
    permitTypedData.message
  ),
};

// Send the request to the execute endpoint
console.log(`Total cost: ${costSummary.totalAmount} USDT.`);
console.log("Executing transaction...");
const { data } = await axiosClient.post(`${endpointPrefix}/execute`, {
  quote,
  signatures,
});

You will receive a response that looks like this:

{
  "transactionId": "<transactionId>",
  "transactionHash": "0x...",
  "blockExplorerUrl": "https://..."
}

That’s it!

You can now now check the status of the transaction using the returned block explorer URL. On a side note, to check the status of a cross-chain transaction on its destination chain, head over to the LayerZero Explorer and simply paste in the returned transaction hash.


Full code

Here we provide a complete version of the code. It includes a few additional features, compared to the above:

  • Error handling, which shows how to extract error messages and details from the API response
  • Better support for cross-chain transactions
  • A failsafe that aborts the transaction if the total cost is above 10 USDT (just in case)
  • Logging of the transaction estimate and execution data
import axios from "axios";
import { Interface, Wallet, parseUnits } from "ethers";
import { config } from "dotenv";
config();

const SRC_CHAIN_ID = 137; // Polygon
const DST_CHAIN_ID = 137; // Polygon
const USDT_TO_WRAP = "0.01"; // 0.01 USDT

// USDT and WUSDT addresses on Polygon
const srcAddresses = {
  USDT: "0xc2132d05d31c914a87c6611c10748aeb04b58e8f",
  WUSDT: "0x421B9eE6D55B67818dB11A3196316AD1Fb4E1a4B",
};
const dstAddresses = srcAddresses; // for our single-chain example

// Define some helper constants (useful if you want to try a cross-chain tx)
const isSingleChain = SRC_CHAIN_ID === DST_CHAIN_ID;
const endpointPrefix = isSingleChain ? "/v1/single-chain" : "/v1/cross-chain";

// Initialize contract interfaces for USDT and WUSDT (to encode tx data)
const usdtTokenInterface = new Interface(["function approve(address,uint256)"]);
const wUsdtTokenInterface = new Interface(["function mint(uint256)"]);

// Initialize the wallet to sign and fund the tx with USDT
const wallet = new Wallet(process.env.WALLET_PRIVATE_KEY!); // set it in .env

// Initialize the axios client
const axiosClient = axios.create({
  baseURL: "https://api.peaze.com/api",
  headers: {
    "Content-Type": "application/json",
    "X-Api-Key": process.env.PEAZE_API_KEY, // for public demo key, see Quickstart
  },
});

// Estimates the cost of the transaction by calling the /estimate endpoint
async function estimateTransaction() {
  // Convert the USDT amount to *destination* chain decimals (6 on supported chains)
  const tokenAmount = parseUnits(USDT_TO_WRAP, 6);

  // Encode a tx to approve the WUSDT contract to transfer USDT from the caller
  const approvalTx = {
    to: dstAddresses.USDT,
    data: usdtTokenInterface.encodeFunctionData("approve", [
      dstAddresses.WUSDT,
      tokenAmount,
    ]),
  };

  // Encode a tx to mint WUSDT tokens
  const mintTx = {
    to: dstAddresses.WUSDT,
    data: wUsdtTokenInterface.encodeFunctionData("mint", [tokenAmount]),
    value: 0n.toString(), // can be omitted here, since this transaction has value 0
  };

  // Specify that we expect to receive WUSDT tokens as a result of the tx
  const expectedERC20Tokens = [dstAddresses.WUSDT];

  // Send the request to the estimate endpoint
  const { data } = await axiosClient.post(`${endpointPrefix}/estimate`, {
    sourceChain: SRC_CHAIN_ID,
    destinationChain: DST_CHAIN_ID,
    sourceToken: srcAddresses.USDT, // THIS ACTIVATES THE NON-USDC FUNDING FEATURE
    userAddress: wallet.address, // address signing and funding the tx with USDT
    tokenAmount: tokenAmount.toString(), // USDT amount to fund the tx with (in dst decimals)
    transactions: [approvalTx, mintTx], // array of individual txs to execute in order
    expectedERC20Tokens, // array of ERC-20 tokens we expect to receive as a result of the tx
  });

  return data;
}

async function main() {
  console.log("-".repeat(60));
  console.log(`USDT tx from chain ${SRC_CHAIN_ID} to ${DST_CHAIN_ID}`);
  console.log("-".repeat(60) + "\n");

  console.log("Getting tx estimate...");

  // Get a quote for the transaction
  const { quote, costSummary } = await estimateTransaction();
  console.log(`Quote data:\n${JSON.stringify(quote, null, 2)}\n`);

  // Extract typed data to sign and generate signatures
  const { peazeTypedData, permitTypedData } = quote;

  const signatures = {
    peazeSignature: await wallet.signTypedData(
      peazeTypedData.domain,
      peazeTypedData.types,
      peazeTypedData.message
    ),
    permitSignature: await wallet.signTypedData(
      permitTypedData.domain,
      permitTypedData.types,
      permitTypedData.message
    ),
  };

  // Show total cost and check if we should proceed
  const totalCost: number = costSummary.totalAmount;
  console.log(`Total cost (tx amount + gas + fees): ${totalCost} USDT\n`);

  if (totalCost > 10) {
    console.log("That's a bit too much, let's bail out of the transaction.");
    return; // for your "peaze" of mind :)
  }

  // Send the request to the execute endpoint
  console.log("Executing transaction...");
  const { data } = await axiosClient.post(`${endpointPrefix}/execute`, {
    quote,
    signatures,
  });

  console.log(`Transaction submitted:\n${JSON.stringify(data, null, 2)}\n`);
  if (isSingleChain) return;
  console.log(
    "Monitor the cross-chain tx status on https://layerzeroscan.com.",
    "Simply search for the tx hash shown above."
  );
}

main().catch((e) => {
  const errorMsg = e.response?.data?.message ?? `${e}`;
  const errorDetails = JSON.stringify(e.response?.data?.data, null, 2);

  console.log("Oops... we got an error :(");
  console.log(errorMsg);
  if (errorDetails) console.log(`Error details:\n${errorDetails}`);
  process.exit(1);
});