Skip to content

Commit

Permalink
added oracle support and multiToken mode
Browse files Browse the repository at this point in the history
  • Loading branch information
vignesha22 committed Apr 9, 2024
1 parent c42caae commit 217d1a3
Show file tree
Hide file tree
Showing 8 changed files with 1,003 additions and 8 deletions.
844 changes: 844 additions & 0 deletions backend/src/abi/MultiTokenPaymasterAbi.ts

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions backend/src/constants/Pimlico.ts

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions backend/src/migrations/002.apiKeys.sql
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ CREATE TABLE IF NOT EXISTS api_keys (
PRIVATE_KEY varchar NOT NULL,
SUPPORTED_NETWORKS varchar DEFAULT NULL,
ERC20_PAYMASTERS varchar DEFAULT NULL,
MULTI_TOKEN_PAYMASTERS varchar DEFAULT NULL,
MULTI_TOKEN_ORACLES varchar DEFAULT NULL,
TRANSACTION_LIMIT INT NOT NULL,
NO_OF_TRANSACTIONS_IN_A_MONTH int,
INDEXER_ENDPOINT varchar
Expand Down
60 changes: 60 additions & 0 deletions backend/src/paymaster/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { PimlicoPaymaster } from './pimlico.js';
import ErrorMessage from '../constants/ErrorMessage.js';
import { PAYMASTER_ADDRESS } from '../constants/Pimlico.js';
import { getEtherscanFee } from '../utils/common.js';
import MultiTokenPaymasterAbi from '../abi/MultiTokenPaymasterAbi.js';

export class Paymaster {
feeMarkUp: BigNumber;
Expand Down Expand Up @@ -64,6 +65,64 @@ export class Paymaster {
}
}

async getPaymasterAndDataForMultiTokenPaymaster(userOp: any, validUntil: string, validAfter: string, feeToken: string, oracleAggregator: string, paymasterContract: Contract, signer: Wallet) {
const exchangeRate = 10000000; // This is for setting min tokens required for the txn that gets validated on estimate
const priceMarkup = 1100000; // 10% more of the actual cost. Can be anything between 1e6 to 2e6
// actual signing...
// priceSource inputs available 0 - for using external exchange price and 1 - for oracle based price
const hash = await paymasterContract.getHash(
userOp,
1,
validUntil,
validAfter,
feeToken,
oracleAggregator,
exchangeRate,
priceMarkup,
);

const sig = await signer.signMessage(arrayify(hash));

const paymasterAndData = hexConcat([
paymasterContract.address,
'0x01',
defaultAbiCoder.encode(
['uint48', 'uint48', 'address', 'address', 'uint256', 'uint32'],
[validUntil, validAfter, feeToken, oracleAggregator, exchangeRate, priceMarkup]
),
sig,
]);

return paymasterAndData;
}

async signMultiTokenPaymaster(userOp: any, validUntil: string, validAfter: string, entryPoint: string, paymasterAddress: string, feeToken: string, oracleAggregator: string, bundlerRpc: string, signer: Wallet, log?: FastifyBaseLogger) {
try {
const provider = new providers.JsonRpcProvider(bundlerRpc);
const paymasterContract = new ethers.Contract(paymasterAddress, MultiTokenPaymasterAbi, provider);
userOp.paymasterAndData = await this.getPaymasterAndDataForMultiTokenPaymaster(userOp, validUntil, validAfter, feeToken, oracleAggregator, paymasterContract, signer);

if (!userOp.signature) userOp.signature = '0x';
const response = await provider.send('eth_estimateUserOperationGas', [userOp, entryPoint]);
userOp.verificationGasLimit = response.verificationGasLimit;
userOp.preVerificationGas = response.preVerificationGas;
userOp.callGasLimit = response.callGasLimit;
const paymasterAndData = await this.getPaymasterAndDataForMultiTokenPaymaster(userOp, validUntil, validAfter, feeToken, oracleAggregator, paymasterContract, signer);

const returnValue = {
paymasterAndData,
verificationGasLimit: response.verificationGasLimit,
preVerificationGas: response.preVerificationGas,
callGasLimit: response.callGasLimit,
}

return returnValue;
} catch (err: any) {
if (log) log.error(err, 'signCombinedPaymaster');
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, log?: FastifyBaseLogger) {
try {
const provider = new providers.JsonRpcProvider(bundlerRpc);
Expand Down Expand Up @@ -253,6 +312,7 @@ export class Paymaster {
const encodedData = paymasterContract.interface.encodeFunctionData('depositFunds', []);

const etherscanFeeData = await getEtherscanFee(chainId);
console.log('etherscanFeeData: ', etherscanFeeData);
let feeData;
if (etherscanFeeData) {
feeData = etherscanFeeData;
Expand Down
4 changes: 4 additions & 0 deletions backend/src/routes/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ const adminRoutes: FastifyPluginAsync = async (server) => {
PRIVATE_KEY, \
SUPPORTED_NETWORKS, \
ERC20_PAYMASTERS, \
MULTI_TOKEN_PAYMASTERS, \
MULTI_TOKEN_ORACLES, \
TRANSACTION_LIMIT, \
NO_OF_TRANSACTIONS_IN_A_MONTH, \
INDEXER_ENDPOINT) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
Expand All @@ -105,6 +107,8 @@ const adminRoutes: FastifyPluginAsync = async (server) => {
hmac,
body.SUPPORTED_NETWORKS,
body.ERC20_PAYMASTERS,
body.MULTI_TOKEN_PAYMASTERS ?? null,
body.MULTI_TOKEN_ORACLES ?? null,
body.TRANSACTION_LIMIT ?? 0,
body.NO_OF_TRANSACTIONS_IN_A_MONTH ?? 10,
body.INDEXER_ENDPOINT ?? process.env.DEFAULT_INDEXER_ENDPOINT
Expand Down
54 changes: 52 additions & 2 deletions backend/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,15 @@ const routes: FastifyPluginAsync = async (server) => {
const userOp = body.params[0];
const entryPoint = body.params[1];
const context = body.params[2];
const gasToken = context?.token ? context.token : null;
let gasToken = context?.token ? context.token : null;
const mode = context?.mode ? String(context.mode) : null;
const chainId = query['chainId'] ?? body.params[3];
const api_key = query['apiKey'] ?? body.params[4];
if (!api_key)
return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY })
let customPaymasters = [];
let multiTokenPaymasters = [];
let multiTokenOracles = [];
let privateKey = '';
let supportedNetworks;
let noOfTxns;
Expand All @@ -83,6 +85,14 @@ const routes: FastifyPluginAsync = async (server) => {
const buffer = Buffer.from(secrets['ERC20_PAYMASTERS'], 'base64');
customPaymasters = JSON.parse(buffer.toString());
}
if (secrets['MULTI_TOKEN_PAYMASTERS']) {
const buffer = Buffer.from(secrets['MULTI_TOKEN_PAYMASTERS'], 'base64');
multiTokenPaymasters = JSON.parse(buffer.toString());
}
if (secrets['MULTI_TOKEN_ORACLES']) {
const buffer = Buffer.from(secrets['MULTI_TOKEN_ORACLES'], 'base64');
multiTokenOracles = JSON.parse(buffer.toString());
}
privateKey = secrets['PRIVATE_KEY'];
supportedNetworks = secrets['SUPPORTED_NETWORKS'];
noOfTxns = secrets['NO_OF_TRANSACTIONS_IN_A_MONTH'] ?? 10;
Expand All @@ -98,12 +108,21 @@ const routes: FastifyPluginAsync = async (server) => {
const buffer = Buffer.from(record['ERC20_PAYMASTERS'], 'base64');
customPaymasters = JSON.parse(buffer.toString());
}
if (record['MULTI_TOKEN_PAYMASTERS']) {
const buffer = Buffer.from(record['MULTI_TOKEN_PAYMASTERS'], 'base64');
multiTokenPaymasters = JSON.parse(buffer.toString());
}
if (record['MULTI_TOKEN_ORACLES']) {
const buffer = Buffer.from(record['MULTI_TOKEN_ORACLES'], 'base64');
multiTokenOracles = JSON.parse(buffer.toString());
}
privateKey = decode(record['PRIVATE_KEY']);
supportedNetworks = record['SUPPORTED_NETWORKS'];
noOfTxns = record['NO_OF_TRANSACTIONS_IN_A_MONTH'];
txnMode = record['TRANSACTION_LIMIT'];
indexerEndpoint = record['INDEXER_ENDPOINT'] ?? process.env.DEFAULT_INDEXER_ENDPOINT;
}

if (
!userOp ||
!entryPoint ||
Expand All @@ -118,13 +137,23 @@ const routes: FastifyPluginAsync = async (server) => {
if (server.config.SUPPORTED_NETWORKS == '' && !SupportedNetworks) {
return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK });
}

if (
mode.toLowerCase() == 'erc20' &&
!(PAYMASTER_ADDRESS[chainId] && PAYMASTER_ADDRESS[chainId][gasToken]) &&
!(customPaymasters[chainId] && customPaymasters[chainId][gasToken])
) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK_TOKEN })

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 ?? '');
if (!networkConfig) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK });

let result;
switch (mode.toLowerCase()) {
case 'sponsor': {
Expand Down Expand Up @@ -160,7 +189,28 @@ const routes: FastifyPluginAsync = async (server) => {
result = await paymaster.pimlico(userOp, networkConfig.bundler, entryPoint, paymasterAddress, server.log);
break;
}
case 'default': {
case 'multitoken': {
const date = new Date();
const provider = new providers.JsonRpcProvider(networkConfig.bundler);
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;
result = await paymaster.signMultiTokenPaymaster(userOp, str, str1, entryPoint, multiTokenPaymasters[chainId][gasToken], gasToken, multiTokenOracles[chainId][gasToken], networkConfig.bundler, signer, server.log);
break;
}
default : {
return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_MODE });
}
}
Expand Down
13 changes: 10 additions & 3 deletions backend/src/utils/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FastifyBaseLogger, FastifyRequest } from "fastify";
import { ethers } from "ethers";
import { BigNumber, ethers } from "ethers";
import { Database } from "sqlite3";
import SupportedNetworks from "../../config.json" assert { type: "json" };
import { EtherscanResponse, getEtherscanFeeResponse } from "./interface.js";
Expand Down Expand Up @@ -44,8 +44,7 @@ export async function getEtherscanFee(chainId: number, log?: FastifyBaseLogger):
if (etherscanUrls[chainId]) {
const data = await fetch(etherscanUrls[chainId]);
const response: EtherscanResponse = await data.json();

if (response.result && response.result.FastGasPrice) {
if (response.result && typeof response.result === "object" && response.status === "1") {
const maxFeePerGas = ethers.utils.parseUnits(response.result.suggestBaseFee, 'gwei')
const fastGasPrice = ethers.utils.parseUnits(response.result.FastGasPrice, 'gwei')
return {
Expand All @@ -54,6 +53,14 @@ export async function getEtherscanFee(chainId: number, log?: FastifyBaseLogger):
gasPrice: maxFeePerGas,
}
}
if (response.result && typeof response.result === "string" && response.jsonrpc) {
const gasPrice = BigNumber.from(response.result)
return {
maxFeePerGas: gasPrice,
maxPriorityFeePerGas: gasPrice,
gasPrice: gasPrice
}
}
return null;
}
return null;
Expand Down
8 changes: 5 additions & 3 deletions backend/src/utils/interface.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { BigNumber } from "ethers";

export interface EtherscanResponse {
status: string;
message: string;
jsonrpc?: string;
id?: string;
status?: string;
message?: string;
result?: {
LastBlock: string;
SafeGasPrice: string;
ProposeGasPrice: string;
FastGasPrice: string;
suggestBaseFee: string;
gasUsedRatio: string;
}
} | string;
}

export interface getEtherscanFeeResponse {
Expand Down

0 comments on commit 217d1a3

Please sign in to comment.