From b1e0fa17af2e82bb62bf1767425732862259a0a5 Mon Sep 17 00:00:00 2001 From: Vignesh Date: Mon, 30 Sep 2024 15:42:23 +0530 Subject: [PATCH 1/4] added pm_getERC20TokenQuotes method --- backend/src/abi/ERC20Abi.ts | 222 ++++++++++++++++++++++ backend/src/constants/ErrorMessage.ts | 1 + backend/src/paymaster/index.ts | 95 +++++++++- backend/src/routes/paymaster-routes.ts | 253 +++++++++++++------------ 4 files changed, 446 insertions(+), 125 deletions(-) create mode 100644 backend/src/abi/ERC20Abi.ts diff --git a/backend/src/abi/ERC20Abi.ts b/backend/src/abi/ERC20Abi.ts new file mode 100644 index 0000000..160ca9c --- /dev/null +++ b/backend/src/abi/ERC20Abi.ts @@ -0,0 +1,222 @@ +export default [ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_spender", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "balance", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + }, + { + "name": "_spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "payable": true, + "stateMutability": "payable", + "type": "fallback" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "from", + "type": "address" + }, + { + "indexed": true, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + } +] \ No newline at end of file diff --git a/backend/src/constants/ErrorMessage.ts b/backend/src/constants/ErrorMessage.ts index c988dcf..370e723 100644 --- a/backend/src/constants/ErrorMessage.ts +++ b/backend/src/constants/ErrorMessage.ts @@ -1,4 +1,5 @@ export default { + CONTEXT_NOT_ARRAY: 'Context sent is not an array', INVALID_DATA: 'Invalid data provided', INVALID_SPONSORSHIP_POLICY: 'Invalid sponsorship policy data', INVALID_SPONSORSHIP_POLICY_ID: 'Invalid sponsorship policy id', diff --git a/backend/src/paymaster/index.ts b/backend/src/paymaster/index.ts index 00fbb15..49ce61d 100644 --- a/backend/src/paymaster/index.ts +++ b/backend/src/paymaster/index.ts @@ -12,6 +12,7 @@ import MultiTokenPaymasterAbi from '../abi/MultiTokenPaymasterAbi.js'; import OrochiOracleAbi from '../abi/OrochiOracleAbi.js'; import ChainlinkOracleAbi from '../abi/ChainlinkOracleAbi.js'; import ERC20PaymasterV07Abi from '../abi/ERC20PaymasterV07Abi.js'; +import ERC20Abi from '../abi/ERC20Abi.js'; export class Paymaster { feeMarkUp: BigNumber; @@ -27,11 +28,11 @@ export class Paymaster { this.EP7_TOKEN_VGL = ep7TokenVGL; } - packUint (high128: BigNumberish, low128: BigNumberish): string { + packUint(high128: BigNumberish, low128: BigNumberish): string { return hexZeroPad(BigNumber.from(high128).shl(128).add(low128).toHexString(), 32) } - packPaymasterData (paymaster: string, paymasterVerificationGasLimit: BigNumberish, postOpGasLimit: BigNumberish, paymasterData?: BytesLike): BytesLike { + packPaymasterData(paymaster: string, paymasterVerificationGasLimit: BigNumberish, postOpGasLimit: BigNumberish, paymasterData?: BytesLike): BytesLike { return ethers.utils.hexConcat([ paymaster, this.packUint(paymasterVerificationGasLimit, postOpGasLimit), @@ -60,7 +61,7 @@ export class Paymaster { return paymasterData; } - async signV07(userOp: any, validUntil: string, validAfter: string, entryPoint: string, paymasterAddress: string, + async signV07(userOp: any, validUntil: string, validAfter: string, entryPoint: string, paymasterAddress: string, bundlerRpc: string, signer: Wallet, estimate: boolean, log?: FastifyBaseLogger) { try { const provider = new providers.JsonRpcProvider(bundlerRpc); @@ -138,7 +139,7 @@ export class Paymaster { return paymasterAndData; } - async signV06(userOp: any, validUntil: string, validAfter: string, entryPoint: string, paymasterAddress: string, + async signV06(userOp: any, validUntil: string, validAfter: string, entryPoint: string, paymasterAddress: string, bundlerRpc: string, signer: Wallet, estimate: boolean, log?: FastifyBaseLogger) { try { const provider = new providers.JsonRpcProvider(bundlerRpc); @@ -146,7 +147,7 @@ export class Paymaster { userOp.paymasterAndData = await this.getPaymasterAndData(userOp, validUntil, validAfter, paymasterContract, signer); if (!userOp.signature) userOp.signature = '0x'; if (estimate) { - const response = await provider.send('eth_estimateUserOperationGas', [userOp, entryPoint]); + const response = await provider.send('eth_estimateUserOperationGas', [userOp, entryPoint]); userOp.verificationGasLimit = response.verificationGasLimit; userOp.preVerificationGas = response.preVerificationGas; userOp.callGasLimit = response.callGasLimit; @@ -175,7 +176,7 @@ export class Paymaster { } } - async getPaymasterAndDataForMultiTokenPaymaster(userOp: any, validUntil: string, validAfter: string, feeToken: string, + async getPaymasterAndDataForMultiTokenPaymaster(userOp: any, validUntil: string, validAfter: string, feeToken: string, ethPrice: string, paymasterContract: Contract, signer: Wallet) { const exchangeRate = 1000000; // This is for setting min tokens required for the txn that gets validated on estimate const rate = ethers.BigNumber.from(exchangeRate).mul(ethPrice); @@ -208,6 +209,82 @@ export class Paymaster { return paymasterAndData; } + async getQuotesMultiToken(userOp: any, entryPoint: string, chainId: number, multiTokenPaymasters: any, tokens_list: string[], oracles: any, bundlerRpc: string, oracleName: string, log?: FastifyBaseLogger) { + try { + const provider = new providers.JsonRpcProvider(bundlerRpc); + const quotes = [], unsupportedTokens = []; + const result = { + "postOpGas": "0x", + "etherUSDExchangeRate": "0x", + "paymasterAddress": "0x", + "gasEstimates": { + "preVerificationGas": "0x", + "verificationGasLimit": "0x", + "callGasLimit": "0x" + }, + "feeEstimates": { + "maxFeePerGas": "0x", + "maxPriorityFeePerGas": "0x" + }, + "quotes": [{}], + "unsupportedTokens": [{}] + } + const response = await provider.send('eth_estimateUserOperationGas', [userOp, entryPoint]); + result.gasEstimates.preVerificationGas = response.preVerificationGas; + result.gasEstimates.callGasLimit = response.callGasLimit; + result.gasEstimates.verificationGasLimit = response.verificationGasLimit; + result.feeEstimates.maxFeePerGas = response.maxFeePerGas; + result.feeEstimates.maxPriorityFeePerGas = response.maxPriorityFeePerGas; + + const paymasterAddress = multiTokenPaymasters[chainId][tokens_list[0]]; + result.paymasterAddress = paymasterAddress; + const paymasterContract = new ethers.Contract(paymasterAddress, MultiTokenPaymasterAbi, provider); + result.postOpGas = await paymasterContract.UNACCOUNTED_COST; + + for (let i = 0; i < tokens_list.length; i++) { + const gasToken = tokens_list[i]; + if (!(multiTokenPaymasters[chainId] && multiTokenPaymasters[chainId][gasToken]) && + !(oracles[chainId] && oracles[chainId][gasToken])) + unsupportedTokens.push({ token: gasToken }) + else { + const oracleAddress = oracles[chainId][gasToken]; + let ethPrice = ""; + if (oracleName === "orochi") { + const oracleContract = new ethers.Contract(oracleAddress, OrochiOracleAbi, provider); + const result = await oracleContract.getLatestData(1, ethers.utils.hexlify(ethers.utils.toUtf8Bytes('ETH')).padEnd(42, '0')) + ethPrice = Number(ethers.utils.formatEther(result)).toFixed(0); + } else { + const chainlinkContract = new ethers.Contract(oracleAddress, ChainlinkOracleAbi, provider); + const decimals = await chainlinkContract.decimals(); + const result = await chainlinkContract.latestAnswer(); + ethPrice = Number(ethers.utils.formatUnits(result, decimals)).toFixed(0); + } + result.etherUSDExchangeRate = BigNumber.from(ethPrice).toHexString(); + const exchangeRate = 1000000; // This is for setting min tokens required for the txn that gets validated on estimate + const rate = ethers.BigNumber.from(exchangeRate).mul(ethPrice); + const tokenContract = new ethers.Contract(gasToken, ERC20Abi, provider) + const decimals = await tokenContract.decimals(); + const symbol = await tokenContract.symbol(); + quotes.push({ + token: gasToken, + symbol: symbol, + decimals: decimals, + etherTokenExchangeRate: rate.toHexString(), + serviceFeePercent: this.multiTokenMarkUp - 1000000 + }) + } + } + result.quotes = quotes; + result.unsupportedTokens = unsupportedTokens; + return result; + } catch (err: any) { + if (err.message.includes("Quota exceeded")) + throw new Error('Failed to process request to bundler since request Quota exceeded for the current apiKey') + if (log) log.error(err, 'getQuotesMultiToken'); + throw new Error('Failed to process request to bundler. Please contact support team RawErrorMsg:' + err.message) + } + } + async signMultiTokenPaymaster(userOp: any, validUntil: string, validAfter: string, entryPoint: string, paymasterAddress: string, feeToken: string, oracleAggregator: string, bundlerRpc: string, signer: Wallet, oracleName: string, log?: FastifyBaseLogger) { try { @@ -274,7 +351,7 @@ export class Paymaster { const tokenContract = new Contract(await erc20Paymaster.tokenAddress, minABI, provider) const tokenBalance = await tokenContract.balanceOf(userOp.sender); - if (tokenAmountRequired.gte(tokenBalance)) + if (tokenAmountRequired.gte(tokenBalance)) throw new Error(`The required token amount ${tokenAmountRequired.toString()} is more than what the sender has ${tokenBalance}`) let paymasterAndData = await erc20Paymaster.generatePaymasterAndDataForTokenAmount(userOp, tokenAmountRequired) @@ -333,7 +410,7 @@ export class Paymaster { const tokenContract = new Contract(tokenAddress, minABI, provider) const tokenBalance = await tokenContract.balanceOf(userOp.sender); - if (tokenAmountRequired.gte(tokenBalance)) + if (tokenAmountRequired.gte(tokenBalance)) throw new Error(`The required token amount ${tokenAmountRequired.toString()} is more than what the sender has ${tokenBalance}`) if (estimate) { userOp.paymaster = paymasterAddress; @@ -510,7 +587,7 @@ export class Paymaster { if (amountInWei.gte(balance)) throw new Error(`${signer.address} Balance is less than the amount to be deposited`) - const encodedData = paymasterContract.interface.encodeFunctionData(isEpv06 ? 'depositFunds': 'deposit', []); + const encodedData = paymasterContract.interface.encodeFunctionData(isEpv06 ? 'depositFunds' : 'deposit', []); const etherscanFeeData = await getEtherscanFee(chainId); let feeData; diff --git a/backend/src/routes/paymaster-routes.ts b/backend/src/routes/paymaster-routes.ts index 6a7f2df..c06774b 100644 --- a/backend/src/routes/paymaster-routes.ts +++ b/backend/src/routes/paymaster-routes.ts @@ -46,8 +46,9 @@ const paymasterRoutes: FastifyPluginAsync = async (server) => { let chainId = query['chainId'] ?? body.params[3]; const api_key = query['apiKey'] ?? body.params[4]; let epVersion: EPVersions = DEFAULT_EP_VERSION; + let tokens_list: string[] = []; + let sponsorDetails = false, estimate = true, tokenQuotes = false; - let sponsorDetails = false, estimate = true; if (body.method) { switch (body.method) { case 'pm_getPaymasterData': { @@ -65,12 +66,20 @@ const paymasterRoutes: FastifyPluginAsync = async (server) => { case 'pm_sponsorUserOperation': { break; } + case 'pm_getERC20TokenQuotes': { + if (!Array.isArray(context)) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.CONTEXT_NOT_ARRAY }); + const validAddresses = context.every((ele: any) => ethers.utils.isAddress(ele.token)); + if (!validAddresses) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_ADDRESS_PASSSED }) + tokens_list = context.map((ele: any) => ethers.utils.getAddress(ele.token)); + tokenQuotes = true; + break; + } default: { return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_METHOD }); } } } - if (!api_key || typeof(api_key) !== "string") + if (!api_key || typeof (api_key) !== "string") return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if (!SUPPORTED_ENTRYPOINTS.EPV_06?.includes(entryPoint) && !SUPPORTED_ENTRYPOINTS.EPV_07?.includes(entryPoint)) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_ENTRYPOINT }) @@ -97,7 +106,7 @@ const paymasterRoutes: FastifyPluginAsync = async (server) => { server.log.error("APIKey not configured in database") return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) } - + if (!unsafeMode) { const AWSresponse = await client.send( new GetSecretValueCommand({ @@ -193,133 +202,143 @@ const paymasterRoutes: FastifyPluginAsync = async (server) => { return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); } - if (gasToken && ethers.utils.isAddress(gasToken)) gasToken = ethers.utils.getAddress(gasToken) - - if (mode.toLowerCase() == 'multitoken' && - !(multiTokenPaymasters[chainId] && multiTokenPaymasters[chainId][gasToken]) && - !(multiTokenOracles[chainId] && multiTokenOracles[chainId][gasToken]) - ) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK_TOKEN }) - const networkConfig = getNetworkConfig(chainId, supportedNetworks ?? '', [entryPoint]); if (!networkConfig) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); let bundlerUrl = networkConfig.bundler; if (networkConfig.bundler.includes('etherspot.io')) bundlerUrl = `${networkConfig.bundler}?api-key=${bundlerApiKey}`; server.log.warn(networkConfig, `Network Config fetched for ${api_key}: `); - let result: any; - switch (mode.toLowerCase()) { - case 'sponsor': { - const date = new Date(); - const provider = new providers.JsonRpcProvider(bundlerUrl); - const signer = new Wallet(privateKey, provider) - - // get chainid from provider - const chainId = await provider.getNetwork(); - - // get wallet_address from api_key - const apiKeyData = await server.apiKeyRepository.findOneByApiKey(api_key); - if (!apiKeyData) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.API_KEY_NOT_CONFIGURED_IN_DATABASE }); - - // get sponsorshipPolicy for the user from walletAddress and entrypoint version - const sponsorshipPolicy: SponsorshipPolicy | null = await server.sponsorshipPolicyRepository.findOneByWalletAddressAndSupportedEPVersion(apiKeyData?.walletAddress, getEPVersion(epVersion)); - if (!sponsorshipPolicy) { - const errorMessage: string = generateErrorMessage(ErrorMessage.ACTIVE_SPONSORSHIP_POLICY_NOT_FOUND, { walletAddress: apiKeyData?.walletAddress, epVersion: epVersion, chainId: chainId.chainId }); - return reply.code(ReturnCode.FAILURE).send({ error: errorMessage }); - } - if (!Object.assign(new SponsorshipPolicy(), sponsorshipPolicy).isApplicable) { - const errorMessage: string = generateErrorMessage(ErrorMessage.NO_ACTIVE_SPONSORSHIP_POLICY_FOR_CURRENT_TIME, { walletAddress: apiKeyData?.walletAddress, epVersion: epVersion, chainId: chainId.chainId }); - return reply.code(ReturnCode.FAILURE).send({ error: errorMessage }); - } + if (tokenQuotes) { + if (epVersion !== EPVersions.EPV_06) + throw new Error('Currently only EPV06 entryPoint address is supported') + if (!networkConfig.MultiTokenPaymasterOracleUsed || + !(networkConfig.MultiTokenPaymasterOracleUsed == "orochi" || networkConfig.MultiTokenPaymasterOracleUsed == "chainlink")) + throw new Error("Oracle is not Defined/Invalid"); + result = await paymaster.getQuotesMultiToken(userOp, entryPoint, chainId, multiTokenPaymasters, tokens_list, multiTokenOracles, bundlerUrl, networkConfig.MultiTokenPaymasterOracleUsed, server.log); + } + else { + if (gasToken && ethers.utils.isAddress(gasToken)) gasToken = ethers.utils.getAddress(gasToken) + + if (mode.toLowerCase() == 'multitoken' && + !(multiTokenPaymasters[chainId] && multiTokenPaymasters[chainId][gasToken]) && + !(multiTokenOracles[chainId] && multiTokenOracles[chainId][gasToken]) + ) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK_TOKEN }) + + switch (mode.toLowerCase()) { + case 'sponsor': { + const date = new Date(); + const provider = new providers.JsonRpcProvider(bundlerUrl); + const signer = new Wallet(privateKey, provider) + + // get chainid from provider + const chainId = await provider.getNetwork(); + + // get wallet_address from api_key + const apiKeyData = await server.apiKeyRepository.findOneByApiKey(api_key); + if (!apiKeyData) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.API_KEY_NOT_CONFIGURED_IN_DATABASE }); + + // get sponsorshipPolicy for the user from walletAddress and entrypoint version + const sponsorshipPolicy: SponsorshipPolicy | null = await server.sponsorshipPolicyRepository.findOneByWalletAddressAndSupportedEPVersion(apiKeyData?.walletAddress, getEPVersion(epVersion)); + if (!sponsorshipPolicy) { + const errorMessage: string = generateErrorMessage(ErrorMessage.ACTIVE_SPONSORSHIP_POLICY_NOT_FOUND, { walletAddress: apiKeyData?.walletAddress, epVersion: epVersion, chainId: chainId.chainId }); + return reply.code(ReturnCode.FAILURE).send({ error: errorMessage }); + } - // get supported networks from sponsorshipPolicy - const supportedNetworks: number[] | undefined | null = sponsorshipPolicy.enabledChains; - if (!supportedNetworks || !supportedNetworks.includes(chainId.chainId)) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + if (!Object.assign(new SponsorshipPolicy(), sponsorshipPolicy).isApplicable) { + const errorMessage: string = generateErrorMessage(ErrorMessage.NO_ACTIVE_SPONSORSHIP_POLICY_FOR_CURRENT_TIME, { walletAddress: apiKeyData?.walletAddress, epVersion: epVersion, chainId: chainId.chainId }); + return reply.code(ReturnCode.FAILURE).send({ error: errorMessage }); + } - if (txnMode) { - const signerAddress = await signer.getAddress(); - const IndexerData = await getIndexerData(signerAddress, userOp.sender, date.getMonth(), date.getFullYear(), noOfTxns, indexerEndpoint); - if (IndexerData.length >= noOfTxns) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.QUOTA_EXCEEDED }) - } - const validUntil = context?.validUntil ? new Date(context.validUntil) : date; - const validAfter = context?.validAfter ? new Date(context.validAfter) : date; - const hex = (Number((validUntil.valueOf() / 1000).toFixed(0)) + 600).toString(16); - const hex1 = (Number((validAfter.valueOf() / 1000).toFixed(0)) - 60).toString(16); - let str = '0x' - let str1 = '0x' - for (let i = 0; i < 14 - hex.length; i++) { - str += '0'; - } - for (let i = 0; i < 14 - hex1.length; i++) { - str1 += '0'; - } - str += hex; - str1 += hex1; - if (contractWhitelistMode) { - const contractWhitelistResult = await checkContractWhitelist(userOp.callData, chainId.chainId, signer.address); - if (!contractWhitelistResult) throw new Error('Contract Method not whitelisted'); - } - if (epVersion === EPVersions.EPV_06) - result = await paymaster.signV06(userOp, str, str1, entryPoint, networkConfig.contracts.etherspotPaymasterAddress, bundlerUrl, signer, estimate, server.log); - else { - const globalWhitelistRecord = await server.whitelistRepository.findOneByApiKeyAndPolicyId(api_key); - if (!globalWhitelistRecord?.addresses.includes(userOp.sender)) { - const existingWhitelistRecord = await server.whitelistRepository.findOneByApiKeyAndPolicyId(api_key, sponsorshipPolicy.id); - if (!existingWhitelistRecord?.addresses.includes(userOp.sender)) throw new Error('This sender address has not been whitelisted yet'); + // get supported networks from sponsorshipPolicy + const supportedNetworks: number[] | undefined | null = sponsorshipPolicy.enabledChains; + if (!supportedNetworks || !supportedNetworks.includes(chainId.chainId)) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + + if (txnMode) { + const signerAddress = await signer.getAddress(); + const IndexerData = await getIndexerData(signerAddress, userOp.sender, date.getMonth(), date.getFullYear(), noOfTxns, indexerEndpoint); + if (IndexerData.length >= noOfTxns) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.QUOTA_EXCEEDED }) + } + const validUntil = context?.validUntil ? new Date(context.validUntil) : date; + const validAfter = context?.validAfter ? new Date(context.validAfter) : date; + const hex = (Number((validUntil.valueOf() / 1000).toFixed(0)) + 600).toString(16); + const hex1 = (Number((validAfter.valueOf() / 1000).toFixed(0)) - 60).toString(16); + let str = '0x' + let str1 = '0x' + for (let i = 0; i < 14 - hex.length; i++) { + str += '0'; + } + for (let i = 0; i < 14 - hex1.length; i++) { + str1 += '0'; + } + str += hex; + str1 += hex1; + if (contractWhitelistMode) { + const contractWhitelistResult = await checkContractWhitelist(userOp.callData, chainId.chainId, signer.address); + if (!contractWhitelistResult) throw new Error('Contract Method not whitelisted'); } - result = await paymaster.signV07(userOp, str, str1, entryPoint, networkConfig.contracts.etherspotPaymasterAddress, bundlerUrl, signer, estimate, server.log); + if (epVersion === EPVersions.EPV_06) + result = await paymaster.signV06(userOp, str, str1, entryPoint, networkConfig.contracts.etherspotPaymasterAddress, bundlerUrl, signer, estimate, server.log); + else { + const globalWhitelistRecord = await server.whitelistRepository.findOneByApiKeyAndPolicyId(api_key); + if (!globalWhitelistRecord?.addresses.includes(userOp.sender)) { + const existingWhitelistRecord = await server.whitelistRepository.findOneByApiKeyAndPolicyId(api_key, sponsorshipPolicy.id); + if (!existingWhitelistRecord?.addresses.includes(userOp.sender)) throw new Error('This sender address has not been whitelisted yet'); + } + result = await paymaster.signV07(userOp, str, str1, entryPoint, networkConfig.contracts.etherspotPaymasterAddress, bundlerUrl, signer, estimate, server.log); + } + break; } - break; - } - case 'erc20': { - if (epVersion === EPVersions.EPV_06) { - if ( - !(PAYMASTER_ADDRESS[chainId] && PAYMASTER_ADDRESS[chainId][gasToken]) && - !(customPaymasters[chainId] && customPaymasters[chainId][gasToken]) - ) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK_TOKEN }) - let paymasterAddress: string; - if (customPaymasters[chainId] && customPaymasters[chainId][gasToken]) paymasterAddress = customPaymasters[chainId][gasToken]; - else paymasterAddress = PAYMASTER_ADDRESS[chainId][gasToken] - result = await paymaster.pimlico(userOp, bundlerUrl, entryPoint, paymasterAddress, server.log); - } else if (epVersion === EPVersions.EPV_07) { - if ( - !(customPaymastersV2[chainId] && customPaymastersV2[chainId][gasToken]) - ) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK_TOKEN }) - const paymasterAddress = customPaymastersV2[chainId][gasToken]; - result = await paymaster.ERC20PaymasterV07(userOp, bundlerUrl, entryPoint, paymasterAddress, estimate, server.log); - } else { - throw new Error(`Currently only ${SUPPORTED_ENTRYPOINTS.EPV_06} & ${SUPPORTED_ENTRYPOINTS.EPV_07} entryPoint addresses are supported`) + case 'erc20': { + if (epVersion === EPVersions.EPV_06) { + if ( + !(PAYMASTER_ADDRESS[chainId] && PAYMASTER_ADDRESS[chainId][gasToken]) && + !(customPaymasters[chainId] && customPaymasters[chainId][gasToken]) + ) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK_TOKEN }) + let paymasterAddress: string; + if (customPaymasters[chainId] && customPaymasters[chainId][gasToken]) paymasterAddress = customPaymasters[chainId][gasToken]; + else paymasterAddress = PAYMASTER_ADDRESS[chainId][gasToken] + result = await paymaster.pimlico(userOp, bundlerUrl, entryPoint, paymasterAddress, server.log); + } else if (epVersion === EPVersions.EPV_07) { + if ( + !(customPaymastersV2[chainId] && customPaymastersV2[chainId][gasToken]) + ) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK_TOKEN }) + const paymasterAddress = customPaymastersV2[chainId][gasToken]; + result = await paymaster.ERC20PaymasterV07(userOp, bundlerUrl, entryPoint, paymasterAddress, estimate, server.log); + } else { + throw new Error(`Currently only ${SUPPORTED_ENTRYPOINTS.EPV_06} & ${SUPPORTED_ENTRYPOINTS.EPV_07} entryPoint addresses are supported`) + } + break; } - break; - } - case 'multitoken': { - if (epVersion !== EPVersions.EPV_06) - throw new Error('Currently only EPV06 entryPoint address is supported') - const date = new Date(); - const provider = new providers.JsonRpcProvider(bundlerUrl); - const signer = new Wallet(privateKey, provider) - const validUntil = context.validUntil ? new Date(context.validUntil) : date; - const validAfter = context.validAfter ? new Date(context.validAfter) : date; - const hex = (Number((validUntil.valueOf() / 1000).toFixed(0)) + 600).toString(16); - const hex1 = (Number((validAfter.valueOf() / 1000).toFixed(0)) - 60).toString(16); - let str = '0x' - let str1 = '0x' - for (let i = 0; i < 14 - hex.length; i++) { - str += '0'; + case 'multitoken': { + if (epVersion !== EPVersions.EPV_06) + throw new Error('Currently only EPV06 entryPoint address is supported') + const date = new Date(); + const provider = new providers.JsonRpcProvider(bundlerUrl); + const signer = new Wallet(privateKey, provider) + const validUntil = context.validUntil ? new Date(context.validUntil) : date; + const validAfter = context.validAfter ? new Date(context.validAfter) : date; + const hex = (Number((validUntil.valueOf() / 1000).toFixed(0)) + 600).toString(16); + const hex1 = (Number((validAfter.valueOf() / 1000).toFixed(0)) - 60).toString(16); + let str = '0x' + let str1 = '0x' + for (let i = 0; i < 14 - hex.length; i++) { + str += '0'; + } + for (let i = 0; i < 14 - hex1.length; i++) { + str1 += '0'; + } + str += hex; + str1 += hex1; + if (!networkConfig.MultiTokenPaymasterOracleUsed || + !(networkConfig.MultiTokenPaymasterOracleUsed == "orochi" || networkConfig.MultiTokenPaymasterOracleUsed == "chainlink")) + throw new Error("Oracle is not Defined/Invalid"); + result = await paymaster.signMultiTokenPaymaster(userOp, str, str1, entryPoint, multiTokenPaymasters[chainId][gasToken], gasToken, multiTokenOracles[chainId][gasToken], bundlerUrl, signer, networkConfig.MultiTokenPaymasterOracleUsed, server.log); + break; } - for (let i = 0; i < 14 - hex1.length; i++) { - str1 += '0'; + default: { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_MODE }); } - str += hex; - str1 += hex1; - if (!networkConfig.MultiTokenPaymasterOracleUsed || - !(networkConfig.MultiTokenPaymasterOracleUsed == "orochi" || networkConfig.MultiTokenPaymasterOracleUsed == "chainlink")) - throw new Error("Oracle is not Defined/Invalid"); - result = await paymaster.signMultiTokenPaymaster(userOp, str, str1, entryPoint, multiTokenPaymasters[chainId][gasToken], gasToken, multiTokenOracles[chainId][gasToken], bundlerUrl, signer, networkConfig.MultiTokenPaymasterOracleUsed, server.log); - break; - } - default: { - return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_MODE }); } } server.log.info(result, 'Response sent: '); @@ -328,6 +347,8 @@ const paymasterRoutes: FastifyPluginAsync = async (server) => { return reply.code(ReturnCode.SUCCESS).send({ jsonrpc: body.jsonrpc, id: body.id, result, error: null }) return reply.code(ReturnCode.SUCCESS).send(result); } catch (err: any) { + if (err.name.includes("invalid address")) + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_ADDRESS_PASSSED }) if (err.name == "ResourceNotFoundException") return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); request.log.error(err); From e57b46bf7887cda219f1c845e8f68700a351ecab Mon Sep 17 00:00:00 2001 From: Vignesh Date: Mon, 30 Sep 2024 15:43:07 +0530 Subject: [PATCH 2/4] updated package version --- backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index caed00d..5575d3c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "arka", - "version": "1.5.4", + "version": "1.6.0", "description": "ARKA - (Albanian for Cashier's case) is the first open source Paymaster as a service software", "type": "module", "directories": { From 6563217b1ae6575ddca41a3dfc0f63b245ce7733 Mon Sep 17 00:00:00 2001 From: Vignesh Date: Mon, 30 Sep 2024 20:07:58 +0530 Subject: [PATCH 3/4] bug fixes --- backend/src/paymaster/index.ts | 28 ++++++++++++++++++-------- backend/src/routes/paymaster-routes.ts | 4 ++-- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/backend/src/paymaster/index.ts b/backend/src/paymaster/index.ts index 49ce61d..732eaf4 100644 --- a/backend/src/paymaster/index.ts +++ b/backend/src/paymaster/index.ts @@ -13,6 +13,7 @@ import OrochiOracleAbi from '../abi/OrochiOracleAbi.js'; import ChainlinkOracleAbi from '../abi/ChainlinkOracleAbi.js'; import ERC20PaymasterV07Abi from '../abi/ERC20PaymasterV07Abi.js'; import ERC20Abi from '../abi/ERC20Abi.js'; +import EtherspotChainlinkOracleAbi from '../abi/EtherspotChainlinkOracleAbi.js'; export class Paymaster { feeMarkUp: BigNumber; @@ -235,11 +236,12 @@ export class Paymaster { result.gasEstimates.verificationGasLimit = response.verificationGasLimit; result.feeEstimates.maxFeePerGas = response.maxFeePerGas; result.feeEstimates.maxPriorityFeePerGas = response.maxPriorityFeePerGas; - - const paymasterAddress = multiTokenPaymasters[chainId][tokens_list[0]]; - result.paymasterAddress = paymasterAddress; - const paymasterContract = new ethers.Contract(paymasterAddress, MultiTokenPaymasterAbi, provider); - result.postOpGas = await paymasterContract.UNACCOUNTED_COST; + if (!multiTokenPaymasters[chainId]) { + const paymasterAddress = multiTokenPaymasters[chainId][tokens_list[0]]; + result.paymasterAddress = paymasterAddress; + const paymasterContract = new ethers.Contract(paymasterAddress, MultiTokenPaymasterAbi, provider); + result.postOpGas = await paymasterContract.UNACCOUNTED_COST; + } for (let i = 0; i < tokens_list.length; i++) { const gasToken = tokens_list[i]; @@ -253,11 +255,16 @@ export class Paymaster { const oracleContract = new ethers.Contract(oracleAddress, OrochiOracleAbi, provider); const result = await oracleContract.getLatestData(1, ethers.utils.hexlify(ethers.utils.toUtf8Bytes('ETH')).padEnd(42, '0')) ethPrice = Number(ethers.utils.formatEther(result)).toFixed(0); - } else { + } else if (oracleName === "chainlink") { const chainlinkContract = new ethers.Contract(oracleAddress, ChainlinkOracleAbi, provider); const decimals = await chainlinkContract.decimals(); const result = await chainlinkContract.latestAnswer(); ethPrice = Number(ethers.utils.formatUnits(result, decimals)).toFixed(0); + } else { + const ecContract = new ethers.Contract(oracleAddress, EtherspotChainlinkOracleAbi, provider); + const decimals = await ecContract.decimals(); + const result = await ecContract.cachedPrice(); + ethPrice = Number(ethers.utils.formatUnits(result, decimals)).toFixed(0); } result.etherUSDExchangeRate = BigNumber.from(ethPrice).toHexString(); const exchangeRate = 1000000; // This is for setting min tokens required for the txn that gets validated on estimate @@ -295,10 +302,15 @@ export class Paymaster { const oracleContract = new ethers.Contract(oracleAggregator, OrochiOracleAbi, provider); const result = await oracleContract.getLatestData(1, ethers.utils.hexlify(ethers.utils.toUtf8Bytes('ETH')).padEnd(42, '0')) ethPrice = Number(ethers.utils.formatEther(result)).toFixed(0); - } else { + } else if (oracleName === "chainlink") { const chainlinkContract = new ethers.Contract(oracleAggregator, ChainlinkOracleAbi, provider); const decimals = await chainlinkContract.decimals(); - const result = await chainlinkContract.latestAnswer(); + const result = await chainlinkContract.latestRoundData(); + ethPrice = Number(ethers.utils.formatUnits(result.answer, decimals)).toFixed(0); + } else { + const ecContract = new ethers.Contract(oracleAggregator, EtherspotChainlinkOracleAbi, provider); + const decimals = await ecContract.decimals(); + const result = await ecContract.cachedPrice(); ethPrice = Number(ethers.utils.formatUnits(result, decimals)).toFixed(0); } userOp.paymasterAndData = await this.getPaymasterAndDataForMultiTokenPaymaster(userOp, validUntil, validAfter, feeToken, ethPrice, paymasterContract, signer); diff --git a/backend/src/routes/paymaster-routes.ts b/backend/src/routes/paymaster-routes.ts index c06774b..a68c95e 100644 --- a/backend/src/routes/paymaster-routes.ts +++ b/backend/src/routes/paymaster-routes.ts @@ -213,7 +213,7 @@ const paymasterRoutes: FastifyPluginAsync = async (server) => { if (epVersion !== EPVersions.EPV_06) throw new Error('Currently only EPV06 entryPoint address is supported') if (!networkConfig.MultiTokenPaymasterOracleUsed || - !(networkConfig.MultiTokenPaymasterOracleUsed == "orochi" || networkConfig.MultiTokenPaymasterOracleUsed == "chainlink")) + !(networkConfig.MultiTokenPaymasterOracleUsed == "orochi" || networkConfig.MultiTokenPaymasterOracleUsed == "chainlink" || networkConfig.MultiTokenPaymasterOracleUsed == "etherspotChainlink")) throw new Error("Oracle is not Defined/Invalid"); result = await paymaster.getQuotesMultiToken(userOp, entryPoint, chainId, multiTokenPaymasters, tokens_list, multiTokenOracles, bundlerUrl, networkConfig.MultiTokenPaymasterOracleUsed, server.log); } @@ -331,7 +331,7 @@ const paymasterRoutes: FastifyPluginAsync = async (server) => { str += hex; str1 += hex1; if (!networkConfig.MultiTokenPaymasterOracleUsed || - !(networkConfig.MultiTokenPaymasterOracleUsed == "orochi" || networkConfig.MultiTokenPaymasterOracleUsed == "chainlink")) + !(networkConfig.MultiTokenPaymasterOracleUsed == "orochi" || networkConfig.MultiTokenPaymasterOracleUsed == "chainlink" || networkConfig.MultiTokenPaymasterOracleUsed == "etherspotChainlink")) throw new Error("Oracle is not Defined/Invalid"); result = await paymaster.signMultiTokenPaymaster(userOp, str, str1, entryPoint, multiTokenPaymasters[chainId][gasToken], gasToken, multiTokenOracles[chainId][gasToken], bundlerUrl, signer, networkConfig.MultiTokenPaymasterOracleUsed, server.log); break; From 4fd140b73f596a9ab0c8a139f011b8c1b53db15e Mon Sep 17 00:00:00 2001 From: Vignesh Date: Mon, 30 Sep 2024 20:17:00 +0530 Subject: [PATCH 4/4] added amoy chain on default --- backend/config.json.default | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/backend/config.json.default b/backend/config.json.default index 0af61c4..ee78494 100644 --- a/backend/config.json.default +++ b/backend/config.json.default @@ -279,6 +279,26 @@ "MultiTokenPaymasterOracleUsed": "chainlink", "entryPoint": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789" }, + { + "chainId": 80002, + "bundler": "https://testnet-rpc.etherspot.io/v1/80002", + "contracts": { + "etherspotPaymasterAddress": "0xe893a26dd53b325bffaacdfa224692eff4c448c4" + }, + "thresholdValue": "0.01", + "MultiTokenPaymasterOracleUsed": "chainlink", + "entryPoint": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789" + }, + { + "chainId": 80002, + "bundler": "https://testnet-rpc.etherspot.io/v2/80002", + "contracts": { + "etherspotPaymasterAddress": "0x9ddB9DC20E904206823184577e9C571c713d2c57" + }, + "thresholdValue": "0.01", + "MultiTokenPaymasterOracleUsed": "chainlink", + "entryPoint": "0x0000000071727De22E5E9d8BAf0edAc6f37da032" + }, { "chainId": 84532, "bundler": "https://testnet-rpc.etherspot.io/v1/84532",