diff --git a/.env.example b/.env.example index 5fcecb7..f925a0e 100644 --- a/.env.example +++ b/.env.example @@ -19,3 +19,4 @@ DATABASE_NAME= DATABASE_SCHEMA_NAME=arka DATABASE_SSL_ENABLED=false DATABASE_SSL_REJECT_UNAUTHORIZED=false +PRICE_UPDATE_CRON_EXP="0 0 * * *" diff --git a/admin_frontend/package-lock.json b/admin_frontend/package-lock.json index ef7e15c..333a723 100644 --- a/admin_frontend/package-lock.json +++ b/admin_frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "admin_frontend", - "version": "1.3.1", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "admin_frontend", - "version": "1.3.1", + "version": "1.4.0", "dependencies": { "@emotion/react": "11.11.3", "@emotion/styled": "11.11.0", diff --git a/admin_frontend/package.json b/admin_frontend/package.json index 30eb497..a2485ea 100644 --- a/admin_frontend/package.json +++ b/admin_frontend/package.json @@ -1,6 +1,6 @@ { "name": "admin_frontend", - "version": "1.3.1", + "version": "1.4.0", "private": true, "dependencies": { "@emotion/react": "11.11.3", diff --git a/backend/.env.example b/backend/.env.example index 9b1855a..4ee8d46 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -20,3 +20,5 @@ DATABASE_SSL_REJECT_UNAUTHORIZED="false" DATABASE_USER="" DATABASE_PASSWORD="" DATABASE_NAME="" + +PRICE_UPDATE_CRON_EXP="0 0 * * *" diff --git a/backend/migrations/2024080800001-create-contract-whitelist.cjs b/backend/migrations/2024080800001-create-contract-whitelist.cjs new file mode 100644 index 0000000..99e5178 --- /dev/null +++ b/backend/migrations/2024080800001-create-contract-whitelist.cjs @@ -0,0 +1,52 @@ +const { Sequelize } = require('sequelize') + +async function up({ context: queryInterface }) { + await queryInterface.createTable('contract_whitelist', { + ID: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false + }, + WALLET_ADDRESS: { + type: Sequelize.TEXT, + allowNull: false, + }, + CONTRACT_ADDRESS: { + type: Sequelize.TEXT, + allowNull: false, + }, + FUNCTION_SELECTORS: { + type: Sequelize.ARRAY(Sequelize.TEXT), + allowNull: false, + }, + ABI: { + type: Sequelize.TEXT, + allowNull: false, + }, + CHAIN_ID: { + 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: 'contract_whitelist', + schema: process.env.DATABASE_SCHEMA_NAME + }) +} + +module.exports = { up, down } \ No newline at end of file diff --git a/backend/migrations/2024080800002-update-apiKey.cjs b/backend/migrations/2024080800002-update-apiKey.cjs new file mode 100644 index 0000000..3175a32 --- /dev/null +++ b/backend/migrations/2024080800002-update-apiKey.cjs @@ -0,0 +1,11 @@ +require('dotenv').config(); + +async function up({ context: queryInterface }) { + await queryInterface.sequelize.query(`ALTER TABLE IF EXISTS "${process.env.DATABASE_SCHEMA_NAME}".api_keys ADD COLUMN "CONTRACT_WHITELIST_MODE" text default false`); +} + +async function down({ context: queryInterface }) { + await queryInterface.sequelize.query(`ALTER TABLE "${process.env.DATABASE_SCHEMA_NAME}".api_keys DROP COLUMN CONTRACT_WHITELIST_MODE;`); +} + +module.exports = { up, down } \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index 2738104..e370afc 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "arka", - "version": "1.3.2", + "version": "1.4.2", "description": "ARKA - (Albanian for Cashier's case) is the first open source Paymaster as a service software", "type": "module", "directories": { diff --git a/backend/src/constants/ErrorMessage.ts b/backend/src/constants/ErrorMessage.ts index 3f8226a..c988dcf 100644 --- a/backend/src/constants/ErrorMessage.ts +++ b/backend/src/constants/ErrorMessage.ts @@ -38,6 +38,11 @@ export default { ADDRESS_NOT_WHITELISTED: 'Addresses sent were not whitelisted', NO_WHITELIST_FOUND: 'No whitelist were found on the given apiKey/policyId', INVALID_ADDRESS_PASSSED: 'Invalid Address passed', + FAILED_TO_CREATE_CONTRACT_WHITELIST: 'Failed to create a record on contract whitelist', + FAILED_TO_UPDATE_CONTRACT_WHITELIST: 'Failed to update the record on contract whitelist', + FAILED_TO_DELETE_CONTRACT_WHITELIST: 'Failed to delete the record on contract whitelist', + NO_CONTRACT_WHITELIST_FOUND: 'No contract whitelist found for the given chainId, apiKey and contractAddress passed', + RECORD_ALREADY_EXISTS_CONTRACT_WHITELIST: 'Record already exists for the chainId, apiKey and contractAddress passed', } export function generateErrorMessage(template: string, values: { [key: string]: string | number }): string { diff --git a/backend/src/constants/Pimlico.ts b/backend/src/constants/Pimlico.ts index a226ede..bf5624b 100644 --- a/backend/src/constants/Pimlico.ts +++ b/backend/src/constants/Pimlico.ts @@ -127,10 +127,12 @@ export const CustomDeployedPaymasters: Record> = "FRAX": "0xE0221Db5bF2F3C22d6639a749B764f52f5B05dfb" }, "114": { - "USDC": "0x8b067387ec0B922483Eadb771bc9290194685522" + "USDC": "0x8b067387ec0B922483Eadb771bc9290194685522", + "USDT": "0xED35f8fa422Ba95A52A000236F1EAFd7e4fA4D52" }, "14": { - "eUSDT": "0x6Bb048981E67f1a0aD41c0BD05635244d3ADaA2c" + "eUSDT": "0x6Bb048981E67f1a0aD41c0BD05635244d3ADaA2c", + "eUSDC": "0xA5589D278778Eaae346383dD710D7913d8A6a2aA" }, "5001": { "USDCT": "0x6Ea25cbb60360243E871dD935225A293a78704a8" @@ -176,6 +178,12 @@ export const PAYMASTER_ADDRESS: Record> = { 80001: { USDC: "0x000000000009B901DeC1aaB9389285965F49D387" }, + 114: { + USDT: "0xED35f8fa422Ba95A52A000236F1EAFd7e4fA4D52" + }, + 14: { + eUSDC: "0xA5589D278778Eaae346383dD710D7913d8A6a2aA" + } } /** diff --git a/backend/src/models/api-key.ts b/backend/src/models/api-key.ts index e098534..6da8801 100644 --- a/backend/src/models/api-key.ts +++ b/backend/src/models/api-key.ts @@ -14,6 +14,7 @@ export class APIKey extends Model { public transactionLimit!: number; public noOfTransactionsInAMonth?: number | null; public indexerEndpoint?: string | null; + public contractWhitelistMode?: boolean | null; public createdAt!: Date; public updatedAt!: Date; } @@ -87,6 +88,11 @@ export function initializeAPIKeyModel(sequelize: Sequelize, schema: string) { allowNull: true, field: 'INDEXER_ENDPOINT' }, + contractWhitelistMode: { + type: DataTypes.BOOLEAN, + allowNull: true, + field: 'CONTRACT_WHITELIST_MODE' + }, createdAt: { type: DataTypes.DATE, allowNull: false, diff --git a/backend/src/models/contract-whitelist.ts b/backend/src/models/contract-whitelist.ts new file mode 100644 index 0000000..89d3e74 --- /dev/null +++ b/backend/src/models/contract-whitelist.ts @@ -0,0 +1,71 @@ +import { Sequelize, DataTypes, Model } from 'sequelize'; + +export class ContractWhitelist extends Model { + public id!: number; + public walletAddress!: string; + public contractAddress!: string; + public functionSelectors!: string[]; + public abi!: string; + public chainId!: number; + public createdAt!: Date; + public updatedAt!: Date; +} + +export function initializeContractWhitelistModel(sequelize: Sequelize, schema: string) { + const initializedContractWhitelistModel = ContractWhitelist.init({ + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + field: 'ID' + }, + walletAddress: { + type: DataTypes.TEXT, + allowNull: false, + field: 'WALLET_ADDRESS' + }, + contractAddress: { + type: DataTypes.TEXT, + allowNull: false, + field: 'CONTRACT_ADDRESS' + }, + functionSelectors: { + type: DataTypes.ARRAY(DataTypes.TEXT), + allowNull: false, + field: 'FUNCTION_SELECTORS' + }, + abi: { + type: DataTypes.TEXT, + allowNull: false, + field: 'ABI' + }, + chainId: { + type: DataTypes.INTEGER, + allowNull: false, + field: 'CHAIN_ID' + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + field: 'CREATED_AT', + defaultValue: DataTypes.NOW, + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + field: 'UPDATED_AT', + defaultValue: DataTypes.NOW, + }, + }, { + tableName: 'contract_whitelist', + sequelize, // passing the `sequelize` instance is required + modelName: '', + timestamps: true, // enabling timestamps + createdAt: 'createdAt', // mapping 'createdAt' to 'CREATED_AT' + updatedAt: 'updatedAt', // mapping 'updatedAt' to 'UPDATED_AT' + freezeTableName: true, + schema: schema, + }); + + return initializedContractWhitelistModel; +} \ No newline at end of file diff --git a/backend/src/paymaster/index.ts b/backend/src/paymaster/index.ts index 4d12009..315b8a2 100644 --- a/backend/src/paymaster/index.ts +++ b/backend/src/paymaster/index.ts @@ -16,11 +16,15 @@ import ERC20PaymasterV07Abi from '../abi/ERC20PaymasterV07Abi.js'; export class Paymaster { feeMarkUp: BigNumber; multiTokenMarkUp: number; + EP7_TOKEN_VGL: string; + EP7_TOKEN_PGL: string; - constructor(feeMarkUp: string, multiTokenMarkUp: string) { + constructor(feeMarkUp: string, multiTokenMarkUp: string, ep7TokenVGL: string, ep7TokenPGL: string) { 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; } packUint (high128: BigNumberish, low128: BigNumberish): string { @@ -325,8 +329,8 @@ export class Paymaster { throw new Error(`The required token amount ${tokenAmountRequired.toString()} is more than what the sender has ${tokenBalance}`) if (estimate) { userOp.paymaster = paymasterAddress; - userOp.paymasterVerificationGasLimit = BigNumber.from('60000').toHexString(); - userOp.paymasterPostOpGasLimit = BigNumber.from('100000').toHexString(); + userOp.paymasterVerificationGasLimit = BigNumber.from(this.EP7_TOKEN_VGL).toHexString(); + userOp.paymasterPostOpGasLimit = BigNumber.from(this.EP7_TOKEN_PGL).toHexString(); const response = await provider.send('eth_estimateUserOperationGas', [userOp, entryPoint]); userOp.verificationGasLimit = response.verificationGasLimit; userOp.callGasLimit = response.callGasLimit; @@ -340,8 +344,8 @@ export class Paymaster { preVerificationGas: BigNumber.from(userOp.preVerificationGas).toHexString(), verificationGasLimit: BigNumber.from(userOp.verificationGasLimit).toHexString(), callGasLimit: BigNumber.from(userOp.callGasLimit).toHexString(), - paymasterVerificationGasLimit: BigNumber.from('60000').toHexString(), - paymasterPostOpGasLimit: BigNumber.from('100000').toHexString() + paymasterVerificationGasLimit: BigNumber.from(this.EP7_TOKEN_VGL).toHexString(), + paymasterPostOpGasLimit: BigNumber.from(this.EP7_TOKEN_PGL).toHexString() } } else { returnValue = { diff --git a/backend/src/plugins/config.ts b/backend/src/plugins/config.ts index 41dcc69..c759720 100644 --- a/backend/src/plugins/config.ts +++ b/backend/src/plugins/config.ts @@ -25,6 +25,8 @@ const ConfigSchema = Type.Strict( DATABASE_SCHEMA_NAME: Type.String() || undefined, HMAC_SECRET: Type.String({ minLength: 1 }), UNSAFE_MODE: Type.Boolean() || undefined, + EP7_TOKEN_VGL: Type.String() || '90000', + EP7_TOKEN_PGL: Type.String() || '150000' }) ); @@ -64,6 +66,8 @@ const configPlugin: FastifyPluginAsync = async (server) => { DATABASE_SCHEMA_NAME: process.env.DATABASE_SCHEMA_NAME ?? 'arka', HMAC_SECRET: process.env.HMAC_SECRET ?? '', UNSAFE_MODE: process.env.UNSAFE_MODE === 'true', + EP7_TOKEN_VGL: process.env.EP7_TOKEN_VGL ?? '90000', + EP7_TOKEN_PGL: process.env.EP7_TOKEN_PGL ?? '150000' } server.decorate("config", config); diff --git a/backend/src/plugins/sequelizePlugin.ts b/backend/src/plugins/sequelizePlugin.ts index 4f094b9..7563416 100644 --- a/backend/src/plugins/sequelizePlugin.ts +++ b/backend/src/plugins/sequelizePlugin.ts @@ -10,6 +10,8 @@ import { ArkaConfigRepository } from "../repository/arka-config-repository.js"; import { SponsorshipPolicyRepository } from "../repository/sponsorship-policy-repository.js"; 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"; const pg = await import('pg'); const Client = pg.default.Client; @@ -52,6 +54,7 @@ const sequelizePlugin: FastifyPluginAsync = async (server) => { server.log.info(`Initialized APIKey model... ${sequelize.models.APIKey}`); initializeSponsorshipPolicyModel(sequelize, server.config.DATABASE_SCHEMA_NAME); initializeArkaWhitelistModel(sequelize, server.config.DATABASE_SCHEMA_NAME); + initializeContractWhitelistModel(sequelize, server.config.DATABASE_SCHEMA_NAME); server.log.info('Initialized SponsorshipPolicy model...'); server.log.info('Initialized all models...'); @@ -66,6 +69,8 @@ const sequelizePlugin: FastifyPluginAsync = async (server) => { server.decorate('sponsorshipPolicyRepository', sponsorshipPolicyRepository); const whitelistRepository: WhitelistRepository = new WhitelistRepository(sequelize); server.decorate('whitelistRepository', whitelistRepository); + const contractWhitelistRepository: ContractWhitelistRepository = new ContractWhitelistRepository(sequelize); + server.decorate('contractWhitelistRepository', contractWhitelistRepository); server.log.info('decorated fastify server with models...'); @@ -83,6 +88,7 @@ declare module "fastify" { arkaConfigRepository: ArkaConfigRepository; sponsorshipPolicyRepository: SponsorshipPolicyRepository; whitelistRepository: WhitelistRepository; + contractWhitelistRepository: ContractWhitelistRepository; } } diff --git a/backend/src/repository/contract-whitelist-repository.ts b/backend/src/repository/contract-whitelist-repository.ts new file mode 100644 index 0000000..4a4045b --- /dev/null +++ b/backend/src/repository/contract-whitelist-repository.ts @@ -0,0 +1,118 @@ +import { Sequelize } from 'sequelize'; +import { ContractWhitelist } from '../models/contract-whitelist.js'; +import { ContractWhitelistDto } from '../types/contractWhitelist-dto.js'; + +export class ContractWhitelistRepository { + private sequelize: Sequelize; + + constructor(sequelize: Sequelize) { + this.sequelize = sequelize; + } + + async create(record: ContractWhitelistDto): Promise { + + // generate APIKey sequelize model instance from APIKeyDto + const result = await this.sequelize.models.ContractWhitelist.create({ + walletAddress: record.walletAddress, + contractAddress: record.contractAddress, + functionSelectors: record.functionSelectors, + abi: record.abi, + chainId: record.chainId, + }) as ContractWhitelist; + + + + return result; + } + + async delete(walletAddress: string): Promise { + const deletedCount = await this.sequelize.models.ContractWhitelist.destroy({ + where + : { walletAddress: walletAddress } + }); + + if (deletedCount === 0) { + throw new Error('Wallet Address deletion failed'); + } + + return deletedCount; + } + + async findAll(): Promise { + const result = await this.sequelize.models.ContractWhitelist.findAll(); + return result.map(id => id.get() as ContractWhitelist); + } + + async findOneByChainIdContractAddressAndWalletAddress(chainId: number, walletAddress: string, contractAddress: string): Promise { + const result = await this.sequelize.models.ContractWhitelist.findOne({ + where: { + chainId: chainId, walletAddress: walletAddress, contractAddress: contractAddress + } + }) as ContractWhitelist; + + if (!result) { + return null; + } + + return result.get() as ContractWhitelist; + } + + async findOneById(id: number): Promise { + const contractWhitelist = await this.sequelize.models.ContractWhitelist.findOne({ where: { id: id } }) as ContractWhitelist; + if (!contractWhitelist) { + return null; + } + + const dataValues = contractWhitelist.get(); + return dataValues as ContractWhitelist; + } + + async updateOneById(record: ContractWhitelist): Promise { + const result = await this.sequelize.models.ContractWhitelist.update({ + functionSelectors: record.functionSelectors, + contractAddress: record.contractAddress, + abi: record.abi + }, { + where: { id: record.id } + }) + + if (result[0] === 0) { + throw new Error(`ContractWhitelist update failed for id: ${record.id}`); + } + + // return the updated record - fetch fresh from database + const updatedWhitelist = await this.findOneById(record.id); + return updatedWhitelist as ContractWhitelist; + } + + async deleteById(id: number): Promise { + + const deletedCount = await this.sequelize.models.ContractWhitelist.destroy({ + where + : { id: id } + }); + + return deletedCount; + } + + async deleteAllByWalletAddress(address: string): Promise { + + const deletedCount = await this.sequelize.models.ContractWhitelist.destroy({ + where + : { walletAddress: address } + }); + + return deletedCount; + } + + async deleteAllWhitelist(): Promise<{ message: string }> { + try { + await this.sequelize.models.ContractWhitelist.destroy({ where: {} }); + return { message: 'Successfully deleted all whitelist' }; + } catch (err) { + console.error(err); + throw new Error('Failed to delete all contract whitelist'); + } + } + +} \ No newline at end of file diff --git a/backend/src/routes/paymaster-routes.ts b/backend/src/routes/paymaster-routes.ts index fc4577c..83cc7f7 100644 --- a/backend/src/routes/paymaster-routes.ts +++ b/backend/src/routes/paymaster-routes.ts @@ -19,7 +19,7 @@ const SUPPORTED_ENTRYPOINTS = { } const paymasterRoutes: FastifyPluginAsync = async (server) => { - const paymaster = new Paymaster(server.config.FEE_MARKUP, server.config.MULTI_TOKEN_MARKUP); + const paymaster = new Paymaster(server.config.FEE_MARKUP, server.config.MULTI_TOKEN_MARKUP, server.config.EP7_TOKEN_VGL, server.config.EP7_TOKEN_PGL); const prefixSecretId = 'arka_'; @@ -88,6 +88,7 @@ const paymasterRoutes: FastifyPluginAsync = async (server) => { let txnMode; let indexerEndpoint; let sponsorName = '', sponsorImage = ''; + let contractWhitelistMode = false; if (!unsafeMode) { const AWSresponse = await client.send( new GetSecretValueCommand({ @@ -122,6 +123,7 @@ const paymasterRoutes: FastifyPluginAsync = async (server) => { noOfTxns = secrets['NO_OF_TRANSACTIONS_IN_A_MONTH'] ?? 10; txnMode = secrets['TRANSACTION_LIMIT'] ?? 0; indexerEndpoint = secrets['INDEXER_ENDPOINT'] ?? process.env.DEFAULT_INDEXER_ENDPOINT; + contractWhitelistMode = (secrets['CONTRACT_WHITELIST_MODE'] ?? false) == 'true' ? true : false; } else { //validate api_key @@ -163,6 +165,7 @@ const paymasterRoutes: FastifyPluginAsync = async (server) => { noOfTxns = apiKeyEntity.noOfTransactionsInAMonth; txnMode = apiKeyEntity.transactionLimit; indexerEndpoint = apiKeyEntity.indexerEndpoint ?? process.env.DEFAULT_INDEXER_ENDPOINT; + contractWhitelistMode = apiKeyEntity.contractWhitelistMode ?? false; } if ( @@ -239,6 +242,10 @@ const paymasterRoutes: FastifyPluginAsync = async (server) => { } 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 (entryPoint == SUPPORTED_ENTRYPOINTS.EPV_06) result = await paymaster.signV06(userOp, str, str1, entryPoint, networkConfig.contracts.etherspotPaymasterAddress, networkConfig.bundler, signer, estimate, server.log); else { @@ -340,6 +347,66 @@ const paymasterRoutes: FastifyPluginAsync = async (server) => { return []; } } + + // This works only when used with etherspot-modular-sdk & etherspot-prime-sdk + async function checkContractWhitelist(callData: string, chainId: number, walletAddress: string): Promise { + let returnValue = true; + const bytes4 = callData.substring(0, 10); + if (bytes4 === '0xe9ae5c53') { // fn executeBatch encoding on epv7 + const iface = new ethers.utils.Interface(['function execute(bytes32, bytes)']); + const decodedData = iface.decodeFunctionData('execute', callData); + const txnDatas = ethers.utils.defaultAbiCoder.decode( + ["tuple(address target,uint256 value,bytes callData)[]"], + decodedData[1], + true + ); + for (let i=0;i { - const paymaster = new Paymaster(server.config.FEE_MARKUP, server.config.MULTI_TOKEN_MARKUP); + const paymaster = new Paymaster(server.config.FEE_MARKUP, server.config.MULTI_TOKEN_MARKUP, server.config.EP7_TOKEN_VGL, server.config.EP7_TOKEN_PGL); const prefixSecretId = 'arka_'; @@ -349,12 +350,10 @@ const whitelistRoutes: FastifyPluginAsync = async (server) => { const query: any = request.query; const address = body.params[0]; const policyId = body.params[1]; - const chainId = query['chainId'] ?? body.params[2]; - const api_key = query['apiKey'] ?? body.params[3]; + const api_key = query['apiKey'] ?? body.params[2]; if (!api_key) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) let privateKey = ''; - let supportedNetworks; if (!unsafeMode) { const AWSresponse = await client.send( new GetSecretValueCommand({ @@ -364,33 +363,27 @@ const whitelistRoutes: FastifyPluginAsync = async (server) => { const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) privateKey = secrets['PRIVATE_KEY']; - supportedNetworks = secrets['SUPPORTED_NETWORKS']; } else { const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) privateKey = decode(apiKeyEntity.privateKey, server.config.HMAC_SECRET); - supportedNetworks = apiKeyEntity.supportedNetworks; } if (!privateKey) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if ( !Array.isArray(address) || - address.length > 10 || - !chainId || - isNaN(chainId) + address.length > 10 ) { return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); } if (server.config.SUPPORTED_NETWORKS == '' && !SupportedNetworks) { return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); } - const networkConfig = getNetworkConfig(chainId, supportedNetworks ?? '', SUPPORTED_ENTRYPOINTS.EPV_07); - if (!networkConfig) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); const validAddresses = address.every(ethers.utils.isAddress); if (!validAddresses) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_ADDRESS_PASSSED }); const signer = new Wallet(privateKey) if (policyId) { const policyRecord = await server.sponsorshipPolicyRepository.findOneById(policyId); - if (!policyRecord || (policyRecord?.walletAddress !== signer.address)) return reply.code(ReturnCode.FAILURE).send({error: ErrorMessage.INVALID_SPONSORSHIP_POLICY_ID }) + if (!policyRecord || (policyRecord?.walletAddress !== signer.address)) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_SPONSORSHIP_POLICY_ID }) } const existingWhitelistRecord = await server.whitelistRepository.findOneByApiKeyAndPolicyId(api_key, policyId); @@ -485,6 +478,199 @@ const whitelistRoutes: FastifyPluginAsync = async (server) => { } } ) + + server.post("/whitelistContractAddress", + async function (request, reply) { + try { + printRequest("/whitelistContractAddress", request, server.log); + const contractWhitelistDto: ContractWhitelistDto = JSON.parse(JSON.stringify(request.body)) as ContractWhitelistDto; + const query: any = request.query; + + const chainId = query['chainId']; + const api_key = query['apiKey']; + if (!api_key) + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + let privateKey = ''; + let supportedNetworks; + if (!unsafeMode) { + const AWSresponse = await client.send( + new GetSecretValueCommand({ + SecretId: prefixSecretId + api_key, + }) + ); + const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + privateKey = secrets['PRIVATE_KEY']; + supportedNetworks = secrets['SUPPORTED_NETWORKS']; + } else { + const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); + if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + privateKey = decode(apiKeyEntity.privateKey, server.config.HMAC_SECRET); + supportedNetworks = apiKeyEntity.supportedNetworks; + } + if (!privateKey) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + if ( + !chainId || + isNaN(chainId) + ) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); + } + if (server.config.SUPPORTED_NETWORKS == '' && !SupportedNetworks) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + } + const networkConfig = getNetworkConfig(chainId, supportedNetworks ?? '', SUPPORTED_ENTRYPOINTS.EPV_07); + if (!networkConfig) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + const provider = new providers.JsonRpcProvider(networkConfig.bundler); + const signer = new Wallet(privateKey, provider) + + const existingRecord = await server.contractWhitelistRepository.findOneByChainIdContractAddressAndWalletAddress(chainId, signer.address, contractWhitelistDto.contractAddress); + if (existingRecord) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.RECORD_ALREADY_EXISTS_CONTRACT_WHITELIST }) + + contractWhitelistDto.chainId = Number(chainId); + contractWhitelistDto.walletAddress = signer.address; + + const result = await server.contractWhitelistRepository.create(contractWhitelistDto); + if (!result) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.FAILED_TO_CREATE_CONTRACT_WHITELIST }); + } + + return reply.code(ReturnCode.SUCCESS).send(result); + } catch (err: any) { + request.log.error(err); + if (err.name == "ResourceNotFoundException") + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_CREATE_CONTRACT_WHITELIST }); + } + } + ) + + server.post("/updateWhitelistContractAddress", + async function (request, reply) { + try { + printRequest("/updateWhitelistContractAddress", request, server.log); + const contractWhitelistDto: ContractWhitelistDto = JSON.parse(JSON.stringify(request.body)) as ContractWhitelistDto; + const query: any = request.query; + const chainId = query['chainId']; + const api_key = query['apiKey']; + if (!api_key) + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + let privateKey = ''; + let supportedNetworks; + if (!unsafeMode) { + const AWSresponse = await client.send( + new GetSecretValueCommand({ + SecretId: prefixSecretId + api_key, + }) + ); + const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + privateKey = secrets['PRIVATE_KEY']; + supportedNetworks = secrets['SUPPORTED_NETWORKS']; + } else { + const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); + if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + privateKey = decode(apiKeyEntity.privateKey, server.config.HMAC_SECRET); + supportedNetworks = apiKeyEntity.supportedNetworks; + } + if (!privateKey) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + if ( + !chainId || + isNaN(chainId) + ) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); + } + if (server.config.SUPPORTED_NETWORKS == '' && !SupportedNetworks) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + } + const networkConfig = getNetworkConfig(chainId, supportedNetworks ?? '', SUPPORTED_ENTRYPOINTS.EPV_07); + if (!networkConfig) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + const provider = new providers.JsonRpcProvider(networkConfig.bundler); + const signer = new Wallet(privateKey, provider) + + const existingRecord = await server.contractWhitelistRepository.findOneByChainIdContractAddressAndWalletAddress(chainId, signer.address, contractWhitelistDto.contractAddress); + if (!existingRecord) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.NO_CONTRACT_WHITELIST_FOUND }) + + existingRecord.contractAddress = contractWhitelistDto.contractAddress; + existingRecord.functionSelectors = contractWhitelistDto.functionSelectors; + existingRecord.abi = contractWhitelistDto.abi; + + const result = await server.contractWhitelistRepository.updateOneById(existingRecord); + if (!result) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.FAILED_TO_UPDATE_CONTRACT_WHITELIST }); + } + + return reply.code(ReturnCode.SUCCESS).send(result); + } catch (err: any) { + request.log.error(err); + if (err.name == "ResourceNotFoundException") + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_UPDATE_CONTRACT_WHITELIST }); + } + } + ) + + + server.post("/deleteContractWhitelist", + async function (request, reply) { + try { + printRequest("/deleteContractWhitelist", request, server.log); + const contractWhitelistDto: ContractWhitelistDto = JSON.parse(JSON.stringify(request.body)) as ContractWhitelistDto; + const query: any = request.query; + const chainId = query['chainId']; + const api_key = query['apiKey']; + if (!api_key) + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + let privateKey = ''; + let supportedNetworks; + if (!unsafeMode) { + const AWSresponse = await client.send( + new GetSecretValueCommand({ + SecretId: prefixSecretId + api_key, + }) + ); + const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + privateKey = secrets['PRIVATE_KEY']; + supportedNetworks = secrets['SUPPORTED_NETWORKS']; + } else { + const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); + if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + privateKey = decode(apiKeyEntity.privateKey, server.config.HMAC_SECRET); + supportedNetworks = apiKeyEntity.supportedNetworks; + } + if (!privateKey) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + if ( + !chainId || + isNaN(chainId) + ) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); + } + if (server.config.SUPPORTED_NETWORKS == '' && !SupportedNetworks) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + } + const networkConfig = getNetworkConfig(chainId, supportedNetworks ?? '', SUPPORTED_ENTRYPOINTS.EPV_07); + if (!networkConfig) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + const provider = new providers.JsonRpcProvider(networkConfig.bundler); + const signer = new Wallet(privateKey, provider) + + const existingRecord = await server.contractWhitelistRepository.findOneByChainIdContractAddressAndWalletAddress(chainId, signer.address, contractWhitelistDto.contractAddress); + if (!existingRecord) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.NO_CONTRACT_WHITELIST_FOUND }) + + const result = await server.contractWhitelistRepository.deleteById(existingRecord.id); + if (!result) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.FAILED_TO_DELETE_CONTRACT_WHITELIST }); + } + + return reply.code(ReturnCode.SUCCESS).send(result); + } catch (err: any) { + request.log.error(err); + if (err.name == "ResourceNotFoundException") + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_DELETE_CONTRACT_WHITELIST }); + } + } + ) + }; export default whitelistRoutes; diff --git a/backend/src/server.ts b/backend/src/server.ts index f532946..5255ff5 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -77,8 +77,6 @@ const initializeServer = async (): Promise => { server.log.info('registered sequelizePlugin...') const arkaConfigRepository = new ArkaConfigRepository(server.sequelize); - const configDatas = await arkaConfigRepository.findAll(); - const configData: ArkaConfig | null = configDatas.length > 0 ? configDatas[0] : null; await server.register(fastifyCron, { jobs: [ @@ -86,14 +84,41 @@ const initializeServer = async (): Promise => { // Only these two properties are required, // the rest is from the node-cron API: // https://github.com/kelektiv/node-cron#api - cronTime: configData?.cronTime ?? '0 0 * * *', // Default: Everyday at midnight UTC, + cronTime: process.env.PRICE_UPDATE_CRON_EXP ?? '0 0 * * *', // Default: Everyday at midnight UTC, name: 'PriceUpdate', // Note: the callbacks (onTick & onComplete) take the server // as an argument, as opposed to nothing in the node-cron API: onTick: async () => { + let configData: any if (process.env.CRON_PRIVATE_KEY) { - const paymastersAdrbase64 = configData?.deployedErc20Paymasters ?? '' + const unsafeMode = process.env.UNSAFE_MODE === "true" ? true : false; + if(!unsafeMode) { + const client = new SecretsManagerClient(); + const api_key = process.env.DEFAULT_API_KEY; + const prefixSecretId = "arka_"; + const AWSresponse = await client.send( + new GetSecretValueCommand({ + SecretId: prefixSecretId + api_key, + }) + ); + const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + configData = { + coingeckoApiUrl: secrets["COINGECKO_API_URL"], + coingeckoIds: secrets["COINGECKO_IDS"], + customChainlinkDeployed: secrets["CUSTOM_CHAINLINK_DEPLOYED"], + deployedErc20Paymasters: secrets["DEPLOYED_ERC20_PAYMASTERS"], + pythMainnetChainIds: secrets["PYTH_MAINNET_CHAINIDS"], + pythMainnetUrl: secrets["PYTH_MAINNET_URL"], + pythTestnetChainIds: secrets["PYTH_TESTNET_CHAINIDS"], + pythTestnetUrl: secrets["PYTH_TESTNET_URL"] + } + client.destroy(); + } else { + const configDatas = await arkaConfigRepository.findAll(); + configData = configDatas.length > 0 ? configDatas[0] : null; + } + const paymastersAdrbase64 = configData?.deployedErc20Paymasters ?? ''; if (paymastersAdrbase64) { const buffer = Buffer.from(paymastersAdrbase64, 'base64'); const DEPLOYED_ERC20_PAYMASTERS = JSON.parse(buffer.toString()); diff --git a/backend/src/types/contractWhitelist-dto.ts b/backend/src/types/contractWhitelist-dto.ts new file mode 100644 index 0000000..f6435fa --- /dev/null +++ b/backend/src/types/contractWhitelist-dto.ts @@ -0,0 +1,8 @@ + +export interface ContractWhitelistDto { + walletAddress: string; + contractAddress: string; + functionSelectors: string[]; + abi: string; + chainId: number; +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 93ea6c9..6830a46 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "arka_frontend", - "version": "1.3.1", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "arka_frontend", - "version": "1.3.1", + "version": "1.4.0", "dependencies": { "@babel/plugin-proposal-private-property-in-object": "7.21.11", "@emotion/react": "^11.11.1", diff --git a/frontend/package.json b/frontend/package.json index 9cbe057..9332314 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "arka_frontend", - "version": "1.3.1", + "version": "1.4.0", "private": true, "dependencies": { "@babel/plugin-proposal-private-property-in-object": "7.21.11",