UNPKG

@verified-network/verified-sdk

Version:

An SDK to develop applications on the Verified Network

478 lines (477 loc) 19.3 kB
// SPDX-License-Identifier: BUSL-1.1 "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.VerifiedContract = exports.DATATYPES = void 0; const ethers_1 = require("ethers"); const constants_1 = require("../utils/constants"); const chains_1 = require("viem/chains"); var STATUS; (function (STATUS) { STATUS[STATUS["SUCCESS"] = 0] = "SUCCESS"; STATUS[STATUS["ERROR"] = 1] = "ERROR"; })(STATUS || (STATUS = {})); var DATATYPES; (function (DATATYPES) { DATATYPES["NUMBER"] = "number"; DATATYPES["STRING"] = "string"; DATATYPES["ADDRESS"] = "address"; DATATYPES["BOOLEAN"] = "boolean"; DATATYPES["BYTE32"] = "byte32"; DATATYPES["BYTE16"] = "byte16"; DATATYPES["BIGNUMBER"] = "bignumber"; })(DATATYPES || (exports.DATATYPES = DATATYPES = {})); class VerifiedContract { constructor(address, abi, signer) { this.signer = signer; this.abiInterface = new ethers_1.utils.Interface(abi); this.contract = new ethers_1.ethers.Contract(address, this.abiInterface, signer); } async validateInput(type, data) { let error = ""; let status = true; switch (type) { case DATATYPES.ADDRESS: if (ethers_1.utils.isAddress(data)) error = "Invalid address value"; else status = false; break; case DATATYPES.NUMBER: if (data !== Number(data)) error = "Invalid numerical value"; else status = false; break; case DATATYPES.BOOLEAN: // const arr = [true, false, "true", "false"] if (typeof data === "boolean") error = "Invalid boolean value"; else status = false; break; case DATATYPES.STRING: if (typeof data === "string" || data instanceof String || Object.prototype.toString.call(data) === "[object String]") error = "Invalid string value"; else status = false; break; } if (!status) throw TypeError(error); return status; } sanitiseInput(type, data) { try { switch (type) { case DATATYPES.BYTE32: /** * Returns a bytes32 string representation of text. * If the length of text exceeds 31 bytes, it will throw an error. * @params (text) * @returns ⇒ string */ return ethers_1.utils.formatBytes32String(data); case DATATYPES.BYTE16: /** * Returns a bytes16 string representation of text. * If the length of text exceeds 31 bytes, it will throw an error. * @params (text) * @returns ⇒ string */ return ethers_1.utils.formatBytes32String(data).slice(16); case DATATYPES.NUMBER: /** * Returns a BigNumber representation of value, parsed with unit digits * (if it is a number) or from the unit specified (if a string). * @param ( value [ , unit = "ether" ] ) * @returns ⇒ BigNumber */ return ethers_1.utils.parseUnits(data); case DATATYPES.BOOLEAN: const arr = [true, false, "true", "false", "TRUE", "FALSE"]; return arr.indexOf(data) !== -1 ? true : new Error("Invalid Boolean value"); default: return data; } } catch (error) { console.error(error); } } sanitiseOutput(type, data) { switch (type) { case DATATYPES.BYTE32: const len = data.length; let finalData = data; if (len == 34) finalData = `${data}00000000000000000000000000000000`; /** * Returns the decoded string represented by the Bytes32 encoded data. * @params (aBytesLike) * @returns string */ return ethers_1.utils.parseBytes32String(finalData); case DATATYPES.NUMBER: /** * Returns a string representation of value formatted with unit * digits (if it is a number) or to the unit specified (if a string). * @params ( value [ , unit = "ether" ] ) * @returns ⇒ string */ return ethers_1.utils.formatUnits(data); case DATATYPES.BIGNUMBER: return data.toString(); case DATATYPES.STRING: return ethers_1.utils.toUtf8String(data); default: return data; } } /** * gets a function state mutability to differenciate between read and write functions * @param functionName * @returns true or false */ isReadFunction(functionName) { const functionFragment = this.abiInterface.getFunction(functionName); if (!functionFragment) { throw new Error(`Function ${functionName} not found in ABI`); } return (functionFragment.stateMutability === "view" || functionFragment.stateMutability === "pure"); } /** * Parses output to standard response * @param data * @returns */ tempOutput(data) { const response = { hash: "", result: [], }; data.forEach(async (element) => { if (element.hash !== undefined || element.transactionHash) { return (response.hash = element.hash || element.transactionHash); } else if (element._isBigNumber) { return response.result.push(element.toString()); } else if (ethers_1.utils.isAddress(element)) { return response.result.push(element); } //if (utils.isBytesLike(element)) return response.result.push(this.sanitiseOutput(DATATYPES.BYTE32, element)) else if (ethers_1.utils.isBytesLike(element)) { return response.result.push(element); } else if (typeof element === "boolean") { return response.result.push(element); } else { return response.result.push(element); } }); return response; } /** Converts any datatype to array */ convertToArray(data) { if (Array.isArray(data)) return data; else return [data]; } /** Checks if a contract support gasless transaction */ supportsGasless(chainId) { return true; } /** Constructs and call function with ethers.js */ async callFunctionWithEthers(functionName, ...args) { let res = {}; try { let options = []; const totalArguments = args.length; if (totalArguments > 1) options = args.splice(-1); if (options == 0) options[0] = {}; /** * Actual Function call using Ethers.js */ let fn = this.contract[functionName]; let _res = await fn(...args, ...options); let _resp = _res.wait !== undefined ? await _res.wait(_res) : _res; res.response = this.tempOutput(this.convertToArray(ethers_1.utils.deepCopy(_resp))); res.status = STATUS.SUCCESS; res.message = ""; return res; } catch (error) { console.error(error); res.status = STATUS.ERROR; res.reason = error.reason; res.message = error.message; res.code = error.code; return res; } } /** Constructs and call function using Alchemy client that allows gassless sponsorship and payment in ERC20 tokens */ async callFunctionWithAlchemyClient(chainId, tx, functionName, paymentToken, pk, isSponsor, ...args) { let res = {}; let txHash = ""; try { const sponsorUrl = constants_1.PaymasterConstants.HOSTED_SPONSOR_URL; let quote; // Execute the transaction using passed paymentToken or sponsored gasless if (isSponsor) { const response = await fetch(`${sponsorUrl}/sponsorship/sign/${chainId?.toString()}`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ isSponsored: true, pk, tx, }), }); if (!response.ok) { quote = null; } else { quote = await response.json(); } } else { const response = await fetch(`${sponsorUrl}/sponsorship/sign/${chainId?.toString()}`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ isSponsored: false, paymentToken, pk, tx, }), }); if (!response.ok) { quote = null; } else { quote = await response.json(); } } if (quote && quote?.receipts?.length > 0) { //always pick last receipt???? const txReceipt = quote?.receipts[quote?.receipts?.length - 1]; if (txReceipt?.status === "success") { res.status = STATUS.SUCCESS; res.response = { hash: txReceipt?.transactionHash, result: txReceipt, }; //TODO: update result on response res.message = ""; } else { if (isSponsor) { console.log("Will use ethers..."); return await this.callFunctionWithEthers(functionName, ...args); } else { res.status = STATUS.ERROR; res.response = { hash: txReceipt?.transactionHash, result: txReceipt, }; //TODO: update result on response res.message = ""; } } return res; } else { console.error("Gassless/ERC20 transaction failed with error: ", "Invalid receipts length"); if (isSponsor) { console.log("Will use ethers..."); return await this.callFunctionWithEthers(functionName, ...args); } else { res.status = STATUS.ERROR; res.response = { hash: "", result: {}, }; //TODO: update result on response res.message = ""; return res; } } } catch (err) { console.error("Gassless/ERC20 payment transaction failed with error: ", err?.message); if (isSponsor) { //for sponsored transaction. Fallback to ethers console.log("Will use ethers..."); return await this.callFunctionWithEthers(functionName, ...args); } else { res.status = STATUS.ERROR; res.response = { hash: txHash, result: {}, }; //TODO: update result on response res.message = ""; return res; } } } async callContract(functionName, ...args) { // Check if the function is a read function if (this.isReadFunction(functionName)) { console.log("read function will use ethers..."); return await this.callFunctionWithEthers(functionName, ...args); } const chainId = await this.signer.getChainId(); if (this.supportsGasless(chainId)) { console.log("gassless supported will use gassless sponsorship or erc20 payment"); //call contract through userop for gasless transaction let options = []; const totalArguments = args.length; const optionsRaw = args.splice(-1); //reduce args to exclude options if (totalArguments > 1) options = optionsRaw; if (options == 0) options[0] = {}; let fn = this.contract.populateTransaction[functionName]; let _res = await fn(...args); const tx1 = { to: this.contract.address, data: _res.data, }; const chainToUse = [ chains_1.base, chains_1.mainnet, chains_1.gnosis, chains_1.polygon, chains_1.sepolia, chains_1.baseSepolia, chains_1.arbitrum, chains_1.arbitrumSepolia, ].find((nt) => Number(nt?.id) === Number(chainId)); if (!chainToUse) { throw new Error(`Chaind id: ${chainId} not supported on Verified Sdk. Supported chain ids are: ${[ chains_1.base, chains_1.mainnet, chains_1.gnosis, chains_1.polygon, chains_1.sepolia, chains_1.baseSepolia, chains_1.arbitrum, chains_1.arbitrumSepolia, ] ?.map((nt) => nt?.id) ?.join(", ")}`); } const signerAny = this.signer; const signerPk = signerAny?._signingKey?.()?.privateKey; if (!signerPk) { //no pk on signer. Assume it's web wallets and use ethers console.log("Signer incomplete, will use ethers"); return await this.callFunctionWithEthers(functionName, ...args); } else if (signerPk) { //pk exists signer. try gassless/erc20 first then ethers if they failed if (optionsRaw[0]?.paymentToken) { console.log("Using ERC20 payment with paymentToken of: ", optionsRaw[0]?.paymentToken); return await this.callFunctionWithAlchemyClient(chainId, tx1, functionName, optionsRaw[0]?.paymentToken, signerPk, undefined, ...args); } else { console.log("Using gassless sponsorship since no payment token"); return await this.callFunctionWithAlchemyClient(chainId, tx1, functionName, "0x", signerPk, true, ...args); } } } else { //call contract through normal ether.js console.log("Gassless not supported for this chain will use ethers"); return await this.callFunctionWithEthers(functionName, ...args); } } async getQuote(paymentTokenAddress, functionName, args, rpc, _apiKey, isReactNative) { const chainId = await this.signer.getChainId(); if (this.supportsGasless(chainId)) { const chainToUse = [ chains_1.base, chains_1.mainnet, chains_1.gnosis, chains_1.polygon, chains_1.sepolia, chains_1.baseSepolia, chains_1.arbitrum, chains_1.arbitrumSepolia, ].find((nt) => Number(nt?.id) === Number(chainId)); const signerAny = this.signer; const signerPk = signerAny?._signingKey?.()?.privateKey; if (chainToUse && paymentTokenAddress && signerPk) { try { let fn = this.contract.populateTransaction[functionName]; let _res = await fn(...args); const tx1 = { to: this.contract.address, data: _res.data, }; const sponsorUrl = constants_1.PaymasterConstants.HOSTED_SPONSOR_URL; const response = await fetch(`${sponsorUrl}/sponsorship/fee/${chainId?.toString()}?paymentToken=${paymentTokenAddress}&pk=${signerPk}&tx=${JSON.stringify(tx1)}`, { method: "GET", headers: { "Content-Type": "application/json", }, }); if (response.ok) { const feeRes = (await response.json()) || { maxAmount: "0" }; return { tokenAddress: paymentTokenAddress, amount: feeRes?.maxAmount, amountInWei: feeRes?.maxAmount, amountValue: feeRes?.maxAmount, chainId, functionName, }; } } catch (err) { if (err?.message?.includes("fn is not a function") || err?.message?.includes("fnTransfer is not a function")) { console.error(`Function ${functionName} not found in contract's ABI`); } else if (err?.message?.includes("code=INVALID_ARGUMENT")) { console.error(`Invalid arguments type`); } console.error(err?.message || "getQuote failed."); } } } return { tokenAddress: "", amount: "0", amountInWei: "0", amouuntValue: "0", chainId, functionName, }; } getEvent(eventName, callback) { let res = {}; this.contract.once(eventName, (...data) => { const dataToBeFormatted = data.splice(0, data.length - 1); res.response = this.tempOutput(ethers_1.utils.deepCopy(dataToBeFormatted)); res.status = STATUS.SUCCESS; res.message = ""; callback(res); }); } } exports.VerifiedContract = VerifiedContract;