biconomy-hyphen
Version:
Hyphen SDK to enable instant and seamless cross-chain deposits & withdrawals
554 lines (553 loc) • 29.9 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SIGNATURE_TYPES = exports.EXIT_STATUS = exports.RESPONSE_CODES = exports.config = exports.Hyphen = void 0;
const ethers_1 = require("ethers");
const config_1 = require("./config");
Object.defineProperty(exports, "config", { enumerable: true, get: function () { return config_1.config; } });
Object.defineProperty(exports, "RESPONSE_CODES", { enumerable: true, get: function () { return config_1.RESPONSE_CODES; } });
Object.defineProperty(exports, "EXIT_STATUS", { enumerable: true, get: function () { return config_1.EXIT_STATUS; } });
Object.defineProperty(exports, "SIGNATURE_TYPES", { enumerable: true, get: function () { return config_1.SIGNATURE_TYPES; } });
const util_1 = require("./meta-transaction/util");
const util_2 = require("./util");
const { Biconomy } = require("@biconomy/mexa");
class Hyphen {
constructor(provider, options) {
this.init = () => {
const self = this;
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
if (self.options.biconomy &&
self.options.biconomy.enable &&
self.biconomy.status !== self.biconomy.READY) {
self.biconomy
.onEvent(self.biconomy.READY, () => __awaiter(this, void 0, void 0, function* () {
yield self._init();
resolve();
}))
.onEvent(self.biconomy.ERROR, (error, message) => {
self._logMessage(error);
self._logMessage(message);
reject(error);
});
}
else {
yield self._init();
resolve();
}
}));
};
this._init = () => __awaiter(this, void 0, void 0, function* () {
if (this.provider) {
const currentNetwork = yield this.provider.getNetwork();
const networkId = currentNetwork.chainId;
if (networkId) {
const supportedTokens = yield this._getSupportedTokensFromServer(networkId);
this.supportedTokens.set(networkId, supportedTokens);
}
else {
throw new Error("Unable to get network id from given provider object");
}
}
});
this._validate = (options) => {
if (!options) {
throw new Error(`Options object needs to be passed to Hyphen Object`);
}
if (options.biconomy &&
options.biconomy.enable &&
!options.biconomy.apiKey) {
throw new Error(`apiKey is required under biconomy option. Either disable biconomy or provide apiKey`);
}
};
this._getProvider = (useBiconomy) => {
if (useBiconomy) {
return this.biconomyProvider ? this.biconomyProvider : this.provider;
}
else {
return this.provider;
}
};
this._getProviderWithAccounts = (useBiconomy) => {
let result;
if (this.walletProvider) {
result = this.walletProvider;
}
else {
result = this._getProvider(useBiconomy);
}
return result;
};
this.getERC20TokenDecimals = (address) => {
const tokenContract = new ethers_1.ethers.Contract(address, config_1.config.erc20TokenABI, this._getProvider(false));
if (tokenContract) {
return tokenContract.decimals();
}
else {
throw new Error("Unable to create token contract object. Please check your network and token address");
}
};
this.getFetchOptions = (method) => {
return {
method,
headers: {
"Content-Type": "application/json;charset=utf-8",
},
};
};
this.getSupportedTokens = (networkId) => {
return this.supportedTokens.get(networkId);
};
this.preDepositStatus = (checkStatusRequest) => {
const self = this;
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
const fetchOptions = this.getFetchOptions("POST");
const body = {
tokenAddress: checkStatusRequest.tokenAddress,
amount: checkStatusRequest.amount,
fromChainId: checkStatusRequest.fromChainId,
toChainId: checkStatusRequest.toChainId,
userAddress: checkStatusRequest.userAddress,
};
fetchOptions.body = JSON.stringify(body);
fetch(`${self._getHyphenBaseURL()}${config_1.config.checkRequestStatusPath}`, fetchOptions)
.then((response) => response.json())
.then((response) => {
self._logMessage(response);
resolve(response);
})
.catch((error) => {
self._logMessage(error);
reject(error);
});
}));
};
this._getSupportedTokensFromServer = (networkId) => {
const self = this;
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
const fetchOptions = this.getFetchOptions("GET");
fetch(`${self._getHyphenBaseURL()}${config_1.config.getSupportedTokensPath}?networkId=${networkId}`, fetchOptions)
.then((response) => response.json())
.then((response) => {
if (response && response.supportedPairList) {
self._logMessage(response.supportedPairList);
resolve(response.supportedPairList);
}
else {
const error = self.formatMessage(config_1.RESPONSE_CODES.ERROR_RESPONSE, `Unable to get supported tokens`);
self._logMessage(error);
self._logMessage("Returning default list from config");
resolve(config_1.config.defaultSupportedTokens.get(networkId));
}
})
.catch((error) => {
self._logMessage(error);
self._logMessage("Returning default list from config");
resolve(config_1.config.defaultSupportedTokens.get(networkId));
});
}));
};
this.getERC20Allowance = (tokenAddress, userAddress, spender) => {
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
try {
if (tokenAddress && userAddress && spender) {
const tokenContract = new ethers_1.ethers.Contract(tokenAddress, config_1.config.erc20TokenABI, this._getProvider(false));
const allowance = yield tokenContract.allowance(userAddress, spender);
resolve(allowance);
}
else {
reject("Bad input parameters. Check if all parameters are valid");
}
}
catch (error) {
reject(error);
}
}));
};
this.deposit = (request) => __awaiter(this, void 0, void 0, function* () {
const provider = this._getProvider(request.useBiconomy);
if (util_2.isNativeAddress(request.tokenAddress)) {
const depositTransaction = yield this._depositTokensToLiquidityPoolManager(request);
if (depositTransaction) {
this.listenForExitTransaction(depositTransaction, typeof request.fromChainId === "number"
? request.fromChainId
: parseInt(request.fromChainId, 10));
}
return depositTransaction;
}
else {
const tokenContract = new ethers_1.ethers.Contract(request.tokenAddress, config_1.config.erc20TokenABI, provider);
const allowance = yield tokenContract.allowance(request.sender, request.depositContractAddress);
this._logMessage(`Allowance given to LiquidityPoolManager is ${allowance}`);
if (ethers_1.BigNumber.from(request.amount).lte(allowance)) {
const depositTransaction = yield this._depositTokensToLiquidityPoolManager(request);
if (depositTransaction) {
this.listenForExitTransaction(depositTransaction, typeof request.fromChainId === "number"
? request.fromChainId
: parseInt(request.fromChainId, 10));
}
return depositTransaction;
}
else {
return Promise.reject(this.formatMessage(config_1.RESPONSE_CODES.ALLOWANCE_NOT_GIVEN, `Not enough allowance given to Liquidity Pool Manager contract`));
}
}
});
this.listenForExitTransaction = (transaction, fromChainId) => __awaiter(this, void 0, void 0, function* () {
const onFundsTransfered = this.options.onFundsTransfered;
if (onFundsTransfered) {
const interval = this.options.exitCheckInterval || config_1.config.defaultExitCheckInterval;
yield transaction.wait(1);
this._logMessage(`Deposit transaction Confirmed. Listening for exit transaction now`);
let invocationCount = 0;
const intervalId = setInterval(() => __awaiter(this, void 0, void 0, function* () {
const depositHash = transaction.hash;
const response = yield this.checkDepositStatus({
depositHash,
fromChainId,
});
invocationCount++;
if (response && response.code === config_1.RESPONSE_CODES.SUCCESS) {
if (response.statusCode === config_1.EXIT_STATUS.PROCESSED &&
response.exitHash) {
onFundsTransfered(response);
clearInterval(this.depositTransactionListenerMap.get(depositHash));
this.depositTransactionListenerMap.delete(depositHash);
}
else if (response.exitHash) {
onFundsTransfered(response);
}
}
if (invocationCount >= config_1.config.maxDepositCheckCallbackCount) {
this._logMessage(`Max callback count reached ${config_1.config.maxDepositCheckCallbackCount}. Clearing interval now`);
clearInterval(this.depositTransactionListenerMap.get(depositHash));
this.depositTransactionListenerMap.delete(depositHash);
}
}), interval);
this.depositTransactionListenerMap.set(transaction.hash, intervalId);
}
else {
this._logMessage(`onFundsTransfered method is missing from options so not listening for exit transaction`);
}
});
this.checkDepositStatus = (depositRequest) => {
const self = this;
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
if (depositRequest &&
depositRequest.depositHash &&
depositRequest.fromChainId) {
const fetchOptions = this.getFetchOptions("GET");
const getURL = `${self._getHyphenBaseURL()}${config_1.config.checkTransferStatusPath}?depositHash=${depositRequest.depositHash}&fromChainId=${depositRequest.fromChainId}`;
fetch(getURL, fetchOptions)
.then((response) => response.json())
.then((response) => {
self._logMessage(response);
resolve(response);
})
.catch((error) => {
self._logMessage(error);
reject(error);
});
}
else {
reject(this.formatMessage(config_1.RESPONSE_CODES.BAD_REQUEST, "Bad input params. depositHash and fromChainId are mandatory parameters"));
}
}));
};
this.approveERC20 = (tokenAddress, spender, amount, userAddress, infiniteApproval, useBiconomy) => __awaiter(this, void 0, void 0, function* () {
const provider = this._getProvider(useBiconomy);
const currentNetwork = yield provider.getNetwork();
let approvalAmount = ethers_1.BigNumber.from(amount);
if (currentNetwork) {
const erc20ABI = this._getERC20ABI(currentNetwork.chainId, tokenAddress);
if (!erc20ABI) {
throw new Error(`ERC20 ABI not found for token address ${tokenAddress} on networkId ${currentNetwork.chainId}`);
}
const tokenContract = new ethers_1.ethers.Contract(tokenAddress, erc20ABI, provider);
const tokenContractInterface = new ethers_1.ethers.utils.Interface(JSON.stringify(erc20ABI));
const tokenInfo = config_1.config.tokenAddressMap[tokenAddress.toLowerCase()]
? config_1.config.tokenAddressMap[tokenAddress.toLowerCase()][currentNetwork.chainId]
: undefined;
if (tokenContract) {
if ((infiniteApproval !== undefined && infiniteApproval) ||
(infiniteApproval === undefined && this.options.infiniteApproval)) {
approvalAmount = ethers_1.ethers.constants.MaxUint256;
this._logMessage(`Infinite approval flag is true, so overwriting the amount with value ${amount}`);
}
if (spender && approvalAmount) {
// check if biconomy enable?
if (this.options.biconomy && this.options.biconomy.enable) {
const customMetaTxSupport = config_1.config.customMetaTxnSupportedNetworksForERC20Tokens[currentNetwork.chainId];
if (customMetaTxSupport &&
customMetaTxSupport.indexOf(tokenAddress.toLowerCase()) > -1) {
// Call executeMetaTransaction method
const functionSignature = tokenContractInterface.encodeFunctionData("approve", [
spender,
approvalAmount.toString(),
]);
const tokenData = util_1.getMetaTxnCompatibleTokenData(tokenAddress, currentNetwork.chainId);
const dataToSign = yield util_1.getERC20ApproveDataToSign({
contract: tokenContract,
abi: erc20ABI,
domainType: config_1.config.erc20MetaTxnDomainType,
metaTransactionType: config_1.config.customMetaTxnType,
userAddress,
spender,
amount: approvalAmount.toString(),
name: tokenData.name,
version: tokenData.version,
address: tokenAddress,
salt: "0x" + currentNetwork.chainId.toString(16).padStart(64, "0"),
});
const _provider = this._getProviderWithAccounts(useBiconomy);
if (_provider) {
const signature = yield _provider.send("eth_signTypedData_v4", [
userAddress,
dataToSign,
]);
const { r, s, v } = util_1.getSignatureParameters(signature);
const { data } = yield tokenContract.populateTransaction.executeMetaTransaction(userAddress, functionSignature, r, s, v);
const txParams = {
data,
to: tokenAddress,
from: userAddress,
value: "0x0",
};
return this._sendTransaction(provider, txParams);
}
else {
throw new Error("Couldn't get a provider to get user signature. Make sure you have passed correct provider or walletProvider field in Hyphen constructor.");
}
}
else if (tokenInfo &&
tokenInfo.symbol === "USDC" &&
tokenInfo.permitSupported) {
// If token is USDC call permit method
const deadline = Number(Math.floor(Date.now() / 1000 + 3600));
const usdcDomainData = {
name: tokenInfo.name,
version: tokenInfo.version,
chainId: currentNetwork.chainId,
verifyingContract: tokenAddress,
};
const nonce = yield tokenContract.nonces(userAddress);
const permitDataToSign = {
types: {
EIP712Domain: config_1.config.domainType,
Permit: config_1.config.eip2612PermitType,
},
domain: usdcDomainData,
primaryType: "Permit",
message: {
owner: userAddress,
spender,
value: approvalAmount.toString(),
nonce: parseInt(nonce, 10),
deadline,
},
};
const _provider = this._getProviderWithAccounts(useBiconomy);
if (_provider) {
const signature = yield _provider.send("eth_signTypedData_v4", [
userAddress,
JSON.stringify(permitDataToSign),
]);
const { r, s, v } = util_1.getSignatureParameters(signature);
const { data } = yield tokenContract.populateTransaction.permit(userAddress, spender, approvalAmount.toString(), deadline, v, r, s);
const txParams = {
data,
to: tokenAddress,
from: userAddress,
value: "0x0",
};
return this._sendTransaction(provider, txParams);
}
else {
throw new Error("Couldn't get a provider to get user signature. Make sure you have passed correct provider or walletProvider field in Hyphen constructor.");
}
}
else {
const { data } = yield tokenContract.populateTransaction.approve(spender, approvalAmount.toString());
const txParams = {
data,
to: tokenAddress,
from: userAddress,
value: "0x0",
};
return this._sendTransaction(provider, txParams);
}
}
else {
const { data } = yield tokenContract.populateTransaction.approve(spender, approvalAmount.toString());
const txParams = {
data,
to: tokenAddress,
from: userAddress,
value: "0x0",
};
return this._sendTransaction(provider, txParams);
}
}
else {
this._logMessage(`One of the inputs is not valid => spender: ${spender}, amount: ${amount}`);
}
}
else {
this._logMessage("Token contract is not defined");
throw new Error("Token contract is not defined. Please check if token address is present on the current chain");
}
}
else {
throw new Error("Unable to get current network from provider during approveERC20 method");
}
});
this.triggerManualTransfer = (depositHash, chainId) => {
const self = this;
return new Promise((resolve, reject) => {
if (depositHash && chainId && depositHash !== "" && chainId !== "") {
const fetchOptions = this.getFetchOptions("POST");
const _data = {
fromChainId: chainId,
depositHash,
};
fetchOptions.body = JSON.stringify(_data);
const getURL = `${self._getHyphenBaseURL()}${config_1.config.getManualTransferPath}`;
fetch(getURL, fetchOptions)
.then((response) => response.json())
.then((response) => {
self._logMessage(response);
resolve(response);
})
.catch((error) => {
self._logMessage(error);
reject(error);
});
}
else {
reject(this.formatMessage(config_1.RESPONSE_CODES.BAD_REQUEST, "Bad input params. depositHash and chainId are mandatory parameters"));
}
});
};
this._getERC20ABI = (networkId, tokenAddress) => {
let abi = config_1.config.erc20ABIByToken.get(tokenAddress.toLowerCase());
if (!abi) {
abi = config_1.config.erc20ABIByNetworkId.get(networkId);
}
// tokenAddress to be used in future for any custom token support
return abi;
};
this._depositTokensToLiquidityPoolManager = (request) => __awaiter(this, void 0, void 0, function* () {
try {
const provider = this._getProvider(request.useBiconomy);
const lpManager = new ethers_1.ethers.Contract(request.depositContractAddress, config_1.config.liquidityPoolManagerABI, provider);
let txData;
let value = "0x0";
if (util_2.isNativeAddress(request.tokenAddress)) {
const { data } = yield lpManager.populateTransaction.depositNative(request.receiver, request.toChainId);
txData = data;
value = ethers_1.ethers.utils.hexValue(ethers_1.ethers.BigNumber.from(request.amount));
}
else {
const { data } = yield lpManager.populateTransaction.depositErc20(request.tokenAddress, request.receiver, request.amount, request.toChainId);
txData = data;
}
const txParams = {
data: txData,
to: request.depositContractAddress,
from: request.sender,
value,
};
if (this.options.signatureType) {
txParams.signatureType = this.options.signatureType;
}
return this._sendTransaction(provider, txParams);
}
catch (error) {
this._logMessage(error);
}
});
this._sendTransaction = (_provider, rawTransaction) => __awaiter(this, void 0, void 0, function* () {
if (_provider && rawTransaction) {
const transactionHash = yield _provider.send("eth_sendTransaction", [
rawTransaction,
]);
const response = {
hash: transactionHash,
wait: (confirmations) => {
return _provider.waitForTransaction(transactionHash, confirmations);
},
};
return response;
}
else {
throw new Error("Error while sending transaction. Either provider or rawTransaction is not defined");
}
});
this._getHyphenBaseURL = () => {
const environment = this.options.environment || "prod";
return config_1.config.hyphenBaseUrl[environment];
};
this.formatMessage = (code, message) => {
return {
code,
message,
};
};
this._logMessage = (message) => {
if (this.options && this.options.debug)
console.log(message);
};
this._validate(options);
this.options = options;
if (ethers_1.ethers.providers.Provider.isProvider(provider)) {
this._logMessage(`Ethers provider detected`);
this.provider = provider;
}
else {
this._logMessage(`Non-Ethers provider detected`);
this.provider = new ethers_1.ethers.providers.Web3Provider(provider);
}
if (this.options.biconomy && this.options.biconomy.enable) {
const biconomyOptions = {
apiKey: this.options.biconomy.apiKey,
debug: this.options.biconomy.debug,
};
if (options.walletProvider) {
biconomyOptions.walletProvider = options.walletProvider;
}
this.biconomy = new Biconomy(this.provider, biconomyOptions);
this.biconomyProvider = new ethers_1.ethers.providers.Web3Provider(this.biconomy);
}
if (options.walletProvider) {
if (util_2.isEthersProvider(options.walletProvider)) {
throw new Error("Wallet Provider in options can't be an ethers provider. Please pass the provider you get from your wallet directly.");
}
this.walletProvider = new ethers_1.ethers.providers.Web3Provider(options.walletProvider);
}
this.supportedTokens = new Map();
this.depositTransactionListenerMap = new Map();
}
getPoolInformation(tokenAddress, fromChainId, toChainId) {
return __awaiter(this, void 0, void 0, function* () {
if (!tokenAddress && fromChainId === void 0 && toChainId === void 0) {
throw new Error(this.formatMessage(config_1.RESPONSE_CODES.BAD_REQUEST, "Bad input params. fromChainId, toChainId and tokenAddress are mandatory parameters").message);
}
try {
const response = yield fetch(`${this._getHyphenBaseURL()}${config_1.config.getPoolInfoPath}?tokenAddress=${tokenAddress}&fromChainId=${fromChainId}&toChainId=${toChainId}`, this.getFetchOptions("GET"));
return yield response.json();
}
catch (error) {
this._logMessage(error);
throw error;
}
});
}
}
exports.Hyphen = Hyphen;