Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PRO-2862 - Coingecko Changes #161

Merged
merged 13 commits into from
Jan 13, 2025
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.INTEGER,
nikhilkumar1612 marked this conversation as resolved.
Show resolved Hide resolved
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.

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
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);

nikhilkumar1612 marked this conversation as resolved.
Show resolved Hide resolved
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
40 changes: 40 additions & 0 deletions backend/src/repository/coingecko-token-repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Sequelize } from 'sequelize';
import { CoingeckoTokens } from 'models/coingecko';

export class CoingeckoTokensRepository {
private sequelize: Sequelize;

constructor(sequelize: Sequelize) {
this.sequelize = sequelize;
}

async findAll(): Promise<CoingeckoTokens[]> {
const result = await this.sequelize.models.CoingeckoTokens.findAll();
return result.map(id => id.get() as CoingeckoTokens);
}

async findOneByChainIdAndTokenAddress(chainId: number, tokenAddress: string): Promise<CoingeckoTokens | null> {
const result = await this.sequelize.models.CoingeckoTokens.findOne({
where: {
chainId: chainId, address: tokenAddress
}
}) as CoingeckoTokens;

if (!result) {
return null;
}

return result.get() as CoingeckoTokens;
}

async findOneById(id: number): Promise<CoingeckoTokens | null> {
const coingeckoTokens = await this.sequelize.models.CoingeckoTokens.findOne({ where: { id: id } }) as CoingeckoTokens;
if (!coingeckoTokens) {
return null;
}

const dataValues = coingeckoTokens.get();
return dataValues as CoingeckoTokens;
}

}
Loading