SDK

Javascript SDK for doing cross-chain transactions

Introduction

Hyphen SDK is a javascript-based SDK written in typescript that helps in integrating Hyphen services easily. It provides some helper methods that make the Hyphen integration very easy. Even though you can interact with the LiquidityPool smart contract directly and deposit tokens using the depositErc20 method from your DApp to do the deposit transactions, we recommend using the SDK as it provides methods like checking available liquidity before doing the transaction, checking exit transaction status, checking approvals, etc.

Let's get started

1. Installation

npm install @biconomy/hyphen

2. Importing and Instantiation

HYPHEN accepts a provider object as the first argument and an option object as the second argument.

import { Hyphen, SIGNATURE_TYPES } from "@biconomy/hyphen";

let hyphen = new Hyphen(<Provider Object>, {
  debug: true,            // If 'true', it prints debug logs on console window
  environment: "test",    // It can be "test" or "prod"
  onFundsTransfered: (data) => {
    // Optional Callback method which will be called when funds transfer across
    // chains will be completed
  }
});

Note: <Provider Object> is the provider object exposed by your connected wallet. It can be window.ethereum for Metamask or portis.provider for Portis and so on. The first provider object should be a provider object with accounts information.

It can either be a JSON RPC Provider object or HttpProvider object with an RPC URL given that you have passed walletProvider object in the options and Biconomy support is present. Check Network Agnostic Support section.

3. Initialization

After the SDK object is instantiated you need to initialize the SDK by calling its init function. The initialization is kept separated from the constructor to give flexibility to the developer to use await statement for initializing the SDK.

await hyphen.init();

Keep your code in a try-catch block to catch any error that might be thrown from the SDK during instantiation or initialization.

Time to do a Cross-Chain Transfer

Before we proceed let's define some terms which will be used frequently in this documentation

Now that we have initialized the SDK and defined the terms we can do the cross-chain transfer transaction. In order to do a cross-chain transfer, some pre-checks need to be done and some pre-actions need to be taken if required.

SDK provides methods to do these pre-checks and do some pre-actions so you don't have to write extra code to do the same.

Pre-Checks

These checks can be done using the method preDepositStatus that will check for a given fromChainId, toChainId, tokenAddress, userAddress and amount, whether there is enough liquidity on toChain, the given tokenAddress is supported, enough approval is given by the user to LiquidityPoolManager contract on fromChain and the networks are supported or not.

import { RESPONSE_CODES } from "@biconomy/hyphen";

let preTransferStatus = await hyphen.depositManager.preDepositStatus({
    tokenAddress: "", // Token address on fromChain which needs to be transferred
    amount: "", // Amount of tokens to be transferred in smallest unit eg wei
    fromChainId: "" // Chain id from where tokens needs to be transferred
    toChainId: "", // Chain id where tokens are supposed to be sent
    userAddress: "" // User wallet address who want's to do the transfer
});

if (preTransferStatus.code === RESPONSE_CODES.OK) {
  // βœ… ALL CHECKS PASSED. Proceed to do deposit transaction
} else if(preTransferStatus.code === RESPONSE_CODES.ALLOWANCE_NOT_GIVEN) {
  // ❌ Not enough apporval from user address on LiquidityPoolManager contract on fromChain
  let infiniteApproval = false;
  let useBiconomy = false;
  let approveTx = await hyphen.tokens.approveERC20(tokenAddress, 
                    preTransferStatus.depositContract, amount.toString(),
                    infiniteApproval, useBiconomy
                  );

  // ⏱Wait for the transaction to confirm, pass a number of blocks to wait as param
  await approveTx.wait(2);
  
  // NOTE: Whenever there is a transaction done via SDK, all responses
  // will be ethers.js compatible with an async wait() function that
  // can be called with 'await' to wait for transaction confirmation.
  
  // πŸ†—Now proceed to do the deposit transaction
  
} else if (preTransferStatus.code === RESPONSE_CODES.UNSUPPORTED_NETWORK) {
  // ❌ Target chain id is not supported yet
} else if (preTransferStatus.code === RESPONSE_CODES.NO_LIQUIDITY) {
  // ❌ No liquidity available on target chain for given tokenn
} else if (preTransferStatus.code === RESPONSE_CODES.UNSUPPORTED_TOKEN) {
  // ❌ Requested token is not supported on fromChain yet
} else {
  // ❌ Any other unexpected error
}

Pre-Actions

If the approval for the token to be transferred is not given to LiquidityPool contract on fromChain then the first approval transaction needs to be done. You can directly interact with the token contract and call its approve or permit method but to make things simple SDK also provides a method to do this.

/**
 * tokenAddress Token address whose approval needs to be given
 * spender LiquidityPool address on fromChain 
 * amount Amount to be transferred. It should be in the smallest unit on token eg in wei
 * userAddress User address who is giving the approval
 * infiniteApproval Boolean flag whether to give infinite approval for the selected token
 * useBiconomy Boolean flag whether to use Biconomy for gasless transaction for approval
 */
let approveTx = await hyphen.tokens.approveERC20(tokenAddress, spender, amount, 
 userAddress, infiniteApproval, useBiconomy);

// Wait for 1 block confirmation
await approveTx.wait(1);

Once the approvals are checked and if required approval transaction is done. All you need to do is make only one deposit transaction on LiquidityPool on fromChain and the rest will be done by Hyphen.

Let's do the Deposit Transaction

You can call the deposit method provided in the SDK to initiate a deposit transaction. Make sure to call preDepositStatus method before calling deposit method.

let depositTx = await hyphen.depositManager.deposit({
    sender: "User wallet address",
    receiver: "Receiver address on toChain. Can be different than sender",
    tokenAddress: "Address of the token on fromChain to be transferred",
    depositContractAddress: "LiquidityPool address on fromChain",
    amount: "30000000000", //Amount to be transferred. Denoted in smallest unit eg in wei",
    fromChainId: 137, // chainId of fromChain
    toChainId: 1,     // chainId of toChain
    useBiconomy: true, // OPTIONAL boolean flag specifying whether to use Biconomy for gas less transaction or not
    dAppName: "Dapp specific identifier" // Can be any string, emitted from the contract during the deposit call; used for analytics
});

// Wait for 1 block confirmation
await depositTx.wait(1);

The deposit method checks if there's enough liquidity available on toChain for the given amount of tokens to be transferred considering all the pending transactions for the same token. It also checks if there's enough approval given to LiquidityPool contract by the sender address for the given amount of tokens to be transferred.

Once the deposit transaction is confirmed, rest of the transactions to make the cross-chain transfer will be done by off-chain Executor nodes run by Biconomy. Now you can wait for the cross-chain transfer transaction to be done.

In the next section, we'll see how to listen for the status of our cross-chain transfers once our deposit transaction is successful. Or what if you don't get the funds on the destination chain within 5 min after your successful deposit transaction.

Enable Network Agnostic Gasless Transactions

You can enable network agnostic gasless transactions using Biconomy so that the user can make Approve and Deposit transactions on a chain e.g., Polygon while still keeping his wallet pointing to the Ethereum chain.

To enable this, you need to pass walletProvider object and biconomy options in the second parameter of Hyphen constructor as mentioned below:

let hyphen = new Hyphen(<Provider Object 1>, {
  ...  
  walletProvider: <Provider Object 2>, // Pass this to enable network agnostic transfers
  biconomy: {
    enable: true,
    debug: true,
    apiKey: "Biconomy API Key for meta transaction support"
  },
  signatureType: SIGNATURE_TYPES.EIP712, // Optional. To specify signature types in case of meta transactions
  ...
});

Here, Provider Object 1 is the JSON RPC provider object pointing to the 'fromChain' where the user will be depositing his tokens. It doesn't need to have any accounts information.

Provider Object 2 has to be a provider object from the connected user wallet. It doesn't matter on which network the User Wallet is because we'll be using this provider object to just get the user signatures to do gasless transactions on the chain pointed by Provider Object 1.

So when the user initiates a deposit transaction using deposit method, Hyphen SDK will use Biconomy SDK to send the deposit transaction, internally Biconomy SDK will check the details of the called method and smart contract from Biconomy Dashboard. This step is mandatory and the called method needs to be registered on the Biconomy Dashboard.

Once the details are matched with the Biconomy dashboard data, Biconomy SDK will ask the user for the signature using walletProviderobject passed and the user will get a signature popup on his wallet.

Once the user provides his signatures, the transaction is routed via Biconomy and the transaction will be executed on the fromChain pointed by Provider Object 1.

Signing Transactions Using An Imported Private Key

In some cases, the SDK may be used outside of the browser and will require an automated experience. The following snippet demonstrates how to perform the same steps above but with your private key.

Instantiate Hyphen and an Ethers Wallet

import { RESPONSE_CODES } from "@biconomy/hyphen";
import { ethers } from "ethers";

const providerURL = process.env.PROVIDER_URL;
const provider = new ethers.providers.JsonRpcProvider(providerURL);

const privateKey = process.env.PRIVATE_KEY!;
const wallet = new ethers.Wallet(privateKey);
const signer = wallet.connect(provider);

// instantiate hyphen
let hyphen = new Hyphen(signer.provider, {
  debug: true,            // If 'true', it prints debug logs on console window
  environment: "test",    // It can be "test" or "prod"
  onFundsTransfered: (data) => {
    // Optional Callback method which will be called when funds transfer across
    // chains will be completed
  },
  // signatureType: SIGNATURE_TYPES.PERSONAL,
  infiniteApproval: false,
  transferCheckInterval: -1, // Interval in milli seconds to check for transfer status
  biconomy: {
      enable: false,
      apiKey: "<your_biconomy_api_key>",
      debug: true
  }
});

Pre-Deposit Operations

These are the same as before except that the approveERC20 function receives the wallet object.

let preTransferStatus = await hyphen.depositManager.preDepositStatus({
    tokenAddress: "", // Token address on fromChain which needs to be transferred
    amount: "", // Amount of tokens to be transferred in smallest unit eg wei
    fromChainId: "" // Chain id from where tokens needs to be transferred
    toChainId: "", // Chain id where tokens are supposed to be sent
    userAddress: "" // User wallet address who want's to do the transfer
});

if (preTransferStatus.code === RESPONSE_CODES.OK) {
  // βœ… ALL CHECKS PASSED. Proceed to do deposit transaction
} else if(preTransferStatus.code === RESPONSE_CODES.ALLOWANCE_NOT_GIVEN) {
  // ❌ Not enough apporval from user address on LiquidityPoolManager contract on fromChain
  let infiniteApproval = false;
  let useBiconomy = false;
  let approveTx = await hyphen.tokens.approveERC20(
                  tokenAddress, 
                  preTransferStatus.depositContract, amount.toString(),
                  wallet, infiniteApproval, useBiconomy
  ); // !!!NOTE: the previously created wallet is added here

  // ⏱Wait for the transaction to confirm, pass a number of blocks to wait as param
  await approveTx.wait(2);
  
  // NOTE: Whenever there is a transaction done via SDK, all responses
  // will be ethers.js compatible with an async wait() function that
  // can be called with 'await' to wait for transaction confirmation.
  
  // πŸ†—Now proceed to do the deposit transaction
  
} else if (preTransferStatus.code === RESPONSE_CODES.UNSUPPORTED_NETWORK) {
  // ❌ Target chain id is not supported yet
} else if (preTransferStatus.code === RESPONSE_CODES.NO_LIQUIDITY) {
  // ❌ No liquidity available on target chain for given tokenn
} else if (preTransferStatus.code === RESPONSE_CODES.UNSUPPORTED_TOKEN) {
  // ❌ Requested token is not supported on fromChain yet
} else {
  // ❌ Any other unexpected error
}

Perform the Deposit Transaction

The wallet object is passed as the last parameter to the deposit function.

let depositTx = await hyphen.depositManager.deposit({
    sender: "User wallet address",
    receiver: "Receiver address on toChain. Can be different than sender",
    tokenAddress: "Address of the token on fromChain to be transferred",
    depositContractAddress: "LiquidityPool address on fromChain",
    amount: "30000000000", //Amount to be transferred. Denoted in smallest unit eg in wei",
    fromChainId: 137, // chainId of fromChain
    toChainId: 1,     // chainId of toChain
    useBiconomy: true, // OPTIONAL boolean flag specifying whether to use Biconomy for gas less transaction or not
    tag: "Dapp specific identifier" // Can be any string, emitted from the contract during the deposit call; used for analytics
}, wallet);  // !!!NOTE: the previously created wallet is added here

// Wait for 1 block confirmation
await depositTx.wait(1);

Last updated