UNPKG

biconomy-hyphen

Version:

Hyphen SDK to enable instant and seamless cross-chain deposits & withdrawals

554 lines (553 loc) 29.9 kB
"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;