Skip to content

Commit

Permalink
PRO-2267 - Etherscan API (#80)
Browse files Browse the repository at this point in the history
* added fetching of gas using etherscan

* updated package version
  • Loading branch information
vignesha22 authored Apr 1, 2024
1 parent 89ea1b1 commit c42caae
Show file tree
Hide file tree
Showing 13 changed files with 154 additions and 42 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ There is an option to run the code locally without using AWS and only using loca
"LINK": "0x0a6Aa1Bd30D6954cA525315287AdeeEcbb6eFB59"
}
} which also needs to be converted into `base64` value
- FEE_MARKUP - this is used to add fee if it gets from the provider. This needs to be inputted as a number in terms of gwei
- ETHERSCAN_GAS_ORACLES - the list of urls for all chains. Note that the response got is in terms of etherscan API Documentation https://docs.polygonscan.com/api-endpoints/gas-tracker#get-gas-oracle
The structure should be as follows
{
"137": "https://api.polygonscan.com/api?module=gastracker&action=gasoracle&apikey=YourApiKeyToken", // Note that you need to replace YourApiKeyToken to actual API key from etherscan
"1": "https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey=YourApiKeyToken"
} which then needs to be converted into `base64` value

## API KEY VALIDATION
- In ARKA Admin Frontend, create an API_KEY with the following format -
Expand Down
4 changes: 2 additions & 2 deletions admin_frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion admin_frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "admin_frontend",
"version": "1.1.6",
"version": "1.1.7",
"private": true,
"dependencies": {
"@emotion/react": "11.11.3",
Expand Down
2 changes: 2 additions & 0 deletions backend/demo.env
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ STACKUP_API_KEY=
SUPPORTED_NETWORKS=
ADMIN_WALLET_ADDRESS=
DEFAULT_INDEXER_ENDPOINT=http://localhost:3003
FEE_MARKUP=
ETHERSCAN_GAS_ORACLES=
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "arka",
"version": "1.1.6",
"version": "1.1.7",
"description": "ARKA - (Albanian for Cashier's case) is the first open source Paymaster as a service software",
"type": "module",
"directories": {
Expand Down
10 changes: 5 additions & 5 deletions backend/src/paymaster/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ describe('Paymaster on Mumbai', () => {
test('SMOKE: validate the deposit function with valid details', async () => {
const amount = '0.0000001'
try {
const depositResponse = await paymaster.deposit(amount, paymasterAddress, bundlerUrl, relayerKey);
const depositResponse = await paymaster.deposit(amount, paymasterAddress, bundlerUrl, relayerKey, chainId);

const expectedMessage = depositResponse.message;
const actualMessage = 'Successfully deposited with transaction Hash';
Expand Down Expand Up @@ -212,7 +212,7 @@ describe('Paymaster on Mumbai', () => {
const wallet = ethers.Wallet.createRandom();
const address = [wallet.address]; // not whitelisted address
try {
const whitelistAddresses = await paymaster.whitelistAddresses(address, paymasterAddress, bundlerUrl, relayerKey);
const whitelistAddresses = await paymaster.whitelistAddresses(address, paymasterAddress, bundlerUrl, relayerKey, chainId);

if (whitelistAddresses.message.includes('Successfully whitelisted with transaction Hash')) {
console.log('The address is whitelisted successfully.')
Expand All @@ -227,7 +227,7 @@ describe('Paymaster on Mumbai', () => {
test('REGRESSION: validate the whitelistAddresses function with already whitelisted address', async () => {
const address = ['0x7b3078b9A28DF76453CDfD2bA5E75f32f0676321']; // already whitelisted address
try {
await paymaster.whitelistAddresses(address, paymasterAddress, bundlerUrl, relayerKey);
await paymaster.whitelistAddresses(address, paymasterAddress, bundlerUrl, relayerKey, chainId);
fail('Address is whitelisted, However it was already whitelisted.')
} catch (e: any) {
const actualMessage = 'already whitelisted';
Expand All @@ -244,7 +244,7 @@ describe('Paymaster on Mumbai', () => {
const address = ['0x7b3078b9A28DF76453CDfD2bA5E75f32f0676321']; // already whitelisted address
const relayerKey = '0xdd45837c9d94e7cc3ed3b24be7c1951eff6ed3c6fd0baf68fc1ba8c0e51debb'; // invalid relayerKey
try {
await paymaster.whitelistAddresses(address, paymasterAddress, bundlerUrl, relayerKey);
await paymaster.whitelistAddresses(address, paymasterAddress, bundlerUrl, relayerKey, chainId);
fail('Address is whitelisted, however the relayerKey is invalid.')
} catch (e: any) {
const actualMessage = 'Please try again later or contact support team RawErrorMsg: hex data is odd-length';
Expand Down Expand Up @@ -306,7 +306,7 @@ describe('Paymaster on Mumbai', () => {
test('REGRESSION: validate the deposit function with invalid amount', async () => {
const amount = '10000' // invalid amount
try {
await paymaster.deposit(amount, paymasterAddress, bundlerUrl, relayerKey);
await paymaster.deposit(amount, paymasterAddress, bundlerUrl, relayerKey, chainId);
fail('The deposite action is performed with invalid amount.')
} catch (e: any) {
const actualMessage = 'Balance is less than the amount to be deposited';
Expand Down
91 changes: 68 additions & 23 deletions backend/src/paymaster/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { providers, Wallet, ethers, Contract, BigNumber } from 'ethers';
import { arrayify, defaultAbiCoder, hexConcat } from 'ethers/lib/utils.js';
import { FastifyBaseLogger } from 'fastify';
import abi from "../abi/EtherspotAbi.js";
import { PimlicoPaymaster, getERC20Paymaster } from './pimlico.js';
import { PimlicoPaymaster } from './pimlico.js';
import ErrorMessage from '../constants/ErrorMessage.js';
import { PAYMASTER_ADDRESS } from '../constants/Pimlico.js';
import { getEtherscanFee } from '../utils/common.js';

export class Paymaster {
feeMarkUp: BigNumber;
Expand Down Expand Up @@ -35,7 +37,7 @@ export class Paymaster {
return paymasterAndData;
}

async sign(userOp: any, validUntil: string, validAfter: string, entryPoint: string, paymasterAddress: string, bundlerRpc: string, signer: Wallet) {
async sign(userOp: any, validUntil: string, validAfter: string, entryPoint: string, paymasterAddress: string, bundlerRpc: string, signer: Wallet, log?: FastifyBaseLogger) {
try {
const provider = new providers.JsonRpcProvider(bundlerRpc);
const paymasterContract = new ethers.Contract(paymasterAddress, abi, provider);
Expand All @@ -57,11 +59,12 @@ export class Paymaster {

return returnValue;
} catch (err: any) {
if (log) log.error(err, 'sign');
throw new Error('Failed to process request to bundler. Please contact support team RawErrorMsg:' + err.message)
}
}

async pimlico(userOp: any, bundlerRpc: string, entryPoint: string, PaymasterAddress: string) {
async pimlico(userOp: any, bundlerRpc: string, entryPoint: string, PaymasterAddress: string, log?: FastifyBaseLogger) {
try {
const provider = new providers.JsonRpcProvider(bundlerRpc);
const erc20Paymaster = new PimlicoPaymaster(PaymasterAddress, provider)
Expand All @@ -87,10 +90,10 @@ export class Paymaster {
const tokenBalance = await tokenContract.balanceOf(userOp.sender);

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)
userOp.paymasterAndData = paymasterAndData;

const response = await provider.send('eth_estimateUserOperationGas', [userOp, entryPoint]);
userOp.verificationGasLimit = ethers.BigNumber.from(response.verificationGasLimit).add(100000).toString();
userOp.preVerificationGas = response.preVerificationGas;
Expand All @@ -105,21 +108,23 @@ export class Paymaster {
};
} catch (err: any) {
if (err.message.includes('The required token amount')) throw new Error(err.message);
if (log) log.error(err, 'pimlico');
throw new Error('Failed to process request to bundler. Please contact support team RawErrorMsg: ' + err.message)
}
}

async pimlicoAddress(gasToken: string, chainId: number) {
async pimlicoAddress(gasToken: string, chainId: number, log?: FastifyBaseLogger) {
try {
return {
message: PAYMASTER_ADDRESS[chainId][gasToken] ?? 'Requested Token Paymaster is not available/deployed',
}
} catch (err: any) {
if (log) log.error(err, 'pimlicoAddress');
throw new Error(err.message)
}
}

async whitelistAddresses(address: string[], paymasterAddress: string, bundlerRpc: string, relayerKey: string) {
async whitelistAddresses(address: string[], paymasterAddress: string, bundlerRpc: string, relayerKey: string, chainId: number, log?: FastifyBaseLogger) {
try {
const provider = new providers.JsonRpcProvider(bundlerRpc);
const paymasterContract = new ethers.Contract(paymasterAddress, abi, provider);
Expand All @@ -131,34 +136,47 @@ export class Paymaster {
}
}
const encodedData = paymasterContract.interface.encodeFunctionData('addBatchToWhitelist', [address]);
const feeData = await provider.getFeeData();

const etherscanFeeData = await getEtherscanFee(chainId);
let feeData;
if (etherscanFeeData) {
feeData = etherscanFeeData;
} else {
feeData = await provider.getFeeData();
feeData.gasPrice = feeData.gasPrice ? feeData.gasPrice.add(this.feeMarkUp) : null;
feeData.maxFeePerGas = feeData.maxFeePerGas ? feeData.maxFeePerGas.add(this.feeMarkUp) : null;
feeData.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas ? feeData.maxPriorityFeePerGas.add(this.feeMarkUp) : null;
}

let tx: providers.TransactionResponse;
if (!feeData.maxFeePerGas) {
tx = await signer.sendTransaction({
to: paymasterAddress,
data: encodedData,
gasPrice: feeData.gasPrice ? feeData.gasPrice.add(this.feeMarkUp) : undefined,
gasPrice: feeData.gasPrice ?? undefined,
})
} else {
tx = await signer.sendTransaction({
to: paymasterAddress,
data: encodedData,
maxFeePerGas: feeData.maxFeePerGas ? feeData.maxFeePerGas.add(this.feeMarkUp) : undefined,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ? feeData.maxPriorityFeePerGas.add(this.feeMarkUp) : undefined,
maxFeePerGas: feeData.maxFeePerGas ?? undefined,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? undefined,
type: 2,
});
}
await tx.wait();

return {
message: `Successfully whitelisted with transaction Hash ${tx.hash}`
};
} catch (err: any) {
if (err.message.includes('already whitelisted')) throw new Error(err.message);
if (log) log.error(err, 'whitelistAddresses')
throw new Error(ErrorMessage.ERROR_ON_SUBMITTING_TXN + ` RawErrorMsg: ${err.message}`);
}
}

async removeWhitelistAddress(address: string[], paymasterAddress: string, bundlerRpc: string, relayerKey: string) {
async removeWhitelistAddress(address: string[], paymasterAddress: string, bundlerRpc: string, relayerKey: string, chainId: number, log?: FastifyBaseLogger) {
try {
const provider = new providers.JsonRpcProvider(bundlerRpc);
const paymasterContract = new ethers.Contract(paymasterAddress, abi, provider);
Expand All @@ -169,81 +187,108 @@ export class Paymaster {
throw new Error(`${address[i]} is not whitelisted`)
}
}

const encodedData = paymasterContract.interface.encodeFunctionData('removeBatchFromWhitelist', [address]);
const feeData = await provider.getFeeData();
const etherscanFeeData = await getEtherscanFee(chainId);
let feeData;
if (etherscanFeeData) {
feeData = etherscanFeeData;
} else {
feeData = await provider.getFeeData();
feeData.gasPrice = feeData.gasPrice ? feeData.gasPrice.add(this.feeMarkUp) : null;
feeData.maxFeePerGas = feeData.maxFeePerGas ? feeData.maxFeePerGas.add(this.feeMarkUp) : null;
feeData.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas ? feeData.maxPriorityFeePerGas.add(this.feeMarkUp) : null;
}

let tx: providers.TransactionResponse;
if (!feeData.maxFeePerGas) {
tx = await signer.sendTransaction({
to: paymasterAddress,
data: encodedData,
gasPrice: feeData.gasPrice ? feeData.gasPrice.add(this.feeMarkUp) : undefined,
gasPrice: feeData.gasPrice ?? undefined,
})
} else {
tx = await signer.sendTransaction({
to: paymasterAddress,
data: encodedData,
maxFeePerGas: feeData.maxFeePerGas ? feeData.maxFeePerGas.add(this.feeMarkUp) : undefined,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ? feeData.maxPriorityFeePerGas.add(this.feeMarkUp) : undefined,
maxFeePerGas: feeData.maxFeePerGas ?? undefined,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? undefined,
type: 2,
});
}
await tx.wait();

return {
message: `Successfully removed whitelisted addresses with transaction Hash ${tx.hash}`
};
} catch (err: any) {
if (err.message.includes('is not whitelisted')) throw new Error(err.message);
if (log) log.error(err, 'removeWhitelistAddress');
throw new Error(ErrorMessage.ERROR_ON_SUBMITTING_TXN);
}
}

async checkWhitelistAddress(accountAddress: string, paymasterAddress: string, bundlerRpc: string, relayerKey: string) {
async checkWhitelistAddress(accountAddress: string, paymasterAddress: string, bundlerRpc: string, relayerKey: string, log?: FastifyBaseLogger) {
try {
const provider = new providers.JsonRpcProvider(bundlerRpc);
const signer = new Wallet(relayerKey, provider)
const paymasterContract = new ethers.Contract(paymasterAddress, abi, provider);
return paymasterContract.check(signer.address, accountAddress);
} catch (err) {
if (log) log.error(err, 'checkWhitelistAddress');
throw new Error(ErrorMessage.RPC_ERROR);
}
}

async deposit(amount: string, paymasterAddress: string, bundlerRpc: string, relayerKey: string) {
async deposit(amount: string, paymasterAddress: string, bundlerRpc: string, relayerKey: string, chainId: number, log?: FastifyBaseLogger) {
try {
const provider = new providers.JsonRpcProvider(bundlerRpc);
const paymasterContract = new ethers.Contract(paymasterAddress, abi, provider);
const signer = new Wallet(relayerKey, provider)
const balance = await signer.getBalance();
const amountInWei = ethers.utils.parseEther(amount);
const amountInWei = ethers.utils.parseEther(amount.toString());
if (amountInWei.gte(balance))
throw new Error(`${signer.address} Balance is less than the amount to be deposited`)

const encodedData = paymasterContract.interface.encodeFunctionData('depositFunds', []);
const feeData = await provider.getFeeData();

const etherscanFeeData = await getEtherscanFee(chainId);
let feeData;
if (etherscanFeeData) {
feeData = etherscanFeeData;
} else {
feeData = await provider.getFeeData();
feeData.gasPrice = feeData.gasPrice ? feeData.gasPrice.add(this.feeMarkUp) : null;
feeData.maxFeePerGas = feeData.maxFeePerGas ? feeData.maxFeePerGas.add(this.feeMarkUp) : null;
feeData.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas ? feeData.maxPriorityFeePerGas.add(this.feeMarkUp) : null;
}

let tx: providers.TransactionResponse;
if (!feeData.maxFeePerGas) {
tx = await signer.sendTransaction({
to: paymasterAddress,
data: encodedData,
value: amountInWei,
gasPrice: feeData.gasPrice ? feeData.gasPrice.add(this.feeMarkUp) : undefined,
gasPrice: feeData.gasPrice ?? undefined,
})
} else {
tx = await signer.sendTransaction({
to: paymasterAddress,
data: encodedData,
value: amountInWei,
maxFeePerGas: feeData.maxFeePerGas ? feeData.maxFeePerGas.add(this.feeMarkUp) : undefined,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ? feeData.maxPriorityFeePerGas.add(this.feeMarkUp) : undefined,
maxFeePerGas: feeData.maxFeePerGas ?? undefined,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? undefined,
type: 2,
});
}
// commented the below line to avoid timeouts for long delays in transaction confirmation.
// await tx.wait();

return {
message: `Successfully deposited with transaction Hash ${tx.hash}`
};
} catch (err: any) {
if (log) log.error(err, 'deposit');
if (err.message.includes('Balance is less than the amount to be deposited')) throw new Error(err.message);
throw new Error(ErrorMessage.ERROR_ON_SUBMITTING_TXN);
}
Expand Down
Loading

0 comments on commit c42caae

Please sign in to comment.