Skip to content

Commit

Permalink
PRO-2862 - Coingecko Changes (#161)
Browse files Browse the repository at this point in the history
* coingecko database addition

* ft: adding cron jobs to increase cache hit ratio for oracle data (#157)

* ft: adding cron jobs to increase cache hit ratio for oracle data

* update version

* changes to make native oracle calls only for chainlink

* Fix/arb token (#158)

* fix for arb token in multi token paymaster

* update cron expression for erc20 oracle update cron

* comment and version update

* Pro 2857 (#153) (#159)

* Pro 2857 (#153)

* ft: adding support for verifying paymaster

* update: package.json version

* fix: changing names for mode, minor fix for entry point error message

* changes: common whitelist endpoints for v1/v2, removed v2 endpoints for whitelist

* adding changelog for backend

* chnages: version change, minor response changes for metadata and add stake routes

* coingecko database addition

* merge bug fixes

* updated version

* added required env vars to demo env

* changed to pro key

* changes done as requested

* changes to retain v2 endpoints, minor changes for change log

* updated changelog

---------

Co-authored-by: Nikhil Kumar <[email protected]>
Co-authored-by: nikhil kumar <[email protected]>
  • Loading branch information
3 people authored Jan 13, 2025
1 parent ccacb0b commit 7a2d589
Show file tree
Hide file tree
Showing 19 changed files with 1,038 additions and 323 deletions.
12 changes: 10 additions & 2 deletions backend/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
# Changelog
## [1.8.0] - 2024-12-25
## [2.0.1] - 2025-01-08
### New
- added `/whitelist/v2`, `/removeWhitelist/v2` and `/checkWhitelist/v2` endpoints back.
- `/whitelist`, `/removeWhitelist`, `checkWhitelist`, `/deposit/v2`, `/` endpoints accept `useVp` param which when set to true uses VerifyingPaymaster contract instead of EtherspotPaymaster.
- added coingecko support for token prices alone
- Added fetching of api_key data from db and for hosted setup only private key gets fetched from aws
- added `metadata/v2` api for ep7 config


## [2.0.0] - 2024-12-25
### Breaking changes
- removed `/whitelist/v1`, `/removeWhitelist/v1`, `/checkWhitelist/v1` endpoints.
- removed `/whitelist/v2`, `/removeWhitelist/v2`, `/checkWhitelist/v2` endpoints.

### New
Expand Down
4 changes: 4 additions & 0 deletions backend/demo.env
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ MULTI_TOKEN_MARKUP=1150000
ETHERSCAN_GAS_ORACLES=
DEFAULT_API_KEY=
WEBHOOK_URL=

# coingecko
COINGECKO_URL=
COINGECKO_API_KEY=
55 changes: 55 additions & 0 deletions backend/migrations/2024123000001-create-coingecko-tokens.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const { Sequelize } = require('sequelize')

async function up({ context: queryInterface }) {
await queryInterface.createTable('coingecko_tokens', {
"ID": {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
"TOKEN": {
allowNull: false,
primaryKey: false,
type: Sequelize.TEXT
},
"ADDRESS": {
type: Sequelize.STRING,
allowNull: false
},
"CHAIN_ID": {
type: Sequelize.BIGINT,
allowNull: false
},
"COIN_ID": {
type: Sequelize.STRING,
allowNull: false,
primaryKey: true
},
"DECIMALS": {
type: Sequelize.INTEGER,
allowNull: false,
},
"CREATED_AT": {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.NOW
},
"UPDATED_AT": {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.NOW
}
}, {
schema: process.env.DATABASE_SCHEMA_NAME
});
}

async function down({ context: queryInterface }) {
await queryInterface.dropTable({
tableName: 'coingecko_tokens',
schema: process.env.DATABASE_SCHEMA_NAME
});
}

module.exports = { up, down }
76 changes: 76 additions & 0 deletions backend/migrations/20241231000001-default_coingecko_tokens.cjs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
require('dotenv').config();

const vp7Contracts = JSON.stringify({
31: "0x805650ce74561C85baA44a8Bd13E19633Fd0F79d",
50: "0xABA00E929d66119A4A7F4B2E27150fC387ee801c",
51: "0x2b7cBFA523E0D0546C6d1F706b79dB7B6d910bdA",
97: "0xD9A97785a91086FDeF17980eDC2f9D290d71153F",
114: "0x5952653F151e844346825050d7157A9a6b46A23A",
123: "0xf6E4486156cc2F982eceC15a90B23047F396EcBE",
5003: "0x42963C58DE382D34CB5a7f77b703e645FcE6DD26",
80002: "0x9ddB9DC20E904206823184577e9C571c713d2c57",
84532: "0xD9A97785a91086FDeF17980eDC2f9D290d71153F",
421614: "0x5FD81CfCAa69F44B6d105795961b3E484ac9e7dB",
534351: "0xD9A97785a91086FDeF17980eDC2f9D290d71153F",
11155111: "0x8B57f6b24C7cd85007068Bf0587382804B225DB6",
11155420: "0x51a62e2B1E295CAe7Db5b91886735f9Ce335AcFB",
28122024: "0xc95A2Fb019445C9B3459c2C59e7cd6Ad2c8FBb1E"
});

async function up({ context: queryInterface }) {
await queryInterface.sequelize.query(
`UPDATE ${process.env.DATABASE_SCHEMA_NAME}."api_keys" SET "VERIFYING_PAYMASTERS_V2"='${vp7Contracts}' WHERE "API_KEY"='arka_public_key'`
);
}

async function down({ context: queryInterface }) {
await queryInterface.sequelize.query(
`UPDATE ${process.env.DATABASE_SCHEMA_NAME}."api_keys" SET "VERIFYING_PAYMASTERS_V2"=${null} WHERE "API_KEYS"='arka_public_key'`
);
}

module.exports = {up, down};
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": "2.0.0",
"version": "2.1.0",
"description": "ARKA - (Albanian for Cashier's case) is the first open source Paymaster as a service software",
"type": "module",
"directories": {
Expand Down
2 changes: 1 addition & 1 deletion backend/src/abi/VerifyingPaymasterAbiV2.ts

Large diffs are not rendered by default.

78 changes: 78 additions & 0 deletions backend/src/models/coingecko.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Sequelize, DataTypes, Model } from 'sequelize';

export class CoingeckoTokens extends Model {
public id!: number; // Note that the `null assertion` `!` is required in strict mode.
public token!: string;
public address!: string;
public chainId!: number;
public coinId!: string;
public decimals!: string;
public readonly createdAt!: Date;
public readonly updatedAt!: Date;
}

const initializeCoingeckoModel = (sequelize: Sequelize, schema: string) => {
CoingeckoTokens.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
field: 'ID'
},
token: {
type: DataTypes.TEXT,
allowNull: false,
primaryKey: true,
field: 'TOKEN'
},
address: {
type: DataTypes.STRING,
allowNull: false,
field: 'ADDRESS'
},
chainId: {
type: DataTypes.BIGINT,
allowNull: false,
field: 'CHAIN_ID',
get() {
const value = this.getDataValue('chainId');
return +value;
}
},
coinId: {
type: DataTypes.STRING,
allowNull: false,
primaryKey: true,
field: 'COIN_ID'
},
decimals: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'DECIMALS'
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
field: 'CREATED_AT'
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
field: 'UPDATED_AT'
},
}, {
sequelize,
tableName: 'coingecko_tokens',
modelName: 'CoingeckoTokens',
timestamps: true,
createdAt: 'createdAt',
updatedAt: 'updatedAt',
freezeTableName: true,
schema: schema,
});
};

export { initializeCoingeckoModel };
82 changes: 74 additions & 8 deletions backend/src/paymaster/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import ERC20Abi from '../abi/ERC20Abi.js';
import EtherspotChainlinkOracleAbi from '../abi/EtherspotChainlinkOracleAbi.js';
import { TokenDecimalsAndSymbol, UnaccountedCost } from '../constants/MultitokenPaymaster.js';
import { NativeOracleDecimals } from '../constants/ChainlinkOracles.js';
import { CoingeckoTokensRepository } from '../repository/coingecko-token-repository.js';
import { CoingeckoService } from '../services/coingecko.js';
import { Sequelize } from 'sequelize';
import { abi as verifyingPaymasterAbi, byteCode as verifyingPaymasterByteCode } from '../abi/VerifyingPaymasterAbi.js';
import { abi as verifyingPaymasterV2Abi, byteCode as verifyingPaymasterV2ByteCode } from '../abi/VerifyingPaymasterAbiV2.js';

const ttl = parseInt(process.env.CACHE_TTL || "600000");
const nativePriceCacheTtl = parseInt(process.env.NATIVE_PRICE_CACHE_TTL || "60000");

Expand All @@ -36,8 +42,10 @@ interface NativeCurrencyPricyCache {
expiry: number;
}

import { abi as verifyingPaymasterAbi, byteCode as verifyingPaymasterByteCode } from '../abi/VerifyingPaymasterAbi.js';
import { abi as verifyingPaymasterV2Abi, byteCode as verifyingPaymasterV2ByteCode } from '../abi/VerifyingPaymasterAbiV2.js';
interface CoingeckoPriceCache {
data: any;
expiry: number;
}

export class Paymaster {
feeMarkUp: BigNumber;
Expand All @@ -46,13 +54,17 @@ export class Paymaster {
EP7_TOKEN_PGL: string;
priceAndMetadata: Map<string, TokenPriceAndMetadataCache> = new Map();
nativeCurrencyPrice: Map<string, NativeCurrencyPricyCache> = new Map();
coingeckoPrice: Map<string, CoingeckoPriceCache> = new Map();
coingeckoService: CoingeckoService = new CoingeckoService();
sequelize: Sequelize;

constructor(feeMarkUp: string, multiTokenMarkUp: string, ep7TokenVGL: string, ep7TokenPGL: string) {
constructor(feeMarkUp: string, multiTokenMarkUp: string, ep7TokenVGL: string, ep7TokenPGL: string, sequelize: Sequelize) {
this.feeMarkUp = ethers.utils.parseUnits(feeMarkUp, 'gwei');
if (isNaN(Number(multiTokenMarkUp))) this.multiTokenMarkUp = 1150000 // 15% more of the actual cost. Can be anything between 1e6 to 2e6
else this.multiTokenMarkUp = Number(multiTokenMarkUp);
this.EP7_TOKEN_PGL = ep7TokenPGL;
this.EP7_TOKEN_VGL = ep7TokenVGL;
this.sequelize = sequelize;
}

packUint(high128: BigNumberish, low128: BigNumberish): string {
Expand Down Expand Up @@ -569,14 +581,18 @@ export class Paymaster {

const promises = [];
for(let i=0;i<tokens_list.length;i++) {
const gasToken = tokens_list[i];
const gasToken = ethers.utils.getAddress(tokens_list[i]);
const isCoingeckoAvailable = this.coingeckoPrice.get(`${chainId}-${gasToken}`);
if (
!(multiTokenPaymasters[chainId] && multiTokenPaymasters[chainId][gasToken]) &&
!(oracles[chainId] && oracles[chainId][gasToken])
!(oracles[chainId] && oracles[chainId][gasToken]) &&
!isCoingeckoAvailable
) unsupportedTokens.push({ token: gasToken });
else {
const oracleAddress = oracles[chainId][gasToken];
if (oracleName === "orochi") {
if (isCoingeckoAvailable) {
promises.push(this.getPriceFromCoingecko(chainId, gasToken, ETHUSDPrice, ETHUSDPriceDecimal))
} else if (oracleName === "orochi") {
promises.push(this.getPriceFromOrochi(oracleAddress, provider, gasToken, chainId));
} else if(oracleName === "chainlink") {
promises.push(this.getPriceFromChainlink(oracleAddress, provider, gasToken, ETHUSDPrice, ETHUSDPriceDecimal, chainId));
Expand Down Expand Up @@ -625,7 +641,14 @@ export class Paymaster {
const paymasterContract = new ethers.Contract(paymasterAddress, MultiTokenPaymasterAbi, provider);
let ethPrice = "";

if (oracleName === "orochi") {
const isCoingeckoAvailable = this.coingeckoPrice.get(`${chainId}-${feeToken}`);

if (isCoingeckoAvailable) {
const {latestAnswer, decimals} = await this.getLatestAnswerAndDecimals(provider, nativeOracleAddress, chainId);
const data = await this.getPriceFromCoingecko(chainId, feeToken, latestAnswer, decimals);

ethPrice = data.ethPrice;
} else if (oracleName === "orochi") {
const data = await this.getPriceFromOrochi(oracleAggregator, provider, feeToken, chainId);
ethPrice = data.ethPrice;
} else if (oracleName === "chainlink") {
Expand Down Expand Up @@ -974,7 +997,7 @@ export class Paymaster {
throw new Error(ErrorMessage.ERROR_ON_SUBMITTING_TXN);
}
}

async deployVp(
privateKey: string,
bundlerRpcUrl: string,
Expand Down Expand Up @@ -1074,4 +1097,47 @@ export class Paymaster {
throw new Error(ErrorMessage.FAILED_TO_ADD_STAKE);
}
}

async getPriceFromCoingecko(chainId: number, tokenAddress: string, ETHUSDPrice: any, ETHUSDPriceDecimal: any): Promise<any> {
const cacheKey = `${chainId}-${tokenAddress}`;
const cache = this.coingeckoPrice.get(cacheKey);

const nativePrice = ethers.utils.formatUnits(ETHUSDPrice, ETHUSDPriceDecimal);
let ethPrice;

if(cache && cache.expiry > Date.now()) {
const data = cache.data;
ethPrice = ethers.utils.parseUnits((Number(nativePrice)/data.price).toFixed(data.decimals), data.decimals)
return {
ethPrice,
...data
}
}

const coingeckoRepo = new CoingeckoTokensRepository(this.sequelize);
const records = await coingeckoRepo.findAll();
const tokenIds = records.map((record: { coinId: any; }) => record.coinId);

const data = await this.coingeckoService.fetchPriceByCoinID(tokenIds);
const tokenPrices: any = [];
records.map(record => {
tokenPrices[ethers.utils.getAddress(record.address)] = { price: Number(data[record.coinId].usd).toFixed(5), decimals: record.decimals, gasToken: tokenAddress, symbol: record.token }
})
const tokenData = tokenPrices[tokenAddress];
ethPrice = ethers.utils.parseUnits((Number(nativePrice)/tokenData.price).toFixed(tokenData.decimals), tokenData.decimals)
this.setPricesFromCoingecko(tokenPrices);

return {
ethPrice,
...tokenData
}
}

async setPricesFromCoingecko(coingeckoPrices: any[]) {
for(const tokenAddress in coingeckoPrices) {
const chainId = coingeckoPrices[tokenAddress].chainId;
const cacheKey = `${chainId}-${ethers.utils.getAddress(tokenAddress)}`;
this.coingeckoPrice.set(cacheKey, {data: coingeckoPrices[tokenAddress], expiry: Date.now() + ttl});
}
}
}
5 changes: 5 additions & 0 deletions backend/src/plugins/sequelizePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { WhitelistRepository } from "../repository/whitelist-repository.js";
import { initializeArkaWhitelistModel } from "../models/whitelist.js";
import { ContractWhitelistRepository } from "../repository/contract-whitelist-repository.js";
import { initializeContractWhitelistModel } from "../models/contract-whitelist.js";
import { CoingeckoTokensRepository } from "../repository/coingecko-token-repository.js";
import { initializeCoingeckoModel } from "../models/coingecko.js";
const pg = await import('pg');
const Client = pg.default.Client;

Expand Down Expand Up @@ -55,6 +57,7 @@ const sequelizePlugin: FastifyPluginAsync = async (server) => {
initializeSponsorshipPolicyModel(sequelize, server.config.DATABASE_SCHEMA_NAME);
initializeArkaWhitelistModel(sequelize, server.config.DATABASE_SCHEMA_NAME);
initializeContractWhitelistModel(sequelize, server.config.DATABASE_SCHEMA_NAME);
initializeCoingeckoModel(sequelize, server.config.DATABASE_SCHEMA_NAME);
server.log.info('Initialized SponsorshipPolicy model...');

server.log.info('Initialized all models...');
Expand All @@ -71,6 +74,8 @@ const sequelizePlugin: FastifyPluginAsync = async (server) => {
server.decorate('whitelistRepository', whitelistRepository);
const contractWhitelistRepository: ContractWhitelistRepository = new ContractWhitelistRepository(sequelize);
server.decorate('contractWhitelistRepository', contractWhitelistRepository);
const coingeckoRepo: CoingeckoTokensRepository = new CoingeckoTokensRepository(sequelize);
server.decorate('coingeckoRepo', coingeckoRepo);

server.log.info('decorated fastify server with models...');

Expand Down
Loading

0 comments on commit 7a2d589

Please sign in to comment.