@verified-network/verified-sdk
Version:
An SDK to develop applications on the Verified Network
654 lines (653 loc) • 31.7 kB
JavaScript
// SPDX-License-Identifier: BUSL-1.1
;
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;