UNPKG

@verified-network/verified-sdk

Version:

An SDK to develop applications on the Verified Network

654 lines (653 loc) 31.7 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 account_1 = require("@biconomy/account"); const constants_1 = require("../utils/constants"); const abstractjs_1 = require("@biconomy/abstractjs"); const chains_1 = require("viem/chains"); const viem_1 = require("viem"); 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) { let isSupported = false; if (constants_1.PaymasterConstants[`${chainId}`] && constants_1.PaymasterConstants[`${chainId}`]["PAYMASTER_API_KEY"] && constants_1.PaymasterConstants[`${chainId}`]["BUNDLER_API_KEY"]) isSupported = true; return isSupported; } /** Creates Biconomy smart account */ async createSmartAccount(chainId) { // Create Biconomy Smart Account instance const signer = this.signer; const smartAccount = await (0, account_1.createSmartAccountClient)({ signer, biconomyPaymasterApiKey: constants_1.PaymasterConstants[`${chainId}`]["PAYMASTER_API_KEY"], bundlerUrl: `${constants_1.PaymasterConstants.BUNDLER_URL_FIRST_SECTION}/${chainId}/${constants_1.PaymasterConstants[`${chainId}`]["BUNDLER_API_KEY"]}`, }); return smartAccount; } async fetchUserOpReceipt(userOpHash) { var _a; try { const chainId = await this.signer.getChainId(); const requestData = { jsonrpc: "2.0", method: "eth_getUserOperationReceipt", id: Date.now(), params: [userOpHash], }; const response = await fetch(`${constants_1.PaymasterConstants.BUNDLER_URL_FIRST_SECTION}/${chainId}/${constants_1.PaymasterConstants[`${chainId}`]["BUNDLER_API_KEY"]}`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(requestData), }); const data = await response.json(); return (_a = data === null || data === void 0 ? void 0 : data.result) === null || _a === void 0 ? void 0 : _a.receipt; } catch (err) { console.error("Error trying to fetch gassless receipt", (err === null || err === void 0 ? void 0 : err.message) || err); return { failed: 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); //console.log('options before', options); if (options == 0) options[0] = {}; //console.log('*********', ...args) //console.log('options after', options); /** * 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 as userop for biconomy gassless(sponsored/erc20 mode) */ async callFunctionAsUserOp(smartAccount, tx, functionName, paymentToken, ...args) { var _a, _b, _c; //send userops transaction and construct transaction response let res = {}; try { const userOpResponse = await smartAccount.sendTransaction(tx, { paymasterServiceData: { mode: "SPONSORED" }, }); const { transactionHash } = await userOpResponse.waitForTxHash(); console.log("Gassless Transaction Hash", transactionHash); const userOpReceipt = await userOpResponse.wait(); if (userOpReceipt.success == "true") { res.status = STATUS.SUCCESS; res.response = { hash: (userOpReceipt === null || userOpReceipt === void 0 ? void 0 : userOpReceipt.transactionHash) || transactionHash, result: userOpReceipt.receipt.result || userOpReceipt.receipt.response || userOpReceipt.receipt, }; //TODO: update result on response res.message = ""; return res; } else { console.log("Gassless failed will try ERC20..."); const ERC20userOpResponse = await smartAccount.sendTransaction(tx, { paymasterServiceData: { mode: "ERC20", preferredToken: paymentToken }, }); const { ERC20transactionHash } = await ERC20userOpResponse.waitForTxHash(); console.log("ERC20 Transaction Hash", ERC20transactionHash); const userOpReceipt = await ERC20userOpResponse.wait(); if (userOpReceipt.success == "true") { res.status = STATUS.SUCCESS; res.response = { hash: (userOpReceipt === null || userOpReceipt === void 0 ? void 0 : userOpReceipt.transactionHash) || ERC20transactionHash, result: ERC20userOpResponse.receipt.result || ERC20userOpResponse.receipt.response || ERC20userOpResponse.receipt, }; //TODO: update result on response res.message = ""; return res; } else { console.error("ERC20 failed"); console.log("will use ethers...."); return await this.callFunctionWithEthers(functionName, ...args); } } } catch (err) { if ((_a = err === null || err === void 0 ? void 0 : err.message) === null || _a === void 0 ? void 0 : _a.includes("Try getting the receipt manually using eth_getUserOperationReceipt rpc method on bundler")) { //extract hash from message due to difference in hash??? const messageArray = (_b = err === null || err === void 0 ? void 0 : err.message) === null || _b === void 0 ? void 0 : _b.split(" "); const txHash = (_c = messageArray === null || messageArray === void 0 ? void 0 : messageArray.find((msg) => msg === null || msg === void 0 ? void 0 : msg.startsWith("0x"))) === null || _c === void 0 ? void 0 : _c.replace(".", ""); //wait up to max round to fetch receipt ??? if (txHash) { // for ( // let i = 0; // i < Number(PaymasterConstants.MAX_WAITING_ROUND); // i++ // ) { // await new Promise((resolve) => { // setTimeout(resolve, 6000); //1 minute delay per round // }); // console.log( // "Gassless timeout exceeded, fetching receipt for round: ", // i + 1, // "out of ", // Number(PaymasterConstants.MAX_WAITING_ROUND) // ); // return await this.fetchUserOpReceipt(txHash).then(async (_res) => { // if (_res && !_res?.failed) { // //if receipt received stop and configure return // res.status = STATUS.SUCCESS; // res.response = { // hash: _res?.transactionHash || txHash, // result: _res, // }; //TODO: update result on response // res.message = ""; // return res; // } else if (_res && _res?.failed) { // //if receipt failed stop and use ethers // console.log("will use ethers...."); // return await this.callFunctionWithEthers(functionName, ...args); // } // }); // } for (let i = 0; i < Number(constants_1.PaymasterConstants.MAX_WAITING_ROUND); i++) { await new Promise((resolve) => setTimeout(resolve, 6000)); // 6 second delay console.log("Gassless timeout exceeded, fetching receipt for round:", i + 1, "out of", Number(constants_1.PaymasterConstants.MAX_WAITING_ROUND)); const _res = await this.fetchUserOpReceipt(txHash); if (_res && !(_res === null || _res === void 0 ? void 0 : _res.failed) && (_res === null || _res === void 0 ? void 0 : _res.status) === "0x1") { res.status = STATUS.SUCCESS; res.response = { hash: (_res === null || _res === void 0 ? void 0 : _res.transactionHash) || txHash, result: _res, }; res.message = ""; break; // Exit the loop } else if (_res && !(_res === null || _res === void 0 ? void 0 : _res.failed) && (_res === null || _res === void 0 ? void 0 : _res.status) !== "0x1") { res.status = STATUS.ERROR; res.response = { hash: (_res === null || _res === void 0 ? void 0 : _res.transactionHash) || txHash, result: _res, }; res.message = (_res === null || _res === void 0 ? void 0 : _res.reason) || (_res === null || _res === void 0 ? void 0 : _res.message) || ""; break; // Exit the loop } else if (_res && (_res === null || _res === void 0 ? void 0 : _res.failed)) { console.log("will use ethers...."); return await this.callFunctionWithEthers(functionName, ...args); } else { //no receipt found?? don't call with ethers transaction may get mined??? res.status = STATUS.ERROR; //create additional status to mark this??? res.response = { hash: txHash, result: [], }; res.message = "Transaction Pending."; } } return res; } else { console.error("gasless transaction failed with error: ", "No TX-hash from error message."); console.log("will use ethers...."); return await this.callFunctionWithEthers(functionName, ...args); } } else { console.error("gasless transaction failed with error: ", (err === null || err === void 0 ? void 0 : err.message) || err); console.log("will use ethers...."); return await this.callFunctionWithEthers(functionName, ...args); } } } /** Constructs and call function using MEE client that allows gas payment in ERC20 tokens */ async callFunctionWithMEEClient(nexusAccount, chainId, tx, paymentToken, _apiKey) { var _a, _b, _c; let res = {}; let txHash = ""; let recp; try { const meeClient = await (0, abstractjs_1.createMeeClient)({ account: nexusAccount, apiKey: _apiKey || constants_1.PaymasterConstants.MEE_API_KEY, }); const transactionInstruction = await nexusAccount.build({ type: "default", data: { chainId, calls: [tx], }, }); const nowInSec = Math.floor(Date.now() / 1000); // Execute the transaction using passed paymentToken const { hash } = await meeClient.execute({ feeToken: { chainId, address: paymentToken, }, instructions: [transactionInstruction], upperBoundTimestamp: nowInSec + 299, //highest is 5 minutes??? }); txHash = hash; console.log(`MEE transaction hash: ${hash}`); // Wait for transaction to complete const receipt = await meeClient.waitForSupertransactionReceipt({ hash }); if (((_a = receipt === null || receipt === void 0 ? void 0 : receipt.receipts) === null || _a === void 0 ? void 0 : _a.length) > 1) { //at least 2 receipts const txReceipt = receipt === null || receipt === void 0 ? void 0 : receipt.receipts[((_b = receipt === null || receipt === void 0 ? void 0 : receipt.receipts) === null || _b === void 0 ? void 0 : _b.length) - 1]; if ((txReceipt === null || txReceipt === void 0 ? void 0 : txReceipt.status) === "success") { res.status = STATUS.SUCCESS; res.response = { hash: txReceipt === null || txReceipt === void 0 ? void 0 : txReceipt.transactionHash, result: txReceipt, }; //TODO: update result on response res.message = ""; } else { res.status = STATUS.ERROR; res.response = { hash: txReceipt === null || txReceipt === void 0 ? void 0 : txReceipt.transactionHash, result: txReceipt, }; //TODO: update result on response res.message = ""; } return res; } else { console.error("MEE client transaction failed with error: ", "Receipts lesser than one."); res.status = STATUS.ERROR; res.response = { hash: (_c = receipt === null || receipt === void 0 ? void 0 : receipt.receipts[0]) === null || _c === void 0 ? void 0 : _c.transactionHash, result: receipt === null || receipt === void 0 ? void 0 : receipt.receipts[0], }; //TODO: update result on response res.message = ""; return res; } } catch (err) { console.error("MEE client transaction failed with error: ", err === null || err === void 0 ? void 0 : err.message); res.status = STATUS.ERROR; res.response = { hash: txHash, result: {}, }; //TODO: update result on response res.message = ""; return res; } } async callContract(functionName, ...args) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; // 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 userop or mee client"); //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; //console.log('options before', options); if (options == 0) options[0] = {}; //create smart account for signer const smartAccount = await this.createSmartAccount(chainId); const account = await smartAccount.getAccountAddress(); // console.log("smart account address: ", account); // const signerAddress = await this.signer.getAddress(); //construct calldata for function let fn = this.contract.populateTransaction[functionName]; let _res = await fn(...args); const tx1 = { to: this.contract.address, data: _res.data, }; if ((_a = optionsRaw[0]) === null || _a === void 0 ? void 0 : _a.paymentToken) { console.log("Using Mee client with paymentToken of: ", (_b = optionsRaw[0]) === null || _b === void 0 ? void 0 : _b.paymentToken); const _signer = this.signer; const chainToUse = [ chains_1.base, chains_1.mainnet, chains_1.gnosis, chains_1.polygon, chains_1.sepolia, chains_1.baseSepolia, ].find((nt) => Number(nt === null || nt === void 0 ? void 0 : nt.id) === Number(chainId)); if (!chainToUse) { throw new Error(`Chaind id: ${chainId} not supported on Verified Sdk. Supported chain ids are: ${(_d = (_c = [ chains_1.base, chains_1.mainnet, chains_1.gnosis, chains_1.polygon, chains_1.sepolia, chains_1.baseSepolia, ]) === null || _c === void 0 ? void 0 : _c.map((nt) => nt === null || nt === void 0 ? void 0 : nt.id)) === null || _d === void 0 ? void 0 : _d.join(", ")}`); } const prov = this.signer.provider; const rpcUrl = (_e = prov === null || prov === void 0 ? void 0 : prov.connection) === null || _e === void 0 ? void 0 : _e.url; const nexusAccount = await (0, abstractjs_1.toMultichainNexusAccount)({ chains: [chainToUse], transports: [ (0, viem_1.http)(rpcUrl || ((_f = optionsRaw[0]) === null || _f === void 0 ? void 0 : _f.rpcUrl) || ((_g = constants_1.PaymasterConstants[Number(chainId)]) === null || _g === void 0 ? void 0 : _g.RPC_URL)), ], signer: _signer, }); const meeAddress = nexusAccount.addressOn(chainId); // console.log("nexus account address: ", meeAddress); return await this.callFunctionWithMEEClient(nexusAccount, chainId, tx1, (_h = optionsRaw[0]) === null || _h === void 0 ? void 0 : _h.paymentToken, (_j = optionsRaw[0]) === null || _j === void 0 ? void 0 : _j.apiKey); } else { console.log("Using Userop since no payment token..."); const paymentToken = ((_k = options[0]) === null || _k === void 0 ? void 0 : _k.paymentToken) || constants_1.PaymasterConstants[`${chainId}`]["PAYMENT_TOKEN"] || ""; return await this.callFunctionAsUserOp(smartAccount, tx1, functionName, paymentToken, ...args); } } else { //call contract through normal ether.js console.log("gassless not supported will use ethers..."); return await this.callFunctionWithEthers(functionName, ...args); } } async getQuote(paymentTokenAddress, functionName, args, rpc, _apiKey) { var _a, _b, _c, _d; const chainId = await this.signer.getChainId(); if (this.supportsGasless(chainId)) { const _signer = this.signer; const chainToUse = [ chains_1.base, chains_1.mainnet, chains_1.gnosis, chains_1.polygon, chains_1.sepolia, chains_1.baseSepolia, ].find((nt) => Number(nt === null || nt === void 0 ? void 0 : nt.id) === Number(chainId)); if (chainToUse) { const prov = this.signer.provider; const rpcUrl = (_a = prov.connection) === null || _a === void 0 ? void 0 : _a.url; const nexusAccount = await (0, abstractjs_1.toMultichainNexusAccount)({ chains: [chainToUse], transports: [ (0, viem_1.http)(rpcUrl || rpc || ((_b = constants_1.PaymasterConstants[Number(chainId)]) === null || _b === void 0 ? void 0 : _b.RPC_URL)), ], signer: _signer, }); if (paymentTokenAddress) { //construct calldata for function try { let fn = this.contract.populateTransaction[functionName]; let _res = await fn(...args); const tx1 = { to: this.contract.address, data: _res.data, }; const meeClient = await (0, abstractjs_1.createMeeClient)({ account: nexusAccount, apiKey: _apiKey || constants_1.PaymasterConstants.MEE_API_KEY, }); const transactionInstruction = await nexusAccount.build({ type: "default", data: { chainId, calls: [tx1], }, }); const tkAddress = paymentTokenAddress; const quote = await meeClient.getQuote({ instructions: [transactionInstruction], feeToken: { address: tkAddress, chainId }, }); // console.log("cont quote: ", quote); return { tokenAddress: paymentTokenAddress, amount: quote === null || quote === void 0 ? void 0 : quote.paymentInfo.tokenAmount, amountInWei: quote === null || quote === void 0 ? void 0 : quote.paymentInfo.tokenWeiAmount, amouuntValue: quote === null || quote === void 0 ? void 0 : quote.paymentInfo.tokenValue, chainId, functionName, }; } catch (err) { if ((_c = err === null || err === void 0 ? void 0 : err.message) === null || _c === void 0 ? void 0 : _c.includes("fn is not a function")) { console.error(`Function ${functionName} not found in contract's ABI`); } else if ((_d = err === null || err === void 0 ? void 0 : err.message) === null || _d === void 0 ? void 0 : _d.includes("code=INVALID_ARGUMENT")) { console.error(`Invalid arguments type`); } console.error((err === null || err === void 0 ? void 0 : 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;