From b4ec24ba2d347d5617ac551a0b23fb9cfb3af2b0 Mon Sep 17 00:00:00 2001 From: kanth Date: Wed, 5 Jun 2024 15:40:35 +0530 Subject: [PATCH 01/23] feat: PRO-2395 Design doc for sponsorship policy management --- .../docs/sponsorship-policies-db-design.png | Bin 0 -> 67963 bytes .../docs/sponsorship-policies-db-design.puml | 47 +++++++++++ backend/docs/sponsorship-policies.MD | 79 ++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 backend/docs/sponsorship-policies-db-design.png create mode 100644 backend/docs/sponsorship-policies-db-design.puml create mode 100644 backend/docs/sponsorship-policies.MD diff --git a/backend/docs/sponsorship-policies-db-design.png b/backend/docs/sponsorship-policies-db-design.png new file mode 100644 index 0000000000000000000000000000000000000000..a11c72ac43fd5389f87f7edaa0d17266d612f281 GIT binary patch literal 67963 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 Fe*k#QU~T{a literal 0 HcmV?d00001 diff --git a/backend/docs/sponsorship-policies-db-design.puml b/backend/docs/sponsorship-policies-db-design.puml new file mode 100644 index 0000000..e3a5dc4 --- /dev/null +++ b/backend/docs/sponsorship-policies-db-design.puml @@ -0,0 +1,47 @@ +@startuml +' Define classes for tables with alias +class API_KEYS { + + API_KEY : TEXT (PK) + + WALLET_ADDRESS : TEXT + + PRIVATE_KEY : VARCHAR + + SUPPORTED_NETWORKS : VARCHAR + + ERC20_PAYMASTERS : VARCHAR + + MULTI_TOKEN_PAYMASTERS : VARCHAR + + MULTI_TOKEN_ORACLES : VARCHAR + + TRANSACTION_LIMIT : INT + + NO_OF_TRANSACTIONS_IN_A_MONTH : INT + + INDEXER_ENDPOINT : VARCHAR +} + +class POLICIES { + + POLICY_ID : INT (PK) + + WALLET_ADDRESS : TEXT (FK) + + NAME : VARCHAR + + DESCRIPTION : TEXT + + START_DATE : DATE + + END_DATE : DATE + + IS_PERPETUAL : BOOLEAN = FALSE + + IS_UNIVERSAL : BOOLEAN = FALSE + + CONTRACT_RESTRICTIONS : TEXT ' JSON storing contract addresses with function names and signatures ' +} + +class POLICY_LIMITS { + + LIMIT_ID : INT (PK) + + POLICY_ID : INT (FK) + + LIMIT_TYPE : VARCHAR + + MAX_USD : NUMERIC + + MAX_ETH : NUMERIC + + MAX_OPERATIONS : INT +} + +class POLICY_CHAINS { + + POLICY_CHAIN_ID : INT (PK) + + POLICY_ID : INT (FK) + + CHAIN_NAME : VARCHAR +} + +' Define relationships +API_KEYS "1" -- "many" POLICIES : contains > +POLICIES "1" -- "many" POLICY_LIMITS : contains > +POLICIES "1" -- "many" POLICY_CHAINS : contains > +@enduml diff --git a/backend/docs/sponsorship-policies.MD b/backend/docs/sponsorship-policies.MD new file mode 100644 index 0000000..ca0dd7e --- /dev/null +++ b/backend/docs/sponsorship-policies.MD @@ -0,0 +1,79 @@ +# Arka Sponsorship Policies + +Arka needs to have the ability to setup sponsorship policies, these will be offered as a backend API within Arka and this can be consumed by our developer dashboard. + +## Reference Create Policy Dashboard + +Sponsorship Policies are created for a wallet address (sponsor address) +Policies drive the whether the UserOp should be sponsored +Constraints for validation is based on policy-configuration params like: + +1. A Policy can be perpetual or can have a start and end Dates + +2. Policy can be made applicable to selected set of chainIds or have a global applicability for all supported chains for that wallet address + +3. 3 Types of Limits can be defined for the Policy: + - GLOBAL : Limit the total amount of USD or Native Tokens you are willing to sponsor. + - PER_USER: Limit the amount of USD or Native Tokens you are willing to sponsor per user. + - PER_OPERATION: Limit the amount of USD or Native Tokens you are willing to sponsor per user operation. + + - For Each LimitType, the LimitValues to be defined are: + + - GLOBAL + 1. Maximum USD: The maximum amount of USD this policy will sponsor globally. + 2. Maximum ETH: The maximum amount of ETH this policy will sponsor globally. + 3. Maximum Number of UserOperations: The maximum number of User Operations this policy will sponsor globally. + + - PER_USER + 1. Maximum USD: The maximum amount of USD this policy will sponsor per user. + 2. Maximum ETH: The maximum amount of ETH this policy will sponsor per user. + 3. Maximum Number of UserOperations: The maximum number of User Operations this policy will sponsor per user. + + - PER_OPERATION + 1. Maximum USD: The maximum amount of USD this policy will sponsor per user operation. + 2. Maximum ETH: The maximum amount of ETH this policy will sponsor per user operation. + +4. Destination Contract Address & Function Filter + +- A JSON-formatted string that stores an array of objects, each containing a contractAddress and a functionName. +- This field allows specifying which contract functions are eligible for sponsorship under this policy. +- The JSON structure provides flexibility and can be easily extended or modified as requirements evolve. + +JSON Structure for CONTRACT_RESTRICTIONS +To facilitate effective checks and validations, store both the function name and its signature in the JSON structure: + +```json +[ + { + "contractAddress": "0x123abc...", + "functions": [ + { + "name": "transfer", + "signature": "transfer(address,uint256)" + }, + { + "name": "approve", + "signature": "approve(address,uint256)" + } + ] + }, + { + "contractAddress": "0x456def...", + "functions": [ + { + "name": "mint", + "signature": "mint(address,uint256)" + }, + { + "name": "burn", + "signature": "burn(uint256)" + } + ] + } +] +``` + + +### DB Design Model + +![sponsorship-policies-db-design](sponsorship-policies-db-design.png) From df5476abb684ef3a5751489d924df402a8362dc3 Mon Sep 17 00:00:00 2001 From: kanth Date: Thu, 6 Jun 2024 14:07:01 +0530 Subject: [PATCH 02/23] feat: PRO-2395 sponsorship policy db migration --- .../src/migrations/003.sponsorship-policy.sql | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 backend/src/migrations/003.sponsorship-policy.sql diff --git a/backend/src/migrations/003.sponsorship-policy.sql b/backend/src/migrations/003.sponsorship-policy.sql new file mode 100644 index 0000000..6c9d116 --- /dev/null +++ b/backend/src/migrations/003.sponsorship-policy.sql @@ -0,0 +1,45 @@ +-------------------------------------------------------------------------------- +-- Up +-------------------------------------------------------------------------------- + +-- Create POLICIES Table +CREATE TABLE IF NOT EXISTS policies ( + POLICY_ID INTEGER PRIMARY KEY, + WALLET_ADDRESS TEXT NOT NULL, + NAME TEXT NOT NULL, + DESCRIPTION TEXT, + START_DATE DATE, + END_DATE DATE, + IS_PERPETUAL BOOLEAN DEFAULT FALSE, + IS_UNIVERSAL BOOLEAN DEFAULT FALSE, + CONTRACT_RESTRICTIONS TEXT, -- Stores JSON string because SQLite doesn't support JSON natively + FOREIGN KEY (WALLET_ADDRESS) REFERENCES api_keys(WALLET_ADDRESS) +); + +-- Create POLICY_LIMITS Table +CREATE TABLE IF NOT EXISTS policy_limits ( + LIMIT_ID INTEGER PRIMARY KEY, + POLICY_ID INTEGER NOT NULL, + LIMIT_TYPE TEXT NOT NULL, + MAX_USD REAL, -- REAL used in SQLite for floating-point numbers + MAX_ETH REAL, + MAX_OPERATIONS INTEGER, + FOREIGN KEY (POLICY_ID) REFERENCES policies(POLICY_ID) +); + +-- Create POLICY_CHAINS Table +CREATE TABLE IF NOT EXISTS policy_chains ( + POLICY_CHAIN_ID INTEGER PRIMARY KEY, + POLICY_ID INTEGER NOT NULL, + CHAIN_NAME TEXT NOT NULL, + FOREIGN KEY (POLICY_ID) REFERENCES policies(POLICY_ID) +); + + +-------------------------------------------------------------------------------- +-- Down +-------------------------------------------------------------------------------- + +DROP TABLE IF EXISTS policy_chains; +DROP TABLE IF EXISTS policy_limits; +DROP TABLE IF EXISTS policies; From ce1a9a2d544c691803385d4bec57fe1f185ec0a1 Mon Sep 17 00:00:00 2001 From: kanth Date: Fri, 7 Jun 2024 11:34:49 +0530 Subject: [PATCH 03/23] feat: PRO-2395 sequelize integration to backend and property name updates for APIKey entity references in backend and frontend components --- .gitignore | 29 +++++ admin_frontend/.gitignore | 1 + admin_frontend/src/components/ApiKeys.jsx | 26 ++-- backend/.gitignore | 4 +- backend/src/models/APIKey.ts | 86 +++++++++++++ backend/src/plugins/sequelizePlugin.ts | 26 ++++ backend/src/repository/APIKeyRepository.ts | 25 ++++ backend/src/routes/admin.ts | 141 +++++++++------------ backend/src/routes/index.ts | 92 ++++++++------ backend/src/routes/metadata.ts | 25 ++-- backend/src/server.ts | 27 ++-- backend/src/utils/common.ts | 18 +-- frontend/.gitignore | 1 + package.json | 9 ++ 14 files changed, 339 insertions(+), 171 deletions(-) create mode 100644 .gitignore create mode 100644 backend/src/models/APIKey.ts create mode 100644 backend/src/plugins/sequelizePlugin.ts create mode 100644 backend/src/repository/APIKeyRepository.ts create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1f7dcdc --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# build +/build/ + +# misc +.DS_Store +.env.production + +# debug +npm-debug.log* + +.nyc_output +coverage + +.env +config.json +database.sqlite + +package-lock.json +pnpm-lock.yaml + +# Ponder +/indexer/.ponder +/indexer/generated +yarn.lock \ No newline at end of file diff --git a/admin_frontend/.gitignore b/admin_frontend/.gitignore index 4d29575..960b87d 100644 --- a/admin_frontend/.gitignore +++ b/admin_frontend/.gitignore @@ -21,3 +21,4 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +yarn.lock \ No newline at end of file diff --git a/admin_frontend/src/components/ApiKeys.jsx b/admin_frontend/src/components/ApiKeys.jsx index 2bfd197..9bb1fd1 100644 --- a/admin_frontend/src/components/ApiKeys.jsx +++ b/admin_frontend/src/components/ApiKeys.jsx @@ -135,12 +135,12 @@ const ApiKeysPage = () => { JSON.stringify(customErc20Paymaster) ).toString("base64"); const requestData = { - API_KEY: apiKey, - PRIVATE_KEY: privateKey, - SUPPORTED_NETWORKS: + apiKey: apiKey, + privateKey: privateKey, + supportedNetworks: Buffer.from(JSON.stringify(supportedNetworks)).toString("base64") ?? "", - ERC20_PAYMASTERS: base64Erc20 ?? "", + erc20Paymasters: base64Erc20 ?? "", }; const data = await fetch( `${process.env.REACT_APP_SERVER_URL}${ENDPOINTS["saveKey"]}`, @@ -280,12 +280,12 @@ const ApiKeysPage = () => { {keys.map((row, index) => ( - - {row.WALLET_ADDRESS} - {row.API_KEY} + + {row.walletAddress} + {row.apiKey}
-
{showPassword ? row.PRIVATE_KEY : "*****"}
+
{showPassword ? row.privateKey : "*****"}
{ @@ -324,7 +324,7 @@ const ApiKeysPage = () => { startIcon={} variant="contained" onClick={() => { - handleDelete(row.API_KEY); + handleDelete(row.apiKey); }} > Delete Row diff --git a/backend/.gitignore b/backend/.gitignore index 6b999ff..0b86d72 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -25,4 +25,6 @@ pnpm-lock.yaml # Ponder /indexer/.ponder -/indexer/generated \ No newline at end of file +/indexer/generated + +yarn.lock \ No newline at end of file diff --git a/backend/src/models/APIKey.ts b/backend/src/models/APIKey.ts new file mode 100644 index 0000000..0a181d1 --- /dev/null +++ b/backend/src/models/APIKey.ts @@ -0,0 +1,86 @@ +import { Sequelize, DataTypes, Model } from 'sequelize'; + +export class APIKey extends Model { + declare apiKey: string; + declare walletAddress: string; + declare privateKey: string; + declare supportedNetworks: string | null; + declare erc20Paymasters: string | null; + declare multiTokenPaymasters: string | null; + declare multiTokenOracles: string | null; + declare sponsorName: string | null; + declare logoUrl: string | null; + declare transactionLimit: number; + declare noOfTransactionsInAMonth: number | null; + declare indexerEndpoint: string | null; +} + +export function initializeAPIKeyModel(sequelize: Sequelize) { + APIKey.init({ + apiKey: { + type: DataTypes.TEXT, + allowNull: false, + primaryKey: true, + field: 'API_KEY' + }, + walletAddress: { + type: DataTypes.TEXT, + allowNull: false, + field: 'WALLET_ADDRESS' + }, + privateKey: { + type: DataTypes.STRING, + allowNull: false, + field: 'PRIVATE_KEY' + }, + supportedNetworks: { + type: DataTypes.STRING, + allowNull: true, + field: 'SUPPORTED_NETWORKS' + }, + erc20Paymasters: { + type: DataTypes.STRING, + allowNull: true, + field: 'ERC20_PAYMASTERS' + }, + multiTokenPaymasters: { + type: DataTypes.STRING, + allowNull: true, + field: 'MULTI_TOKEN_PAYMASTERS' + }, + multiTokenOracles: { + type: DataTypes.STRING, + allowNull: true, + field: 'MULTI_TOKEN_ORACLES' + }, + sponsorName: { + type: DataTypes.STRING, + allowNull: true, + field: 'SPONSOR_NAME' + }, + logoUrl: { + type: DataTypes.STRING, + allowNull: true, + field: 'LOGO_URL' + }, + transactionLimit: { + type: DataTypes.INTEGER, + allowNull: false, + field: 'TRANSACTION_LIMIT' + }, + noOfTransactionsInAMonth: { + type: DataTypes.INTEGER, + allowNull: true, + field: 'NO_OF_TRANSACTIONS_IN_A_MONTH' + }, + indexerEndpoint: { + type: DataTypes.STRING, + allowNull: true, + field: 'INDEXER_ENDPOINT' + }, + }, { + tableName: 'api_keys', + sequelize, // passing the `sequelize` instance is required + timestamps: false, // this will deactivate the `createdAt` and `updatedAt` columns + }); +} \ No newline at end of file diff --git a/backend/src/plugins/sequelizePlugin.ts b/backend/src/plugins/sequelizePlugin.ts new file mode 100644 index 0000000..349a09f --- /dev/null +++ b/backend/src/plugins/sequelizePlugin.ts @@ -0,0 +1,26 @@ +import fp from "fastify-plugin"; +import { FastifyPluginAsync } from "fastify"; +import { Sequelize } from 'sequelize'; +import sqlite3 from 'sqlite3'; + +const sequelizePlugin: FastifyPluginAsync = async (server) => { + const sequelize = new Sequelize({ + dialect: 'sqlite', + storage: './database.sqlite', + dialectModule: sqlite3 + }); + + server.decorate('sequelize', sequelize); + + server.addHook('onClose', (instance, done) => { + instance.sequelize.close().then(() => done(), done); + }); +}; + +declare module "fastify" { + interface FastifyInstance { + sequelize: Sequelize; + } +} + +export default fp(sequelizePlugin); \ No newline at end of file diff --git a/backend/src/repository/APIKeyRepository.ts b/backend/src/repository/APIKeyRepository.ts new file mode 100644 index 0000000..131085c --- /dev/null +++ b/backend/src/repository/APIKeyRepository.ts @@ -0,0 +1,25 @@ +import { Sequelize } from 'sequelize'; +import { APIKey } from '../models/APIKey'; + +export class APIKeyRepository { + private sequelize: Sequelize; + + constructor(sequelize: Sequelize) { + this.sequelize = sequelize; + } + + async findAll(): Promise { + const result = await this.sequelize.models.APIKey.findAll(); + return result.map(apiKey => apiKey.get() as APIKey); + } + + async findOneByApiKey(apiKey: string): Promise { + const result = await this.sequelize.models.APIKey.findOne({ where: { apiKey: apiKey } }); + return result ? result.get() as APIKey : null; + } + + async findOneByWalletAddress(walletAddress: string): Promise { + const result = await this.sequelize.models.APIKey.findOne({ where: { walletAddress: walletAddress } }); + return result ? result.get() as APIKey : null; + } +} \ No newline at end of file diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts index 5c851b9..0e1aa5e 100644 --- a/backend/src/routes/admin.ts +++ b/backend/src/routes/admin.ts @@ -6,16 +6,18 @@ import ErrorMessage from "../constants/ErrorMessage.js"; import ReturnCode from "../constants/ReturnCode.js"; import { encode, decode } from "../utils/crypto.js"; import SupportedNetworks from "../../config.json" assert { type: "json" }; +import { Op } from 'sequelize'; +import { APIKey } from "models/APIKey.js"; +import { APIKeyRepository } from "repository/APIKeyRepository.js"; const adminRoutes: FastifyPluginAsync = async (server) => { - server.post('/adminLogin', async function (request, reply) { try { const body: any = JSON.parse(request.body as string); if (!body) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); if (!body.WALLET_ADDRESS) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); console.log(body, server.config.ADMIN_WALLET_ADDRESS) - if (ethers.utils.getAddress(body.WALLET_ADDRESS) === server.config.ADMIN_WALLET_ADDRESS) return reply.code(ReturnCode.SUCCESS).send({error: null, message: "Successfully Logged in"}); + if (ethers.utils.getAddress(body.WALLET_ADDRESS) === server.config.ADMIN_WALLET_ADDRESS) return reply.code(ReturnCode.SUCCESS).send({ error: null, message: "Successfully Logged in" }); return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_USER }); } catch (err: any) { return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_USER }); @@ -74,53 +76,43 @@ const adminRoutes: FastifyPluginAsync = async (server) => { try { const body: any = JSON.parse(request.body as string); if (!body) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); - if (!body.API_KEY || !body.PRIVATE_KEY) + if (!body.apiKey || !body.privateKey) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); - if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*-_&])[A-Za-z\d@$!%*-_&]{8,}$/.test(body.API_KEY)) + if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*-_&])[A-Za-z\d@$!%*-_&]{8,}$/.test(body.apiKey)) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.API_KEY_VALIDATION_FAILED }) - const wallet = new ethers.Wallet(body.PRIVATE_KEY); + const wallet = new ethers.Wallet(body.privateKey); const publicAddress = await wallet.getAddress(); - const result: any[] = await new Promise((resolve, reject) => { - server.sqlite.db.get("SELECT * FROM api_keys WHERE WALLET_ADDRESS=?", [publicAddress], (err: any, row: any) => { - if (err) reject(err); - resolve(row); - }) - }) - if (result && result.length > 0) + + // Use Sequelize to find the API key + const result = await server.sequelize.models.APIKey.findOne({ where: { walletAddress: publicAddress } }); + if (result) { + request.log.error('Duplicate record found'); return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.DUPLICATE_RECORD }); - const privateKey = body.PRIVATE_KEY; + } + + const privateKey = body.privateKey; const hmac = encode(privateKey); - await new Promise((resolve, reject) => { - server.sqlite.db.run("INSERT INTO api_keys ( \ - API_KEY, \ - WALLET_ADDRESS, \ - PRIVATE_KEY, \ - SUPPORTED_NETWORKS, \ - ERC20_PAYMASTERS, \ - MULTI_TOKEN_PAYMASTERS, \ - MULTI_TOKEN_ORACLES, \ - SPONSOR_NAME, \ - LOGO_URL, \ - TRANSACTION_LIMIT, \ - NO_OF_TRANSACTIONS_IN_A_MONTH, \ - INDEXER_ENDPOINT) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ - body.API_KEY, - publicAddress, - hmac, - body.SUPPORTED_NETWORKS, - body.ERC20_PAYMASTERS, - body.MULTI_TOKEN_PAYMASTERS ?? null, - body.MULTI_TOKEN_ORACLES ?? null, - body.SPONSOR_NAME ?? null, - body.LOGO_URL ?? null, - body.TRANSACTION_LIMIT ?? 0, - body.NO_OF_TRANSACTIONS_IN_A_MONTH ?? 10, - body.INDEXER_ENDPOINT ?? process.env.DEFAULT_INDEXER_ENDPOINT - ], (err: any, row: any) => { - if (err) reject(err); - resolve(row); - }); + + // console.log(`support network on request.body is: ${body.supportedNetworks}`); + // console.log(`erc20 paymasters on request.body is: ${body.erc20Paymasters}`); + // console.log(`request body is: ${JSON.stringify(body)}`); + + // Use Sequelize to insert the new API key + await server.sequelize.models.APIKey.create({ + apiKey: body.apiKey, + walletAddress: publicAddress, + privateKey: hmac, + supportedNetworks: body.supportedNetworks, + erc20Paymasters: body.erc20Paymasters, + multiTokenPaymasters: body.multiTokenPaymasters ?? null, + multiTokenOracles: body.multiTokenOracles ?? null, + sponsorName: body.sponsorName ?? null, + logoUrl: body.logoUrl ?? null, + transactionLimit: body.transactionLimit ?? 0, + noOfTransactionsInAMonth: body.noOfTransactionsInAMonth ?? 10, + indexerEndpoint: body.indexerEndpoint ?? process.env.DEFAULT_INDEXER_ENDPOINT }); + return reply.code(ReturnCode.SUCCESS).send({ error: null, message: 'Successfully saved' }); } catch (err: any) { request.log.error(err); @@ -128,6 +120,7 @@ const adminRoutes: FastifyPluginAsync = async (server) => { } }) + server.post('/updateKey', async function (request, reply) { try { const body: any = JSON.parse(request.body as string); @@ -135,28 +128,20 @@ const adminRoutes: FastifyPluginAsync = async (server) => { if (!body.API_KEY) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*-_&])[A-Za-z\d@$!%*-_&]{8,}$/.test(body.API_KEY)) - return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.API_KEY_VALIDATION_FAILED }) - const result: any[] = await new Promise((resolve, reject) => { - server.sqlite.db.get("SELECT * FROM api_keys WHERE API_KEY=?", [body.API_KEY], (err: any, row: any) => { - if (err) reject(err); - resolve(row); - }) - }); - if (!result || result.length == 0) + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.API_KEY_VALIDATION_FAILED }); + + const apiKeyInstance = await server.sequelize.models.APIKey.findOne({ where: { apiKey: body.API_KEY } }); + if (!apiKeyInstance) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.RECORD_NOT_FOUND }); - await new Promise((resolve, reject) => { - server.sqlite.db.run("UPDATE api_keys SET SUPPORTED_NETWORKS = ?, \ - ERC20_PAYMASTERS = ?, \ - TRANSACTION_LIMIT = ?, \ - NO_OF_TRANSACTIONS_IN_A_MONTH = ?, \ - INDEXER_ENDPOINT = ?, \ - WHERE API_KEY = ?", [body.SUPPORTED_NETWORKS, body.ERC20_PAYMASTERS, body.TRANSACTION_LIMIT ?? 0, body.NO_OF_TRANSACTIONS_IN_A_MONTH ?? 10, - body.INDEXER_ENDPOINT ?? process.env.DEFAULT_INDEXER_ENDPOINT, body.API_KEY - ], (err: any, row: any) => { - if (err) reject(err); - resolve(row); - }) + + await apiKeyInstance.update({ + supportedNetworks: body.SUPPORTED_NETWORKS, + erc20Paymasters: body.ERC20_PAYMASTERS, + transactionLimit: body.TRANSACTION_LIMIT ?? 0, + noOfTransactionsInAMonth: body.NO_OF_TRANSACTIONS_IN_A_MONTH ?? 10, + indexerEndpoint: body.INDEXER_ENDPOINT ?? process.env.DEFAULT_INDEXER_ENDPOINT }); + return reply.code(ReturnCode.SUCCESS).send({ error: null, message: 'Successfully updated' }); } catch (err: any) { server.log.error(err); @@ -166,16 +151,13 @@ const adminRoutes: FastifyPluginAsync = async (server) => { server.get('/getKeys', async function (request, reply) { try { - const result: any[] = await new Promise((resolve, reject) => { - server.sqlite.db.all("SELECT * FROM api_keys", (err: any, rows: any[]) => { - if (err) reject(err); - resolve(rows); - }) - }) - result.map((value) => { - value.PRIVATE_KEY = decode(value.PRIVATE_KEY) + if(!server.sequelize) throw new Error('Sequelize instance is not available'); + const apiKeyRepository = new APIKeyRepository(server.sequelize); + const apiKeys: APIKey[] = await apiKeyRepository.findAll(); + apiKeys.forEach((apiKeyEntity: APIKey) => { + apiKeyEntity.privateKey = decode(apiKeyEntity.privateKey); }); - return reply.code(ReturnCode.SUCCESS).send(result); + return reply.code(ReturnCode.SUCCESS).send(apiKeys); } catch (err: any) { request.log.error(err); return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); @@ -189,13 +171,14 @@ const adminRoutes: FastifyPluginAsync = async (server) => { if (!body.API_KEY) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*-_&])[A-Za-z\d@$!%*-_&]{8,}$/.test(body.API_KEY)) - return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.API_KEY_VALIDATION_FAILED }) - await new Promise((resolve, reject) => { - server.sqlite.db.run("DELETE FROM api_keys WHERE API_KEY=?", [body.API_KEY], (err: any, rows: any) => { - if (err) reject(err); - resolve(rows); - }) - }) + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.API_KEY_VALIDATION_FAILED }); + + const apiKeyInstance = await server.sequelize.models.APIKey.findOne({ where: { apiKey: body.API_KEY } }); + if (!apiKeyInstance) + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.RECORD_NOT_FOUND }); + + await apiKeyInstance.destroy(); + return reply.code(ReturnCode.SUCCESS).send({ error: null, message: 'Successfully deleted' }); } catch (err: any) { request.log.error(err); diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 824358f..a3d683b 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -10,7 +10,9 @@ import { PAYMASTER_ADDRESS } from "../constants/Pimlico.js"; import ErrorMessage from "../constants/ErrorMessage.js"; import ReturnCode from "../constants/ReturnCode.js"; import { decode } from "../utils/crypto.js"; -import { printRequest, getNetworkConfig, getSQLdata } from "../utils/common.js"; +import { printRequest, getNetworkConfig } from "../utils/common.js"; +import { APIKeyRepository } from "repository/APIKeyRepository.js"; +import { APIKey } from "models/APIKey.js"; const SUPPORTED_ENTRYPOINTS = { 'EPV_06' : "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", @@ -126,30 +128,33 @@ const routes: FastifyPluginAsync = async (server) => { txnMode = secrets['TRANSACTION_LIMIT'] ?? 0; indexerEndpoint = secrets['INDEXER_ENDPOINT'] ?? process.env.DEFAULT_INDEXER_ENDPOINT; } else { - const record: any = await getSQLdata(api_key, server.sqlite.db, server.log); - if (!record) { + const apiKeyEntity = await new APIKeyRepository(server.sequelize).findOneByApiKey(api_key); + + if (!apiKeyEntity) { server.log.info("Invalid Api Key provided") return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) } - if (record['ERC20_PAYMASTERS']) { - const buffer = Buffer.from(record['ERC20_PAYMASTERS'], 'base64'); + + if (apiKeyEntity.erc20Paymasters) { + const buffer = Buffer.from(apiKeyEntity.erc20Paymasters, 'base64'); customPaymasters = JSON.parse(buffer.toString()); } - if (record['MULTI_TOKEN_PAYMASTERS']) { - const buffer = Buffer.from(record['MULTI_TOKEN_PAYMASTERS'], 'base64'); + + if (apiKeyEntity.multiTokenPaymasters) { + const buffer = Buffer.from(apiKeyEntity.multiTokenPaymasters, 'base64'); multiTokenPaymasters = JSON.parse(buffer.toString()); } - if (record['MULTI_TOKEN_ORACLES']) { - const buffer = Buffer.from(record['MULTI_TOKEN_ORACLES'], 'base64'); + if (apiKeyEntity.multiTokenOracles) { + const buffer = Buffer.from(apiKeyEntity.multiTokenOracles, 'base64'); multiTokenOracles = JSON.parse(buffer.toString()); } - sponsorName = record['SPONSOR_NAME']; - sponsorImage = record['LOGO_URL']; - 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; + sponsorName = apiKeyEntity.sponsorName ? apiKeyEntity.sponsorName : ''; + sponsorImage = apiKeyEntity.logoUrl ? apiKeyEntity.logoUrl : ''; + privateKey = decode(apiKeyEntity.privateKey); + supportedNetworks = apiKeyEntity.supportedNetworks; + noOfTxns = apiKeyEntity.noOfTransactionsInAMonth; + txnMode = apiKeyEntity.transactionLimit; + indexerEndpoint = apiKeyEntity.indexerEndpoint ?? process.env.DEFAULT_INDEXER_ENDPOINT; } if ( @@ -299,14 +304,17 @@ const routes: FastifyPluginAsync = async (server) => { privateKey = secrets['PRIVATE_KEY']; supportedNetworks = secrets['SUPPORTED_NETWORKS']; } else { - const record: any = await getSQLdata(api_key, server.sqlite.db, server.log); - if (!record) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) - if (record['ERC20_PAYMASTERS']) { - const buffer = Buffer.from(record['ERC20_PAYMASTERS'], 'base64'); + const result = await server.sequelize.models.APIKey.findOne({ where: { apiKey: api_key } }); + if (!result) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + const apiKeyEntity: APIKey = result as APIKey; + if (apiKeyEntity.erc20Paymasters) { + const buffer = Buffer.from(apiKeyEntity.erc20Paymasters, 'base64'); customPaymasters = JSON.parse(buffer.toString()); } - privateKey = decode(record['PRIVATE_KEY']); - supportedNetworks = record['SUPPORTED_NETWORKS']; + + privateKey = decode(apiKeyEntity.privateKey); + + supportedNetworks = apiKeyEntity.supportedNetworks; } if (!privateKey) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if ( @@ -366,10 +374,10 @@ const routes: FastifyPluginAsync = async (server) => { privateKey = secrets['PRIVATE_KEY']; supportedNetworks = secrets['SUPPORTED_NETWORKS']; } else { - const record: any = await getSQLdata(api_key, server.sqlite.db, server.log); - if (!record) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) - privateKey = decode(record['PRIVATE_KEY']); - supportedNetworks = record['SUPPORTED_NETWORKS']; + const apiKeyEntity: APIKey | null = await new APIKeyRepository(server.sequelize).findOneByApiKey(api_key); + if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + privateKey = decode(apiKeyEntity.privateKey); + supportedNetworks = apiKeyEntity.supportedNetworks; } if (!privateKey) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if ( @@ -424,10 +432,10 @@ const routes: FastifyPluginAsync = async (server) => { privateKey = secrets['PRIVATE_KEY']; supportedNetworks = secrets['SUPPORTED_NETWORKS']; } else { - const record: any = await getSQLdata(api_key, server.sqlite.db, server.log); - if (!record) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) - privateKey = decode(record['PRIVATE_KEY']); - supportedNetworks = record['SUPPORTED_NETWORKS']; + const apiKeyEntity: APIKey | null = await new APIKeyRepository(server.sequelize).findOneByApiKey(api_key); + if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + privateKey = decode(apiKeyEntity.apiKey); + supportedNetworks = apiKeyEntity.supportedNetworks; } if (!privateKey) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if ( @@ -482,10 +490,10 @@ const routes: FastifyPluginAsync = async (server) => { privateKey = secrets['PRIVATE_KEY']; supportedNetworks = secrets['SUPPORTED_NETWORKS']; } else { - const record: any = await getSQLdata(api_key, server.sqlite.db, server.log); - if (!record) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) - privateKey = decode(record['PRIVATE_KEY']); - supportedNetworks = record['SUPPORTED_NETWORKS']; + const apiKeyEntity: APIKey | null = await new APIKeyRepository(server.sequelize).findOneByApiKey(api_key); + if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + privateKey = decode(apiKeyEntity.privateKey); + supportedNetworks = apiKeyEntity.supportedNetworks; } if (!privateKey) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if ( @@ -541,10 +549,10 @@ const routes: FastifyPluginAsync = async (server) => { privateKey = secrets['PRIVATE_KEY']; supportedNetworks = secrets['SUPPORTED_NETWORKS']; } else { - const record: any = await getSQLdata(api_key, server.sqlite.db, server.log); - if (!record) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) - privateKey = decode(record['PRIVATE_KEY']); - supportedNetworks = record['SUPPORTED_NETWORKS']; + const apiKeyEntity: APIKey | null = await new APIKeyRepository(server.sequelize).findOneByApiKey(api_key); + if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + privateKey = decode(apiKeyEntity.apiKey); + supportedNetworks = apiKeyEntity.supportedNetworks; } if ( isNaN(amount) || @@ -594,10 +602,10 @@ const routes: FastifyPluginAsync = async (server) => { privateKey = secrets['PRIVATE_KEY']; supportedNetworks = secrets['SUPPORTED_NETWORKS']; } else { - const record: any = await getSQLdata(api_key, server.sqlite.db, server.log); - if (!record) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) - privateKey = decode(record['PRIVATE_KEY']); - supportedNetworks = record['SUPPORTED_NETWORKS']; + const apiKeyEntity: APIKey | null = await new APIKeyRepository(server.sequelize).findOneByApiKey(api_key); + if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + privateKey = decode(apiKeyEntity.privateKey); + supportedNetworks = apiKeyEntity.supportedNetworks; } if ( isNaN(amount) || diff --git a/backend/src/routes/metadata.ts b/backend/src/routes/metadata.ts index 4007af3..500d613 100644 --- a/backend/src/routes/metadata.ts +++ b/backend/src/routes/metadata.ts @@ -2,12 +2,13 @@ import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-sec import { FastifyPluginAsync } from "fastify"; import { Wallet, providers } from "ethers"; import SupportedNetworks from "../../config.json" assert { type: "json" }; -import { getNetworkConfig, printRequest, getSQLdata } from "../utils/common.js"; +import { getNetworkConfig, printRequest } from "../utils/common.js"; import ReturnCode from "../constants/ReturnCode.js"; import ErrorMessage from "../constants/ErrorMessage.js"; import { decode } from "../utils/crypto.js"; import { PAYMASTER_ADDRESS } from "../constants/Pimlico.js"; - +import { APIKey } from "models/APIKey"; +import { APIKeyRepository } from "repository/APIKeyRepository"; const metadataRoutes: FastifyPluginAsync = async (server) => { @@ -62,23 +63,23 @@ const metadataRoutes: FastifyPluginAsync = async (server) => { privateKey = secrets['PRIVATE_KEY']; supportedNetworks = secrets['SUPPORTED_NETWORKS']; } else { - const record: any = await getSQLdata(api_key, server.sqlite.db, server.log); - if (!record) { + const apiKeyEntity: APIKey | null = await new APIKeyRepository(server.sequelize).findOneByApiKey(api_key); + if (!apiKeyEntity) { server.log.info("Invalid Api Key provided") return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) } - if (record['ERC20_PAYMASTERS']) { - const buffer = Buffer.from(record['ERC20_PAYMASTERS'], 'base64'); + if (apiKeyEntity.erc20Paymasters) { + const buffer = Buffer.from(apiKeyEntity.erc20Paymasters, 'base64'); customPaymasters = JSON.parse(buffer.toString()); } - if (record['MULTI_TOKEN_PAYMASTERS']) { - const buffer = Buffer.from(record['MULTI_TOKEN_PAYMASTERS'], 'base64'); + if (apiKeyEntity.multiTokenPaymasters) { + const buffer = Buffer.from(apiKeyEntity.multiTokenPaymasters, 'base64'); multiTokenPaymasters = JSON.parse(buffer.toString()); } - sponsorName = record['SPONSOR_NAME']; - sponsorImage = record['LOGO_URL']; - privateKey = decode(record['PRIVATE_KEY']); - supportedNetworks = record['SUPPORTED_NETWORKS']; + sponsorName = apiKeyEntity.sponsorName ? apiKeyEntity.sponsorName : ""; + sponsorImage = apiKeyEntity.logoUrl ? apiKeyEntity.logoUrl : ""; + privateKey = decode(apiKeyEntity.privateKey); + supportedNetworks = apiKeyEntity.supportedNetworks; } if (server.config.SUPPORTED_NETWORKS == '' && !SupportedNetworks) { return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); diff --git a/backend/src/server.ts b/backend/src/server.ts index 07e5615..07f6639 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -7,6 +7,7 @@ import { providers, ethers } from 'ethers'; import { GetSecretValueCommand, SecretsManagerClient } from '@aws-sdk/client-secrets-manager'; import fetch from 'node-fetch'; import database from './plugins/db.js'; +import sequelizePlugin from './plugins/sequelizePlugin.js'; import config from './plugins/config.js'; import routes from './routes/index.js'; import adminRoutes from './routes/admin.js'; @@ -14,8 +15,10 @@ import metadataRoutes from './routes/metadata.js'; import EtherspotChainlinkOracleAbi from './abi/EtherspotChainlinkOracleAbi.js'; import PimlicoAbi from './abi/PimlicoAbi.js'; import PythOracleAbi from './abi/PythOracleAbi.js'; -import { getNetworkConfig, getSQLdata } from './utils/common.js'; +import { getNetworkConfig } from './utils/common.js'; import { checkDeposit } from './utils/monitorTokenPaymaster.js'; +import { APIKey, initializeAPIKeyModel } from 'models/APIKey.js'; +import { APIKeyRepository } from 'repository/APIKeyRepository.js'; let server: FastifyInstance; @@ -55,6 +58,12 @@ const initializeServer = async (): Promise => { // Database await server.register(database); + // Register the sequelizePlugin + await server.register(sequelizePlugin); + + // Initialize the APIKey model + initializeAPIKeyModel(server.sequelize); + const ConfigData: any = await new Promise(resolve => { server.sqlite.db.get("SELECT * FROM config", (err, row) => { if (err) resolve(null); @@ -191,17 +200,19 @@ const initializeServer = async (): Promise => { multiTokenPaymasters = JSON.parse(buffer.toString()); } } else { - const record: any = await getSQLdata(api_key, server.sqlite.db, server.log); - if (record['ERC20_PAYMASTERS']) { - const buffer = Buffer.from(record['ERC20_PAYMASTERS'], 'base64'); + const apiKeyRepository = new APIKeyRepository(server.sequelize); + const apiKeyEntity: APIKey | null = await apiKeyRepository.findOneByApiKey(api_key); + + if (apiKeyEntity?.erc20Paymasters) { + const buffer = Buffer.from(apiKeyEntity.erc20Paymasters, '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 (apiKeyEntity?.multiTokenPaymasters) { + const buffer = Buffer.from(apiKeyEntity.multiTokenPaymasters, 'base64'); + multiTokenPaymasters = JSON.parse(buffer.toString()); } } - customPaymasters = {...customPaymasters, ...multiTokenPaymasters}; + customPaymasters = { ...customPaymasters, ...multiTokenPaymasters }; for (const chainId in customPaymasters) { const networkConfig = getNetworkConfig(chainId, ''); if (networkConfig) { diff --git a/backend/src/utils/common.ts b/backend/src/utils/common.ts index bfc2624..6fbea0e 100644 --- a/backend/src/utils/common.ts +++ b/backend/src/utils/common.ts @@ -3,6 +3,7 @@ import { BigNumber, ethers } from "ethers"; import { Database } from "sqlite3"; import SupportedNetworks from "../../config.json" assert { type: "json" }; import { EtherscanResponse, getEtherscanFeeResponse } from "./interface.js"; +import { APIKey } from "models/APIKey"; export function printRequest(methodName: string, request: FastifyRequest, log: FastifyBaseLogger) { log.info(methodName, "called: "); @@ -19,21 +20,6 @@ export function getNetworkConfig(key: any, supportedNetworks: any, entryPoint: s return SupportedNetworks.find((chain) => chain.chainId == key && chain.entryPoint == entryPoint); } -export async function getSQLdata(apiKey: string, db: Database, log: FastifyBaseLogger) { - try { - const result: any[] = await new Promise((resolve, reject) => { - db.get("SELECT * FROM api_keys WHERE API_KEY = ?", [apiKey], (err: any, rows: any[]) => { - if (err) reject(err); - resolve(rows); - }) - }) - return result; - } catch (err) { - log.error(err); - return null; - } -} - export async function getEtherscanFee(chainId: number, log?: FastifyBaseLogger): Promise { try { const etherscanUrlsBase64 = process.env.ETHERSCAN_GAS_ORACLES; @@ -50,7 +36,7 @@ export async function getEtherscanFee(chainId: number, log?: FastifyBaseLogger): console.log('setting maxFeePerGas and maxPriorityFeePerGas as received') const maxFeePerGas = ethers.utils.parseUnits(response.result.suggestBaseFee, 'gwei') const fastGasPrice = ethers.utils.parseUnits(response.result.FastGasPrice, 'gwei') - return { + return { maxPriorityFeePerGas: fastGasPrice.sub(maxFeePerGas), maxFeePerGas, gasPrice: maxFeePerGas, diff --git a/frontend/.gitignore b/frontend/.gitignore index 2c69316..076645b 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -16,3 +16,4 @@ chrome-user-data *.swo .env.local +yarn.lock \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..c7ab87d --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "sequelize": "^6.37.3" + }, + "devDependencies": { + "@types/sequelize": "^4.28.20", + "@types/sqlite3": "^3.1.11" + } +} From 508a357444b118e2d6d9d730874e0e7a9b5393f0 Mon Sep 17 00:00:00 2001 From: kanth Date: Fri, 7 Jun 2024 13:34:47 +0530 Subject: [PATCH 04/23] feat: PRO-2395 update apiKey sequelize based response reference in admin-frontend --- admin_frontend/src/components/ApiKeys.jsx | 16 ++++++++------ .../src/modals/AddSupportedNetworksModal.jsx | 4 +++- .../src/modals/ViewSupportedNetworksModal.jsx | 1 + backend/src/routes/admin.ts | 22 +++++++++---------- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/admin_frontend/src/components/ApiKeys.jsx b/admin_frontend/src/components/ApiKeys.jsx index 9bb1fd1..01da37a 100644 --- a/admin_frontend/src/components/ApiKeys.jsx +++ b/admin_frontend/src/components/ApiKeys.jsx @@ -72,6 +72,7 @@ const ApiKeysPage = () => { setViewErc20Open(false); }; const handleViewOpen = (networks) => { + console.log(`setting received supportedNetworks value to state: ${JSON.stringify(networks)}`); setSupportedNetworks(networks); setViewModalOpen(true); }; @@ -97,15 +98,16 @@ const ApiKeysPage = () => { ); const dataJson = await data.json(); dataJson.filter((element) => { - if (element.SUPPORTED_NETWORKS) { - const buffer = Buffer.from(element.SUPPORTED_NETWORKS, "base64"); + if (element.supportedNetworks) { + const buffer = Buffer.from(element.supportedNetworks, "base64"); const parsedSupportedNetowrks = JSON.parse(buffer.toString()); - element.SUPPORTED_NETWORKS = parsedSupportedNetowrks; + element.supportedNetworks = parsedSupportedNetowrks; } - if (element.ERC20_PAYMASTERS) { - const buffer = Buffer.from(element.ERC20_PAYMASTERS, "base64"); + if (element.erc20Paymasters) { + const buffer = Buffer.from(element.erc20Paymasters, "base64"); const parsedErc20Paymasters = JSON.parse(buffer.toString()); - element.ERC20_PAYMASTERS = parsedErc20Paymasters; + console.log(`parsedErc20Paymasters: ${JSON.stringify(parsedErc20Paymasters)}`); + element.erc20Paymasters = parsedErc20Paymasters; } return element; }); @@ -174,7 +176,7 @@ const ApiKeysPage = () => { `${process.env.REACT_APP_SERVER_URL}${ENDPOINTS["deleteKey"]}`, { method: "POST", - body: JSON.stringify({ API_KEY: key }), + body: JSON.stringify({ apiKey: key }), } ); const dataJson = data.json(); diff --git a/admin_frontend/src/modals/AddSupportedNetworksModal.jsx b/admin_frontend/src/modals/AddSupportedNetworksModal.jsx index 2676159..a531e88 100644 --- a/admin_frontend/src/modals/AddSupportedNetworksModal.jsx +++ b/admin_frontend/src/modals/AddSupportedNetworksModal.jsx @@ -180,7 +180,9 @@ const AddSupportedNetworksModal = ({ - {supportedNetworks.map((network, index) => { + {console.log(typeof supportedNetworks)} + {console.log(`supportedNetworks loop: ${JSON.stringify(supportedNetworks)}`)} + {Array.isArray(supportedNetworks) && supportedNetworks.map((network, index) => { return ( {network.chainId} diff --git a/admin_frontend/src/modals/ViewSupportedNetworksModal.jsx b/admin_frontend/src/modals/ViewSupportedNetworksModal.jsx index 73b2657..1f77f32 100644 --- a/admin_frontend/src/modals/ViewSupportedNetworksModal.jsx +++ b/admin_frontend/src/modals/ViewSupportedNetworksModal.jsx @@ -25,6 +25,7 @@ const ViewSupportedNetworksModal = ({ open, handleClose, }) => { + console.log(`supportedNetworks: ${JSON.stringify(supportedNetworks)}`); return ( { try { const body: any = JSON.parse(request.body as string); if (!body) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); - if (!body.API_KEY) + if (!body.apiKey) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); - if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*-_&])[A-Za-z\d@$!%*-_&]{8,}$/.test(body.API_KEY)) + if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*-_&])[A-Za-z\d@$!%*-_&]{8,}$/.test(body.apiKey)) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.API_KEY_VALIDATION_FAILED }); - const apiKeyInstance = await server.sequelize.models.APIKey.findOne({ where: { apiKey: body.API_KEY } }); + const apiKeyInstance = await server.sequelize.models.APIKey.findOne({ where: { apiKey: body.apiKey } }); if (!apiKeyInstance) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.RECORD_NOT_FOUND }); await apiKeyInstance.update({ - supportedNetworks: body.SUPPORTED_NETWORKS, - erc20Paymasters: body.ERC20_PAYMASTERS, - transactionLimit: body.TRANSACTION_LIMIT ?? 0, - noOfTransactionsInAMonth: body.NO_OF_TRANSACTIONS_IN_A_MONTH ?? 10, - indexerEndpoint: body.INDEXER_ENDPOINT ?? process.env.DEFAULT_INDEXER_ENDPOINT + supportedNetworks: body.supportedNetworks, + erc20Paymasters: body.erc20Paymasters, + transactionLimit: body.transactionLimit ?? 0, + noOfTransactionsInAMonth: body.noOfTransactionsInAMonth ?? 10, + indexerEndpoint: body.indexerEndpoint ?? process.env.DEFAULT_INDEXER_ENDPOINT }); return reply.code(ReturnCode.SUCCESS).send({ error: null, message: 'Successfully updated' }); @@ -168,12 +168,12 @@ const adminRoutes: FastifyPluginAsync = async (server) => { try { const body: any = JSON.parse(request.body as string); if (!body) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); - if (!body.API_KEY) + if (!body.apiKey) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); - if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*-_&])[A-Za-z\d@$!%*-_&]{8,}$/.test(body.API_KEY)) + if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*-_&])[A-Za-z\d@$!%*-_&]{8,}$/.test(body.apiKey)) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.API_KEY_VALIDATION_FAILED }); - const apiKeyInstance = await server.sequelize.models.APIKey.findOne({ where: { apiKey: body.API_KEY } }); + const apiKeyInstance = await server.sequelize.models.APIKey.findOne({ where: { apiKey: body.apiKey } }); if (!apiKeyInstance) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.RECORD_NOT_FOUND }); From b571783b4192068a888886dc027e3052d7df5445 Mon Sep 17 00:00:00 2001 From: kanth Date: Mon, 10 Jun 2024 11:08:11 +0530 Subject: [PATCH 05/23] feat: PRO-2395 sequelize models and centralised model association files for sponsorship_policy tables and api_keys association --- backend/src/models/Policy.ts | 73 ++++++++++++++++++++++++++++++ backend/src/models/PolicyChain.ts | 31 +++++++++++++ backend/src/models/PolicyLimit.ts | 49 ++++++++++++++++++++ backend/src/models/associations.ts | 52 +++++++++++++++++++++ 4 files changed, 205 insertions(+) create mode 100644 backend/src/models/Policy.ts create mode 100644 backend/src/models/PolicyChain.ts create mode 100644 backend/src/models/PolicyLimit.ts create mode 100644 backend/src/models/associations.ts diff --git a/backend/src/models/Policy.ts b/backend/src/models/Policy.ts new file mode 100644 index 0000000..4fbcc68 --- /dev/null +++ b/backend/src/models/Policy.ts @@ -0,0 +1,73 @@ +import { Sequelize, DataTypes, Model } from 'sequelize'; + +export class Policy extends Model { + declare policyId: number; + declare walletAddress: string; + declare name: string; + declare description: string | null; + declare startDate: Date | null; + declare endDate: Date | null; + declare isPerpetual: boolean; + declare isUniversal: boolean; + declare contractRestrictions: string | null; +} + +export function initializePolicyModel(sequelize: Sequelize) { + Policy.init({ + policyId: { + type: DataTypes.INTEGER, + primaryKey: true, + field: 'POLICY_ID' + }, + walletAddress: { + type: DataTypes.STRING, + allowNull: false, + field: 'WALLET_ADDRESS', + references: { + model: 'api_keys', // This is the table name of the model being referenced + key: 'wallet_address', // This is the key column in the APIKey model + }, + onDelete: 'CASCADE', + onUpdate: 'CASCADE' + }, + name: { + type: DataTypes.STRING, + allowNull: false, + field: 'NAME' + }, + description: { + type: DataTypes.STRING, + allowNull: true, + field: 'DESCRIPTION' + }, + startDate: { + type: DataTypes.DATE, + allowNull: true, + field: 'START_DATE' + }, + endDate: { + type: DataTypes.DATE, + allowNull: true, + field: 'END_DATE' + }, + isPerpetual: { + type: DataTypes.BOOLEAN, + defaultValue: false, + field: 'IS_PERPETUAL' + }, + isUniversal: { + type: DataTypes.BOOLEAN, + defaultValue: false, + field: 'IS_UNIVERSAL' + }, + contractRestrictions: { + type: DataTypes.STRING, + allowNull: true, + field: 'CONTRACT_RESTRICTIONS' + }, + }, { + tableName: 'policies', + sequelize, + timestamps: false, + }); +} \ No newline at end of file diff --git a/backend/src/models/PolicyChain.ts b/backend/src/models/PolicyChain.ts new file mode 100644 index 0000000..dd90a6f --- /dev/null +++ b/backend/src/models/PolicyChain.ts @@ -0,0 +1,31 @@ +import { Sequelize, DataTypes, Model } from 'sequelize'; + +export class PolicyChain extends Model { + declare policyChainId: number; + declare policyId: number; + declare chainName: string; +} + +export function initializePolicyChainModel(sequelize: Sequelize) { + PolicyChain.init({ + policyChainId: { + type: DataTypes.INTEGER, + primaryKey: true, + field: 'POLICY_CHAIN_ID' + }, + policyId: { + type: DataTypes.INTEGER, + allowNull: false, + field: 'POLICY_ID' + }, + chainName: { + type: DataTypes.STRING, + allowNull: false, + field: 'CHAIN_NAME' + }, + }, { + tableName: 'policy_chains', + sequelize, + timestamps: false, + }); +} \ No newline at end of file diff --git a/backend/src/models/PolicyLimit.ts b/backend/src/models/PolicyLimit.ts new file mode 100644 index 0000000..800e2a3 --- /dev/null +++ b/backend/src/models/PolicyLimit.ts @@ -0,0 +1,49 @@ +import { Sequelize, DataTypes, Model } from 'sequelize'; + +export class PolicyLimit extends Model { + declare limitId: number; + declare policyId: number; + declare limitType: string; + declare maxUsd: number | null; + declare maxEth: number | null; + declare maxOperations: number | null; +} + +export function initializePolicyLimitModel(sequelize: Sequelize) { + PolicyLimit.init({ + limitId: { + type: DataTypes.INTEGER, + primaryKey: true, + field: 'LIMIT_ID' + }, + policyId: { + type: DataTypes.INTEGER, + allowNull: false, + field: 'POLICY_ID' + }, + limitType: { + type: DataTypes.STRING, + allowNull: false, + field: 'LIMIT_TYPE' + }, + maxUsd: { + type: DataTypes.REAL, + allowNull: true, + field: 'MAX_USD' + }, + maxEth: { + type: DataTypes.REAL, + allowNull: true, + field: 'MAX_ETH' + }, + maxOperations: { + type: DataTypes.INTEGER, + allowNull: true, + field: 'MAX_OPERATIONS' + }, + }, { + tableName: 'policy_limits', + sequelize, + timestamps: false, + }); +} \ No newline at end of file diff --git a/backend/src/models/associations.ts b/backend/src/models/associations.ts new file mode 100644 index 0000000..10477ab --- /dev/null +++ b/backend/src/models/associations.ts @@ -0,0 +1,52 @@ +// associations.ts +import { APIKey } from './APIKey'; +import { Policy } from './Policy'; +import { PolicyChain } from './PolicyChain'; +import { PolicyLimit } from './PolicyLimit'; + +export function setupAssociations() { + // APIKey to Policy + APIKey.hasMany(Policy, { + foreignKey: 'walletAddress', + as: 'policies', // Optional alias for easier access in code + onDelete: 'CASCADE', // Ensures related policies are deleted when an APIKey is deleted + onUpdate: 'CASCADE' // Ensures changes in APIKey are cascaded to policies + }); + + Policy.belongsTo(APIKey, { + foreignKey: 'walletAddress', + as: 'apiKey', // Optional alias + onDelete: 'CASCADE', + onUpdate: 'CASCADE' + }); + + // Policy to PolicyChain + Policy.hasMany(PolicyChain, { + foreignKey: 'policyId', + as: 'policyChains', // Optional alias for easier access in code + onDelete: 'CASCADE', // Ensures related policy chains are deleted when a Policy is deleted + onUpdate: 'CASCADE' + }); + + PolicyChain.belongsTo(Policy, { + foreignKey: 'policyId', + as: 'policy', // Optional alias + onDelete: 'CASCADE', + onUpdate: 'CASCADE' + }); + + // Policy to PolicyLimit + Policy.hasMany(PolicyLimit, { + foreignKey: 'policyId', + as: 'policyLimits', // Optional alias for easier access in code + onDelete: 'CASCADE', // Ensures related policy limits are deleted when a Policy is deleted + onUpdate: 'CASCADE' + }); + + PolicyLimit.belongsTo(Policy, { + foreignKey: 'policyId', + as: 'policy', // Optional alias + onDelete: 'CASCADE', + onUpdate: 'CASCADE' + }); +} From 17d36034f429df5ab5286cbb45202007da6ece0a Mon Sep 17 00:00:00 2001 From: kanth Date: Mon, 10 Jun 2024 11:37:54 +0530 Subject: [PATCH 06/23] feat: PRO-2395 refactor sequelize models and their initialisation in the sequelizePlugin --- .../src/migrations/003.sponsorship-policy.sql | 12 ++++++------ .../models/{Policy.ts => SponsorshipPolicy.ts} | 8 ++++---- ...olicyChain.ts => SponsorshipPolicyChain.ts} | 8 ++++---- ...olicyLimit.ts => SponsorshipPolicyLimit.ts} | 8 ++++---- backend/src/models/associations.ts | 18 +++++++++--------- backend/src/plugins/sequelizePlugin.ts | 12 +++++++++++- backend/src/server.ts | 5 ++--- 7 files changed, 40 insertions(+), 31 deletions(-) rename backend/src/models/{Policy.ts => SponsorshipPolicy.ts} (90%) rename backend/src/models/{PolicyChain.ts => SponsorshipPolicyChain.ts} (74%) rename backend/src/models/{PolicyLimit.ts => SponsorshipPolicyLimit.ts} (83%) diff --git a/backend/src/migrations/003.sponsorship-policy.sql b/backend/src/migrations/003.sponsorship-policy.sql index 6c9d116..823296b 100644 --- a/backend/src/migrations/003.sponsorship-policy.sql +++ b/backend/src/migrations/003.sponsorship-policy.sql @@ -3,7 +3,7 @@ -------------------------------------------------------------------------------- -- Create POLICIES Table -CREATE TABLE IF NOT EXISTS policies ( +CREATE TABLE IF NOT EXISTS sponsorship_policies ( POLICY_ID INTEGER PRIMARY KEY, WALLET_ADDRESS TEXT NOT NULL, NAME TEXT NOT NULL, @@ -17,7 +17,7 @@ CREATE TABLE IF NOT EXISTS policies ( ); -- Create POLICY_LIMITS Table -CREATE TABLE IF NOT EXISTS policy_limits ( +CREATE TABLE IF NOT EXISTS sponsorship_policy_limits ( LIMIT_ID INTEGER PRIMARY KEY, POLICY_ID INTEGER NOT NULL, LIMIT_TYPE TEXT NOT NULL, @@ -28,7 +28,7 @@ CREATE TABLE IF NOT EXISTS policy_limits ( ); -- Create POLICY_CHAINS Table -CREATE TABLE IF NOT EXISTS policy_chains ( +CREATE TABLE IF NOT EXISTS sponsorship_policy_chains ( POLICY_CHAIN_ID INTEGER PRIMARY KEY, POLICY_ID INTEGER NOT NULL, CHAIN_NAME TEXT NOT NULL, @@ -40,6 +40,6 @@ CREATE TABLE IF NOT EXISTS policy_chains ( -- Down -------------------------------------------------------------------------------- -DROP TABLE IF EXISTS policy_chains; -DROP TABLE IF EXISTS policy_limits; -DROP TABLE IF EXISTS policies; +DROP TABLE IF EXISTS sponsorship_policy_chains; +DROP TABLE IF EXISTS sponsorship_policy_limits; +DROP TABLE IF EXISTS sponsorship_policies; diff --git a/backend/src/models/Policy.ts b/backend/src/models/SponsorshipPolicy.ts similarity index 90% rename from backend/src/models/Policy.ts rename to backend/src/models/SponsorshipPolicy.ts index 4fbcc68..5da62be 100644 --- a/backend/src/models/Policy.ts +++ b/backend/src/models/SponsorshipPolicy.ts @@ -1,6 +1,6 @@ import { Sequelize, DataTypes, Model } from 'sequelize'; -export class Policy extends Model { +export class SponsorshipPolicy extends Model { declare policyId: number; declare walletAddress: string; declare name: string; @@ -12,8 +12,8 @@ export class Policy extends Model { declare contractRestrictions: string | null; } -export function initializePolicyModel(sequelize: Sequelize) { - Policy.init({ +export function initializeSponsorshipPolicyModel(sequelize: Sequelize) { + SponsorshipPolicy.init({ policyId: { type: DataTypes.INTEGER, primaryKey: true, @@ -66,7 +66,7 @@ export function initializePolicyModel(sequelize: Sequelize) { field: 'CONTRACT_RESTRICTIONS' }, }, { - tableName: 'policies', + tableName: 'sponsorship_policies', sequelize, timestamps: false, }); diff --git a/backend/src/models/PolicyChain.ts b/backend/src/models/SponsorshipPolicyChain.ts similarity index 74% rename from backend/src/models/PolicyChain.ts rename to backend/src/models/SponsorshipPolicyChain.ts index dd90a6f..f6092ba 100644 --- a/backend/src/models/PolicyChain.ts +++ b/backend/src/models/SponsorshipPolicyChain.ts @@ -1,13 +1,13 @@ import { Sequelize, DataTypes, Model } from 'sequelize'; -export class PolicyChain extends Model { +export class SponsorshipPolicyChain extends Model { declare policyChainId: number; declare policyId: number; declare chainName: string; } -export function initializePolicyChainModel(sequelize: Sequelize) { - PolicyChain.init({ +export function initializeSponsorshipPolicyChainModel(sequelize: Sequelize) { + SponsorshipPolicyChain.init({ policyChainId: { type: DataTypes.INTEGER, primaryKey: true, @@ -24,7 +24,7 @@ export function initializePolicyChainModel(sequelize: Sequelize) { field: 'CHAIN_NAME' }, }, { - tableName: 'policy_chains', + tableName: 'sponsorship_policy_chains', sequelize, timestamps: false, }); diff --git a/backend/src/models/PolicyLimit.ts b/backend/src/models/SponsorshipPolicyLimit.ts similarity index 83% rename from backend/src/models/PolicyLimit.ts rename to backend/src/models/SponsorshipPolicyLimit.ts index 800e2a3..5ef6811 100644 --- a/backend/src/models/PolicyLimit.ts +++ b/backend/src/models/SponsorshipPolicyLimit.ts @@ -1,6 +1,6 @@ import { Sequelize, DataTypes, Model } from 'sequelize'; -export class PolicyLimit extends Model { +export class SponsorshipPolicyLimit extends Model { declare limitId: number; declare policyId: number; declare limitType: string; @@ -9,8 +9,8 @@ export class PolicyLimit extends Model { declare maxOperations: number | null; } -export function initializePolicyLimitModel(sequelize: Sequelize) { - PolicyLimit.init({ +export function initializeSponsorshipPolicyLimitModel(sequelize: Sequelize) { + SponsorshipPolicyLimit.init({ limitId: { type: DataTypes.INTEGER, primaryKey: true, @@ -42,7 +42,7 @@ export function initializePolicyLimitModel(sequelize: Sequelize) { field: 'MAX_OPERATIONS' }, }, { - tableName: 'policy_limits', + tableName: 'sponsorship_policy_limits', sequelize, timestamps: false, }); diff --git a/backend/src/models/associations.ts b/backend/src/models/associations.ts index 10477ab..6ace5c2 100644 --- a/backend/src/models/associations.ts +++ b/backend/src/models/associations.ts @@ -1,19 +1,19 @@ // associations.ts import { APIKey } from './APIKey'; -import { Policy } from './Policy'; -import { PolicyChain } from './PolicyChain'; -import { PolicyLimit } from './PolicyLimit'; +import { SponsorshipPolicy } from './SponsorshipPolicy'; +import { SponsorshipPolicyChain } from './SponsorshipPolicyChain'; +import { SponsorshipPolicyLimit } from './SponsorshipPolicyLimit'; export function setupAssociations() { // APIKey to Policy - APIKey.hasMany(Policy, { + APIKey.hasMany(SponsorshipPolicy, { foreignKey: 'walletAddress', as: 'policies', // Optional alias for easier access in code onDelete: 'CASCADE', // Ensures related policies are deleted when an APIKey is deleted onUpdate: 'CASCADE' // Ensures changes in APIKey are cascaded to policies }); - Policy.belongsTo(APIKey, { + SponsorshipPolicy.belongsTo(APIKey, { foreignKey: 'walletAddress', as: 'apiKey', // Optional alias onDelete: 'CASCADE', @@ -21,14 +21,14 @@ export function setupAssociations() { }); // Policy to PolicyChain - Policy.hasMany(PolicyChain, { + SponsorshipPolicy.hasMany(SponsorshipPolicyChain, { foreignKey: 'policyId', as: 'policyChains', // Optional alias for easier access in code onDelete: 'CASCADE', // Ensures related policy chains are deleted when a Policy is deleted onUpdate: 'CASCADE' }); - PolicyChain.belongsTo(Policy, { + SponsorshipPolicyChain.belongsTo(SponsorshipPolicy, { foreignKey: 'policyId', as: 'policy', // Optional alias onDelete: 'CASCADE', @@ -36,14 +36,14 @@ export function setupAssociations() { }); // Policy to PolicyLimit - Policy.hasMany(PolicyLimit, { + SponsorshipPolicy.hasMany(SponsorshipPolicyLimit, { foreignKey: 'policyId', as: 'policyLimits', // Optional alias for easier access in code onDelete: 'CASCADE', // Ensures related policy limits are deleted when a Policy is deleted onUpdate: 'CASCADE' }); - PolicyLimit.belongsTo(Policy, { + SponsorshipPolicyLimit.belongsTo(SponsorshipPolicy, { foreignKey: 'policyId', as: 'policy', // Optional alias onDelete: 'CASCADE', diff --git a/backend/src/plugins/sequelizePlugin.ts b/backend/src/plugins/sequelizePlugin.ts index 349a09f..d16be3b 100644 --- a/backend/src/plugins/sequelizePlugin.ts +++ b/backend/src/plugins/sequelizePlugin.ts @@ -2,6 +2,10 @@ import fp from "fastify-plugin"; import { FastifyPluginAsync } from "fastify"; import { Sequelize } from 'sequelize'; import sqlite3 from 'sqlite3'; +import { initializeAPIKeyModel } from '../models/APIKey'; // Assuming path correctness +import { initializeSponsorshipPolicyModel } from '../models/SponsorshipPolicy'; +import { initializeSponsorshipPolicyChainModel } from '../models/SponsorshipPolicyChain'; +import { initializeSponsorshipPolicyLimitModel } from "models/SponsorshipPolicyLimit"; const sequelizePlugin: FastifyPluginAsync = async (server) => { const sequelize = new Sequelize({ @@ -10,6 +14,12 @@ const sequelizePlugin: FastifyPluginAsync = async (server) => { dialectModule: sqlite3 }); + // Initialize models + initializeAPIKeyModel(sequelize); + initializeSponsorshipPolicyModel(sequelize); + initializeSponsorshipPolicyChainModel(sequelize); + initializeSponsorshipPolicyLimitModel(sequelize); + server.decorate('sequelize', sequelize); server.addHook('onClose', (instance, done) => { @@ -23,4 +33,4 @@ declare module "fastify" { } } -export default fp(sequelizePlugin); \ No newline at end of file +export default fp(sequelizePlugin, { name: 'sequelizePlugin' }); diff --git a/backend/src/server.ts b/backend/src/server.ts index 07f6639..fda89da 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -19,6 +19,8 @@ import { getNetworkConfig } from './utils/common.js'; import { checkDeposit } from './utils/monitorTokenPaymaster.js'; import { APIKey, initializeAPIKeyModel } from 'models/APIKey.js'; import { APIKeyRepository } from 'repository/APIKeyRepository.js'; +import { initializeSponsorshipPolicyModel } from 'models/SponsorshipPolicy.js'; +import { initializeSponsorshipPolicyChainModel } from 'models/SponsorshipPolicyChain.js'; let server: FastifyInstance; @@ -61,9 +63,6 @@ const initializeServer = async (): Promise => { // Register the sequelizePlugin await server.register(sequelizePlugin); - // Initialize the APIKey model - initializeAPIKeyModel(server.sequelize); - const ConfigData: any = await new Promise(resolve => { server.sqlite.db.get("SELECT * FROM config", (err, row) => { if (err) resolve(null); From 41757fe5c5a510d281bd1a306463b4c63a668709 Mon Sep 17 00:00:00 2001 From: kanth Date: Mon, 10 Jun 2024 13:24:13 +0530 Subject: [PATCH 07/23] fix: PRO-2395 admin-frontend to parse the error message from response while saving apiKey --- admin_frontend/src/components/ApiKeys.jsx | 4 ++-- backend/src/migrations/002.apiKeys.sql | 2 +- backend/src/migrations/003.sponsorship-policy.sql | 8 ++++---- backend/src/models/associations.ts | 8 +++++--- backend/src/routes/admin.ts | 6 ++++++ backend/src/server.ts | 4 +--- 6 files changed, 19 insertions(+), 13 deletions(-) diff --git a/admin_frontend/src/components/ApiKeys.jsx b/admin_frontend/src/components/ApiKeys.jsx index 01da37a..8575310 100644 --- a/admin_frontend/src/components/ApiKeys.jsx +++ b/admin_frontend/src/components/ApiKeys.jsx @@ -151,7 +151,7 @@ const ApiKeysPage = () => { body: JSON.stringify(requestData), } ); - const dataJson = data.json(); + const dataJson = await data.json(); if (!dataJson.error) { toast.success("Saved Successfully"); setApiKey(""); @@ -159,7 +159,7 @@ const ApiKeysPage = () => { fetchData(); } else { setLoading(false); - toast.error(`${dataJson.message} Please try again or contant Arka support team`); + toast.error(`${dataJson.error} Please try again or contant Arka support team`); } } catch (err) { if (err?.message?.includes("Failed to fetch")) { diff --git a/backend/src/migrations/002.apiKeys.sql b/backend/src/migrations/002.apiKeys.sql index fd83ae8..34c6309 100644 --- a/backend/src/migrations/002.apiKeys.sql +++ b/backend/src/migrations/002.apiKeys.sql @@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS api_keys ( API_KEY TEXT NOT NULL PRIMARY KEY, - WALLET_ADDRESS TEXT NOT NULL, + WALLET_ADDRESS TEXT NOT NULL UNIQUE, PRIVATE_KEY varchar NOT NULL, SUPPORTED_NETWORKS varchar DEFAULT NULL, ERC20_PAYMASTERS varchar DEFAULT NULL, diff --git a/backend/src/migrations/003.sponsorship-policy.sql b/backend/src/migrations/003.sponsorship-policy.sql index 823296b..1f59dff 100644 --- a/backend/src/migrations/003.sponsorship-policy.sql +++ b/backend/src/migrations/003.sponsorship-policy.sql @@ -13,7 +13,7 @@ CREATE TABLE IF NOT EXISTS sponsorship_policies ( IS_PERPETUAL BOOLEAN DEFAULT FALSE, IS_UNIVERSAL BOOLEAN DEFAULT FALSE, CONTRACT_RESTRICTIONS TEXT, -- Stores JSON string because SQLite doesn't support JSON natively - FOREIGN KEY (WALLET_ADDRESS) REFERENCES api_keys(WALLET_ADDRESS) + FOREIGN KEY (WALLET_ADDRESS) REFERENCES api_keys(WALLET_ADDRESS) ON DELETE CASCADE ); -- Create POLICY_LIMITS Table @@ -24,7 +24,7 @@ CREATE TABLE IF NOT EXISTS sponsorship_policy_limits ( MAX_USD REAL, -- REAL used in SQLite for floating-point numbers MAX_ETH REAL, MAX_OPERATIONS INTEGER, - FOREIGN KEY (POLICY_ID) REFERENCES policies(POLICY_ID) + FOREIGN KEY (POLICY_ID) REFERENCES sponsorship_policies(POLICY_ID) ON DELETE CASCADE ); -- Create POLICY_CHAINS Table @@ -32,7 +32,7 @@ CREATE TABLE IF NOT EXISTS sponsorship_policy_chains ( POLICY_CHAIN_ID INTEGER PRIMARY KEY, POLICY_ID INTEGER NOT NULL, CHAIN_NAME TEXT NOT NULL, - FOREIGN KEY (POLICY_ID) REFERENCES policies(POLICY_ID) + FOREIGN KEY (POLICY_ID) REFERENCES sponsorship_policies(POLICY_ID) ON DELETE CASCADE ); @@ -42,4 +42,4 @@ CREATE TABLE IF NOT EXISTS sponsorship_policy_chains ( DROP TABLE IF EXISTS sponsorship_policy_chains; DROP TABLE IF EXISTS sponsorship_policy_limits; -DROP TABLE IF EXISTS sponsorship_policies; +DROP TABLE IF EXISTS sponsorship_policies; \ No newline at end of file diff --git a/backend/src/models/associations.ts b/backend/src/models/associations.ts index 6ace5c2..b74e896 100644 --- a/backend/src/models/associations.ts +++ b/backend/src/models/associations.ts @@ -8,13 +8,15 @@ export function setupAssociations() { // APIKey to Policy APIKey.hasMany(SponsorshipPolicy, { foreignKey: 'walletAddress', - as: 'policies', // Optional alias for easier access in code - onDelete: 'CASCADE', // Ensures related policies are deleted when an APIKey is deleted - onUpdate: 'CASCADE' // Ensures changes in APIKey are cascaded to policies + sourceKey: 'walletAddress', // This is the new line + as: 'sponsorshipPolicies', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' }); SponsorshipPolicy.belongsTo(APIKey, { foreignKey: 'walletAddress', + targetKey: 'walletAddress', as: 'apiKey', // Optional alias onDelete: 'CASCADE', onUpdate: 'CASCADE' diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts index b4215a2..b49bd51 100644 --- a/backend/src/routes/admin.ts +++ b/backend/src/routes/admin.ts @@ -75,13 +75,19 @@ const adminRoutes: FastifyPluginAsync = async (server) => { server.post('/saveKey', async function (request, reply) { try { const body: any = JSON.parse(request.body as string); + request.log.info(`Request body is: ${JSON.stringify(body)}`); if (!body) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); if (!body.apiKey || !body.privateKey) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); + + request.log.info(`API Key is: ${body.apiKey}`); + request.log.info(`Private Key is: ${body.privateKey}`); if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*-_&])[A-Za-z\d@$!%*-_&]{8,}$/.test(body.apiKey)) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.API_KEY_VALIDATION_FAILED }) + request.log.info(`API Key is valid`); const wallet = new ethers.Wallet(body.privateKey); const publicAddress = await wallet.getAddress(); + request.log.info(`Public address is: ${publicAddress}`); // Use Sequelize to find the API key const result = await server.sequelize.models.APIKey.findOne({ where: { walletAddress: publicAddress } }); diff --git a/backend/src/server.ts b/backend/src/server.ts index fda89da..2e6411a 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -17,10 +17,8 @@ import PimlicoAbi from './abi/PimlicoAbi.js'; import PythOracleAbi from './abi/PythOracleAbi.js'; import { getNetworkConfig } from './utils/common.js'; import { checkDeposit } from './utils/monitorTokenPaymaster.js'; -import { APIKey, initializeAPIKeyModel } from 'models/APIKey.js'; +import { APIKey } from 'models/APIKey.js'; import { APIKeyRepository } from 'repository/APIKeyRepository.js'; -import { initializeSponsorshipPolicyModel } from 'models/SponsorshipPolicy.js'; -import { initializeSponsorshipPolicyChainModel } from 'models/SponsorshipPolicyChain.js'; let server: FastifyInstance; From 2b14746fd1047b4e75426702e7c2f290bd58ee0b Mon Sep 17 00:00:00 2001 From: kanth Date: Mon, 10 Jun 2024 19:26:47 +0530 Subject: [PATCH 08/23] feat: PRO-2395 sequelize models updated with new association relationships --- backend/src/migrations/001.default.sql | 22 +++---- backend/src/migrations/002.apiKeys.sql | 20 +++--- .../src/migrations/003.sponsorship-policy.sql | 33 +++++----- backend/src/models/APIKey.ts | 4 +- backend/src/models/SponsorshipPolicy.ts | 6 +- backend/src/models/SponsorshipPolicyChain.ts | 16 ++--- backend/src/models/SponsorshipPolicyLimit.ts | 31 +++------ backend/src/models/associations.ts | 63 ++++++++++++------- 8 files changed, 100 insertions(+), 95 deletions(-) diff --git a/backend/src/migrations/001.default.sql b/backend/src/migrations/001.default.sql index b808ab9..01501f2 100644 --- a/backend/src/migrations/001.default.sql +++ b/backend/src/migrations/001.default.sql @@ -3,7 +3,7 @@ -------------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS config ( - id INTEGER PRIMARY KEY, + ID SERIAL PRIMARY KEY, DEPLOYED_ERC20_PAYMASTERS TEXT NOT NULL, PYTH_MAINNET_URL TEXT NOT NULL, PYTH_TESTNET_URL TEXT NOT NULL, @@ -25,18 +25,18 @@ INSERT INTO config ( CUSTOM_CHAINLINK_DEPLOYED, COINGECKO_IDS, COINGECKO_API_URL) VALUES ( - "ewogICAgIjQyMCI6IFsiMHg1M0Y0ODU3OTMwOWY4ZEJmRkU0ZWRFOTIxQzUwMjAwODYxQzI0ODJhIl0sCiAgICAiNDIxNjEzIjogWyIweDBhNkFhMUJkMzBENjk1NGNBNTI1MzE1Mjg3QWRlZUVjYmI2ZUZCNTkiXSwKICAgICI1MDAxIjogWyIweDZFYTI1Y2JiNjAzNjAyNDNFODcxZEQ5MzUyMjVBMjkzYTc4NzA0YTgiXSwKICAgICI4MDAwMSI6IFsiMHhjMzNjMzhBN0JGRUJiQjk5N2RENDAxMUNEZEFmNGViRDFlODgwM0MwIl0KfQ==", - "https://hermes.pyth.network/api/latest_vaas?ids%5B%5D=", - "https://hermes-beta.pyth.network/api/latest_vaas?ids%5B%5D=", - "5001", - "5000", - "0 0 * * *", - "ewogICAgIjgwMDAxIjogWyIweGMzM2MzOEE3QkZFQmJCOTk3ZEQ0MDExQ0RkQWY0ZWJEMWU4ODAzQzAiXQp9", - "eyI4MDAwMSI6WyJwYW50aGVyIl19", - "https://api.coingecko.com/api/v3/simple/price?vs_currencies=usd&precision=8&ids="); + 'ewogICAgIjQyMCI6IFsiMHg1M0Y0ODU3OTMwOWY4ZEJmRkU0ZWRFOTIxQzUwMjAwODYxQzI0ODJhIl0sCiAgICAiNDIxNjEzIjogWyIweDBhNkFhMUJkMzBENjk1NGNBNTI1MzE1Mjg3QWRlZUVjYmI2ZUZCNTkiXSwKICAgICI1MDAxIjogWyIweDZFYTI1Y2JiNjAzNjAyNDNFODcxZEQ5MzUyMjVBMjkzYTc4NzA0YTgiXSwKICAgICI4MDAwMSI6IFsiMHhjMzNjMzhBN0JGRUJiQjk5N2RENDAxMUNEZEFmNGViRDFlODgwM0MwIl0KfQ==', + 'https://hermes.pyth.network/api/latest_vaas?ids%5B%5D=', + 'https://hermes-beta.pyth.network/api/latest_vaas?ids%5B%5D=', + '5001', + '5000', + '0 0 * * *', + 'ewogICAgIjgwMDAxIjogWyIweGMzM2MzOEE3QkZFQmJCOTk3ZEQ0MDExQ0RkQWY0ZWJEMWU4ODAzQzAiXQp9', + 'eyI4MDAwMSI6WyJwYW50aGVyIl19', + 'https://api.coingecko.com/api/v3/simple/price?vs_currencies=usd&precision=8&ids='); -------------------------------------------------------------------------------- -- Down -------------------------------------------------------------------------------- -DROP TABLE IF EXISTS config +DROP TABLE IF EXISTS config; \ No newline at end of file diff --git a/backend/src/migrations/002.apiKeys.sql b/backend/src/migrations/002.apiKeys.sql index 34c6309..b8e3d02 100644 --- a/backend/src/migrations/002.apiKeys.sql +++ b/backend/src/migrations/002.apiKeys.sql @@ -5,20 +5,20 @@ CREATE TABLE IF NOT EXISTS api_keys ( API_KEY TEXT NOT NULL PRIMARY KEY, WALLET_ADDRESS TEXT NOT NULL UNIQUE, - 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, - SPONSOR_NAME varchar DEFAULT NULL, - LOGO_URL varchar DEFAULT NULL, + PRIVATE_KEY TEXT NOT NULL, + SUPPORTED_NETWORKS TEXT DEFAULT NULL, + ERC20_PAYMASTERS TEXT DEFAULT NULL, + MULTI_TOKEN_PAYMASTERS TEXT DEFAULT NULL, + MULTI_TOKEN_ORACLES TEXT DEFAULT NULL, + SPONSOR_NAME TEXT DEFAULT NULL, + LOGO_URL TEXT DEFAULT NULL, TRANSACTION_LIMIT INT NOT NULL, - NO_OF_TRANSACTIONS_IN_A_MONTH int, - INDEXER_ENDPOINT varchar + NO_OF_TRANSACTIONS_IN_A_MONTH INT, + INDEXER_ENDPOINT TEXT ); -------------------------------------------------------------------------------- -- Down -------------------------------------------------------------------------------- -DROP TABLE IF EXISTS api_keys +DROP TABLE IF EXISTS api_keys; \ No newline at end of file diff --git a/backend/src/migrations/003.sponsorship-policy.sql b/backend/src/migrations/003.sponsorship-policy.sql index 1f59dff..e7c70ee 100644 --- a/backend/src/migrations/003.sponsorship-policy.sql +++ b/backend/src/migrations/003.sponsorship-policy.sql @@ -2,40 +2,39 @@ -- Up -------------------------------------------------------------------------------- --- Create POLICIES Table +-- Create sponsorship_policies Table CREATE TABLE IF NOT EXISTS sponsorship_policies ( - POLICY_ID INTEGER PRIMARY KEY, - WALLET_ADDRESS TEXT NOT NULL, + ID SERIAL PRIMARY KEY, + WALLET_ADDRESS TEXT NOT NULL UNIQUE, NAME TEXT NOT NULL, DESCRIPTION TEXT, START_DATE DATE, END_DATE DATE, IS_PERPETUAL BOOLEAN DEFAULT FALSE, IS_UNIVERSAL BOOLEAN DEFAULT FALSE, - CONTRACT_RESTRICTIONS TEXT, -- Stores JSON string because SQLite doesn't support JSON natively + CONTRACT_RESTRICTIONS TEXT, -- Stores JSON string because PostgreSQL supports JSON natively FOREIGN KEY (WALLET_ADDRESS) REFERENCES api_keys(WALLET_ADDRESS) ON DELETE CASCADE ); --- Create POLICY_LIMITS Table +-- Create sponsorship_policy_limits Table CREATE TABLE IF NOT EXISTS sponsorship_policy_limits ( - LIMIT_ID INTEGER PRIMARY KEY, - POLICY_ID INTEGER NOT NULL, + POLICY_ID INT NOT NULL, LIMIT_TYPE TEXT NOT NULL, - MAX_USD REAL, -- REAL used in SQLite for floating-point numbers - MAX_ETH REAL, - MAX_OPERATIONS INTEGER, - FOREIGN KEY (POLICY_ID) REFERENCES sponsorship_policies(POLICY_ID) ON DELETE CASCADE + MAX_USD FLOAT, -- FLOAT used in PostgreSQL for floating-point numbers + MAX_ETH FLOAT, + MAX_OPERATIONS INT, + FOREIGN KEY (POLICY_ID) REFERENCES sponsorship_policies(ID) ON DELETE CASCADE, + PRIMARY KEY (POLICY_ID, LIMIT_TYPE) -- Composite primary key ); --- Create POLICY_CHAINS Table +-- Create sponsorship_policy_chains Table CREATE TABLE IF NOT EXISTS sponsorship_policy_chains ( - POLICY_CHAIN_ID INTEGER PRIMARY KEY, - POLICY_ID INTEGER NOT NULL, - CHAIN_NAME TEXT NOT NULL, - FOREIGN KEY (POLICY_ID) REFERENCES sponsorship_policies(POLICY_ID) ON DELETE CASCADE + POLICY_ID INT NOT NULL, + CHAIN_ID TEXT NOT NULL, + FOREIGN KEY (POLICY_ID) REFERENCES sponsorship_policies(ID) ON DELETE CASCADE, + PRIMARY KEY (POLICY_ID, CHAIN_ID) -- Composite primary key ); - -------------------------------------------------------------------------------- -- Down -------------------------------------------------------------------------------- diff --git a/backend/src/models/APIKey.ts b/backend/src/models/APIKey.ts index 0a181d1..3a3c8c3 100644 --- a/backend/src/models/APIKey.ts +++ b/backend/src/models/APIKey.ts @@ -13,6 +13,8 @@ export class APIKey extends Model { declare transactionLimit: number; declare noOfTransactionsInAMonth: number | null; declare indexerEndpoint: string | null; + declare createdAt: Date; // Added this line + declare updatedAt: Date; // Added this line } export function initializeAPIKeyModel(sequelize: Sequelize) { @@ -26,6 +28,7 @@ export function initializeAPIKeyModel(sequelize: Sequelize) { walletAddress: { type: DataTypes.TEXT, allowNull: false, + unique: true, field: 'WALLET_ADDRESS' }, privateKey: { @@ -81,6 +84,5 @@ export function initializeAPIKeyModel(sequelize: Sequelize) { }, { tableName: 'api_keys', sequelize, // passing the `sequelize` instance is required - timestamps: false, // this will deactivate the `createdAt` and `updatedAt` columns }); } \ No newline at end of file diff --git a/backend/src/models/SponsorshipPolicy.ts b/backend/src/models/SponsorshipPolicy.ts index 5da62be..4d9dc2a 100644 --- a/backend/src/models/SponsorshipPolicy.ts +++ b/backend/src/models/SponsorshipPolicy.ts @@ -1,7 +1,7 @@ import { Sequelize, DataTypes, Model } from 'sequelize'; export class SponsorshipPolicy extends Model { - declare policyId: number; + declare id: number; declare walletAddress: string; declare name: string; declare description: string | null; @@ -14,10 +14,10 @@ export class SponsorshipPolicy extends Model { export function initializeSponsorshipPolicyModel(sequelize: Sequelize) { SponsorshipPolicy.init({ - policyId: { + id: { type: DataTypes.INTEGER, primaryKey: true, - field: 'POLICY_ID' + field: 'ID' }, walletAddress: { type: DataTypes.STRING, diff --git a/backend/src/models/SponsorshipPolicyChain.ts b/backend/src/models/SponsorshipPolicyChain.ts index f6092ba..c0c48ae 100644 --- a/backend/src/models/SponsorshipPolicyChain.ts +++ b/backend/src/models/SponsorshipPolicyChain.ts @@ -1,27 +1,23 @@ import { Sequelize, DataTypes, Model } from 'sequelize'; export class SponsorshipPolicyChain extends Model { - declare policyChainId: number; declare policyId: number; - declare chainName: string; + declare chainId: number; } export function initializeSponsorshipPolicyChainModel(sequelize: Sequelize) { SponsorshipPolicyChain.init({ - policyChainId: { - type: DataTypes.INTEGER, - primaryKey: true, - field: 'POLICY_CHAIN_ID' - }, policyId: { type: DataTypes.INTEGER, + primaryKey: true, allowNull: false, field: 'POLICY_ID' }, - chainName: { - type: DataTypes.STRING, + chainId: { + type: DataTypes.INTEGER, + primaryKey: true, allowNull: false, - field: 'CHAIN_NAME' + field: 'CHAIN_ID' }, }, { tableName: 'sponsorship_policy_chains', diff --git a/backend/src/models/SponsorshipPolicyLimit.ts b/backend/src/models/SponsorshipPolicyLimit.ts index 5ef6811..56fb96a 100644 --- a/backend/src/models/SponsorshipPolicyLimit.ts +++ b/backend/src/models/SponsorshipPolicyLimit.ts @@ -1,7 +1,6 @@ import { Sequelize, DataTypes, Model } from 'sequelize'; export class SponsorshipPolicyLimit extends Model { - declare limitId: number; declare policyId: number; declare limitType: string; declare maxUsd: number | null; @@ -11,36 +10,24 @@ export class SponsorshipPolicyLimit extends Model { export function initializeSponsorshipPolicyLimitModel(sequelize: Sequelize) { SponsorshipPolicyLimit.init({ - limitId: { - type: DataTypes.INTEGER, - primaryKey: true, - field: 'LIMIT_ID' - }, policyId: { type: DataTypes.INTEGER, + primaryKey: true, allowNull: false, + references: { + model: 'SponsorshipPolicy', // name of your model for sponsorship policies + key: 'id', // key in SponsorshipPolicy that policyId references + }, + onDelete: 'CASCADE', // Add this line field: 'POLICY_ID' }, limitType: { - type: DataTypes.STRING, + type: DataTypes.STRING, // Adjust this if limitType is not a string + primaryKey: true, allowNull: false, field: 'LIMIT_TYPE' }, - maxUsd: { - type: DataTypes.REAL, - allowNull: true, - field: 'MAX_USD' - }, - maxEth: { - type: DataTypes.REAL, - allowNull: true, - field: 'MAX_ETH' - }, - maxOperations: { - type: DataTypes.INTEGER, - allowNull: true, - field: 'MAX_OPERATIONS' - }, + // ... other fields ... }, { tableName: 'sponsorship_policy_limits', sequelize, diff --git a/backend/src/models/associations.ts b/backend/src/models/associations.ts index b74e896..7d877ce 100644 --- a/backend/src/models/associations.ts +++ b/backend/src/models/associations.ts @@ -5,50 +5,71 @@ import { SponsorshipPolicyChain } from './SponsorshipPolicyChain'; import { SponsorshipPolicyLimit } from './SponsorshipPolicyLimit'; export function setupAssociations() { - // APIKey to Policy + + /** + * APIKey to SponsorshipPolicy + * A single APIKey (the parent) can have many SponsorshipPolicies (the children). + * The link between them is made using the 'walletAddress' field of the APIKey and the 'walletAddress' field of the SponsorshipPolicy. + */ APIKey.hasMany(SponsorshipPolicy, { foreignKey: 'walletAddress', - sourceKey: 'walletAddress', // This is the new line - as: 'sponsorshipPolicies', - onDelete: 'CASCADE', - onUpdate: 'CASCADE' + sourceKey: 'walletAddress', + as: 'sponsorshipPolicies' }); + /** + * SponsorshipPolicy to APIKey + * A single SponsorshipPolicy (the child) belongs to one APIKey (the parent). + * The link between them is made using the 'walletAddress' field of the SponsorshipPolicy and the 'walletAddress' field of the APIKey. + */ SponsorshipPolicy.belongsTo(APIKey, { foreignKey: 'walletAddress', targetKey: 'walletAddress', as: 'apiKey', // Optional alias - onDelete: 'CASCADE', - onUpdate: 'CASCADE' }); - // Policy to PolicyChain + /** + * SponsorshipPolicy to SponsorshipPolicyChain + * A single SponsorshipPolicy (the parent) can have many SponsorshipPolicyChains (the children). + * The link between them is made using the 'id' field of the SponsorshipPolicy and the 'policyId' field of the SponsorshipPolicyChain + */ SponsorshipPolicy.hasMany(SponsorshipPolicyChain, { foreignKey: 'policyId', - as: 'policyChains', // Optional alias for easier access in code - onDelete: 'CASCADE', // Ensures related policy chains are deleted when a Policy is deleted - onUpdate: 'CASCADE' + sourceKey: 'id', + as: 'policyChains' }); + + /** + * SponsorshipPolicyChain to SponsorshipPolicy + * A single SponsorshipPolicyChain (the child) belongs to one SponsorshipPolicy (the parent). + * The link between them is made using the 'policyId' field of the SponsorshipPolicyChain and the 'id' field of the SponsorshipPolicy. + */ SponsorshipPolicyChain.belongsTo(SponsorshipPolicy, { foreignKey: 'policyId', - as: 'policy', // Optional alias - onDelete: 'CASCADE', - onUpdate: 'CASCADE' + targetKey: 'id', + as: 'policy' }); - // Policy to PolicyLimit + /** + * SponsorshipPolicy to SponsorshipPolicyLimit + * A single SponsorshipPolicy (the parent) can have many SponsorshipPolicyLimits (the children). + * The link between them is made using the 'id' field of the SponsorshipPolicy and the 'policyId' field of the SponsorshipPolicyLimit. + */ SponsorshipPolicy.hasMany(SponsorshipPolicyLimit, { foreignKey: 'policyId', - as: 'policyLimits', // Optional alias for easier access in code - onDelete: 'CASCADE', // Ensures related policy limits are deleted when a Policy is deleted - onUpdate: 'CASCADE' + sourceKey: 'id', + as: 'policyLimits' }); + /** + * SponsorshipPolicyLimit to SponsorshipPolicy + * A single SponsorshipPolicyLimit (the child) belongs to one SponsorshipPolicy (the parent). + * The link between them is made using the 'policyId' field of the SponsorshipPolicyLimit and the 'id' field of the SponsorshipPolicy. + */ SponsorshipPolicyLimit.belongsTo(SponsorshipPolicy, { foreignKey: 'policyId', - as: 'policy', // Optional alias - onDelete: 'CASCADE', - onUpdate: 'CASCADE' + targetKey: 'id', + as: 'policy' }); } From 82b42b26d1c34a947e4ddfe7cf56e39073830633 Mon Sep 17 00:00:00 2001 From: kanth Date: Tue, 11 Jun 2024 01:17:08 +0530 Subject: [PATCH 09/23] feat: PRO-2395 sequelize init and test --- backend/local-setup/docker-compose.yml | 14 ++++ backend/local-setup/init.sql | 1 + backend/package.json | 10 ++- .../{001.default.sql => 001-default.do.sql} | 0 .../{002.apiKeys.sql => 002-apiKeys.do.sql} | 0 ...licy.sql => 003-sponsorship-policy.do.sql} | 0 backend/src/models/APIKey.ts | 5 ++ backend/src/plugins/config.ts | 8 +- backend/src/plugins/db.ts | 71 +++++++++++++++--- backend/src/plugins/sequelizePlugin.ts | 74 +++++++++++++++++-- backend/src/plugins/test.ts | 29 ++++++++ backend/src/server.ts | 10 +-- 12 files changed, 196 insertions(+), 26 deletions(-) create mode 100644 backend/local-setup/docker-compose.yml create mode 100644 backend/local-setup/init.sql rename backend/src/migrations/{001.default.sql => 001-default.do.sql} (100%) rename backend/src/migrations/{002.apiKeys.sql => 002-apiKeys.do.sql} (100%) rename backend/src/migrations/{003.sponsorship-policy.sql => 003-sponsorship-policy.do.sql} (100%) create mode 100644 backend/src/plugins/test.ts diff --git a/backend/local-setup/docker-compose.yml b/backend/local-setup/docker-compose.yml new file mode 100644 index 0000000..680f7a0 --- /dev/null +++ b/backend/local-setup/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3.1' + +services: + db: + image: postgres + restart: always + environment: + POSTGRES_PASSWORD: paymaster + POSTGRES_USER: arkauser + POSTGRES_DB: arkadev + ports: + - 5432:5432 + volumes: + - ./init.sql:/docker-entrypoint-initdb.d/init.sql \ No newline at end of file diff --git a/backend/local-setup/init.sql b/backend/local-setup/init.sql new file mode 100644 index 0000000..521f216 --- /dev/null +++ b/backend/local-setup/init.sql @@ -0,0 +1 @@ +CREATE SCHEMA IF NOT EXISTS arka AUTHORIZATION arkauser; \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index 11d1509..a94d82c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -32,6 +32,7 @@ "@fastify/cors": "8.4.1", "@ponder/core": "0.2.7", "@sinclair/typebox": "0.31.28", + "@types/sequelize": "^4.28.20", "ajv": "8.11.2", "crypto": "^1.0.1", "dotenv": "16.0.3", @@ -44,8 +45,14 @@ "getmac": "6.6.0", "graphql-request": "6.1.0", "node-fetch": "3.3.2", + "node-pg-migrate": "^7.4.0", + "pg": "^8.12.0", + "postgrator": "^7.2.0", + "sequelize": "^6.37.3", "sqlite": "5.1.1", "sqlite3": "5.1.7-rc.0", + "ts-node": "^10.9.2", + "typescript": "^5.4.5", "viem": "2.7.6" }, "devDependencies": { @@ -53,6 +60,8 @@ "@babel/preset-env": "7.23.2", "@types/jest": "29.5.3", "@types/node": "18.11.15", + "@types/node-pg-migrate": "^2.3.1", + "@types/pg": "^8.11.6", "@typescript-eslint/eslint-plugin": "5.45.0", "@typescript-eslint/parser": "5.45.0", "babel-jest": "29.6.2", @@ -65,7 +74,6 @@ "prettier": "2.8.0", "ts-jest": "29.1.1", "tsx": "3.12.1", - "typescript": "5.0.4", "vitest": "0.25.8" } } diff --git a/backend/src/migrations/001.default.sql b/backend/src/migrations/001-default.do.sql similarity index 100% rename from backend/src/migrations/001.default.sql rename to backend/src/migrations/001-default.do.sql diff --git a/backend/src/migrations/002.apiKeys.sql b/backend/src/migrations/002-apiKeys.do.sql similarity index 100% rename from backend/src/migrations/002.apiKeys.sql rename to backend/src/migrations/002-apiKeys.do.sql diff --git a/backend/src/migrations/003.sponsorship-policy.sql b/backend/src/migrations/003-sponsorship-policy.do.sql similarity index 100% rename from backend/src/migrations/003.sponsorship-policy.sql rename to backend/src/migrations/003-sponsorship-policy.do.sql diff --git a/backend/src/models/APIKey.ts b/backend/src/models/APIKey.ts index 3a3c8c3..faf2159 100644 --- a/backend/src/models/APIKey.ts +++ b/backend/src/models/APIKey.ts @@ -18,6 +18,9 @@ export class APIKey extends Model { } export function initializeAPIKeyModel(sequelize: Sequelize) { + + console.log('Initializing APIKey model...') + APIKey.init({ apiKey: { type: DataTypes.TEXT, @@ -85,4 +88,6 @@ export function initializeAPIKeyModel(sequelize: Sequelize) { tableName: 'api_keys', sequelize, // passing the `sequelize` instance is required }); + + console.log('APIKey model initialized.') } \ No newline at end of file diff --git a/backend/src/plugins/config.ts b/backend/src/plugins/config.ts index 990be71..dbc16fb 100644 --- a/backend/src/plugins/config.ts +++ b/backend/src/plugins/config.ts @@ -20,6 +20,8 @@ const ConfigSchema = Type.Strict( ADMIN_WALLET_ADDRESS: Type.String() || undefined, FEE_MARKUP: Type.String() || undefined, MULTI_TOKEN_MARKUP: Type.String() || undefined, + DATABASE_URL: Type.String() || undefined, + DATABASE_SSL_ENABLED: Type.Boolean() || undefined, }) ); @@ -39,7 +41,7 @@ const configPlugin: FastifyPluginAsync = async (server) => { if (!valid) { throw new Error( ".env file validation failed - " + - JSON.stringify(validate.errors, null, 2) + JSON.stringify(validate.errors, null, 2) ); } @@ -50,7 +52,9 @@ const configPlugin: FastifyPluginAsync = async (server) => { SUPPORTED_NETWORKS: process.env.SUPPORTED_NETWORKS ?? '', ADMIN_WALLET_ADDRESS: process.env.ADMIN_WALLET_ADDRESS ?? '0x80a1874E1046B1cc5deFdf4D3153838B72fF94Ac', FEE_MARKUP: process.env.FEE_MARKUP ?? '10', - MULTI_TOKEN_MARKUP: process.env.MULTI_TOKEN_MARKUP ?? '1150000' + MULTI_TOKEN_MARKUP: process.env.MULTI_TOKEN_MARKUP ?? '1150000', + DATABASE_URL: process.env.DATABASE_URL ?? '', + DATABASE_SSL_ENABLED: process.env.DATABASE_SSL_ENABLED === 'true', } server.decorate("config", config); diff --git a/backend/src/plugins/db.ts b/backend/src/plugins/db.ts index 2065a2c..1951705 100644 --- a/backend/src/plugins/db.ts +++ b/backend/src/plugins/db.ts @@ -1,24 +1,75 @@ import fp from "fastify-plugin"; import { FastifyPluginAsync } from "fastify"; -import sqlite3 from 'sqlite3'; -import { Database, open } from "sqlite"; +const pg = await import('pg'); +const Client = pg.default.Client; +import * as migrate from 'node-pg-migrate'; +import path from 'path'; +import { Sequelize } from 'sequelize'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +import Postgrator from 'postgrator'; const databasePlugin: FastifyPluginAsync = async (server) => { - const db = await open({ - filename: './database.sqlite', - driver: sqlite3.Database, - }) - await db.migrate({ - migrationsPath: './build/migrations' + const client: InstanceType = new Client({ + connectionString: server.config.DATABASE_URL }); - server.decorate('sqlite', db); + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + + console.log(`__dirname: ${__dirname}`) + console.log(`${path.join(__dirname, '../migrations/')}`) + + // await migrate.default({ + // direction: 'up', + // dir: path.join(__dirname, './build/migrations'), + // databaseUrl: server.config.DATABASE_URL, + // migrationsTable: 'arka_migrations' + // }); + + try { + await client.connect(); + + const migrationPattern = path.join(__dirname, '../migrations/*'); + console.log(`Migration pattern: ${migrationPattern}`) + + const postgrator = new Postgrator({ + migrationPattern: path.join(__dirname, './migrations/*'), + driver: 'pg', + database: 'arkadev', + currentSchema: 'arka', + schemaTable: 'migrations', + execQuery: (query) => client.query(query), + }); + + console.log('Migrating db...') + const result = await postgrator.migrate() + console.log(`Migration done. ${result.length} migrations applied.`) + + if (result.length === 0) { + console.log( + 'No migrations run for schema. Already at the latest one.' + ) + } + + console.log('Migration done.') + + process.exitCode = 0 + } catch (err) { + console.error(err) + process.exitCode = 1 + } + + await client.end(); + + server.decorate('pg', client); }; declare module "fastify" { interface FastifyInstance { - sqlite: Database; + sequelize: Sequelize; + pg: InstanceType; } } export default fp(databasePlugin); diff --git a/backend/src/plugins/sequelizePlugin.ts b/backend/src/plugins/sequelizePlugin.ts index d16be3b..f579a4c 100644 --- a/backend/src/plugins/sequelizePlugin.ts +++ b/backend/src/plugins/sequelizePlugin.ts @@ -1,30 +1,63 @@ import fp from "fastify-plugin"; import { FastifyPluginAsync } from "fastify"; -import { Sequelize } from 'sequelize'; -import sqlite3 from 'sqlite3'; +import { Sequelize, QueryTypes } from 'sequelize'; +import dotenv from 'dotenv'; import { initializeAPIKeyModel } from '../models/APIKey'; // Assuming path correctness import { initializeSponsorshipPolicyModel } from '../models/SponsorshipPolicy'; import { initializeSponsorshipPolicyChainModel } from '../models/SponsorshipPolicyChain'; import { initializeSponsorshipPolicyLimitModel } from "models/SponsorshipPolicyLimit"; +const pg = await import('pg'); +const Client = pg.default.Client; + +dotenv.config(); const sequelizePlugin: FastifyPluginAsync = async (server) => { - const sequelize = new Sequelize({ - dialect: 'sqlite', - storage: './database.sqlite', - dialectModule: sqlite3 + + try { + const client: InstanceType = new Client({ + connectionString: server.config.DATABASE_URL + }); + await client.connect(); + console.log('Connected to database'); + } catch (err) { + console.error(err); + } + + const sequelize = new Sequelize(server.config.DATABASE_URL, { + dialect: 'postgres', + protocol: 'postgres', + dialectOptions: { + searchPath: 'arka', + // ssl: { + // require: false, + // rejectUnauthorized: false + // } + }, }); + sequelize.authenticate() + .then(() => console.log('Connection has been established successfully.')) + .catch(err => console.error('Unable to connect to the database:', err)); + + console.log('Initializing models...'); + // Initialize models initializeAPIKeyModel(sequelize); initializeSponsorshipPolicyModel(sequelize); initializeSponsorshipPolicyChainModel(sequelize); initializeSponsorshipPolicyLimitModel(sequelize); + console.log('Initialized all models...'); + server.decorate('sequelize', sequelize); + console.log('decorated fastify server with models...'); + server.addHook('onClose', (instance, done) => { instance.sequelize.close().then(() => done(), done); }); + + console.log('added hooks...'); }; declare module "fastify" { @@ -33,4 +66,31 @@ declare module "fastify" { } } -export default fp(sequelizePlugin, { name: 'sequelizePlugin' }); +async function runQuery() { + + console.log('Running test query...'); + + // Replace with your actual connection string + const sequelize = new Sequelize('postgresql://arkauser:paymaster@localhost:5432/arkadev', { + dialect: 'postgres', + protocol: 'postgres', + dialectOptions: { + searchPath: 'arka', + }, + }); + + try { + await sequelize.authenticate(); + console.log('Connection has been established successfully.'); + + // Replace with your actual SQL query + const result = await sequelize.query('SELECT * FROM arka.config', { type: QueryTypes.SELECT }); + console.log(result); + } catch (error) { + console.error('Unable to connect to the database:', error); + } finally { + await sequelize.close(); + } +} + +export default fp(sequelizePlugin, { name: 'sequelizePlugin' }); \ No newline at end of file diff --git a/backend/src/plugins/test.ts b/backend/src/plugins/test.ts new file mode 100644 index 0000000..11d72e5 --- /dev/null +++ b/backend/src/plugins/test.ts @@ -0,0 +1,29 @@ + +import { Sequelize, QueryTypes } from 'sequelize'; + +// npx ts-node backend/src/plugins/test.ts +async function runQuery() { + // Replace with your actual connection string + const sequelize = new Sequelize('postgresql://arkauser:paymaster@localhost:5432/arkadev', { + dialect: 'postgres', + protocol: 'postgres', + dialectOptions: { + searchPath: 'arka', + }, + }); + + try { + await sequelize.authenticate(); + console.log('Connection has been established successfully.'); + + // Replace with your actual SQL query + const result = await sequelize.query('SELECT * FROM arka.config', { type: QueryTypes.SELECT }); + console.log(result); + } catch (error) { + console.error('Unable to connect to the database:', error); + } finally { + await sequelize.close(); + } +} + +runQuery(); \ No newline at end of file diff --git a/backend/src/server.ts b/backend/src/server.ts index 2e6411a..e802b51 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -19,6 +19,7 @@ import { getNetworkConfig } from './utils/common.js'; import { checkDeposit } from './utils/monitorTokenPaymaster.js'; import { APIKey } from 'models/APIKey.js'; import { APIKeyRepository } from 'repository/APIKeyRepository.js'; +import { Sequelize, QueryTypes } from 'sequelize'; let server: FastifyInstance; @@ -61,12 +62,9 @@ const initializeServer = async (): Promise => { // Register the sequelizePlugin await server.register(sequelizePlugin); - const ConfigData: any = await new Promise(resolve => { - server.sqlite.db.get("SELECT * FROM config", (err, row) => { - if (err) resolve(null); - resolve(row); - }); - }); + console.log('registered sequelizePlugin...') + + const ConfigData: any = await server.sequelize.query("SELECT * FROM config", { type: QueryTypes.SELECT }); await server.register(fastifyCron, { jobs: [ From e2a2d451f1cd7ec71ea3b2f0181a0264e8c3d06c Mon Sep 17 00:00:00 2001 From: kanth Date: Wed, 12 Jun 2024 22:37:46 +0530 Subject: [PATCH 10/23] feat: PRO-2395 migration scripts refactored and updated --- .../20240611000000-create-config.cjs | 68 +++++++++++++++ .../20240611000001-create-api-key.cjs | 77 +++++++++++++++++ ...240611000002-create-sponsorship-policy.cjs | 82 +++++++++++++++++++ ...00003-create-sponsorship-policy-limits.cjs | 62 ++++++++++++++ ...00004-create-sponsorship-policy-chains.cjs | 47 +++++++++++ .../migrations/20240611000005-seed-config.cjs | 11 +++ 6 files changed, 347 insertions(+) create mode 100644 backend/migrations/20240611000000-create-config.cjs create mode 100644 backend/migrations/20240611000001-create-api-key.cjs create mode 100644 backend/migrations/20240611000002-create-sponsorship-policy.cjs create mode 100644 backend/migrations/20240611000003-create-sponsorship-policy-limits.cjs create mode 100644 backend/migrations/20240611000004-create-sponsorship-policy-chains.cjs create mode 100644 backend/migrations/20240611000005-seed-config.cjs diff --git a/backend/migrations/20240611000000-create-config.cjs b/backend/migrations/20240611000000-create-config.cjs new file mode 100644 index 0000000..150b69f --- /dev/null +++ b/backend/migrations/20240611000000-create-config.cjs @@ -0,0 +1,68 @@ +const { Sequelize } = require('sequelize') + +async function up({ context: queryInterface }) { + await queryInterface.createTable('config', { + ID: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false + }, + DEPLOYED_ERC20_PAYMASTERS: { + type: Sequelize.TEXT, + allowNull: false + }, + PYTH_MAINNET_URL: { + type: Sequelize.TEXT, + allowNull: false + }, + PYTH_TESTNET_URL: { + type: Sequelize.TEXT, + allowNull: false + }, + PYTH_TESTNET_CHAIN_IDS: { + type: Sequelize.TEXT, + allowNull: false + }, + PYTH_MAINNET_CHAIN_IDS: { + type: Sequelize.TEXT, + allowNull: false + }, + CRON_TIME: { + type: Sequelize.TEXT, + allowNull: false + }, + CUSTOM_CHAINLINK_DEPLOYED: { + type: Sequelize.TEXT, + allowNull: false + }, + COINGECKO_IDS: { + type: Sequelize.TEXT, + allowNull: true + }, + COINGECKO_API_URL: { + type: Sequelize.TEXT, + allowNull: true + }, + 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: 'config', + schema: process.env.DATABASE_SCHEMA_NAME + }) +} + +module.exports = { up, down } diff --git a/backend/migrations/20240611000001-create-api-key.cjs b/backend/migrations/20240611000001-create-api-key.cjs new file mode 100644 index 0000000..32692d5 --- /dev/null +++ b/backend/migrations/20240611000001-create-api-key.cjs @@ -0,0 +1,77 @@ +const { Sequelize } = require('sequelize') + +async function up({ context: queryInterface }) { + await queryInterface.createTable('api_keys', { + "API_KEY": { + allowNull: false, + primaryKey: true, + type: Sequelize.TEXT + }, + "WALLET_ADDRESS": { + type: Sequelize.TEXT, + allowNull: false, + unique: true, + }, + "PRIVATE_KEY": { + type: Sequelize.STRING, + allowNull: false, + }, + "SUPPORTED_NETWORKS": { + type: Sequelize.TEXT, + allowNull: true, + }, + "ERC20_PAYMASTERS": { + type: Sequelize.TEXT, + allowNull: true, + }, + "MULTI_TOKEN_PAYMASTERS": { + type: Sequelize.TEXT, + allowNull: true, + }, + "MULTI_TOKEN_ORACLES": { + type: Sequelize.TEXT, + allowNull: true, + }, + "SPONSOR_NAME": { + type: Sequelize.STRING, + allowNull: true, + }, + "LOGO_URL": { + type: Sequelize.STRING, + allowNull: true, + }, + "TRANSACTION_LIMIT": { + type: Sequelize.INTEGER, + allowNull: false, + }, + "NO_OF_TRANSACTIONS_IN_A_MONTH": { + type: Sequelize.INTEGER, + allowNull: true, + }, + "INDEXER_ENDPOINT": { + type: Sequelize.STRING, + allowNull: true, + }, + "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: 'api_keys', + schema: process.env.DATABASE_SCHEMA_NAME + }); +} + +module.exports = { up, down } diff --git a/backend/migrations/20240611000002-create-sponsorship-policy.cjs b/backend/migrations/20240611000002-create-sponsorship-policy.cjs new file mode 100644 index 0000000..180597c --- /dev/null +++ b/backend/migrations/20240611000002-create-sponsorship-policy.cjs @@ -0,0 +1,82 @@ +const { Sequelize } = require('sequelize') + +async function up({ context: queryInterface }) { + await queryInterface.createTable('sponsorship_policies', { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + field: 'ID' + }, + walletAddress: { + type: Sequelize.STRING, + allowNull: false, + field: 'WALLET_ADDRESS', + references: { + model: 'api_keys', + key: 'WALLET_ADDRESS' + }, + onDelete: 'CASCADE', + onUpdate: 'CASCADE' + }, + name: { + type: Sequelize.STRING, + allowNull: false, + field: 'NAME' + }, + description: { + type: Sequelize.STRING, + allowNull: true, + field: 'DESCRIPTION' + }, + startDate: { + type: Sequelize.DATE, + allowNull: true, + field: 'START_DATE' + }, + endDate: { + type: Sequelize.DATE, + allowNull: true, + field: 'END_DATE' + }, + isPerpetual: { + type: Sequelize.BOOLEAN, + defaultValue: false, + field: 'IS_PERPETUAL' + }, + isUniversal: { + type: Sequelize.BOOLEAN, + defaultValue: false, + field: 'IS_UNIVERSAL' + }, + contractRestrictions: { + type: Sequelize.STRING, + allowNull: true, + field: 'CONTRACT_RESTRICTIONS' + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.NOW, + field: 'CREATED_AT' + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.NOW, + field: 'UPDATED_AT' + } + }, { + schema: process.env.DATABASE_SCHEMA_NAME + }); +} + +async function down({ context: queryInterface }) { + await queryInterface.dropTable({ + tableName: 'sponsorship_policies', + schema: process.env.DATABASE_SCHEMA_NAME + }); +} + +module.exports = { up, down } diff --git a/backend/migrations/20240611000003-create-sponsorship-policy-limits.cjs b/backend/migrations/20240611000003-create-sponsorship-policy-limits.cjs new file mode 100644 index 0000000..4dd7b73 --- /dev/null +++ b/backend/migrations/20240611000003-create-sponsorship-policy-limits.cjs @@ -0,0 +1,62 @@ +const { Sequelize } = require('sequelize') + +async function up({ context: queryInterface }) { + await queryInterface.createTable('sponsorship_policy_limits', { + policyId: { + type: Sequelize.INTEGER, + primaryKey: true, + allowNull: false, + field: 'POLICY_ID', + references: { + model: 'sponsorship_policies', + key: 'ID' + }, + onDelete: 'CASCADE', + onUpdate: 'CASCADE' + }, + limitType: { + type: Sequelize.STRING, + primaryKey: true, + allowNull: false, + field: 'LIMIT_TYPE' + }, + maxUsd: { + type: Sequelize.FLOAT, + allowNull: true, + field: 'MAX_USD' + }, + maxEth: { + type: Sequelize.FLOAT, + allowNull: true, + field: 'MAX_ETH' + }, + maxOperations: { + type: Sequelize.INTEGER, + allowNull: true, + field: 'MAX_OPERATIONS' + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.NOW, + field: 'CREATED_AT' + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.NOW, + field: 'UPDATED_AT' + }, + }, { + schema: process.env.DATABASE_SCHEMA_NAME + }); +} + +async function down({ context: queryInterface }) { + await queryInterface.dropTable({ + tableName: 'sponsorship_policy_limits', + schema: process.env.DATABASE_SCHEMA_NAME + }); +} + +module.exports = { up, down } diff --git a/backend/migrations/20240611000004-create-sponsorship-policy-chains.cjs b/backend/migrations/20240611000004-create-sponsorship-policy-chains.cjs new file mode 100644 index 0000000..627151a --- /dev/null +++ b/backend/migrations/20240611000004-create-sponsorship-policy-chains.cjs @@ -0,0 +1,47 @@ +const { Sequelize } = require('sequelize') + +async function up({ context: queryInterface }) { + await queryInterface.createTable('sponsorship_policy_chains', { + policyId: { + type: Sequelize.INTEGER, + primaryKey: true, + allowNull: false, + field: 'POLICY_ID', + references: { + model: 'sponsorship_policies', + key: 'ID' + }, + onDelete: 'CASCADE', + onUpdate: 'CASCADE' + }, + chainId: { + type: Sequelize.INTEGER, + primaryKey: true, + allowNull: false, + field: 'CHAIN_ID' + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.NOW, + field: 'CREATED_AT' + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.NOW, + field: 'UPDATED_AT' + }, + }, { + schema: process.env.DATABASE_SCHEMA_NAME + }); +} + +async function down({ context: queryInterface }) { + await queryInterface.dropTable({ + tableName: 'sponsorship_policy_chains', + schema: process.env.DATABASE_SCHEMA_NAME + }); +} + +module.exports = { up, down } diff --git a/backend/migrations/20240611000005-seed-config.cjs b/backend/migrations/20240611000005-seed-config.cjs new file mode 100644 index 0000000..9772219 --- /dev/null +++ b/backend/migrations/20240611000005-seed-config.cjs @@ -0,0 +1,11 @@ +require('dotenv').config(); + +async function up({ context: queryInterface }) { + await queryInterface.sequelize.query(`INSERT INTO "${process.env.DATABASE_SCHEMA_NAME}".config ("DEPLOYED_ERC20_PAYMASTERS", "PYTH_MAINNET_URL", "PYTH_TESTNET_URL", "PYTH_TESTNET_CHAIN_IDS", "PYTH_MAINNET_CHAIN_IDS", "CRON_TIME", "CUSTOM_CHAINLINK_DEPLOYED", "COINGECKO_IDS", "COINGECKO_API_URL", "CREATED_AT", "UPDATED_AT") VALUES ('ewogICAgIjQyM...', 'https://hermes.pyth.network/api/latest_vaas?ids%5B%5D=', 'https://hermes-beta.pyth.network/api/latest_vaas?ids%5B%5D=', '5001', '5000', '0 0 * * *', 'ewogICAgIjgwMDAxIjogWyIweGMzM2MzOEE3QkZFQmJCOTk3ZEQ0MDExQ0RkQWY0ZWJEMWU4ODAzQzAiXQp9', 'eyI4MDAwMSI6WyJwYW50aGVyIl19', 'https://api.coingecko.com/api/v3/simple/price?vs_currencies=usd&precision=8&ids=', NOW(), NOW());`); +} + +async function down({ context: queryInterface }) { + await queryInterface.sequelize.query(`DELETE FROM "${process.env.DATABASE_SCHEMA_NAME}".config;`); +} + +module.exports = { up, down } \ No newline at end of file From 8ea021798e3d5b9c33bf760cec9daccd9c678171 Mon Sep 17 00:00:00 2001 From: kanth Date: Thu, 13 Jun 2024 00:43:49 +0530 Subject: [PATCH 11/23] feat: PRO-2395 sequelize repository updates and conflict resolution --- admin_frontend/src/components/Dashboard.jsx | 16 ++-- backend/package.json | 1 + backend/src/index.ts | 2 +- backend/src/migrations/001-default.do.sql | 42 -------- backend/src/migrations/002-apiKeys.do.sql | 24 ----- .../migrations/003-sponsorship-policy.do.sql | 44 --------- backend/src/models/APIKey.ts | 24 ++++- backend/src/models/Config.ts | 96 +++++++++++++++++++ backend/src/models/SponsorshipPolicy.ts | 43 ++++++--- backend/src/models/SponsorshipPolicyChain.ts | 29 +++++- backend/src/models/SponsorshipPolicyLimit.ts | 55 ++++++++--- backend/src/models/associations.ts | 1 - backend/src/plugins/config.ts | 2 + backend/src/plugins/db.ts | 72 +++++--------- backend/src/plugins/sequelizePlugin.ts | 58 ++++------- backend/src/repository/APIKeyRepository.ts | 5 + backend/src/repository/ConfigRepository.ts | 68 +++++++++++++ backend/src/routes/admin.ts | 93 ++++++++---------- backend/src/server.ts | 32 ++++--- backend/src/types/config-data.ts | 13 +++ backend/src/utils/common.ts | 1 - 21 files changed, 415 insertions(+), 306 deletions(-) delete mode 100644 backend/src/migrations/001-default.do.sql delete mode 100644 backend/src/migrations/002-apiKeys.do.sql delete mode 100644 backend/src/migrations/003-sponsorship-policy.do.sql create mode 100644 backend/src/models/Config.ts create mode 100644 backend/src/repository/ConfigRepository.ts create mode 100644 backend/src/types/config-data.ts diff --git a/admin_frontend/src/components/Dashboard.jsx b/admin_frontend/src/components/Dashboard.jsx index 2d63dad..33fb472 100644 --- a/admin_frontend/src/components/Dashboard.jsx +++ b/admin_frontend/src/components/Dashboard.jsx @@ -87,23 +87,23 @@ const Dashboard = () => { setConfig(dataJson); setEdittedConfig(dataJson); let buffer; - if (data.COINGECKO_IDS && data.COINGECKO_IDS !== "") { - buffer = Buffer.from(data.COINGECKO_IDS, "base64"); + if (data.coingeckoIds && data.coingeckoIds !== "") { + buffer = Buffer.from(data.coingeckoIds, "base64"); const coingeckoIds = JSON.parse(buffer.toString()); setCoingeckoIds(coingeckoIds); } if ( - data.DEPLOYED_ERC20_PAYMASTERS && - data.DEPLOYED_ERC20_PAYMASTERS !== "" + data.deployedErc20Paymasters && + data.deployedErc20Paymasters !== "" ) { - buffer = Buffer.from(data.DEPLOYED_ERC20_PAYMASTERS, "base64"); + buffer = Buffer.from(data.deployedErc20Paymasters, "base64"); setDeployedPaymasters(JSON.parse(buffer.toString())); } if ( - data.CUSTOM_CHAINLINK_DEPLOYED && - data.CUSTOM_CHAINLINK_DEPLOYED !== "" + data.customChainlinkDeployed && + data.customChainlinkDeployed !== "" ) { - buffer = Buffer.from(data.CUSTOM_CHAINLINK_DEPLOYED, "base64"); + buffer = Buffer.from(data.customChainlinkDeployed, "base64"); setCustomChainlink(JSON.parse(buffer.toString())); } setDisableSave(true); diff --git a/backend/package.json b/backend/package.json index cf44c86..cfb0de3 100644 --- a/backend/package.json +++ b/backend/package.json @@ -53,6 +53,7 @@ "sqlite3": "5.1.7-rc.0", "ts-node": "^10.9.2", "typescript": "^5.4.5", + "umzug": "^3.8.1", "viem": "2.7.6" }, "devDependencies": { diff --git a/backend/src/index.ts b/backend/src/index.ts index 96915d9..b24c65d 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -15,7 +15,7 @@ setTimeout(async () => { for (const signal of ['SIGINT', 'SIGTERM']) { process.on(signal, () => server.close().then((err) => { - server.sqlite.close(); + server.sequelize.close(); console.log(`close application on ${signal}`); process.exit(err ? 1 : 0); }), diff --git a/backend/src/migrations/001-default.do.sql b/backend/src/migrations/001-default.do.sql deleted file mode 100644 index 01501f2..0000000 --- a/backend/src/migrations/001-default.do.sql +++ /dev/null @@ -1,42 +0,0 @@ --------------------------------------------------------------------------------- --- Up --------------------------------------------------------------------------------- - -CREATE TABLE IF NOT EXISTS config ( - ID SERIAL PRIMARY KEY, - DEPLOYED_ERC20_PAYMASTERS TEXT NOT NULL, - PYTH_MAINNET_URL TEXT NOT NULL, - PYTH_TESTNET_URL TEXT NOT NULL, - PYTH_TESTNET_CHAIN_IDS TEXT NOT NULL, - PYTH_MAINNET_CHAIN_IDS TEXT NOT NULL, - CRON_TIME TEXT NOT NULL, - CUSTOM_CHAINLINK_DEPLOYED TEXT NOT NULL, - COINGECKO_IDS TEXT, - COINGECKO_API_URL TEXT -); - -INSERT INTO config ( - DEPLOYED_ERC20_PAYMASTERS, - PYTH_MAINNET_URL, - PYTH_TESTNET_URL, - PYTH_TESTNET_CHAIN_IDS, - PYTH_MAINNET_CHAIN_IDS, - CRON_TIME, - CUSTOM_CHAINLINK_DEPLOYED, - COINGECKO_IDS, - COINGECKO_API_URL) VALUES ( - 'ewogICAgIjQyMCI6IFsiMHg1M0Y0ODU3OTMwOWY4ZEJmRkU0ZWRFOTIxQzUwMjAwODYxQzI0ODJhIl0sCiAgICAiNDIxNjEzIjogWyIweDBhNkFhMUJkMzBENjk1NGNBNTI1MzE1Mjg3QWRlZUVjYmI2ZUZCNTkiXSwKICAgICI1MDAxIjogWyIweDZFYTI1Y2JiNjAzNjAyNDNFODcxZEQ5MzUyMjVBMjkzYTc4NzA0YTgiXSwKICAgICI4MDAwMSI6IFsiMHhjMzNjMzhBN0JGRUJiQjk5N2RENDAxMUNEZEFmNGViRDFlODgwM0MwIl0KfQ==', - 'https://hermes.pyth.network/api/latest_vaas?ids%5B%5D=', - 'https://hermes-beta.pyth.network/api/latest_vaas?ids%5B%5D=', - '5001', - '5000', - '0 0 * * *', - 'ewogICAgIjgwMDAxIjogWyIweGMzM2MzOEE3QkZFQmJCOTk3ZEQ0MDExQ0RkQWY0ZWJEMWU4ODAzQzAiXQp9', - 'eyI4MDAwMSI6WyJwYW50aGVyIl19', - 'https://api.coingecko.com/api/v3/simple/price?vs_currencies=usd&precision=8&ids='); - --------------------------------------------------------------------------------- --- Down --------------------------------------------------------------------------------- - -DROP TABLE IF EXISTS config; \ No newline at end of file diff --git a/backend/src/migrations/002-apiKeys.do.sql b/backend/src/migrations/002-apiKeys.do.sql deleted file mode 100644 index b8e3d02..0000000 --- a/backend/src/migrations/002-apiKeys.do.sql +++ /dev/null @@ -1,24 +0,0 @@ --------------------------------------------------------------------------------- --- Up --------------------------------------------------------------------------------- - -CREATE TABLE IF NOT EXISTS api_keys ( - API_KEY TEXT NOT NULL PRIMARY KEY, - WALLET_ADDRESS TEXT NOT NULL UNIQUE, - PRIVATE_KEY TEXT NOT NULL, - SUPPORTED_NETWORKS TEXT DEFAULT NULL, - ERC20_PAYMASTERS TEXT DEFAULT NULL, - MULTI_TOKEN_PAYMASTERS TEXT DEFAULT NULL, - MULTI_TOKEN_ORACLES TEXT DEFAULT NULL, - SPONSOR_NAME TEXT DEFAULT NULL, - LOGO_URL TEXT DEFAULT NULL, - TRANSACTION_LIMIT INT NOT NULL, - NO_OF_TRANSACTIONS_IN_A_MONTH INT, - INDEXER_ENDPOINT TEXT -); - --------------------------------------------------------------------------------- --- Down --------------------------------------------------------------------------------- - -DROP TABLE IF EXISTS api_keys; \ No newline at end of file diff --git a/backend/src/migrations/003-sponsorship-policy.do.sql b/backend/src/migrations/003-sponsorship-policy.do.sql deleted file mode 100644 index e7c70ee..0000000 --- a/backend/src/migrations/003-sponsorship-policy.do.sql +++ /dev/null @@ -1,44 +0,0 @@ --------------------------------------------------------------------------------- --- Up --------------------------------------------------------------------------------- - --- Create sponsorship_policies Table -CREATE TABLE IF NOT EXISTS sponsorship_policies ( - ID SERIAL PRIMARY KEY, - WALLET_ADDRESS TEXT NOT NULL UNIQUE, - NAME TEXT NOT NULL, - DESCRIPTION TEXT, - START_DATE DATE, - END_DATE DATE, - IS_PERPETUAL BOOLEAN DEFAULT FALSE, - IS_UNIVERSAL BOOLEAN DEFAULT FALSE, - CONTRACT_RESTRICTIONS TEXT, -- Stores JSON string because PostgreSQL supports JSON natively - FOREIGN KEY (WALLET_ADDRESS) REFERENCES api_keys(WALLET_ADDRESS) ON DELETE CASCADE -); - --- Create sponsorship_policy_limits Table -CREATE TABLE IF NOT EXISTS sponsorship_policy_limits ( - POLICY_ID INT NOT NULL, - LIMIT_TYPE TEXT NOT NULL, - MAX_USD FLOAT, -- FLOAT used in PostgreSQL for floating-point numbers - MAX_ETH FLOAT, - MAX_OPERATIONS INT, - FOREIGN KEY (POLICY_ID) REFERENCES sponsorship_policies(ID) ON DELETE CASCADE, - PRIMARY KEY (POLICY_ID, LIMIT_TYPE) -- Composite primary key -); - --- Create sponsorship_policy_chains Table -CREATE TABLE IF NOT EXISTS sponsorship_policy_chains ( - POLICY_ID INT NOT NULL, - CHAIN_ID TEXT NOT NULL, - FOREIGN KEY (POLICY_ID) REFERENCES sponsorship_policies(ID) ON DELETE CASCADE, - PRIMARY KEY (POLICY_ID, CHAIN_ID) -- Composite primary key -); - --------------------------------------------------------------------------------- --- Down --------------------------------------------------------------------------------- - -DROP TABLE IF EXISTS sponsorship_policy_chains; -DROP TABLE IF EXISTS sponsorship_policy_limits; -DROP TABLE IF EXISTS sponsorship_policies; \ No newline at end of file diff --git a/backend/src/models/APIKey.ts b/backend/src/models/APIKey.ts index faf2159..1c60823 100644 --- a/backend/src/models/APIKey.ts +++ b/backend/src/models/APIKey.ts @@ -17,11 +17,11 @@ export class APIKey extends Model { declare updatedAt: Date; // Added this line } -export function initializeAPIKeyModel(sequelize: Sequelize) { +export function initializeAPIKeyModel(sequelize: Sequelize, schema: string) { console.log('Initializing APIKey model...') - APIKey.init({ + const initializedAPIKeyModel = APIKey.init({ apiKey: { type: DataTypes.TEXT, allowNull: false, @@ -84,10 +84,30 @@ export function initializeAPIKeyModel(sequelize: Sequelize) { allowNull: true, field: 'INDEXER_ENDPOINT' }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + field: 'CREATED_AT' + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + field: 'UPDATED_AT' + }, }, { tableName: 'api_keys', sequelize, // passing the `sequelize` instance is required + //modelName: 'APIKey', + timestamps: true, // enabling timestamps + createdAt: 'createdAt', // mapping 'createdAt' to 'CREATED_AT' + updatedAt: 'updatedAt', // mapping 'updatedAt' to 'UPDATED_AT' + freezeTableName: true, + schema: schema, }); + console.log(`apiKey inited as: ${initializedAPIKeyModel}`) + console.log('APIKey model initialized.') + + return initializedAPIKeyModel; } \ No newline at end of file diff --git a/backend/src/models/Config.ts b/backend/src/models/Config.ts new file mode 100644 index 0000000..12865a4 --- /dev/null +++ b/backend/src/models/Config.ts @@ -0,0 +1,96 @@ +import { Sequelize, DataTypes, Model } from 'sequelize'; + +export class Config extends Model { + public id!: number; // Note that the `null assertion` `!` is required in strict mode. + public deployedErc20Paymasters!: string; + public pythMainnetUrl!: string; + public pythTestnetUrl!: string; + public pythTestnetChainIds!: string; + public pythMainnetChainIds!: string; + public cronTime!: string; + public customChainlinkDeployed!: string; + public coingeckoIds!: string; + public coingeckoApiUrl!: string; + public readonly createdAt!: Date; + public readonly updatedAt!: Date; +} + +const initializeConfigModel = (sequelize: Sequelize, schema: string) => { + Config.init({ + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + field: 'ID' + }, + deployedErc20Paymasters: { + type: DataTypes.TEXT, + allowNull: false, + field: 'DEPLOYED_ERC20_PAYMASTERS' + }, + pythMainnetUrl: { + type: DataTypes.TEXT, + allowNull: false, + field: 'PYTH_MAINNET_URL' + }, + pythTestnetUrl: { + type: DataTypes.TEXT, + allowNull: false, + field: 'PYTH_TESTNET_URL' + }, + pythTestnetChainIds: { + type: DataTypes.TEXT, + allowNull: false, + field: 'PYTH_TESTNET_CHAIN_IDS' + }, + pythMainnetChainIds: { + type: DataTypes.TEXT, + allowNull: false, + field: 'PYTH_MAINNET_CHAIN_IDS' + }, + cronTime: { + type: DataTypes.TEXT, + allowNull: false, + field: 'CRON_TIME' + }, + customChainlinkDeployed: { + type: DataTypes.TEXT, + allowNull: false, + field: 'CUSTOM_CHAINLINK_DEPLOYED' + }, + coingeckoIds: { + type: DataTypes.TEXT, + allowNull: true, + field: 'COINGECKO_IDS' + }, + coingeckoApiUrl: { + type: DataTypes.TEXT, + allowNull: true, + field: 'COINGECKO_API_URL' + }, + 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: 'config', + modelName: 'Config', + timestamps: true, + // createdAt: 'createdAt', + // updatedAt: 'updatedAt', + freezeTableName: true, + schema: schema, + }); +}; + +export { initializeConfigModel }; \ No newline at end of file diff --git a/backend/src/models/SponsorshipPolicy.ts b/backend/src/models/SponsorshipPolicy.ts index 4d9dc2a..b9f1f9c 100644 --- a/backend/src/models/SponsorshipPolicy.ts +++ b/backend/src/models/SponsorshipPolicy.ts @@ -1,18 +1,20 @@ import { Sequelize, DataTypes, Model } from 'sequelize'; export class SponsorshipPolicy extends Model { - declare id: number; - declare walletAddress: string; - declare name: string; - declare description: string | null; - declare startDate: Date | null; - declare endDate: Date | null; - declare isPerpetual: boolean; - declare isUniversal: boolean; - declare contractRestrictions: string | null; + public id!: number; + public walletAddress!: string; + public name!: string; + public description!: string | null; + public startDate!: Date | null; + public endDate!: Date | null; + public isPerpetual!: boolean; + public isUniversal!: boolean; + public contractRestrictions!: string | null; + public readonly createdAt!: Date; + public readonly updatedAt!: Date; } -export function initializeSponsorshipPolicyModel(sequelize: Sequelize) { +export function initializeSponsorshipPolicyModel(sequelize: Sequelize, schema: string) { SponsorshipPolicy.init({ id: { type: DataTypes.INTEGER, @@ -65,9 +67,26 @@ export function initializeSponsorshipPolicyModel(sequelize: Sequelize) { allowNull: true, field: 'CONTRACT_RESTRICTIONS' }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + field: 'CREATED_AT' + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + field: 'UPDATED_AT' + }, }, { - tableName: 'sponsorship_policies', sequelize, - timestamps: false, + tableName: 'sponsorship_policies', + modelName: 'SponsorshipPolicy', + timestamps: true, + createdAt: 'createdAt', + updatedAt: 'updatedAt', + freezeTableName: true, + schema: schema, }); } \ No newline at end of file diff --git a/backend/src/models/SponsorshipPolicyChain.ts b/backend/src/models/SponsorshipPolicyChain.ts index c0c48ae..13f9356 100644 --- a/backend/src/models/SponsorshipPolicyChain.ts +++ b/backend/src/models/SponsorshipPolicyChain.ts @@ -1,11 +1,13 @@ import { Sequelize, DataTypes, Model } from 'sequelize'; export class SponsorshipPolicyChain extends Model { - declare policyId: number; - declare chainId: number; + public policyId!: number; + public chainId!: number; + public readonly createdAt!: Date; + public readonly updatedAt!: Date; } -export function initializeSponsorshipPolicyChainModel(sequelize: Sequelize) { +export function initializeSponsorshipPolicyChainModel(sequelize: Sequelize, schema: string) { SponsorshipPolicyChain.init({ policyId: { type: DataTypes.INTEGER, @@ -19,9 +21,26 @@ export function initializeSponsorshipPolicyChainModel(sequelize: Sequelize) { allowNull: false, field: 'CHAIN_ID' }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + field: 'CREATED_AT' + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + field: 'UPDATED_AT' + }, }, { - tableName: 'sponsorship_policy_chains', sequelize, - timestamps: false, + tableName: 'sponsorship_policy_chains', + modelName: 'SponsorshipPolicyChain', + timestamps: true, + createdAt: 'createdAt', + updatedAt: 'updatedAt', + freezeTableName: true, + schema: schema, }); } \ No newline at end of file diff --git a/backend/src/models/SponsorshipPolicyLimit.ts b/backend/src/models/SponsorshipPolicyLimit.ts index 56fb96a..e2f6ce3 100644 --- a/backend/src/models/SponsorshipPolicyLimit.ts +++ b/backend/src/models/SponsorshipPolicyLimit.ts @@ -1,14 +1,16 @@ import { Sequelize, DataTypes, Model } from 'sequelize'; export class SponsorshipPolicyLimit extends Model { - declare policyId: number; - declare limitType: string; - declare maxUsd: number | null; - declare maxEth: number | null; - declare maxOperations: number | null; + public policyId!: number; + public limitType!: string; + public maxUsd!: number | null; + public maxEth!: number | null; + public maxOperations!: number | null; + public readonly createdAt!: Date; + public readonly updatedAt!: Date; } -export function initializeSponsorshipPolicyLimitModel(sequelize: Sequelize) { +export function initializeSponsorshipPolicyLimitModel(sequelize: Sequelize, schema: string) { SponsorshipPolicyLimit.init({ policyId: { type: DataTypes.INTEGER, @@ -18,19 +20,50 @@ export function initializeSponsorshipPolicyLimitModel(sequelize: Sequelize) { model: 'SponsorshipPolicy', // name of your model for sponsorship policies key: 'id', // key in SponsorshipPolicy that policyId references }, - onDelete: 'CASCADE', // Add this line + onDelete: 'CASCADE', field: 'POLICY_ID' }, limitType: { - type: DataTypes.STRING, // Adjust this if limitType is not a string + type: DataTypes.STRING, primaryKey: true, allowNull: false, field: 'LIMIT_TYPE' }, - // ... other fields ... + maxUsd: { + type: DataTypes.FLOAT, + allowNull: true, + field: 'MAX_USD' + }, + maxEth: { + type: DataTypes.FLOAT, + allowNull: true, + field: 'MAX_ETH' + }, + maxOperations: { + type: DataTypes.INTEGER, + allowNull: true, + field: 'MAX_OPERATIONS' + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + field: 'CREATED_AT' + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + field: 'UPDATED_AT' + }, }, { - tableName: 'sponsorship_policy_limits', sequelize, - timestamps: false, + tableName: 'sponsorship_policy_limits', + modelName: 'SponsorshipPolicyLimit', + timestamps: true, + createdAt: 'createdAt', + updatedAt: 'updatedAt', + freezeTableName: true, + schema: schema, }); } \ No newline at end of file diff --git a/backend/src/models/associations.ts b/backend/src/models/associations.ts index 7d877ce..5f6091c 100644 --- a/backend/src/models/associations.ts +++ b/backend/src/models/associations.ts @@ -1,4 +1,3 @@ -// associations.ts import { APIKey } from './APIKey'; import { SponsorshipPolicy } from './SponsorshipPolicy'; import { SponsorshipPolicyChain } from './SponsorshipPolicyChain'; diff --git a/backend/src/plugins/config.ts b/backend/src/plugins/config.ts index dbc16fb..b8451dc 100644 --- a/backend/src/plugins/config.ts +++ b/backend/src/plugins/config.ts @@ -22,6 +22,7 @@ const ConfigSchema = Type.Strict( MULTI_TOKEN_MARKUP: Type.String() || undefined, DATABASE_URL: Type.String() || undefined, DATABASE_SSL_ENABLED: Type.Boolean() || undefined, + DATABASE_SCHEMA_NAME: Type.String() || undefined, }) ); @@ -55,6 +56,7 @@ const configPlugin: FastifyPluginAsync = async (server) => { MULTI_TOKEN_MARKUP: process.env.MULTI_TOKEN_MARKUP ?? '1150000', DATABASE_URL: process.env.DATABASE_URL ?? '', DATABASE_SSL_ENABLED: process.env.DATABASE_SSL_ENABLED === 'true', + DATABASE_SCHEMA_NAME: process.env.DATABASE_SCHEMA_NAME ?? 'arka', } server.decorate("config", config); diff --git a/backend/src/plugins/db.ts b/backend/src/plugins/db.ts index 1951705..7a198f5 100644 --- a/backend/src/plugins/db.ts +++ b/backend/src/plugins/db.ts @@ -1,75 +1,47 @@ import fp from "fastify-plugin"; import { FastifyPluginAsync } from "fastify"; -const pg = await import('pg'); -const Client = pg.default.Client; -import * as migrate from 'node-pg-migrate'; -import path from 'path'; import { Sequelize } from 'sequelize'; +import path from 'path'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; -import Postgrator from 'postgrator'; +import { Umzug, SequelizeStorage } from 'umzug'; const databasePlugin: FastifyPluginAsync = async (server) => { - const client: InstanceType = new Client({ - connectionString: server.config.DATABASE_URL + const sequelize = new Sequelize(server.config.DATABASE_URL, { + schema: 'arka', }); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); - - console.log(`__dirname: ${__dirname}`) - console.log(`${path.join(__dirname, '../migrations/')}`) - - // await migrate.default({ - // direction: 'up', - // dir: path.join(__dirname, './build/migrations'), - // databaseUrl: server.config.DATABASE_URL, - // migrationsTable: 'arka_migrations' - // }); + + const migrationPath = path.join(__dirname, '../../migrations/*.cjs'); + + console.log('Migration path:', migrationPath); + + const umzug = new Umzug({ + migrations: {glob: migrationPath}, + context: sequelize.getQueryInterface(), + storage: new SequelizeStorage({sequelize}), + logger: console, + }) try { - await client.connect(); - - const migrationPattern = path.join(__dirname, '../migrations/*'); - console.log(`Migration pattern: ${migrationPattern}`) - - const postgrator = new Postgrator({ - migrationPattern: path.join(__dirname, './migrations/*'), - driver: 'pg', - database: 'arkadev', - currentSchema: 'arka', - schemaTable: 'migrations', - execQuery: (query) => client.query(query), - }); - - console.log('Migrating db...') - const result = await postgrator.migrate() - console.log(`Migration done. ${result.length} migrations applied.`) - - if (result.length === 0) { - console.log( - 'No migrations run for schema. Already at the latest one.' - ) - } - - console.log('Migration done.') - - process.exitCode = 0 + console.log('Running migrations...') + await umzug.up(); + console.log('Migrations done.') } catch (err) { - console.error(err) + console.error('Migration failed:', err) process.exitCode = 1 } - await client.end(); - - server.decorate('pg', client); + //server.decorate('sequelize', sequelize); }; declare module "fastify" { interface FastifyInstance { sequelize: Sequelize; - pg: InstanceType; } } -export default fp(databasePlugin); + +export default fp(databasePlugin); \ No newline at end of file diff --git a/backend/src/plugins/sequelizePlugin.ts b/backend/src/plugins/sequelizePlugin.ts index f579a4c..85c1e5b 100644 --- a/backend/src/plugins/sequelizePlugin.ts +++ b/backend/src/plugins/sequelizePlugin.ts @@ -2,10 +2,13 @@ import fp from "fastify-plugin"; import { FastifyPluginAsync } from "fastify"; import { Sequelize, QueryTypes } from 'sequelize'; import dotenv from 'dotenv'; -import { initializeAPIKeyModel } from '../models/APIKey'; // Assuming path correctness +import { APIKey, initializeAPIKeyModel } from '../models/APIKey'; // Assuming path correctness import { initializeSponsorshipPolicyModel } from '../models/SponsorshipPolicy'; import { initializeSponsorshipPolicyChainModel } from '../models/SponsorshipPolicyChain'; -import { initializeSponsorshipPolicyLimitModel } from "models/SponsorshipPolicyLimit"; +import { initializeSponsorshipPolicyLimitModel } from "../models/SponsorshipPolicyLimit"; +import { initializeConfigModel } from "../models/Config"; +import { APIKeyRepository } from "repository/APIKeyRepository"; +import { ConfigRepository } from "repository/ConfigRepository"; const pg = await import('pg'); const Client = pg.default.Client; @@ -35,22 +38,26 @@ const sequelizePlugin: FastifyPluginAsync = async (server) => { }, }); - sequelize.authenticate() - .then(() => console.log('Connection has been established successfully.')) - .catch(err => console.error('Unable to connect to the database:', err)); - - console.log('Initializing models...'); + await sequelize.authenticate(); + + console.log(`Initializing models... with schema name: ${server.config.DATABASE_SCHEMA_NAME}`); // Initialize models - initializeAPIKeyModel(sequelize); - initializeSponsorshipPolicyModel(sequelize); - initializeSponsorshipPolicyChainModel(sequelize); - initializeSponsorshipPolicyLimitModel(sequelize); + initializeConfigModel(sequelize, server.config.DATABASE_SCHEMA_NAME); + const initializedAPIKeyModel = initializeAPIKeyModel(sequelize, server.config.DATABASE_SCHEMA_NAME); + sequelize.models.APIKey = initializedAPIKeyModel; + console.log(`Initialized APIKey model... ${sequelize.models.APIKey}`); + initializeSponsorshipPolicyModel(sequelize, server.config.DATABASE_SCHEMA_NAME); + initializeSponsorshipPolicyChainModel(sequelize, server.config.DATABASE_SCHEMA_NAME); + initializeSponsorshipPolicyLimitModel(sequelize, server.config.DATABASE_SCHEMA_NAME); console.log('Initialized all models...'); server.decorate('sequelize', sequelize); + const apiKeyRepository : APIKeyRepository = new APIKeyRepository(sequelize); + const configRepository : ConfigRepository = new ConfigRepository(sequelize); + console.log('decorated fastify server with models...'); server.addHook('onClose', (instance, done) => { @@ -63,33 +70,8 @@ const sequelizePlugin: FastifyPluginAsync = async (server) => { declare module "fastify" { interface FastifyInstance { sequelize: Sequelize; - } -} - -async function runQuery() { - - console.log('Running test query...'); - - // Replace with your actual connection string - const sequelize = new Sequelize('postgresql://arkauser:paymaster@localhost:5432/arkadev', { - dialect: 'postgres', - protocol: 'postgres', - dialectOptions: { - searchPath: 'arka', - }, - }); - - try { - await sequelize.authenticate(); - console.log('Connection has been established successfully.'); - - // Replace with your actual SQL query - const result = await sequelize.query('SELECT * FROM arka.config', { type: QueryTypes.SELECT }); - console.log(result); - } catch (error) { - console.error('Unable to connect to the database:', error); - } finally { - await sequelize.close(); + apiKeyRepository: APIKeyRepository; + configRepository: ConfigRepository; } } diff --git a/backend/src/repository/APIKeyRepository.ts b/backend/src/repository/APIKeyRepository.ts index 131085c..5a15910 100644 --- a/backend/src/repository/APIKeyRepository.ts +++ b/backend/src/repository/APIKeyRepository.ts @@ -8,6 +8,11 @@ export class APIKeyRepository { this.sequelize = sequelize; } + async create(apiKey: APIKeyCreationAttributes): Promise { + const result = await this.sequelize.models.APIKey.create(apiKey); + return result ? result.get() as APIKey : null; + } + async findAll(): Promise { const result = await this.sequelize.models.APIKey.findAll(); return result.map(apiKey => apiKey.get() as APIKey); diff --git a/backend/src/repository/ConfigRepository.ts b/backend/src/repository/ConfigRepository.ts new file mode 100644 index 0000000..955d564 --- /dev/null +++ b/backend/src/repository/ConfigRepository.ts @@ -0,0 +1,68 @@ +import { Sequelize } from 'sequelize'; +import { Config } from '../models/Config'; + +export class ConfigRepository { + private sequelize: Sequelize; + + constructor(sequelize: Sequelize) { + this.sequelize = sequelize; + } + + async findAll(): Promise { + const result = await this.sequelize.models.Config.findAll(); + return result.map(config => config.get() as Config); + } + + async findFirstConfig(): Promise { + const result = await this.sequelize.models.Config.findOne(); + return result ? result.get() as Config : null; + } + + async updateConfig(body: any): Promise { + try { + // Check if the record exists + const existingRecord = await this.sequelize.models.config.findOne({ + where: { + id: body.id + } + }); + + // If the record doesn't exist, throw an error + if (!existingRecord) { + throw new Error('Record not found'); + } + + // Update the record + await this.sequelize.models.config.update( + { + deployedErc20Paymasters: body.deployedErc20Paymasters, + pythMainnetUrl: body.pythMainnetUrl, + pythTestnetUrl: body.pythTestnetUrl, + pythTestnetChainIds: body.pythTestnetChainIds, + pythMainnetChainIds: body.pythMainnetChainIds, + cronTime: body.cronTime, + customChainlinkDeployed: body.customChainlinkDeployed, + coingeckoIds: body.coingeckoIds, + coingeckoApiUrl: body.coingeckoApiUrl + }, + { + where: { + id: body.id + } + } + ); + + // Get the updated record + const updatedRecord = await this.sequelize.models.config.findOne({ + where: { + id: body.id + } + }); + + return updatedRecord; + } catch (error) { + console.error(error); + throw error; + } + } +} \ No newline at end of file diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts index 70cf4e2..89900d8 100644 --- a/backend/src/routes/admin.ts +++ b/backend/src/routes/admin.ts @@ -6,9 +6,9 @@ import ErrorMessage from "../constants/ErrorMessage.js"; import ReturnCode from "../constants/ReturnCode.js"; import { encode, decode } from "../utils/crypto.js"; import SupportedNetworks from "../../config.json" assert { type: "json" }; -import { Op } from 'sequelize'; -import { APIKey } from "models/APIKey.js"; -import { APIKeyRepository } from "repository/APIKeyRepository.js"; +import { APIKey } from "../models/APIKey.js"; +import { Config } from "models/Config.js"; +import { ConfigUpdateData } from "types/config-data.js"; const adminRoutes: FastifyPluginAsync = async (server) => { server.post('/adminLogin', async function (request, reply) { @@ -26,12 +26,7 @@ const adminRoutes: FastifyPluginAsync = async (server) => { server.get("/getConfig", async function (request, reply) { try { - const result: any = await new Promise((resolve, reject) => { - server.sqlite.db.get("SELECT * FROM config", (err: any, row: any) => { - if (err) reject(err); - resolve(row); - }) - }) + const result: Config[] = await server.configRepository.findAll(); return reply.code(ReturnCode.SUCCESS).send(result); } catch (err: any) { request.log.error(err); @@ -41,32 +36,26 @@ const adminRoutes: FastifyPluginAsync = async (server) => { server.post("/saveConfig", async function (request, reply) { try { - const body: any = JSON.parse(request.body as string); + const body: ConfigUpdateData = JSON.parse(request.body as string); if (!body) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); - if (!body.DEPLOYED_ERC20_PAYMASTERS || !body.PYTH_MAINNET_URL || !body.PYTH_TESTNET_URL || !body.PYTH_TESTNET_CHAIN_IDS || - !body.PYTH_MAINNET_CHAIN_IDS || !body.CRON_TIME || !body.CUSTOM_CHAINLINK_DEPLOYED || !body.COINGECKO_IDS || !body.COINGECKO_API_URL || !body.id) + if (Object.values(body).every(value => value)) { + try { + const result = await server.configRepository.updateConfig(body); + server.log.info(`config entity after database update: ${JSON.stringify(result)}`); + } catch (error) { + server.log.error('Error while updating the config:', error); + throw error; + } + + server.cron.getJobByName('PriceUpdate')?.stop(); + server.cron.getJobByName('PriceUpdate')?.setTime(new CronTime(body.cronTime)); + server.cron.getJobByName('PriceUpdate')?.start(); + return reply.code(ReturnCode.SUCCESS).send({ error: null, message: 'Successfully saved' }); + } else { return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); - await new Promise((resolve, reject) => { - server.sqlite.db.run("UPDATE config SET DEPLOYED_ERC20_PAYMASTERS = ?, \ - PYTH_MAINNET_URL = ?, \ - PYTH_TESTNET_URL = ?, \ - PYTH_TESTNET_CHAIN_IDS = ?, \ - PYTH_MAINNET_CHAIN_IDS = ?, \ - CRON_TIME = ?, \ - CUSTOM_CHAINLINK_DEPLOYED = ?, \ - COINGECKO_IDS = ?, \ - COINGECKO_API_URL = ? WHERE id = ?", [body.DEPLOYED_ERC20_PAYMASTERS, body.PYTH_MAINNET_URL, body.PYTH_TESTNET_URL, body.PYTH_TESTNET_CHAIN_IDS, - body.PYTH_MAINNET_CHAIN_IDS, body.CRON_TIME, body.CUSTOM_CHAINLINK_DEPLOYED, body.COINGECKO_IDS, body.COINGECKO_API_URL, body.id - ], (err: any, row: any) => { - if (err) reject(err); - resolve(row); - }) - }); - server.cron.getJobByName('PriceUpdate')?.stop(); - server.cron.getJobByName('PriceUpdate')?.setTime(new CronTime(body.CRON_TIME)); - server.cron.getJobByName('PriceUpdate')?.start(); - return reply.code(ReturnCode.SUCCESS).send({ error: null, message: 'Successfully saved' }); - } catch (err: any) { + } + } + catch (err: any) { request.log.error(err); return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); } @@ -80,17 +69,16 @@ const adminRoutes: FastifyPluginAsync = async (server) => { if (!body.apiKey || !body.privateKey) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); - request.log.info(`API Key is: ${body.apiKey}`); - request.log.info(`Private Key is: ${body.privateKey}`); if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*-_&])[A-Za-z\d@$!%*-_&]{8,}$/.test(body.apiKey)) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.API_KEY_VALIDATION_FAILED }) - request.log.info(`API Key is valid`); + const wallet = new ethers.Wallet(body.privateKey); const publicAddress = await wallet.getAddress(); request.log.info(`Public address is: ${publicAddress}`); // Use Sequelize to find the API key - const result = await server.sequelize.models.APIKey.findOne({ where: { walletAddress: publicAddress } }); + const result = await server.apiKeyRepository.findOneByWalletAddress(publicAddress); + if (result) { request.log.error('Duplicate record found'); return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.DUPLICATE_RECORD }); @@ -100,10 +88,6 @@ const adminRoutes: FastifyPluginAsync = async (server) => { const hmac = encode(privateKey); - // console.log(`support network on request.body is: ${body.supportedNetworks}`); - // console.log(`erc20 paymasters on request.body is: ${body.erc20Paymasters}`); - // console.log(`request body is: ${JSON.stringify(body)}`); - // Use Sequelize to insert the new API key await server.sequelize.models.APIKey.create({ apiKey: body.apiKey, @@ -137,7 +121,7 @@ const adminRoutes: FastifyPluginAsync = async (server) => { if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*-_&])[A-Za-z\d@$!%*-_&]{8,}$/.test(body.apiKey)) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.API_KEY_VALIDATION_FAILED }); - const apiKeyInstance = await server.sequelize.models.APIKey.findOne({ where: { apiKey: body.apiKey } }); + const apiKeyInstance = await server.apiKeyRepository.findOneByApiKey(body.apiKey); if (!apiKeyInstance) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.RECORD_NOT_FOUND }); @@ -158,9 +142,9 @@ const adminRoutes: FastifyPluginAsync = async (server) => { server.get('/getKeys', async function (request, reply) { try { - if(!server.sequelize) throw new Error('Sequelize instance is not available'); - const apiKeyRepository = new APIKeyRepository(server.sequelize); - const apiKeys: APIKey[] = await apiKeyRepository.findAll(); + if (!server.sequelize) throw new Error('Sequelize instance is not available'); + + const apiKeys = await server.apiKeyRepository.findAll(); apiKeys.forEach((apiKeyEntity: APIKey) => { apiKeyEntity.privateKey = decode(apiKeyEntity.privateKey); }); @@ -180,7 +164,7 @@ const adminRoutes: FastifyPluginAsync = async (server) => { if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*-_&])[A-Za-z\d@$!%*-_&]{8,}$/.test(body.apiKey)) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.API_KEY_VALIDATION_FAILED }); - const apiKeyInstance = await server.sequelize.models.APIKey.findOne({ where: { apiKey: body.apiKey } }); + const apiKeyInstance = await server.apiKeyRepository.findOneByApiKey(body.apiKey); if (!apiKeyInstance) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.RECORD_NOT_FOUND }); @@ -200,17 +184,16 @@ const adminRoutes: FastifyPluginAsync = async (server) => { if (!body.WALLET_ADDRESS) { return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); } - const result: any = await new Promise((resolve, reject) => { - server.sqlite.db.get("SELECT SUPPORTED_NETWORKS from api_keys WHERE WALLET_ADDRESS=?", [ethers.utils.getAddress(body.WALLET_ADDRESS)], (err: any, row: any) => { - if (err) reject(err); - resolve(row); - }) - }) - if (!result) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); + + const apiKeyEntity = await server.apiKeyRepository.findOneByWalletAddress(body.WALLET_ADDRESS); + if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); + let supportedNetworks; - if (result.SUPPORTED_NETWORKS == '') supportedNetworks = SupportedNetworks; + if (!apiKeyEntity.supportedNetworks || apiKeyEntity.supportedNetworks == '') { + supportedNetworks = SupportedNetworks; + } else { - const buffer = Buffer.from(result.SUPPORTED_NETWORKS, 'base64'); + const buffer = Buffer.from(apiKeyEntity.supportedNetworks as string, 'base64'); supportedNetworks = JSON.parse(buffer.toString()) } return reply.code(ReturnCode.SUCCESS).send(supportedNetworks); diff --git a/backend/src/server.ts b/backend/src/server.ts index e802b51..0db24f4 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -18,8 +18,9 @@ import PythOracleAbi from './abi/PythOracleAbi.js'; import { getNetworkConfig } from './utils/common.js'; import { checkDeposit } from './utils/monitorTokenPaymaster.js'; import { APIKey } from 'models/APIKey.js'; -import { APIKeyRepository } from 'repository/APIKeyRepository.js'; -import { Sequelize, QueryTypes } from 'sequelize'; +import { APIKeyRepository } from './repository/APIKeyRepository.js'; +import { Config } from 'models/Config.js'; +import { ConfigRepository } from 'repository/ConfigRepository.js'; let server: FastifyInstance; @@ -62,9 +63,16 @@ const initializeServer = async (): Promise => { // Register the sequelizePlugin await server.register(sequelizePlugin); + // Synchronize all models + await server.sequelize.sync(); + console.log('registered sequelizePlugin...') - const ConfigData: any = await server.sequelize.query("SELECT * FROM config", { type: QueryTypes.SELECT }); + const configRepository = new ConfigRepository(server.sequelize); + const configDatas = await configRepository.findAll(); + const configData: Config | null = configDatas.length > 0 ? configDatas[0] : null; + console.log('configData:', configData); + await server.register(fastifyCron, { jobs: [ @@ -72,14 +80,14 @@ 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?.CRON_TIME ?? '0 0 * * *', // Default: Everyday at midnight UTC, + cronTime: configData?.cronTime ?? '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 () => { if (process.env.CRON_PRIVATE_KEY) { - const paymastersAdrbase64 = ConfigData.DEPLOYED_ERC20_PAYMASTERS ?? '' + const paymastersAdrbase64 = configData?.deployedErc20Paymasters ?? '' if (paymastersAdrbase64) { const buffer = Buffer.from(paymastersAdrbase64, 'base64'); const DEPLOYED_ERC20_PAYMASTERS = JSON.parse(buffer.toString()); @@ -91,15 +99,15 @@ const initializeServer = async (): Promise => { const signer = new ethers.Wallet(process.env.CRON_PRIVATE_KEY ?? '', provider); deployedPaymasters.forEach(async (deployedPaymaster) => { const paymasterContract = new ethers.Contract(deployedPaymaster, PimlicoAbi, signer) - const pythMainnetChains = ConfigData.PYTH_MAINNET_CHAIN_IDS?.split(',') ?? []; - const pythTestnetChains = ConfigData.PYTH_TESTNET_CHAIN_IDS?.split(',') ?? []; + const pythMainnetChains = configData?.pythMainnetChainIds?.split(',') ?? []; + const pythTestnetChains = configData?.pythTestnetChainIds?.split(',') ?? []; if (pythMainnetChains?.includes(chain) || pythTestnetChains?.includes(chain)) { try { const oracleAddress = await paymasterContract.tokenOracle(); const oracleContract = new ethers.Contract(oracleAddress, PythOracleAbi, provider) const priceId = await oracleContract.priceLocator(); - const TESTNET_API_URL = ConfigData.PYTH_TESTNET_URL; - const MAINNET_API_URL = ConfigData.PYTH_MAINNET_URL; + const TESTNET_API_URL = configData?.pythTestnetUrl; + const MAINNET_API_URL = configData?.pythMainnetUrl; const requestURL = `${chain === '5000' ? MAINNET_API_URL : TESTNET_API_URL}${priceId}`; const response = await fetch(requestURL); const vaa: any = await response.json(); @@ -116,8 +124,8 @@ const initializeServer = async (): Promise => { server.log.error(err); } } - const customChainlinkDeploymentsbase64 = ConfigData.CUSTOM_CHAINLINK_DEPLOYED; - const coingeckoIdsbase64 = ConfigData.COINGECKO_IDS; + const customChainlinkDeploymentsbase64 = configData?.customChainlinkDeployed; + const coingeckoIdsbase64 = configData?.coingeckoIds as string; if (customChainlinkDeploymentsbase64) { try { let buffer = Buffer.from(customChainlinkDeploymentsbase64, 'base64'); @@ -127,7 +135,7 @@ const initializeServer = async (): Promise => { const customChainlinkDeployments = customChainlinks[chain] ?? []; if (customChainlinkDeployments.includes(deployedPaymaster)) { const coingeckoId = coingeckoIds[chain][customChainlinkDeployments.indexOf(deployedPaymaster)] - const response: any = await (await fetch(`${ConfigData.COINGECKO_API_URL}${coingeckoId}`)).json(); + const response: any = await (await fetch(`${configData.coingeckoApiUrl}${coingeckoId}`)).json(); const price = ethers.utils.parseUnits(response[coingeckoId].usd.toString(), 8); if (price) { const oracleAddress = await paymasterContract.tokenOracle(); diff --git a/backend/src/types/config-data.ts b/backend/src/types/config-data.ts new file mode 100644 index 0000000..1e294dd --- /dev/null +++ b/backend/src/types/config-data.ts @@ -0,0 +1,13 @@ + +export interface ConfigUpdateData { + deployedErc20Paymasters: string; + pythMainnetUrl: string; + pythTestnetUrl: string; + pythTestnetChainIds: string; + pythMainnetChainIds: string; + cronTime: string; + customChainlinkDeployed: string; + coingeckoIds: string; + coingeckoApiUrl: string; +} + diff --git a/backend/src/utils/common.ts b/backend/src/utils/common.ts index 6fbea0e..abbab86 100644 --- a/backend/src/utils/common.ts +++ b/backend/src/utils/common.ts @@ -1,6 +1,5 @@ import { FastifyBaseLogger, FastifyRequest } from "fastify"; import { BigNumber, ethers } from "ethers"; -import { Database } from "sqlite3"; import SupportedNetworks from "../../config.json" assert { type: "json" }; import { EtherscanResponse, getEtherscanFeeResponse } from "./interface.js"; import { APIKey } from "models/APIKey"; From 227452a9fca0952ca359128e7d7f8c50fc5d260e Mon Sep 17 00:00:00 2001 From: kanth Date: Thu, 13 Jun 2024 01:15:53 +0530 Subject: [PATCH 12/23] fix: PRO-2395 admin-frontend global-config components fix --- admin_frontend/src/components/Dashboard.jsx | 62 +++++++++++---------- backend/src/plugins/sequelizePlugin.ts | 2 + backend/src/repository/APIKeyRepository.ts | 7 +++ backend/src/routes/admin.ts | 3 +- 4 files changed, 43 insertions(+), 31 deletions(-) diff --git a/admin_frontend/src/components/Dashboard.jsx b/admin_frontend/src/components/Dashboard.jsx index 33fb472..eaa7c4b 100644 --- a/admin_frontend/src/components/Dashboard.jsx +++ b/admin_frontend/src/components/Dashboard.jsx @@ -35,15 +35,15 @@ const InfoTextStyle = { const Dashboard = () => { const defaultConfig = { - COINGECKO_API_URL: "", - COINGECKO_IDS: "", - CRON_TIME: "", - CUSTOM_CHAINLINK_DEPLOYED: "", - DEPLOYED_ERC20_PAYMASTERS: "", - PYTH_MAINNET_CHAIN_IDS: "", - PYTH_MAINNET_URL: "", - PYTH_TESTNET_CHAIN_IDS: "", - PYTH_TESTNET_URL: "", + coinGeckoApiUrl: "", + coingeckoIds: "", + cronTime: "", + customChainlinkDeployed: "", + deployedErc20Paymasters: "", + pythMainnetChainIds: "", + pythMainnetUrl: "", + pythTestnetChainIds: "", + pythTestnetUrl: "", id: 1, }; const [config, setConfig] = useState(defaultConfig); @@ -84,8 +84,10 @@ const Dashboard = () => { } ); const dataJson = await data.json(); + console.log(`getConfig on admin-frontend dataJson: ${JSON.stringify(dataJson)}`); setConfig(dataJson); setEdittedConfig(dataJson); + console.log(`set config: ${JSON.stringify(dataJson)}`) let buffer; if (data.coingeckoIds && data.coingeckoIds !== "") { buffer = Buffer.from(data.coingeckoIds, "base64"); @@ -135,13 +137,13 @@ const Dashboard = () => { if (signedIn) { try { setLoading(true); - edittedConfig.COINGECKO_IDS = Buffer.from( + edittedConfig.coingeckoIds = Buffer.from( JSON.stringify(coingeckoIds) ).toString("base64"); - edittedConfig.DEPLOYED_ERC20_PAYMASTERS = Buffer.from( + edittedConfig.deployedErc20Paymasters = Buffer.from( JSON.stringify(deployedPaymasters) ).toString("base64"); - edittedConfig.CUSTOM_CHAINLINK_DEPLOYED = Buffer.from( + edittedConfig.customChainlinkDeployed = Buffer.from( JSON.stringify(customChainlink) ).toString("base64"); const data = await fetch( @@ -202,16 +204,16 @@ const Dashboard = () => { onChange={(e) => { setEdittedConfig({ ...edittedConfig, - COINGECKO_API_URL: e.target.value, + coinGeckoApiUrl: e.target.value, }); if (disableSave) setDisableSave(false); else if ( !disableSave && - e.target.value === config.COINGECKO_API_URL + e.target.value === config.coinGeckoApiUrl ) setDisableSave(true); }} - value={edittedConfig.COINGECKO_API_URL} + value={edittedConfig.coinGeckoApiUrl} required fullWidth multiline @@ -232,13 +234,13 @@ const Dashboard = () => { onChange={(e) => { setEdittedConfig({ ...edittedConfig, - CRON_TIME: e.target.value, + cronTime: e.target.value, }); if (disableSave) setDisableSave(false); - else if (!disableSave && e.target.value === config.CRON_TIME) + else if (!disableSave && e.target.value === config.cronTime) setDisableSave(true); }} - value={edittedConfig.CRON_TIME} + value={edittedConfig.cronTime} required fullWidth /> @@ -291,16 +293,16 @@ const Dashboard = () => { onChange={(e) => { setEdittedConfig({ ...edittedConfig, - PYTH_MAINNET_CHAIN_IDS: e.target.value, + pythMainnetChainIds: e.target.value, }); if (disableSave) setDisableSave(false); else if ( !disableSave && - e.target.value === config.PYTH_MAINNET_CHAIN_IDS + e.target.value === config.pythMainnetChainIds ) setDisableSave(true); }} - value={edittedConfig.PYTH_MAINNET_CHAIN_IDS} + value={edittedConfig.pythMainnetChainIds} required fullWidth multiline @@ -322,13 +324,13 @@ const Dashboard = () => { onChange={(e) => { setEdittedConfig({ ...edittedConfig, - PYTH_MAINNET_URL: e.target.value, + pythMainnetUrl: e.target.value, }); if (disableSave) setDisableSave(false); - else if (!disableSave && e.target.value === config.PYTH_MAINNET_URL) + else if (!disableSave && e.target.value === config.pythMainnetUrl) setDisableSave(true); }} - value={edittedConfig.PYTH_MAINNET_URL} + value={edittedConfig.pythMainnetUrl} required fullWidth multiline @@ -350,16 +352,16 @@ const Dashboard = () => { onChange={(e) => { setEdittedConfig({ ...edittedConfig, - PYTH_TESTNET_CHAIN_IDS: e.target.value, + pythTestnetChainIds: e.target.value, }); if (disableSave) setDisableSave(false); else if ( !disableSave && - e.target.value === config.PYTH_TESTNET_CHAIN_IDS + e.target.value === config.pythTestnetChainIds ) setDisableSave(true); }} - value={edittedConfig.PYTH_TESTNET_CHAIN_IDS} + value={edittedConfig.pythTestnetChainIds} required fullWidth multiline @@ -381,13 +383,13 @@ const Dashboard = () => { onChange={(e) => { setEdittedConfig({ ...edittedConfig, - PYTH_TESTNET_URL: e.target.value, + pythTestnetUrl: e.target.value, }); if (disableSave) setDisableSave(false); - else if (!disableSave && e.target.value === config.PYTH_TESTNET_URL) + else if (!disableSave && e.target.value === config.pythTestnetUrl) setDisableSave(true); }} - value={edittedConfig.PYTH_TESTNET_URL} + value={edittedConfig.pythTestnetUrl} required fullWidth multiline diff --git a/backend/src/plugins/sequelizePlugin.ts b/backend/src/plugins/sequelizePlugin.ts index 85c1e5b..d40dc9e 100644 --- a/backend/src/plugins/sequelizePlugin.ts +++ b/backend/src/plugins/sequelizePlugin.ts @@ -56,7 +56,9 @@ const sequelizePlugin: FastifyPluginAsync = async (server) => { server.decorate('sequelize', sequelize); const apiKeyRepository : APIKeyRepository = new APIKeyRepository(sequelize); + server.decorate('apiKeyRepository', apiKeyRepository); const configRepository : ConfigRepository = new ConfigRepository(sequelize); + server.decorate('configRepository', configRepository); console.log('decorated fastify server with models...'); diff --git a/backend/src/repository/APIKeyRepository.ts b/backend/src/repository/APIKeyRepository.ts index 5a15910..6002727 100644 --- a/backend/src/repository/APIKeyRepository.ts +++ b/backend/src/repository/APIKeyRepository.ts @@ -13,6 +13,13 @@ export class APIKeyRepository { return result ? result.get() as APIKey : null; } + async delete(apiKey: string): Promise { + return await this.sequelize.models.APIKey.destroy({ + where + : { apiKey: apiKey } + }); + } + async findAll(): Promise { const result = await this.sequelize.models.APIKey.findAll(); return result.map(apiKey => apiKey.get() as APIKey); diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts index 89900d8..2f36060 100644 --- a/backend/src/routes/admin.ts +++ b/backend/src/routes/admin.ts @@ -165,10 +165,11 @@ const adminRoutes: FastifyPluginAsync = async (server) => { return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.API_KEY_VALIDATION_FAILED }); const apiKeyInstance = await server.apiKeyRepository.findOneByApiKey(body.apiKey); + console.log(`before delete: ${JSON.stringify(apiKeyInstance)}`); if (!apiKeyInstance) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.RECORD_NOT_FOUND }); - await apiKeyInstance.destroy(); + await server.apiKeyRepository.delete(body.apiKey); return reply.code(ReturnCode.SUCCESS).send({ error: null, message: 'Successfully deleted' }); } catch (err: any) { From 4006f8b937526d70931e7b7378f70c82837403ca Mon Sep 17 00:00:00 2001 From: kanth Date: Thu, 13 Jun 2024 15:28:58 +0530 Subject: [PATCH 13/23] chore: PRO-2395 logging updates --- admin_frontend/src/components/ApiKeys.jsx | 2 -- admin_frontend/src/components/Dashboard.jsx | 10 +++--- .../src/modals/AddERC20Paymaster.jsx | 10 ------ .../src/modals/AddSupportedNetworksModal.jsx | 2 -- admin_frontend/src/modals/CoingeckoId.jsx | 1 - .../src/modals/DeployedPaymasters.jsx | 2 -- .../src/modals/ViewSupportedNetworksModal.jsx | 1 - backend/src/index.ts | 2 +- backend/src/models/APIKey.ts | 35 ++++++++----------- backend/src/paymaster/index.ts | 1 - backend/src/plugins/db.ts | 8 ++--- backend/src/plugins/sequelizePlugin.ts | 12 +++---- backend/src/repository/APIKeyRepository.ts | 32 ++++++++++++++--- backend/src/repository/ConfigRepository.ts | 4 +-- backend/src/routes/admin.ts | 28 +++++++-------- backend/src/routes/index.ts | 1 - backend/src/server.ts | 4 +-- backend/src/types/apikey-dto.ts | 15 ++++++++ .../types/{config-data.ts => config-dto.ts} | 0 backend/src/utils/common.ts | 7 ++-- 20 files changed, 88 insertions(+), 89 deletions(-) create mode 100644 backend/src/types/apikey-dto.ts rename backend/src/types/{config-data.ts => config-dto.ts} (100%) diff --git a/admin_frontend/src/components/ApiKeys.jsx b/admin_frontend/src/components/ApiKeys.jsx index 8575310..b1ebdb3 100644 --- a/admin_frontend/src/components/ApiKeys.jsx +++ b/admin_frontend/src/components/ApiKeys.jsx @@ -72,7 +72,6 @@ const ApiKeysPage = () => { setViewErc20Open(false); }; const handleViewOpen = (networks) => { - console.log(`setting received supportedNetworks value to state: ${JSON.stringify(networks)}`); setSupportedNetworks(networks); setViewModalOpen(true); }; @@ -106,7 +105,6 @@ const ApiKeysPage = () => { if (element.erc20Paymasters) { const buffer = Buffer.from(element.erc20Paymasters, "base64"); const parsedErc20Paymasters = JSON.parse(buffer.toString()); - console.log(`parsedErc20Paymasters: ${JSON.stringify(parsedErc20Paymasters)}`); element.erc20Paymasters = parsedErc20Paymasters; } return element; diff --git a/admin_frontend/src/components/Dashboard.jsx b/admin_frontend/src/components/Dashboard.jsx index eaa7c4b..b46c969 100644 --- a/admin_frontend/src/components/Dashboard.jsx +++ b/admin_frontend/src/components/Dashboard.jsx @@ -35,7 +35,7 @@ const InfoTextStyle = { const Dashboard = () => { const defaultConfig = { - coinGeckoApiUrl: "", + coingeckoApiUrl: "", coingeckoIds: "", cronTime: "", customChainlinkDeployed: "", @@ -84,10 +84,8 @@ const Dashboard = () => { } ); const dataJson = await data.json(); - console.log(`getConfig on admin-frontend dataJson: ${JSON.stringify(dataJson)}`); setConfig(dataJson); setEdittedConfig(dataJson); - console.log(`set config: ${JSON.stringify(dataJson)}`) let buffer; if (data.coingeckoIds && data.coingeckoIds !== "") { buffer = Buffer.from(data.coingeckoIds, "base64"); @@ -204,16 +202,16 @@ const Dashboard = () => { onChange={(e) => { setEdittedConfig({ ...edittedConfig, - coinGeckoApiUrl: e.target.value, + coingeckoApiUrl: e.target.value, }); if (disableSave) setDisableSave(false); else if ( !disableSave && - e.target.value === config.coinGeckoApiUrl + e.target.value === config.coingeckoApiUrl ) setDisableSave(true); }} - value={edittedConfig.coinGeckoApiUrl} + value={edittedConfig.coingeckoApiUrl} required fullWidth multiline diff --git a/admin_frontend/src/modals/AddERC20Paymaster.jsx b/admin_frontend/src/modals/AddERC20Paymaster.jsx index 0fb83b5..0808afa 100644 --- a/admin_frontend/src/modals/AddERC20Paymaster.jsx +++ b/admin_frontend/src/modals/AddERC20Paymaster.jsx @@ -69,11 +69,6 @@ const AddERC20PaymasterModal = ({ setERC20Row(defaultERC20Row); Object.keys(supportedNetworks).map((key) => { Object.keys(supportedNetworks[key]).map((sym) => { - console.log( - tokens.find( - (element) => element.chainId == key && element.token == sym - ) - ); if ( !tokens.find( (element) => element.chainId == key && element.token == sym @@ -112,11 +107,6 @@ const AddERC20PaymasterModal = ({ useEffect(() => { Object.keys(supportedNetworks).map((key) => { Object.keys(supportedNetworks[key]).map((sym) => { - console.log( - tokens.find( - (element) => element.chainId == key && element.token == sym - ) - ); if ( !tokens.find( (element) => element.chainId == key && element.token == sym diff --git a/admin_frontend/src/modals/AddSupportedNetworksModal.jsx b/admin_frontend/src/modals/AddSupportedNetworksModal.jsx index a531e88..a357ad8 100644 --- a/admin_frontend/src/modals/AddSupportedNetworksModal.jsx +++ b/admin_frontend/src/modals/AddSupportedNetworksModal.jsx @@ -180,8 +180,6 @@ const AddSupportedNetworksModal = ({ - {console.log(typeof supportedNetworks)} - {console.log(`supportedNetworks loop: ${JSON.stringify(supportedNetworks)}`)} {Array.isArray(supportedNetworks) && supportedNetworks.map((network, index) => { return ( diff --git a/admin_frontend/src/modals/CoingeckoId.jsx b/admin_frontend/src/modals/CoingeckoId.jsx index fb07c0d..5263359 100644 --- a/admin_frontend/src/modals/CoingeckoId.jsx +++ b/admin_frontend/src/modals/CoingeckoId.jsx @@ -107,7 +107,6 @@ const CoingeckoIdModal = ({ } } setIds(coingeckoIds); - console.log(coingeckoIds); }, [supportedNetworks]); return ( diff --git a/admin_frontend/src/modals/DeployedPaymasters.jsx b/admin_frontend/src/modals/DeployedPaymasters.jsx index ccb4242..23c6e26 100644 --- a/admin_frontend/src/modals/DeployedPaymasters.jsx +++ b/admin_frontend/src/modals/DeployedPaymasters.jsx @@ -106,9 +106,7 @@ const DeployedPaymastersModal = ({ }); } } - console.log(addr); setAddresses(addr); - console.log(supportedNetworks); }, [supportedNetworks]); return ( diff --git a/admin_frontend/src/modals/ViewSupportedNetworksModal.jsx b/admin_frontend/src/modals/ViewSupportedNetworksModal.jsx index 1f77f32..73b2657 100644 --- a/admin_frontend/src/modals/ViewSupportedNetworksModal.jsx +++ b/admin_frontend/src/modals/ViewSupportedNetworksModal.jsx @@ -25,7 +25,6 @@ const ViewSupportedNetworksModal = ({ open, handleClose, }) => { - console.log(`supportedNetworks: ${JSON.stringify(supportedNetworks)}`); return ( server.close().then((err) => { server.sequelize.close(); - console.log(`close application on ${signal}`); + server.log.info(`close application on ${signal}`); process.exit(err ? 1 : 0); }), ); diff --git a/backend/src/models/APIKey.ts b/backend/src/models/APIKey.ts index 1c60823..9aee224 100644 --- a/backend/src/models/APIKey.ts +++ b/backend/src/models/APIKey.ts @@ -1,26 +1,23 @@ import { Sequelize, DataTypes, Model } from 'sequelize'; export class APIKey extends Model { - declare apiKey: string; - declare walletAddress: string; - declare privateKey: string; - declare supportedNetworks: string | null; - declare erc20Paymasters: string | null; - declare multiTokenPaymasters: string | null; - declare multiTokenOracles: string | null; - declare sponsorName: string | null; - declare logoUrl: string | null; - declare transactionLimit: number; - declare noOfTransactionsInAMonth: number | null; - declare indexerEndpoint: string | null; - declare createdAt: Date; // Added this line - declare updatedAt: Date; // Added this line + public apiKey!: string; + public walletAddress!: string; + public privateKey!: string; + public supportedNetworks?: string | null; + public erc20Paymasters?: string | null; + public multiTokenPaymasters?: string | null; + public multiTokenOracles?: string | null; + public sponsorName?: string | null; + public logoUrl?: string | null; + public transactionLimit!: number; + public noOfTransactionsInAMonth?: number | null; + public indexerEndpoint?: string | null; + public createdAt!: Date; + public updatedAt!: Date; } export function initializeAPIKeyModel(sequelize: Sequelize, schema: string) { - - console.log('Initializing APIKey model...') - const initializedAPIKeyModel = APIKey.init({ apiKey: { type: DataTypes.TEXT, @@ -105,9 +102,5 @@ export function initializeAPIKeyModel(sequelize: Sequelize, schema: string) { schema: schema, }); - console.log(`apiKey inited as: ${initializedAPIKeyModel}`) - - console.log('APIKey model initialized.') - return initializedAPIKeyModel; } \ No newline at end of file diff --git a/backend/src/paymaster/index.ts b/backend/src/paymaster/index.ts index 92e7ac4..244afee 100644 --- a/backend/src/paymaster/index.ts +++ b/backend/src/paymaster/index.ts @@ -430,7 +430,6 @@ export class Paymaster { const encodedData = paymasterContract.interface.encodeFunctionData(isEpv06 ? 'depositFunds': 'deposit', []); const etherscanFeeData = await getEtherscanFee(chainId); - console.log('etherscanFeeData: ', etherscanFeeData); let feeData; if (etherscanFeeData) { feeData = etherscanFeeData; diff --git a/backend/src/plugins/db.ts b/backend/src/plugins/db.ts index 7a198f5..160b869 100644 --- a/backend/src/plugins/db.ts +++ b/backend/src/plugins/db.ts @@ -16,8 +16,6 @@ const databasePlugin: FastifyPluginAsync = async (server) => { const __dirname = dirname(__filename); const migrationPath = path.join(__dirname, '../../migrations/*.cjs'); - - console.log('Migration path:', migrationPath); const umzug = new Umzug({ migrations: {glob: migrationPath}, @@ -27,11 +25,11 @@ const databasePlugin: FastifyPluginAsync = async (server) => { }) try { - console.log('Running migrations...') + server.log.info('Running migrations...') await umzug.up(); - console.log('Migrations done.') + server.log.info('Migrations done.') } catch (err) { - console.error('Migration failed:', err) + server.log.error('Migration failed:', err) process.exitCode = 1 } diff --git a/backend/src/plugins/sequelizePlugin.ts b/backend/src/plugins/sequelizePlugin.ts index d40dc9e..b0164d1 100644 --- a/backend/src/plugins/sequelizePlugin.ts +++ b/backend/src/plugins/sequelizePlugin.ts @@ -21,7 +21,7 @@ const sequelizePlugin: FastifyPluginAsync = async (server) => { connectionString: server.config.DATABASE_URL }); await client.connect(); - console.log('Connected to database'); + server.log.info('Connected to database'); } catch (err) { console.error(err); } @@ -40,18 +40,18 @@ const sequelizePlugin: FastifyPluginAsync = async (server) => { await sequelize.authenticate(); - console.log(`Initializing models... with schema name: ${server.config.DATABASE_SCHEMA_NAME}`); + server.log.info(`Initializing models... with schema name: ${server.config.DATABASE_SCHEMA_NAME}`); // Initialize models initializeConfigModel(sequelize, server.config.DATABASE_SCHEMA_NAME); const initializedAPIKeyModel = initializeAPIKeyModel(sequelize, server.config.DATABASE_SCHEMA_NAME); sequelize.models.APIKey = initializedAPIKeyModel; - console.log(`Initialized APIKey model... ${sequelize.models.APIKey}`); + server.log.info(`Initialized APIKey model... ${sequelize.models.APIKey}`); initializeSponsorshipPolicyModel(sequelize, server.config.DATABASE_SCHEMA_NAME); initializeSponsorshipPolicyChainModel(sequelize, server.config.DATABASE_SCHEMA_NAME); initializeSponsorshipPolicyLimitModel(sequelize, server.config.DATABASE_SCHEMA_NAME); - console.log('Initialized all models...'); + server.log.info('Initialized all models...'); server.decorate('sequelize', sequelize); @@ -60,13 +60,13 @@ const sequelizePlugin: FastifyPluginAsync = async (server) => { const configRepository : ConfigRepository = new ConfigRepository(sequelize); server.decorate('configRepository', configRepository); - console.log('decorated fastify server with models...'); + server.log.info('decorated fastify server with models...'); server.addHook('onClose', (instance, done) => { instance.sequelize.close().then(() => done(), done); }); - console.log('added hooks...'); + server.log.info('added hooks...'); }; declare module "fastify" { diff --git a/backend/src/repository/APIKeyRepository.ts b/backend/src/repository/APIKeyRepository.ts index 6002727..e4dd602 100644 --- a/backend/src/repository/APIKeyRepository.ts +++ b/backend/src/repository/APIKeyRepository.ts @@ -1,5 +1,6 @@ import { Sequelize } from 'sequelize'; import { APIKey } from '../models/APIKey'; +import { ApiKeyDto } from '../types/apikey-dto'; export class APIKeyRepository { private sequelize: Sequelize; @@ -8,16 +9,39 @@ export class APIKeyRepository { this.sequelize = sequelize; } - async create(apiKey: APIKeyCreationAttributes): Promise { - const result = await this.sequelize.models.APIKey.create(apiKey); - return result ? result.get() as APIKey : null; + async create(apiKey: ApiKeyDto): Promise { + // generate APIKey sequelize model instance from APIKeyDto + const result = await this.sequelize.models.APIKey.create({ + apiKey: apiKey.apiKey, + walletAddress: apiKey.walletAddress, + privateKey: apiKey.privateKey, + supportedNetworks: apiKey.supportedNetworks, + erc20Paymasters: apiKey.erc20Paymasters, + multiTokenPaymasters: apiKey.multiTokenPaymasters, + multiTokenOracles: apiKey.multiTokenOracles, + sponsorName: apiKey.sponsorName, + logoUrl: apiKey.logoUrl, + transactionLimit: apiKey.transactionLimit, + noOfTransactionsInAMonth: apiKey.noOfTransactionsInAMonth, + indexerEndpoint: apiKey.indexerEndpoint + }) as APIKey; + + + + return result; } async delete(apiKey: string): Promise { - return await this.sequelize.models.APIKey.destroy({ + const deletedCount = await this.sequelize.models.APIKey.destroy({ where : { apiKey: apiKey } }); + + if (deletedCount === 0) { + throw new Error('APIKey deletion failed'); + } + + return deletedCount; } async findAll(): Promise { diff --git a/backend/src/repository/ConfigRepository.ts b/backend/src/repository/ConfigRepository.ts index 955d564..275e382 100644 --- a/backend/src/repository/ConfigRepository.ts +++ b/backend/src/repository/ConfigRepository.ts @@ -21,7 +21,7 @@ export class ConfigRepository { async updateConfig(body: any): Promise { try { // Check if the record exists - const existingRecord = await this.sequelize.models.config.findOne({ + const existingRecord = await this.sequelize.models.Config.findOne({ where: { id: body.id } @@ -33,7 +33,7 @@ export class ConfigRepository { } // Update the record - await this.sequelize.models.config.update( + await this.sequelize.models.Config.update( { deployedErc20Paymasters: body.deployedErc20Paymasters, pythMainnetUrl: body.pythMainnetUrl, diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts index 2f36060..6a708eb 100644 --- a/backend/src/routes/admin.ts +++ b/backend/src/routes/admin.ts @@ -7,8 +7,8 @@ import ReturnCode from "../constants/ReturnCode.js"; import { encode, decode } from "../utils/crypto.js"; import SupportedNetworks from "../../config.json" assert { type: "json" }; import { APIKey } from "../models/APIKey.js"; -import { Config } from "models/Config.js"; -import { ConfigUpdateData } from "types/config-data.js"; +import { ConfigUpdateData } from "../types/config-dto.js"; +import { ApiKeyDto } from "../types/apikey-dto.js"; const adminRoutes: FastifyPluginAsync = async (server) => { server.post('/adminLogin', async function (request, reply) { @@ -16,7 +16,6 @@ const adminRoutes: FastifyPluginAsync = async (server) => { const body: any = JSON.parse(request.body as string); if (!body) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); if (!body.WALLET_ADDRESS) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); - console.log(body, server.config.ADMIN_WALLET_ADDRESS) if (ethers.utils.getAddress(body.WALLET_ADDRESS) === server.config.ADMIN_WALLET_ADDRESS) return reply.code(ReturnCode.SUCCESS).send({ error: null, message: "Successfully Logged in" }); return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_USER }); } catch (err: any) { @@ -26,7 +25,12 @@ const adminRoutes: FastifyPluginAsync = async (server) => { server.get("/getConfig", async function (request, reply) { try { - const result: Config[] = await server.configRepository.findAll(); + const result = await server.configRepository.findFirstConfig(); + + if (!result) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.FAILED_TO_PROCESS }); + } + return reply.code(ReturnCode.SUCCESS).send(result); } catch (err: any) { request.log.error(err); @@ -63,8 +67,7 @@ const adminRoutes: FastifyPluginAsync = async (server) => { server.post('/saveKey', async function (request, reply) { try { - const body: any = JSON.parse(request.body as string); - request.log.info(`Request body is: ${JSON.stringify(body)}`); + const body: any = JSON.parse(request.body as string) as ApiKeyDto; if (!body) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); if (!body.apiKey || !body.privateKey) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); @@ -84,15 +87,10 @@ const adminRoutes: FastifyPluginAsync = async (server) => { return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.DUPLICATE_RECORD }); } - const privateKey = body.privateKey; - - const hmac = encode(privateKey); - - // Use Sequelize to insert the new API key - await server.sequelize.models.APIKey.create({ + await server.apiKeyRepository.create({ apiKey: body.apiKey, walletAddress: publicAddress, - privateKey: hmac, + privateKey: encode(body.privateKey), supportedNetworks: body.supportedNetworks, erc20Paymasters: body.erc20Paymasters, multiTokenPaymasters: body.multiTokenPaymasters ?? null, @@ -111,10 +109,9 @@ const adminRoutes: FastifyPluginAsync = async (server) => { } }) - server.post('/updateKey', async function (request, reply) { try { - const body: any = JSON.parse(request.body as string); + const body = JSON.parse(request.body as string) as ApiKeyDto; if (!body) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); if (!body.apiKey) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); @@ -165,7 +162,6 @@ const adminRoutes: FastifyPluginAsync = async (server) => { return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.API_KEY_VALIDATION_FAILED }); const apiKeyInstance = await server.apiKeyRepository.findOneByApiKey(body.apiKey); - console.log(`before delete: ${JSON.stringify(apiKeyInstance)}`); if (!apiKeyInstance) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.RECORD_NOT_FOUND }); diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index eb4de06..25008b6 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -85,7 +85,6 @@ const routes: FastifyPluginAsync = async (server) => { } if (!api_key) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) - console.log('entryPoint: ', entryPoint); if ((entryPoint != SUPPORTED_ENTRYPOINTS.EPV_06) && (entryPoint != SUPPORTED_ENTRYPOINTS.EPV_07)) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_ENTRYPOINT }) let customPaymasters = []; diff --git a/backend/src/server.ts b/backend/src/server.ts index 0db24f4..1e552b6 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -66,13 +66,11 @@ const initializeServer = async (): Promise => { // Synchronize all models await server.sequelize.sync(); - console.log('registered sequelizePlugin...') + server.log.info('registered sequelizePlugin...') const configRepository = new ConfigRepository(server.sequelize); const configDatas = await configRepository.findAll(); const configData: Config | null = configDatas.length > 0 ? configDatas[0] : null; - console.log('configData:', configData); - await server.register(fastifyCron, { jobs: [ diff --git a/backend/src/types/apikey-dto.ts b/backend/src/types/apikey-dto.ts new file mode 100644 index 0000000..e6babac --- /dev/null +++ b/backend/src/types/apikey-dto.ts @@ -0,0 +1,15 @@ + +export interface ApiKeyDto { + apiKey: string; + walletAddress: string | null; + privateKey: string | null; + supportedNetworks: string | null; + erc20Paymasters: string | null; + multiTokenPaymasters: string | null; + multiTokenOracles: string | null; + sponsorName: string | null; + logoUrl: string | null; + transactionLimit: number | null; + noOfTransactionsInAMonth: number | null; + indexerEndpoint: string | null; +} diff --git a/backend/src/types/config-data.ts b/backend/src/types/config-dto.ts similarity index 100% rename from backend/src/types/config-data.ts rename to backend/src/types/config-dto.ts diff --git a/backend/src/utils/common.ts b/backend/src/utils/common.ts index abbab86..1da5cd4 100644 --- a/backend/src/utils/common.ts +++ b/backend/src/utils/common.ts @@ -25,14 +25,11 @@ export async function getEtherscanFee(chainId: number, log?: FastifyBaseLogger): if (etherscanUrlsBase64) { const buffer = Buffer.from(etherscanUrlsBase64, 'base64'); const etherscanUrls = JSON.parse(buffer.toString()); - console.log('etherscanUrl: ', etherscanUrls[chainId]); - if (etherscanUrls[chainId]) { const data = await fetch(etherscanUrls[chainId]); const response: EtherscanResponse = await data.json(); - console.log('Etherscan Response: ', response); if (response.result && typeof response.result === "object" && response.status === "1") { - console.log('setting maxFeePerGas and maxPriorityFeePerGas as received') + if(log) log.info('setting maxFeePerGas and maxPriorityFeePerGas as received') const maxFeePerGas = ethers.utils.parseUnits(response.result.suggestBaseFee, 'gwei') const fastGasPrice = ethers.utils.parseUnits(response.result.FastGasPrice, 'gwei') return { @@ -43,7 +40,7 @@ export async function getEtherscanFee(chainId: number, log?: FastifyBaseLogger): } if (response.result && typeof response.result === "string" && response.jsonrpc) { const gasPrice = BigNumber.from(response.result) - console.log('setting gas price as received') + if(log) log.info('setting gas price as received') return { maxFeePerGas: gasPrice, maxPriorityFeePerGas: gasPrice, From 6ac302b4466a66085b558d14ff9dcb9fc4486f28 Mon Sep 17 00:00:00 2001 From: kanth Date: Thu, 13 Jun 2024 16:08:00 +0530 Subject: [PATCH 14/23] feat: PRO-2395 restricted sequelize references from repository, server and index files only --- backend/src/routes/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 25008b6..7fb5f4f 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -303,7 +303,7 @@ const routes: FastifyPluginAsync = async (server) => { privateKey = secrets['PRIVATE_KEY']; supportedNetworks = secrets['SUPPORTED_NETWORKS']; } else { - const result = await server.sequelize.models.APIKey.findOne({ where: { apiKey: api_key } }); + const result = await server.apiKeyRepository.findOneByApiKey(api_key); if (!result) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) const apiKeyEntity: APIKey = result as APIKey; if (apiKeyEntity.erc20Paymasters) { From 2709a72339701b411ff5752c84f7355087b24464 Mon Sep 17 00:00:00 2001 From: kanth Date: Thu, 13 Jun 2024 19:26:08 +0530 Subject: [PATCH 15/23] fix: PRO-2395 Fix SponsorshipPolicy migrations and sequelize models naming issues --- ... => 20240611000000-create-arka-config.cjs} | 4 +- ...cjs => 20240611000001-create-api-keys.cjs} | 0 ...611000002-create-sponsorship-policies.cjs} | 13 +++-- ...00003-create-sponsorship-policy-limits.cjs | 4 +- ...00004-create-sponsorship-policy-chains.cjs | 47 ------------------- .../migrations/20240611000004-seed-config.cjs | 11 +++++ .../migrations/20240611000005-seed-config.cjs | 11 ----- backend/src/models/SponsorshipPolicyChain.ts | 46 ------------------ backend/src/models/{APIKey.ts => api-key.ts} | 2 +- .../src/models/{Config.ts => arka-config.ts} | 16 +++---- ...ociations.ts => sequelize-associations.ts} | 36 +++----------- ...cyLimit.ts => sponsorship-policy-limit.ts} | 8 ++-- ...sorshipPolicy.ts => sponsorship-policy.ts} | 14 ++++-- backend/src/plugins/config.ts | 4 +- backend/src/plugins/db.ts | 2 +- backend/src/plugins/sequelizePlugin.ts | 22 ++++----- ...KeyRepository.ts => api-key-repository.ts} | 2 +- ...epository.ts => arka-config-repository.ts} | 22 ++++----- backend/src/routes/admin.ts | 10 ++-- backend/src/routes/index.ts | 4 +- backend/src/routes/metadata.ts | 4 +- backend/src/server.ts | 14 +++--- .../{config-dto.ts => arka-config-dto.ts} | 2 +- backend/src/types/sponsorship-policy-dto.ts | 27 +++++++++++ backend/src/utils/common.ts | 2 +- 25 files changed, 123 insertions(+), 204 deletions(-) rename backend/migrations/{20240611000000-create-config.cjs => 20240611000000-create-arka-config.cjs} (95%) rename backend/migrations/{20240611000001-create-api-key.cjs => 20240611000001-create-api-keys.cjs} (100%) rename backend/migrations/{20240611000002-create-sponsorship-policy.cjs => 20240611000002-create-sponsorship-policies.cjs} (87%) delete mode 100644 backend/migrations/20240611000004-create-sponsorship-policy-chains.cjs create mode 100644 backend/migrations/20240611000004-seed-config.cjs delete mode 100644 backend/migrations/20240611000005-seed-config.cjs delete mode 100644 backend/src/models/SponsorshipPolicyChain.ts rename backend/src/models/{APIKey.ts => api-key.ts} (99%) rename backend/src/models/{Config.ts => arka-config.ts} (89%) rename backend/src/models/{associations.ts => sequelize-associations.ts} (59%) rename backend/src/models/{SponsorshipPolicyLimit.ts => sponsorship-policy-limit.ts} (83%) rename backend/src/models/{SponsorshipPolicy.ts => sponsorship-policy.ts} (87%) rename backend/src/repository/{APIKeyRepository.ts => api-key-repository.ts} (97%) rename backend/src/repository/{ConfigRepository.ts => arka-config-repository.ts} (65%) rename backend/src/types/{config-dto.ts => arka-config-dto.ts} (87%) create mode 100644 backend/src/types/sponsorship-policy-dto.ts diff --git a/backend/migrations/20240611000000-create-config.cjs b/backend/migrations/20240611000000-create-arka-config.cjs similarity index 95% rename from backend/migrations/20240611000000-create-config.cjs rename to backend/migrations/20240611000000-create-arka-config.cjs index 150b69f..02e86e2 100644 --- a/backend/migrations/20240611000000-create-config.cjs +++ b/backend/migrations/20240611000000-create-arka-config.cjs @@ -1,7 +1,7 @@ const { Sequelize } = require('sequelize') async function up({ context: queryInterface }) { - await queryInterface.createTable('config', { + await queryInterface.createTable('arka_config', { ID: { type: Sequelize.INTEGER, primaryKey: true, @@ -60,7 +60,7 @@ async function up({ context: queryInterface }) { } async function down({ context: queryInterface }) { await queryInterface.dropTable({ - tableName: 'config', + tableName: 'arka_config', schema: process.env.DATABASE_SCHEMA_NAME }) } diff --git a/backend/migrations/20240611000001-create-api-key.cjs b/backend/migrations/20240611000001-create-api-keys.cjs similarity index 100% rename from backend/migrations/20240611000001-create-api-key.cjs rename to backend/migrations/20240611000001-create-api-keys.cjs diff --git a/backend/migrations/20240611000002-create-sponsorship-policy.cjs b/backend/migrations/20240611000002-create-sponsorship-policies.cjs similarity index 87% rename from backend/migrations/20240611000002-create-sponsorship-policy.cjs rename to backend/migrations/20240611000002-create-sponsorship-policies.cjs index 180597c..ed4df63 100644 --- a/backend/migrations/20240611000002-create-sponsorship-policy.cjs +++ b/backend/migrations/20240611000002-create-sponsorship-policies.cjs @@ -10,7 +10,7 @@ async function up({ context: queryInterface }) { field: 'ID' }, walletAddress: { - type: Sequelize.STRING, + type: Sequelize.TEXT, allowNull: false, field: 'WALLET_ADDRESS', references: { @@ -21,15 +21,20 @@ async function up({ context: queryInterface }) { onUpdate: 'CASCADE' }, name: { - type: Sequelize.STRING, + type: Sequelize.TEXT, allowNull: false, field: 'NAME' }, description: { - type: Sequelize.STRING, + type: Sequelize.TEXT, allowNull: true, field: 'DESCRIPTION' }, + enabledChains: { + type: Sequelize.ARRAY(Sequelize.INTEGER), + allowNull: true, + field: 'ENABLED_CHAINS' + }, startDate: { type: Sequelize.DATE, allowNull: true, @@ -51,7 +56,7 @@ async function up({ context: queryInterface }) { field: 'IS_UNIVERSAL' }, contractRestrictions: { - type: Sequelize.STRING, + type: Sequelize.TEXT, allowNull: true, field: 'CONTRACT_RESTRICTIONS' }, diff --git a/backend/migrations/20240611000003-create-sponsorship-policy-limits.cjs b/backend/migrations/20240611000003-create-sponsorship-policy-limits.cjs index 4dd7b73..c5b811f 100644 --- a/backend/migrations/20240611000003-create-sponsorship-policy-limits.cjs +++ b/backend/migrations/20240611000003-create-sponsorship-policy-limits.cjs @@ -21,12 +21,12 @@ async function up({ context: queryInterface }) { field: 'LIMIT_TYPE' }, maxUsd: { - type: Sequelize.FLOAT, + type: Sequelize.DECIMAL(10, 4), // max 10 digits, 4 of which can be after the decimal point allowNull: true, field: 'MAX_USD' }, maxEth: { - type: Sequelize.FLOAT, + type: Sequelize.DECIMAL(22, 18), // max 22 digits, 18 of which can be after the decimal point allowNull: true, field: 'MAX_ETH' }, diff --git a/backend/migrations/20240611000004-create-sponsorship-policy-chains.cjs b/backend/migrations/20240611000004-create-sponsorship-policy-chains.cjs deleted file mode 100644 index 627151a..0000000 --- a/backend/migrations/20240611000004-create-sponsorship-policy-chains.cjs +++ /dev/null @@ -1,47 +0,0 @@ -const { Sequelize } = require('sequelize') - -async function up({ context: queryInterface }) { - await queryInterface.createTable('sponsorship_policy_chains', { - policyId: { - type: Sequelize.INTEGER, - primaryKey: true, - allowNull: false, - field: 'POLICY_ID', - references: { - model: 'sponsorship_policies', - key: 'ID' - }, - onDelete: 'CASCADE', - onUpdate: 'CASCADE' - }, - chainId: { - type: Sequelize.INTEGER, - primaryKey: true, - allowNull: false, - field: 'CHAIN_ID' - }, - createdAt: { - type: Sequelize.DATE, - allowNull: false, - defaultValue: Sequelize.NOW, - field: 'CREATED_AT' - }, - updatedAt: { - type: Sequelize.DATE, - allowNull: false, - defaultValue: Sequelize.NOW, - field: 'UPDATED_AT' - }, - }, { - schema: process.env.DATABASE_SCHEMA_NAME - }); -} - -async function down({ context: queryInterface }) { - await queryInterface.dropTable({ - tableName: 'sponsorship_policy_chains', - schema: process.env.DATABASE_SCHEMA_NAME - }); -} - -module.exports = { up, down } diff --git a/backend/migrations/20240611000004-seed-config.cjs b/backend/migrations/20240611000004-seed-config.cjs new file mode 100644 index 0000000..7ad74d9 --- /dev/null +++ b/backend/migrations/20240611000004-seed-config.cjs @@ -0,0 +1,11 @@ +require('dotenv').config(); + +async function up({ context: queryInterface }) { + await queryInterface.sequelize.query(`INSERT INTO "${process.env.DATABASE_SCHEMA_NAME}".arka_config ("DEPLOYED_ERC20_PAYMASTERS", "PYTH_MAINNET_URL", "PYTH_TESTNET_URL", "PYTH_TESTNET_CHAIN_IDS", "PYTH_MAINNET_CHAIN_IDS", "CRON_TIME", "CUSTOM_CHAINLINK_DEPLOYED", "COINGECKO_IDS", "COINGECKO_API_URL", "CREATED_AT", "UPDATED_AT") VALUES ('ewogICAgIjQyM...', 'https://hermes.pyth.network/api/latest_vaas?ids%5B%5D=', 'https://hermes-beta.pyth.network/api/latest_vaas?ids%5B%5D=', '5001', '5000', '0 0 * * *', 'ewogICAgIjgwMDAxIjogWyIweGMzM2MzOEE3QkZFQmJCOTk3ZEQ0MDExQ0RkQWY0ZWJEMWU4ODAzQzAiXQp9', 'eyI4MDAwMSI6WyJwYW50aGVyIl19', 'https://api.coingecko.com/api/v3/simple/price?vs_currencies=usd&precision=8&ids=', NOW(), NOW());`); +} + +async function down({ context: queryInterface }) { + await queryInterface.sequelize.query(`DELETE FROM "${process.env.DATABASE_SCHEMA_NAME}".arka_config;`); +} + +module.exports = { up, down } \ No newline at end of file diff --git a/backend/migrations/20240611000005-seed-config.cjs b/backend/migrations/20240611000005-seed-config.cjs deleted file mode 100644 index 9772219..0000000 --- a/backend/migrations/20240611000005-seed-config.cjs +++ /dev/null @@ -1,11 +0,0 @@ -require('dotenv').config(); - -async function up({ context: queryInterface }) { - await queryInterface.sequelize.query(`INSERT INTO "${process.env.DATABASE_SCHEMA_NAME}".config ("DEPLOYED_ERC20_PAYMASTERS", "PYTH_MAINNET_URL", "PYTH_TESTNET_URL", "PYTH_TESTNET_CHAIN_IDS", "PYTH_MAINNET_CHAIN_IDS", "CRON_TIME", "CUSTOM_CHAINLINK_DEPLOYED", "COINGECKO_IDS", "COINGECKO_API_URL", "CREATED_AT", "UPDATED_AT") VALUES ('ewogICAgIjQyM...', 'https://hermes.pyth.network/api/latest_vaas?ids%5B%5D=', 'https://hermes-beta.pyth.network/api/latest_vaas?ids%5B%5D=', '5001', '5000', '0 0 * * *', 'ewogICAgIjgwMDAxIjogWyIweGMzM2MzOEE3QkZFQmJCOTk3ZEQ0MDExQ0RkQWY0ZWJEMWU4ODAzQzAiXQp9', 'eyI4MDAwMSI6WyJwYW50aGVyIl19', 'https://api.coingecko.com/api/v3/simple/price?vs_currencies=usd&precision=8&ids=', NOW(), NOW());`); -} - -async function down({ context: queryInterface }) { - await queryInterface.sequelize.query(`DELETE FROM "${process.env.DATABASE_SCHEMA_NAME}".config;`); -} - -module.exports = { up, down } \ No newline at end of file diff --git a/backend/src/models/SponsorshipPolicyChain.ts b/backend/src/models/SponsorshipPolicyChain.ts deleted file mode 100644 index 13f9356..0000000 --- a/backend/src/models/SponsorshipPolicyChain.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Sequelize, DataTypes, Model } from 'sequelize'; - -export class SponsorshipPolicyChain extends Model { - public policyId!: number; - public chainId!: number; - public readonly createdAt!: Date; - public readonly updatedAt!: Date; -} - -export function initializeSponsorshipPolicyChainModel(sequelize: Sequelize, schema: string) { - SponsorshipPolicyChain.init({ - policyId: { - type: DataTypes.INTEGER, - primaryKey: true, - allowNull: false, - field: 'POLICY_ID' - }, - chainId: { - type: DataTypes.INTEGER, - primaryKey: true, - allowNull: false, - field: 'CHAIN_ID' - }, - 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: 'sponsorship_policy_chains', - modelName: 'SponsorshipPolicyChain', - timestamps: true, - createdAt: 'createdAt', - updatedAt: 'updatedAt', - freezeTableName: true, - schema: schema, - }); -} \ No newline at end of file diff --git a/backend/src/models/APIKey.ts b/backend/src/models/api-key.ts similarity index 99% rename from backend/src/models/APIKey.ts rename to backend/src/models/api-key.ts index 9aee224..e291f9d 100644 --- a/backend/src/models/APIKey.ts +++ b/backend/src/models/api-key.ts @@ -94,7 +94,7 @@ export function initializeAPIKeyModel(sequelize: Sequelize, schema: string) { }, { tableName: 'api_keys', sequelize, // passing the `sequelize` instance is required - //modelName: 'APIKey', + modelName: 'APIKey', timestamps: true, // enabling timestamps createdAt: 'createdAt', // mapping 'createdAt' to 'CREATED_AT' updatedAt: 'updatedAt', // mapping 'updatedAt' to 'UPDATED_AT' diff --git a/backend/src/models/Config.ts b/backend/src/models/arka-config.ts similarity index 89% rename from backend/src/models/Config.ts rename to backend/src/models/arka-config.ts index 12865a4..33828a8 100644 --- a/backend/src/models/Config.ts +++ b/backend/src/models/arka-config.ts @@ -1,6 +1,6 @@ import { Sequelize, DataTypes, Model } from 'sequelize'; -export class Config extends Model { +export class ArkaConfig extends Model { public id!: number; // Note that the `null assertion` `!` is required in strict mode. public deployedErc20Paymasters!: string; public pythMainnetUrl!: string; @@ -15,8 +15,8 @@ export class Config extends Model { public readonly updatedAt!: Date; } -const initializeConfigModel = (sequelize: Sequelize, schema: string) => { - Config.init({ +const initializeArkaConfigModel = (sequelize: Sequelize, schema: string) => { + ArkaConfig.init({ id: { type: DataTypes.INTEGER, primaryKey: true, @@ -83,14 +83,14 @@ const initializeConfigModel = (sequelize: Sequelize, schema: string) => { }, }, { sequelize, - tableName: 'config', - modelName: 'Config', + tableName: 'arka_config', + modelName: 'ArkaConfig', timestamps: true, - // createdAt: 'createdAt', - // updatedAt: 'updatedAt', + createdAt: 'createdAt', + updatedAt: 'updatedAt', freezeTableName: true, schema: schema, }); }; -export { initializeConfigModel }; \ No newline at end of file +export { initializeArkaConfigModel }; \ No newline at end of file diff --git a/backend/src/models/associations.ts b/backend/src/models/sequelize-associations.ts similarity index 59% rename from backend/src/models/associations.ts rename to backend/src/models/sequelize-associations.ts index 5f6091c..71e471f 100644 --- a/backend/src/models/associations.ts +++ b/backend/src/models/sequelize-associations.ts @@ -1,13 +1,12 @@ -import { APIKey } from './APIKey'; -import { SponsorshipPolicy } from './SponsorshipPolicy'; -import { SponsorshipPolicyChain } from './SponsorshipPolicyChain'; -import { SponsorshipPolicyLimit } from './SponsorshipPolicyLimit'; +import { APIKey } from './api-key'; +import { SponsorshipPolicy } from './sponsorship-policy'; +import { SponsorshipPolicyLimit } from './sponsorship-policy-limit'; export function setupAssociations() { /** * APIKey to SponsorshipPolicy - * A single APIKey (the parent) can have many SponsorshipPolicies (the children). + * A single APIKey (the parent) can have many SponsorshipPolicy (the children). * The link between them is made using the 'walletAddress' field of the APIKey and the 'walletAddress' field of the SponsorshipPolicy. */ APIKey.hasMany(SponsorshipPolicy, { @@ -27,29 +26,6 @@ export function setupAssociations() { as: 'apiKey', // Optional alias }); - /** - * SponsorshipPolicy to SponsorshipPolicyChain - * A single SponsorshipPolicy (the parent) can have many SponsorshipPolicyChains (the children). - * The link between them is made using the 'id' field of the SponsorshipPolicy and the 'policyId' field of the SponsorshipPolicyChain - */ - SponsorshipPolicy.hasMany(SponsorshipPolicyChain, { - foreignKey: 'policyId', - sourceKey: 'id', - as: 'policyChains' - }); - - - /** - * SponsorshipPolicyChain to SponsorshipPolicy - * A single SponsorshipPolicyChain (the child) belongs to one SponsorshipPolicy (the parent). - * The link between them is made using the 'policyId' field of the SponsorshipPolicyChain and the 'id' field of the SponsorshipPolicy. - */ - SponsorshipPolicyChain.belongsTo(SponsorshipPolicy, { - foreignKey: 'policyId', - targetKey: 'id', - as: 'policy' - }); - /** * SponsorshipPolicy to SponsorshipPolicyLimit * A single SponsorshipPolicy (the parent) can have many SponsorshipPolicyLimits (the children). @@ -58,7 +34,7 @@ export function setupAssociations() { SponsorshipPolicy.hasMany(SponsorshipPolicyLimit, { foreignKey: 'policyId', sourceKey: 'id', - as: 'policyLimits' + as: 'sponsorshipPolicyLimits' }); /** @@ -69,6 +45,6 @@ export function setupAssociations() { SponsorshipPolicyLimit.belongsTo(SponsorshipPolicy, { foreignKey: 'policyId', targetKey: 'id', - as: 'policy' + as: 'sponsorshipPolicy' }); } diff --git a/backend/src/models/SponsorshipPolicyLimit.ts b/backend/src/models/sponsorship-policy-limit.ts similarity index 83% rename from backend/src/models/SponsorshipPolicyLimit.ts rename to backend/src/models/sponsorship-policy-limit.ts index e2f6ce3..3387e34 100644 --- a/backend/src/models/SponsorshipPolicyLimit.ts +++ b/backend/src/models/sponsorship-policy-limit.ts @@ -17,8 +17,8 @@ export function initializeSponsorshipPolicyLimitModel(sequelize: Sequelize, sche primaryKey: true, allowNull: false, references: { - model: 'SponsorshipPolicy', // name of your model for sponsorship policies - key: 'id', // key in SponsorshipPolicy that policyId references + model: 'sponsorship_policies', // name of your model for sponsorship policies + key: 'ID', // key in SponsorshipPolicy that policyId references }, onDelete: 'CASCADE', field: 'POLICY_ID' @@ -30,12 +30,12 @@ export function initializeSponsorshipPolicyLimitModel(sequelize: Sequelize, sche field: 'LIMIT_TYPE' }, maxUsd: { - type: DataTypes.FLOAT, + type: DataTypes.DECIMAL(10, 4), // max 10 digits, 4 of which can be after the decimal point allowNull: true, field: 'MAX_USD' }, maxEth: { - type: DataTypes.FLOAT, + type: DataTypes.DECIMAL(22, 18), // max 22 digits, 18 of which can be after the decimal point allowNull: true, field: 'MAX_ETH' }, diff --git a/backend/src/models/SponsorshipPolicy.ts b/backend/src/models/sponsorship-policy.ts similarity index 87% rename from backend/src/models/SponsorshipPolicy.ts rename to backend/src/models/sponsorship-policy.ts index b9f1f9c..8cc1fcc 100644 --- a/backend/src/models/SponsorshipPolicy.ts +++ b/backend/src/models/sponsorship-policy.ts @@ -5,6 +5,7 @@ export class SponsorshipPolicy extends Model { public walletAddress!: string; public name!: string; public description!: string | null; + public enabledChains?: number[]; public startDate!: Date | null; public endDate!: Date | null; public isPerpetual!: boolean; @@ -27,21 +28,26 @@ export function initializeSponsorshipPolicyModel(sequelize: Sequelize, schema: s field: 'WALLET_ADDRESS', references: { model: 'api_keys', // This is the table name of the model being referenced - key: 'wallet_address', // This is the key column in the APIKey model + key: 'WALLET_ADDRESS', // This is the key column in the APIKey model }, onDelete: 'CASCADE', onUpdate: 'CASCADE' }, name: { - type: DataTypes.STRING, + type: DataTypes.TEXT, allowNull: false, field: 'NAME' }, description: { - type: DataTypes.STRING, + type: DataTypes.TEXT, allowNull: true, field: 'DESCRIPTION' }, + enabledChains: { + type: DataTypes.ARRAY(DataTypes.INTEGER), + allowNull: true, + field: 'ENABLED_CHAINS' + }, startDate: { type: DataTypes.DATE, allowNull: true, @@ -63,7 +69,7 @@ export function initializeSponsorshipPolicyModel(sequelize: Sequelize, schema: s field: 'IS_UNIVERSAL' }, contractRestrictions: { - type: DataTypes.STRING, + type: DataTypes.TEXT, allowNull: true, field: 'CONTRACT_RESTRICTIONS' }, diff --git a/backend/src/plugins/config.ts b/backend/src/plugins/config.ts index b8451dc..e09177d 100644 --- a/backend/src/plugins/config.ts +++ b/backend/src/plugins/config.ts @@ -34,7 +34,7 @@ const ajv = new Ajv({ allowUnionTypes: true, }); -export type Config = Static; +export type ArkaConfig = Static; const configPlugin: FastifyPluginAsync = async (server) => { const validate = ajv.compile(ConfigSchema); @@ -64,7 +64,7 @@ const configPlugin: FastifyPluginAsync = async (server) => { declare module "fastify" { interface FastifyInstance { - config: Config; + config: ArkaConfig; } } diff --git a/backend/src/plugins/db.ts b/backend/src/plugins/db.ts index 160b869..c02afa8 100644 --- a/backend/src/plugins/db.ts +++ b/backend/src/plugins/db.ts @@ -29,7 +29,7 @@ const databasePlugin: FastifyPluginAsync = async (server) => { await umzug.up(); server.log.info('Migrations done.') } catch (err) { - server.log.error('Migration failed:', err) + console.error('Migration failed:', err) process.exitCode = 1 } diff --git a/backend/src/plugins/sequelizePlugin.ts b/backend/src/plugins/sequelizePlugin.ts index b0164d1..3956557 100644 --- a/backend/src/plugins/sequelizePlugin.ts +++ b/backend/src/plugins/sequelizePlugin.ts @@ -2,13 +2,12 @@ import fp from "fastify-plugin"; import { FastifyPluginAsync } from "fastify"; import { Sequelize, QueryTypes } from 'sequelize'; import dotenv from 'dotenv'; -import { APIKey, initializeAPIKeyModel } from '../models/APIKey'; // Assuming path correctness -import { initializeSponsorshipPolicyModel } from '../models/SponsorshipPolicy'; -import { initializeSponsorshipPolicyChainModel } from '../models/SponsorshipPolicyChain'; -import { initializeSponsorshipPolicyLimitModel } from "../models/SponsorshipPolicyLimit"; -import { initializeConfigModel } from "../models/Config"; -import { APIKeyRepository } from "repository/APIKeyRepository"; -import { ConfigRepository } from "repository/ConfigRepository"; +import { APIKey, initializeAPIKeyModel } from '../models/api-key'; // Assuming path correctness +import { initializeSponsorshipPolicyModel } from '../models/sponsorship-policy'; +import { initializeSponsorshipPolicyLimitModel } from "../models/sponsorship-policy-limit"; +import { initializeArkaConfigModel } from "../models/arka-config"; +import { APIKeyRepository } from "repository/api-key-repository"; +import { ArkaConfigRepository } from "repository/arka-config-repository"; const pg = await import('pg'); const Client = pg.default.Client; @@ -43,12 +42,11 @@ const sequelizePlugin: FastifyPluginAsync = async (server) => { server.log.info(`Initializing models... with schema name: ${server.config.DATABASE_SCHEMA_NAME}`); // Initialize models - initializeConfigModel(sequelize, server.config.DATABASE_SCHEMA_NAME); + initializeArkaConfigModel(sequelize, server.config.DATABASE_SCHEMA_NAME); const initializedAPIKeyModel = initializeAPIKeyModel(sequelize, server.config.DATABASE_SCHEMA_NAME); sequelize.models.APIKey = initializedAPIKeyModel; server.log.info(`Initialized APIKey model... ${sequelize.models.APIKey}`); initializeSponsorshipPolicyModel(sequelize, server.config.DATABASE_SCHEMA_NAME); - initializeSponsorshipPolicyChainModel(sequelize, server.config.DATABASE_SCHEMA_NAME); initializeSponsorshipPolicyLimitModel(sequelize, server.config.DATABASE_SCHEMA_NAME); server.log.info('Initialized all models...'); @@ -57,8 +55,8 @@ const sequelizePlugin: FastifyPluginAsync = async (server) => { const apiKeyRepository : APIKeyRepository = new APIKeyRepository(sequelize); server.decorate('apiKeyRepository', apiKeyRepository); - const configRepository : ConfigRepository = new ConfigRepository(sequelize); - server.decorate('configRepository', configRepository); + const arkaConfigRepository : ArkaConfigRepository = new ArkaConfigRepository(sequelize); + server.decorate('arkaConfigRepository', arkaConfigRepository); server.log.info('decorated fastify server with models...'); @@ -73,7 +71,7 @@ declare module "fastify" { interface FastifyInstance { sequelize: Sequelize; apiKeyRepository: APIKeyRepository; - configRepository: ConfigRepository; + arkaConfigRepository: ArkaConfigRepository; } } diff --git a/backend/src/repository/APIKeyRepository.ts b/backend/src/repository/api-key-repository.ts similarity index 97% rename from backend/src/repository/APIKeyRepository.ts rename to backend/src/repository/api-key-repository.ts index e4dd602..65b0ea9 100644 --- a/backend/src/repository/APIKeyRepository.ts +++ b/backend/src/repository/api-key-repository.ts @@ -1,5 +1,5 @@ import { Sequelize } from 'sequelize'; -import { APIKey } from '../models/APIKey'; +import { APIKey } from '../models/api-key'; import { ApiKeyDto } from '../types/apikey-dto'; export class APIKeyRepository { diff --git a/backend/src/repository/ConfigRepository.ts b/backend/src/repository/arka-config-repository.ts similarity index 65% rename from backend/src/repository/ConfigRepository.ts rename to backend/src/repository/arka-config-repository.ts index 275e382..b9ecb4e 100644 --- a/backend/src/repository/ConfigRepository.ts +++ b/backend/src/repository/arka-config-repository.ts @@ -1,27 +1,27 @@ import { Sequelize } from 'sequelize'; -import { Config } from '../models/Config'; +import { ArkaConfig } from '../models/arka-config'; -export class ConfigRepository { +export class ArkaConfigRepository { private sequelize: Sequelize; constructor(sequelize: Sequelize) { this.sequelize = sequelize; } - async findAll(): Promise { - const result = await this.sequelize.models.Config.findAll(); - return result.map(config => config.get() as Config); + async findAll(): Promise { + const result = await this.sequelize.models.ArkaConfig.findAll(); + return result.map(config => config.get() as ArkaConfig); } - async findFirstConfig(): Promise { - const result = await this.sequelize.models.Config.findOne(); - return result ? result.get() as Config : null; + async findFirstConfig(): Promise { + const result = await this.sequelize.models.ArkaConfig.findOne(); + return result ? result.get() as ArkaConfig : null; } async updateConfig(body: any): Promise { try { // Check if the record exists - const existingRecord = await this.sequelize.models.Config.findOne({ + const existingRecord = await this.sequelize.models.ArkaConfig.findOne({ where: { id: body.id } @@ -33,7 +33,7 @@ export class ConfigRepository { } // Update the record - await this.sequelize.models.Config.update( + await this.sequelize.models.ArkaConfig.update( { deployedErc20Paymasters: body.deployedErc20Paymasters, pythMainnetUrl: body.pythMainnetUrl, @@ -53,7 +53,7 @@ export class ConfigRepository { ); // Get the updated record - const updatedRecord = await this.sequelize.models.config.findOne({ + const updatedRecord = await this.sequelize.models.ArkaConfig.findOne({ where: { id: body.id } diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts index 6a708eb..8203eca 100644 --- a/backend/src/routes/admin.ts +++ b/backend/src/routes/admin.ts @@ -6,8 +6,8 @@ import ErrorMessage from "../constants/ErrorMessage.js"; import ReturnCode from "../constants/ReturnCode.js"; import { encode, decode } from "../utils/crypto.js"; import SupportedNetworks from "../../config.json" assert { type: "json" }; -import { APIKey } from "../models/APIKey.js"; -import { ConfigUpdateData } from "../types/config-dto.js"; +import { APIKey } from "../models/api-key.js"; +import { ArkaConfigUpdateData } from "../types/arka-config-dto.js"; import { ApiKeyDto } from "../types/apikey-dto.js"; const adminRoutes: FastifyPluginAsync = async (server) => { @@ -25,7 +25,7 @@ const adminRoutes: FastifyPluginAsync = async (server) => { server.get("/getConfig", async function (request, reply) { try { - const result = await server.configRepository.findFirstConfig(); + const result = await server.arkaConfigRepository.findFirstConfig(); if (!result) { return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.FAILED_TO_PROCESS }); @@ -40,11 +40,11 @@ const adminRoutes: FastifyPluginAsync = async (server) => { server.post("/saveConfig", async function (request, reply) { try { - const body: ConfigUpdateData = JSON.parse(request.body as string); + const body: ArkaConfigUpdateData = JSON.parse(request.body as string); if (!body) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); if (Object.values(body).every(value => value)) { try { - const result = await server.configRepository.updateConfig(body); + const result = await server.arkaConfigRepository.updateConfig(body); server.log.info(`config entity after database update: ${JSON.stringify(result)}`); } catch (error) { server.log.error('Error while updating the config:', error); diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 7fb5f4f..87cd889 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -11,8 +11,8 @@ import ErrorMessage from "../constants/ErrorMessage.js"; import ReturnCode from "../constants/ReturnCode.js"; import { decode } from "../utils/crypto.js"; import { printRequest, getNetworkConfig } from "../utils/common.js"; -import { APIKeyRepository } from "repository/APIKeyRepository.js"; -import { APIKey } from "models/APIKey.js"; +import { APIKeyRepository } from "repository/api-key-repository.js"; +import { APIKey } from "models/api-key.js"; const SUPPORTED_ENTRYPOINTS = { 'EPV_06' : "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", diff --git a/backend/src/routes/metadata.ts b/backend/src/routes/metadata.ts index abafc28..11a2de6 100644 --- a/backend/src/routes/metadata.ts +++ b/backend/src/routes/metadata.ts @@ -7,8 +7,8 @@ import ReturnCode from "../constants/ReturnCode.js"; import ErrorMessage from "../constants/ErrorMessage.js"; import { decode } from "../utils/crypto.js"; import { PAYMASTER_ADDRESS } from "../constants/Pimlico.js"; -import { APIKey } from "../models/APIKey"; -import { APIKeyRepository } from "repository/APIKeyRepository"; +import { APIKey } from "../models/api-key"; +import { APIKeyRepository } from "repository/api-key-repository"; import * as EtherspotAbi from "../abi/EtherspotAbi.js"; const metadataRoutes: FastifyPluginAsync = async (server) => { diff --git a/backend/src/server.ts b/backend/src/server.ts index 1e552b6..6d6ebbe 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -17,10 +17,10 @@ import PimlicoAbi from './abi/PimlicoAbi.js'; import PythOracleAbi from './abi/PythOracleAbi.js'; import { getNetworkConfig } from './utils/common.js'; import { checkDeposit } from './utils/monitorTokenPaymaster.js'; -import { APIKey } from 'models/APIKey.js'; -import { APIKeyRepository } from './repository/APIKeyRepository.js'; -import { Config } from 'models/Config.js'; -import { ConfigRepository } from 'repository/ConfigRepository.js'; +import { APIKey } from 'models/api-key.js'; +import { APIKeyRepository } from './repository/api-key-repository.js'; +import { ArkaConfig } from 'models/arka-config.js'; +import { ArkaConfigRepository } from 'repository/arka-config-repository.js'; let server: FastifyInstance; @@ -68,9 +68,9 @@ const initializeServer = async (): Promise => { server.log.info('registered sequelizePlugin...') - const configRepository = new ConfigRepository(server.sequelize); - const configDatas = await configRepository.findAll(); - const configData: Config | null = configDatas.length > 0 ? configDatas[0] : null; + 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: [ diff --git a/backend/src/types/config-dto.ts b/backend/src/types/arka-config-dto.ts similarity index 87% rename from backend/src/types/config-dto.ts rename to backend/src/types/arka-config-dto.ts index 1e294dd..72947be 100644 --- a/backend/src/types/config-dto.ts +++ b/backend/src/types/arka-config-dto.ts @@ -1,5 +1,5 @@ -export interface ConfigUpdateData { +export interface ArkaConfigUpdateData { deployedErc20Paymasters: string; pythMainnetUrl: string; pythTestnetUrl: string; diff --git a/backend/src/types/sponsorship-policy-dto.ts b/backend/src/types/sponsorship-policy-dto.ts new file mode 100644 index 0000000..dae58a3 --- /dev/null +++ b/backend/src/types/sponsorship-policy-dto.ts @@ -0,0 +1,27 @@ + +// DTO for receiving data in the POST request to create a sponsorship policy +export interface CreateSponsorshipPolicyDTO { + walletAddress: string; // The wallet address associated with the API key + name: string; // Name of the sponsorship policy + description: string; // Description of the sponsorship policy + startDate?: string; // Optional start date for the policy + endDate?: string; // Optional end date for the policy + isPerpetual: boolean; // Flag to indicate if the policy is perpetual + contractRestrictions?: string; // JSON string containing any contract-specific restrictions + limits: SponsorshipPolicyLimitDTO[]; // Array of limits associated with the policy +} + +// DTO for sponsorship policy limits +export interface SponsorshipPolicyLimitDTO { + limitType: LimitType; // Type of limit (GLOBAL, PER_USER, PER_OPERATION) + maxUsd?: number; // Optional maximum USD limit + maxEth?: number; // Optional maximum ETH limit + maxOperations?: number; // Optional maximum number of operations +} + +// enum for LimitTypes +export enum LimitType { + GLOBAL = 'GLOBAL', + PER_USER = 'PER_USER', + PER_OPERATION = 'PER_OPERATION' +} \ No newline at end of file diff --git a/backend/src/utils/common.ts b/backend/src/utils/common.ts index 1da5cd4..c093fd9 100644 --- a/backend/src/utils/common.ts +++ b/backend/src/utils/common.ts @@ -2,7 +2,7 @@ import { FastifyBaseLogger, FastifyRequest } from "fastify"; import { BigNumber, ethers } from "ethers"; import SupportedNetworks from "../../config.json" assert { type: "json" }; import { EtherscanResponse, getEtherscanFeeResponse } from "./interface.js"; -import { APIKey } from "models/APIKey"; +import { APIKey } from "models/api-key"; export function printRequest(methodName: string, request: FastifyRequest, log: FastifyBaseLogger) { log.info(methodName, "called: "); From 234d43cb066ec1752dd09b11b22b01ddfd776941 Mon Sep 17 00:00:00 2001 From: kanth Date: Thu, 13 Jun 2024 19:29:53 +0530 Subject: [PATCH 16/23] chore: PRO-2395 delete obselete file at root directory --- package.json | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 package.json diff --git a/package.json b/package.json deleted file mode 100644 index c7ab87d..0000000 --- a/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "dependencies": { - "sequelize": "^6.37.3" - }, - "devDependencies": { - "@types/sequelize": "^4.28.20", - "@types/sqlite3": "^3.1.11" - } -} From 03d1f1d48b073174cc9c5cafa4f946f0aa37d091 Mon Sep 17 00:00:00 2001 From: kanth Date: Thu, 13 Jun 2024 22:41:03 +0530 Subject: [PATCH 17/23] feat: PRO-2395 denormalize SponsorshipPolicy table data --- ...0611000002-create-sponsorship-policies.cjs | 66 ++++++++++-- ...00003-create-sponsorship-policy-limits.cjs | 62 ----------- backend/src/models/sequelize-associations.ts | 23 ---- .../src/models/sponsorship-policy-limit.ts | 69 ------------ backend/src/models/sponsorship-policy.ts | 102 ++++++++++++++++-- backend/src/plugins/sequelizePlugin.ts | 10 +- backend/src/types/sponsorship-policy-dto.ts | 23 +++- 7 files changed, 174 insertions(+), 181 deletions(-) delete mode 100644 backend/migrations/20240611000003-create-sponsorship-policy-limits.cjs delete mode 100644 backend/src/models/sponsorship-policy-limit.ts diff --git a/backend/migrations/20240611000002-create-sponsorship-policies.cjs b/backend/migrations/20240611000002-create-sponsorship-policies.cjs index ed4df63..6936475 100644 --- a/backend/migrations/20240611000002-create-sponsorship-policies.cjs +++ b/backend/migrations/20240611000002-create-sponsorship-policies.cjs @@ -30,11 +30,31 @@ async function up({ context: queryInterface }) { allowNull: true, field: 'DESCRIPTION' }, + isPublic: { + type: Sequelize.BOOLEAN, + defaultValue: false, + field: 'IS_PUBLIC' + }, + isEnabled: { + type: Sequelize.BOOLEAN, + defaultValue: false, + field: 'IS_ENABLED' + }, + isUniversal: { + type: Sequelize.BOOLEAN, + defaultValue: false, + field: 'IS_UNIVERSAL' + }, enabledChains: { type: Sequelize.ARRAY(Sequelize.INTEGER), allowNull: true, field: 'ENABLED_CHAINS' }, + isPerpetual: { + type: Sequelize.BOOLEAN, + defaultValue: false, + field: 'IS_PERPETUAL' + }, startDate: { type: Sequelize.DATE, allowNull: true, @@ -45,15 +65,45 @@ async function up({ context: queryInterface }) { allowNull: true, field: 'END_DATE' }, - isPerpetual: { - type: Sequelize.BOOLEAN, - defaultValue: false, - field: 'IS_PERPETUAL' + globalMaximumUsd: { + type: Sequelize.DECIMAL(10, 4), // max 10 digits, 4 of which can be after the decimal point + allowNull: true, + field: 'GLOBAL_MAX_USD' }, - isUniversal: { - type: Sequelize.BOOLEAN, - defaultValue: false, - field: 'IS_UNIVERSAL' + globalMaximumNative: { + type: Sequelize.DECIMAL(22, 18), // max 22 digits, 18 of which can be after the decimal point + allowNull: true, + field: 'GLOBAL_MAX_NATIVE' + }, + globalMaximumOpCount: { + type: Sequelize.INTEGER, + allowNull: true, + field: 'GLOBAL_MAX_OP_COUNT' + }, + perUserMaximumUsd: { + type: Sequelize.DECIMAL(10, 4), // max 10 digits, 4 of which can be after the decimal point + allowNull: true, + field: 'PER_USER_MAX_USD' + }, + perUserMaximumNative: { + type: Sequelize.DECIMAL(22, 18), // max 22 digits, 18 of which can be after the decimal point + allowNull: true, + field: 'PER_USER_MAX_NATIVE' + }, + perUserMaximumOpCount: { + type: Sequelize.INTEGER, + allowNull: true, + field: 'PER_USER_MAX_OP_COUNT' + }, + perOpMaximumUsd: { + type: Sequelize.DECIMAL(10, 4), // max 10 digits, 4 of which can be after the decimal point + allowNull: true, + field: 'PER_OP_MAX_USD' + }, + perOpMaximumNative: { + type: Sequelize.DECIMAL(22, 18), // max 22 digits, 18 of which can be after the decimal point + allowNull: true, + field: 'PER_OP_MAX_NATIVE' }, contractRestrictions: { type: Sequelize.TEXT, diff --git a/backend/migrations/20240611000003-create-sponsorship-policy-limits.cjs b/backend/migrations/20240611000003-create-sponsorship-policy-limits.cjs deleted file mode 100644 index c5b811f..0000000 --- a/backend/migrations/20240611000003-create-sponsorship-policy-limits.cjs +++ /dev/null @@ -1,62 +0,0 @@ -const { Sequelize } = require('sequelize') - -async function up({ context: queryInterface }) { - await queryInterface.createTable('sponsorship_policy_limits', { - policyId: { - type: Sequelize.INTEGER, - primaryKey: true, - allowNull: false, - field: 'POLICY_ID', - references: { - model: 'sponsorship_policies', - key: 'ID' - }, - onDelete: 'CASCADE', - onUpdate: 'CASCADE' - }, - limitType: { - type: Sequelize.STRING, - primaryKey: true, - allowNull: false, - field: 'LIMIT_TYPE' - }, - maxUsd: { - type: Sequelize.DECIMAL(10, 4), // max 10 digits, 4 of which can be after the decimal point - allowNull: true, - field: 'MAX_USD' - }, - maxEth: { - type: Sequelize.DECIMAL(22, 18), // max 22 digits, 18 of which can be after the decimal point - allowNull: true, - field: 'MAX_ETH' - }, - maxOperations: { - type: Sequelize.INTEGER, - allowNull: true, - field: 'MAX_OPERATIONS' - }, - createdAt: { - type: Sequelize.DATE, - allowNull: false, - defaultValue: Sequelize.NOW, - field: 'CREATED_AT' - }, - updatedAt: { - type: Sequelize.DATE, - allowNull: false, - defaultValue: Sequelize.NOW, - field: 'UPDATED_AT' - }, - }, { - schema: process.env.DATABASE_SCHEMA_NAME - }); -} - -async function down({ context: queryInterface }) { - await queryInterface.dropTable({ - tableName: 'sponsorship_policy_limits', - schema: process.env.DATABASE_SCHEMA_NAME - }); -} - -module.exports = { up, down } diff --git a/backend/src/models/sequelize-associations.ts b/backend/src/models/sequelize-associations.ts index 71e471f..1ff9e74 100644 --- a/backend/src/models/sequelize-associations.ts +++ b/backend/src/models/sequelize-associations.ts @@ -1,6 +1,5 @@ import { APIKey } from './api-key'; import { SponsorshipPolicy } from './sponsorship-policy'; -import { SponsorshipPolicyLimit } from './sponsorship-policy-limit'; export function setupAssociations() { @@ -25,26 +24,4 @@ export function setupAssociations() { targetKey: 'walletAddress', as: 'apiKey', // Optional alias }); - - /** - * SponsorshipPolicy to SponsorshipPolicyLimit - * A single SponsorshipPolicy (the parent) can have many SponsorshipPolicyLimits (the children). - * The link between them is made using the 'id' field of the SponsorshipPolicy and the 'policyId' field of the SponsorshipPolicyLimit. - */ - SponsorshipPolicy.hasMany(SponsorshipPolicyLimit, { - foreignKey: 'policyId', - sourceKey: 'id', - as: 'sponsorshipPolicyLimits' - }); - - /** - * SponsorshipPolicyLimit to SponsorshipPolicy - * A single SponsorshipPolicyLimit (the child) belongs to one SponsorshipPolicy (the parent). - * The link between them is made using the 'policyId' field of the SponsorshipPolicyLimit and the 'id' field of the SponsorshipPolicy. - */ - SponsorshipPolicyLimit.belongsTo(SponsorshipPolicy, { - foreignKey: 'policyId', - targetKey: 'id', - as: 'sponsorshipPolicy' - }); } diff --git a/backend/src/models/sponsorship-policy-limit.ts b/backend/src/models/sponsorship-policy-limit.ts deleted file mode 100644 index 3387e34..0000000 --- a/backend/src/models/sponsorship-policy-limit.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Sequelize, DataTypes, Model } from 'sequelize'; - -export class SponsorshipPolicyLimit extends Model { - public policyId!: number; - public limitType!: string; - public maxUsd!: number | null; - public maxEth!: number | null; - public maxOperations!: number | null; - public readonly createdAt!: Date; - public readonly updatedAt!: Date; -} - -export function initializeSponsorshipPolicyLimitModel(sequelize: Sequelize, schema: string) { - SponsorshipPolicyLimit.init({ - policyId: { - type: DataTypes.INTEGER, - primaryKey: true, - allowNull: false, - references: { - model: 'sponsorship_policies', // name of your model for sponsorship policies - key: 'ID', // key in SponsorshipPolicy that policyId references - }, - onDelete: 'CASCADE', - field: 'POLICY_ID' - }, - limitType: { - type: DataTypes.STRING, - primaryKey: true, - allowNull: false, - field: 'LIMIT_TYPE' - }, - maxUsd: { - type: DataTypes.DECIMAL(10, 4), // max 10 digits, 4 of which can be after the decimal point - allowNull: true, - field: 'MAX_USD' - }, - maxEth: { - type: DataTypes.DECIMAL(22, 18), // max 22 digits, 18 of which can be after the decimal point - allowNull: true, - field: 'MAX_ETH' - }, - maxOperations: { - type: DataTypes.INTEGER, - allowNull: true, - field: 'MAX_OPERATIONS' - }, - 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: 'sponsorship_policy_limits', - modelName: 'SponsorshipPolicyLimit', - timestamps: true, - createdAt: 'createdAt', - updatedAt: 'updatedAt', - freezeTableName: true, - schema: schema, - }); -} \ No newline at end of file diff --git a/backend/src/models/sponsorship-policy.ts b/backend/src/models/sponsorship-policy.ts index 8cc1fcc..ec2f1b0 100644 --- a/backend/src/models/sponsorship-policy.ts +++ b/backend/src/models/sponsorship-policy.ts @@ -5,14 +5,44 @@ export class SponsorshipPolicy extends Model { public walletAddress!: string; public name!: string; public description!: string | null; + public isPublic: boolean = false; + public isEnabled: boolean = false; + public isUniversal!: boolean; public enabledChains?: number[]; - public startDate!: Date | null; - public endDate!: Date | null; public isPerpetual!: boolean; - public isUniversal!: boolean; + public startTime!: Date | null; + public endTime!: Date | null; + public globalMaximumUsd!: number | null; + public globalMaximumNative!: number | null; + public globalMaximumOpCount!: number | null; + public perUserMaximumUsd!: number | null; + public perUserMaximumNative!: number | null; + public perUserMaximumOpCount!: number | null; + public perOpMaximumUsd!: number | null; + public perOpMaximumNative!: number | null; public contractRestrictions!: string | null; public readonly createdAt!: Date; public readonly updatedAt!: Date; + + public get isExpired(): boolean { + const now = new Date(); + if (this.isPerpetual) { + return false; + } + return Boolean(this.endTime && this.endTime < now); + } + + public get isCurrent(): boolean { + const now = new Date(); + if (this.isPerpetual) { + return true; + } + return Boolean(this.startTime && this.startTime <= now && (!this.endTime || this.endTime >= now)); + } + + public get isApplicable(): boolean { + return this.isEnabled && !this.isExpired && this.isCurrent; + } } export function initializeSponsorshipPolicyModel(sequelize: Sequelize, schema: string) { @@ -43,11 +73,31 @@ export function initializeSponsorshipPolicyModel(sequelize: Sequelize, schema: s allowNull: true, field: 'DESCRIPTION' }, + isPublic: { + type: DataTypes.BOOLEAN, + defaultValue: false, + field: 'IS_PUBLIC' + }, + isEnabled: { + type: DataTypes.BOOLEAN, + defaultValue: false, + field: 'IS_ENABLED' + }, + isUniversal: { + type: DataTypes.BOOLEAN, + defaultValue: false, + field: 'IS_UNIVERSAL' + }, enabledChains: { type: DataTypes.ARRAY(DataTypes.INTEGER), allowNull: true, field: 'ENABLED_CHAINS' }, + isPerpetual: { + type: DataTypes.BOOLEAN, + defaultValue: false, + field: 'IS_PERPETUAL' + }, startDate: { type: DataTypes.DATE, allowNull: true, @@ -58,15 +108,45 @@ export function initializeSponsorshipPolicyModel(sequelize: Sequelize, schema: s allowNull: true, field: 'END_DATE' }, - isPerpetual: { - type: DataTypes.BOOLEAN, - defaultValue: false, - field: 'IS_PERPETUAL' + globalMaximumUsd: { + type: DataTypes.DECIMAL(10, 4), // max 10 digits, 4 of which can be after the decimal point + allowNull: true, + field: 'GLOBAL_MAX_USD' }, - isUniversal: { - type: DataTypes.BOOLEAN, - defaultValue: false, - field: 'IS_UNIVERSAL' + globalMaximumNative: { + type: DataTypes.DECIMAL(22, 18), // max 22 digits, 18 of which can be after the decimal point + allowNull: true, + field: 'GLOBAL_MAX_NATIVE' + }, + globalMaximumOpCount: { + type: DataTypes.INTEGER, + allowNull: true, + field: 'GLOBAL_MAX_OP_COUNT' + }, + perUserMaximumUsd: { + type: DataTypes.DECIMAL(10, 4), // max 10 digits, 4 of which can be after the decimal point + allowNull: true, + field: 'PER_USER_MAX_USD' + }, + perUserMaximumNative: { + type: DataTypes.DECIMAL(22, 18), // max 22 digits, 18 of which can be after the decimal point + allowNull: true, + field: 'PER_USER_MAX_NATIVE' + }, + perUserMaximumOpCount: { + type: DataTypes.INTEGER, + allowNull: true, + field: 'PER_USER_MAX_OP_COUNT' + }, + perOpMaximumUsd: { + type: DataTypes.DECIMAL(10, 4), // max 10 digits, 4 of which can be after the decimal point + allowNull: true, + field: 'PER_OP_MAX_USD' + }, + perOpMaximumNative: { + type: DataTypes.DECIMAL(22, 18), // max 22 digits, 18 of which can be after the decimal point + allowNull: true, + field: 'PER_OP_MAX_NATIVE' }, contractRestrictions: { type: DataTypes.TEXT, diff --git a/backend/src/plugins/sequelizePlugin.ts b/backend/src/plugins/sequelizePlugin.ts index 3956557..6a6af30 100644 --- a/backend/src/plugins/sequelizePlugin.ts +++ b/backend/src/plugins/sequelizePlugin.ts @@ -1,13 +1,12 @@ import fp from "fastify-plugin"; import { FastifyPluginAsync } from "fastify"; -import { Sequelize, QueryTypes } from 'sequelize'; +import { Sequelize } from 'sequelize'; import dotenv from 'dotenv'; -import { APIKey, initializeAPIKeyModel } from '../models/api-key'; // Assuming path correctness +import { initializeAPIKeyModel } from '../models/api-key'; // Assuming path correctness import { initializeSponsorshipPolicyModel } from '../models/sponsorship-policy'; -import { initializeSponsorshipPolicyLimitModel } from "../models/sponsorship-policy-limit"; import { initializeArkaConfigModel } from "../models/arka-config"; -import { APIKeyRepository } from "repository/api-key-repository"; -import { ArkaConfigRepository } from "repository/arka-config-repository"; +import { APIKeyRepository } from "../repository/api-key-repository"; +import { ArkaConfigRepository } from "../repository/arka-config-repository"; const pg = await import('pg'); const Client = pg.default.Client; @@ -47,7 +46,6 @@ const sequelizePlugin: FastifyPluginAsync = async (server) => { sequelize.models.APIKey = initializedAPIKeyModel; server.log.info(`Initialized APIKey model... ${sequelize.models.APIKey}`); initializeSponsorshipPolicyModel(sequelize, server.config.DATABASE_SCHEMA_NAME); - initializeSponsorshipPolicyLimitModel(sequelize, server.config.DATABASE_SCHEMA_NAME); server.log.info('Initialized all models...'); diff --git a/backend/src/types/sponsorship-policy-dto.ts b/backend/src/types/sponsorship-policy-dto.ts index dae58a3..52f8b39 100644 --- a/backend/src/types/sponsorship-policy-dto.ts +++ b/backend/src/types/sponsorship-policy-dto.ts @@ -1,14 +1,25 @@ - // DTO for receiving data in the POST request to create a sponsorship policy -export interface CreateSponsorshipPolicyDTO { +export interface SponsorshipPolicyDto { + policyId: number; // ID of the policy walletAddress: string; // The wallet address associated with the API key name: string; // Name of the sponsorship policy description: string; // Description of the sponsorship policy startDate?: string; // Optional start date for the policy endDate?: string; // Optional end date for the policy isPerpetual: boolean; // Flag to indicate if the policy is perpetual + isUniversal: boolean; // Flag to indicate if the policy is universal contractRestrictions?: string; // JSON string containing any contract-specific restrictions + enabledChains?: number[]; // Array of enabled chain IDs limits: SponsorshipPolicyLimitDTO[]; // Array of limits associated with the policy + isEnabled: boolean; // Flag to indicate if the policy is enabled/disabled + isExpired: boolean; // Flag to indicate if the policy is expired + isCurrent: boolean; // Flag to indicate if the policy is current + isApplicable: boolean; // Flag to indicate if the policy is applicable +} + +export interface SponsorshipPolicyActionDto { + policyId: number; + action: ActionType; } // DTO for sponsorship policy limits @@ -24,4 +35,12 @@ export enum LimitType { GLOBAL = 'GLOBAL', PER_USER = 'PER_USER', PER_OPERATION = 'PER_OPERATION' +} + +export enum ActionType { + CREATE = 'CREATE', + UPDATE = 'UPDATE', + DISABLE = 'DISABLE', + ENABLE = 'ENABLE', + DELETE = 'DELETE' } \ No newline at end of file From 11fb2d788b97414ef2c98298fa77a728b67854b8 Mon Sep 17 00:00:00 2001 From: kanth Date: Fri, 14 Jun 2024 00:46:10 +0530 Subject: [PATCH 18/23] feat: PRO-2395 refactor types and optimise imports --- ...0611000002-create-sponsorship-policies.cjs | 26 +++++++- backend/src/models/sponsorship-policy.ts | 32 ++++++++-- backend/src/plugins/db.ts | 2 +- backend/src/plugins/sequelizePlugin.ts | 2 +- backend/src/routes/index.ts | 13 ++-- backend/src/routes/metadata.ts | 4 +- backend/src/types/sponsorship-policy-dto.ts | 62 +++++++------------ 7 files changed, 84 insertions(+), 57 deletions(-) diff --git a/backend/migrations/20240611000002-create-sponsorship-policies.cjs b/backend/migrations/20240611000002-create-sponsorship-policies.cjs index 6936475..f65ee33 100644 --- a/backend/migrations/20240611000002-create-sponsorship-policies.cjs +++ b/backend/migrations/20240611000002-create-sponsorship-policies.cjs @@ -65,6 +65,11 @@ async function up({ context: queryInterface }) { allowNull: true, field: 'END_DATE' }, + globalMaxApplicable: { + type: Sequelize.BOOLEAN, + defaultValue: false, + field: 'GLOBAL_MAX_APPLICABLE' + }, globalMaximumUsd: { type: Sequelize.DECIMAL(10, 4), // max 10 digits, 4 of which can be after the decimal point allowNull: true, @@ -80,6 +85,11 @@ async function up({ context: queryInterface }) { allowNull: true, field: 'GLOBAL_MAX_OP_COUNT' }, + perUserMaxApplicable: { + type: Sequelize.BOOLEAN, + defaultValue: false, + field: 'PER_USER_MAX_APPLICABLE' + }, perUserMaximumUsd: { type: Sequelize.DECIMAL(10, 4), // max 10 digits, 4 of which can be after the decimal point allowNull: true, @@ -95,6 +105,11 @@ async function up({ context: queryInterface }) { allowNull: true, field: 'PER_USER_MAX_OP_COUNT' }, + perOpMaxApplicable: { + type: Sequelize.BOOLEAN, + defaultValue: false, + field: 'PER_OP_MAX_APPLICABLE' + }, perOpMaximumUsd: { type: Sequelize.DECIMAL(10, 4), // max 10 digits, 4 of which can be after the decimal point allowNull: true, @@ -105,10 +120,15 @@ async function up({ context: queryInterface }) { allowNull: true, field: 'PER_OP_MAX_NATIVE' }, - contractRestrictions: { - type: Sequelize.TEXT, + addressAllowList: { + type: Sequelize.ARRAY(Sequelize.STRING), + allowNull: true, + field: 'ADDRESS_ALLOW_LIST' + }, + addressBlockList: { + type: Sequelize.ARRAY(Sequelize.STRING), allowNull: true, - field: 'CONTRACT_RESTRICTIONS' + field: 'ADDRESS_BLOCK_LIST' }, createdAt: { type: Sequelize.DATE, diff --git a/backend/src/models/sponsorship-policy.ts b/backend/src/models/sponsorship-policy.ts index ec2f1b0..bffc487 100644 --- a/backend/src/models/sponsorship-policy.ts +++ b/backend/src/models/sponsorship-policy.ts @@ -12,15 +12,19 @@ export class SponsorshipPolicy extends Model { public isPerpetual!: boolean; public startTime!: Date | null; public endTime!: Date | null; + public globalMaximumApplicable: boolean = false; public globalMaximumUsd!: number | null; public globalMaximumNative!: number | null; public globalMaximumOpCount!: number | null; + public perUserMaximumApplicable: boolean = false; public perUserMaximumUsd!: number | null; public perUserMaximumNative!: number | null; public perUserMaximumOpCount!: number | null; + public perOpMaximumApplicable: boolean = false; public perOpMaximumUsd!: number | null; public perOpMaximumNative!: number | null; - public contractRestrictions!: string | null; + public addressAllowList: string[] | null = null; + public addressBlockList: string[] | null = null; public readonly createdAt!: Date; public readonly updatedAt!: Date; @@ -108,6 +112,11 @@ export function initializeSponsorshipPolicyModel(sequelize: Sequelize, schema: s allowNull: true, field: 'END_DATE' }, + globalMaximumApplicable: { + type: DataTypes.BOOLEAN, + defaultValue: false, + field: 'GLOBAL_MAX_APPLICABLE' + }, globalMaximumUsd: { type: DataTypes.DECIMAL(10, 4), // max 10 digits, 4 of which can be after the decimal point allowNull: true, @@ -123,6 +132,11 @@ export function initializeSponsorshipPolicyModel(sequelize: Sequelize, schema: s allowNull: true, field: 'GLOBAL_MAX_OP_COUNT' }, + perUserMaximumApplicable: { + type: DataTypes.BOOLEAN, + defaultValue: false, + field: 'PER_USER_MAX_APPLICABLE' + }, perUserMaximumUsd: { type: DataTypes.DECIMAL(10, 4), // max 10 digits, 4 of which can be after the decimal point allowNull: true, @@ -138,6 +152,11 @@ export function initializeSponsorshipPolicyModel(sequelize: Sequelize, schema: s allowNull: true, field: 'PER_USER_MAX_OP_COUNT' }, + perOpMaximumApplicable: { + type: DataTypes.BOOLEAN, + defaultValue: false, + field: 'PER_OP_MAX_APPLICABLE' + }, perOpMaximumUsd: { type: DataTypes.DECIMAL(10, 4), // max 10 digits, 4 of which can be after the decimal point allowNull: true, @@ -148,10 +167,15 @@ export function initializeSponsorshipPolicyModel(sequelize: Sequelize, schema: s allowNull: true, field: 'PER_OP_MAX_NATIVE' }, - contractRestrictions: { - type: DataTypes.TEXT, + addressAllowList: { + type: DataTypes.ARRAY(DataTypes.STRING), + allowNull: true, + field: 'ADDRESS_ALLOW_LIST' + }, + addressBlockList: { + type: DataTypes.ARRAY(DataTypes.STRING), allowNull: true, - field: 'CONTRACT_RESTRICTIONS' + field: 'ADDRESS_BLOCK_LIST' }, createdAt: { type: DataTypes.DATE, diff --git a/backend/src/plugins/db.ts b/backend/src/plugins/db.ts index c02afa8..86647bb 100644 --- a/backend/src/plugins/db.ts +++ b/backend/src/plugins/db.ts @@ -9,7 +9,7 @@ import { Umzug, SequelizeStorage } from 'umzug'; const databasePlugin: FastifyPluginAsync = async (server) => { const sequelize = new Sequelize(server.config.DATABASE_URL, { - schema: 'arka', + schema: server.config.DATABASE_SCHEMA_NAME, }); const __filename = fileURLToPath(import.meta.url); diff --git a/backend/src/plugins/sequelizePlugin.ts b/backend/src/plugins/sequelizePlugin.ts index 6a6af30..0649525 100644 --- a/backend/src/plugins/sequelizePlugin.ts +++ b/backend/src/plugins/sequelizePlugin.ts @@ -28,7 +28,7 @@ const sequelizePlugin: FastifyPluginAsync = async (server) => { dialect: 'postgres', protocol: 'postgres', dialectOptions: { - searchPath: 'arka', + searchPath: server.config.DATABASE_SCHEMA_NAME, // ssl: { // require: false, // rejectUnauthorized: false diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 87cd889..1c452c2 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -11,7 +11,6 @@ import ErrorMessage from "../constants/ErrorMessage.js"; import ReturnCode from "../constants/ReturnCode.js"; import { decode } from "../utils/crypto.js"; import { printRequest, getNetworkConfig } from "../utils/common.js"; -import { APIKeyRepository } from "repository/api-key-repository.js"; import { APIKey } from "models/api-key.js"; const SUPPORTED_ENTRYPOINTS = { @@ -127,7 +126,7 @@ const routes: FastifyPluginAsync = async (server) => { txnMode = secrets['TRANSACTION_LIMIT'] ?? 0; indexerEndpoint = secrets['INDEXER_ENDPOINT'] ?? process.env.DEFAULT_INDEXER_ENDPOINT; } else { - const apiKeyEntity = await new APIKeyRepository(server.sequelize).findOneByApiKey(api_key); + const apiKeyEntity = await server.apiKeyRepository.findOneByApiKey(api_key); if (!apiKeyEntity) { server.log.info("Invalid Api Key provided") @@ -373,7 +372,7 @@ const routes: FastifyPluginAsync = async (server) => { privateKey = secrets['PRIVATE_KEY']; supportedNetworks = secrets['SUPPORTED_NETWORKS']; } else { - const apiKeyEntity: APIKey | null = await new APIKeyRepository(server.sequelize).findOneByApiKey(api_key); + 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); supportedNetworks = apiKeyEntity.supportedNetworks; @@ -431,7 +430,7 @@ const routes: FastifyPluginAsync = async (server) => { privateKey = secrets['PRIVATE_KEY']; supportedNetworks = secrets['SUPPORTED_NETWORKS']; } else { - const apiKeyEntity: APIKey | null = await new APIKeyRepository(server.sequelize).findOneByApiKey(api_key); + 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.apiKey); supportedNetworks = apiKeyEntity.supportedNetworks; @@ -489,7 +488,7 @@ const routes: FastifyPluginAsync = async (server) => { privateKey = secrets['PRIVATE_KEY']; supportedNetworks = secrets['SUPPORTED_NETWORKS']; } else { - const apiKeyEntity: APIKey | null = await new APIKeyRepository(server.sequelize).findOneByApiKey(api_key); + 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); supportedNetworks = apiKeyEntity.supportedNetworks; @@ -548,7 +547,7 @@ const routes: FastifyPluginAsync = async (server) => { privateKey = secrets['PRIVATE_KEY']; supportedNetworks = secrets['SUPPORTED_NETWORKS']; } else { - const apiKeyEntity: APIKey | null = await new APIKeyRepository(server.sequelize).findOneByApiKey(api_key); + 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.apiKey); supportedNetworks = apiKeyEntity.supportedNetworks; @@ -601,7 +600,7 @@ const routes: FastifyPluginAsync = async (server) => { privateKey = secrets['PRIVATE_KEY']; supportedNetworks = secrets['SUPPORTED_NETWORKS']; } else { - const apiKeyEntity: APIKey | null = await new APIKeyRepository(server.sequelize).findOneByApiKey(api_key); + 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); supportedNetworks = apiKeyEntity.supportedNetworks; diff --git a/backend/src/routes/metadata.ts b/backend/src/routes/metadata.ts index 11a2de6..9ee7aef 100644 --- a/backend/src/routes/metadata.ts +++ b/backend/src/routes/metadata.ts @@ -8,7 +8,6 @@ import ErrorMessage from "../constants/ErrorMessage.js"; import { decode } from "../utils/crypto.js"; import { PAYMASTER_ADDRESS } from "../constants/Pimlico.js"; import { APIKey } from "../models/api-key"; -import { APIKeyRepository } from "repository/api-key-repository"; import * as EtherspotAbi from "../abi/EtherspotAbi.js"; const metadataRoutes: FastifyPluginAsync = async (server) => { @@ -36,7 +35,6 @@ const metadataRoutes: FastifyPluginAsync = async (server) => { return reply.code(ReturnCode.FAILURE).send({error: ErrorMessage.INVALID_DATA}) let customPaymasters = []; let multiTokenPaymasters = []; - let multiTokenOracles = []; let privateKey = ''; let supportedNetworks; let sponsorName = '', sponsorImage = ''; @@ -64,7 +62,7 @@ const metadataRoutes: FastifyPluginAsync = async (server) => { privateKey = secrets['PRIVATE_KEY']; supportedNetworks = secrets['SUPPORTED_NETWORKS']; } else { - const apiKeyEntity: APIKey | null = await new APIKeyRepository(server.sequelize).findOneByApiKey(api_key); + const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); if (!apiKeyEntity) { server.log.info("Invalid Api Key provided") return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) diff --git a/backend/src/types/sponsorship-policy-dto.ts b/backend/src/types/sponsorship-policy-dto.ts index 52f8b39..3c3bc2f 100644 --- a/backend/src/types/sponsorship-policy-dto.ts +++ b/backend/src/types/sponsorship-policy-dto.ts @@ -1,46 +1,32 @@ // DTO for receiving data in the POST request to create a sponsorship policy export interface SponsorshipPolicyDto { - policyId: number; // ID of the policy + id: number; // ID of the policy walletAddress: string; // The wallet address associated with the API key name: string; // Name of the sponsorship policy description: string; // Description of the sponsorship policy - startDate?: string; // Optional start date for the policy - endDate?: string; // Optional end date for the policy - isPerpetual: boolean; // Flag to indicate if the policy is perpetual + isPublic: boolean; // Flag to indicate if the policy is public + isEnabled: boolean; // Flag to indicate if the policy is enabled isUniversal: boolean; // Flag to indicate if the policy is universal - contractRestrictions?: string; // JSON string containing any contract-specific restrictions enabledChains?: number[]; // Array of enabled chain IDs - limits: SponsorshipPolicyLimitDTO[]; // Array of limits associated with the policy - isEnabled: boolean; // Flag to indicate if the policy is enabled/disabled - isExpired: boolean; // Flag to indicate if the policy is expired - isCurrent: boolean; // Flag to indicate if the policy is current - isApplicable: boolean; // Flag to indicate if the policy is applicable -} - -export interface SponsorshipPolicyActionDto { - policyId: number; - action: ActionType; -} - -// DTO for sponsorship policy limits -export interface SponsorshipPolicyLimitDTO { - limitType: LimitType; // Type of limit (GLOBAL, PER_USER, PER_OPERATION) - maxUsd?: number; // Optional maximum USD limit - maxEth?: number; // Optional maximum ETH limit - maxOperations?: number; // Optional maximum number of operations -} - -// enum for LimitTypes -export enum LimitType { - GLOBAL = 'GLOBAL', - PER_USER = 'PER_USER', - PER_OPERATION = 'PER_OPERATION' + isPerpetual: boolean; // Flag to indicate if the policy is perpetual + startDate?: string; // Optional start date for the policy + endDate?: string; // Optional end date for the policy + globalMaximumApplicable: boolean; // Flag to indicate if the global maximum is applicable + globalMaximumUsd?: number; // Optional global maximum USD limit + globalMaximumNative?: number; // Optional global maximum native limit + globalMaximumOpCount?: number; // Optional global maximum operation count + perUserMaximumApplicable: boolean; // Flag to indicate if the per user maximum is applicable + perUserMaximumUsd?: number; // Optional per user maximum USD limit + perUserMaximumNative?: number; // Optional per user maximum native limit + perUserMaximumOpCount?: number; // Optional per user maximum operation count + perOpMaximumApplicable: boolean; // Flag to indicate if the per operation maximum is applicable + perOpMaximumUsd?: number; // Optional per operation maximum USD limit + perOpMaximumNative?: number; // Optional per operation maximum native limit + addressAllowList?: string[]; // Optional array of allowed addresses + addressBlockList?: string[]; // Optional array of blocked addresses + isExpired: boolean; // Flag to indicate if the policy is expired + isCurrent: boolean; // Flag to indicate if the policy is current + isApplicable: boolean; // Flag to indicate if the policy is applicable + createdAt: Date; // Date the policy was created + updatedAt: Date; // Date the policy was last updated } - -export enum ActionType { - CREATE = 'CREATE', - UPDATE = 'UPDATE', - DISABLE = 'DISABLE', - ENABLE = 'ENABLE', - DELETE = 'DELETE' -} \ No newline at end of file From 625afd08d4309f6f5ca27c8e0d369debada1c48d Mon Sep 17 00:00:00 2001 From: kanth Date: Fri, 14 Jun 2024 00:50:41 +0530 Subject: [PATCH 19/23] fix: PRO-2395 adjust the property name to typescript model fieldnames --- admin_frontend/src/context/AuthContext.js | 4 ++-- backend/src/routes/admin.ts | 8 ++++---- frontend/src/components/Dashboard.jsx | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/admin_frontend/src/context/AuthContext.js b/admin_frontend/src/context/AuthContext.js index 3d893ce..a91de68 100644 --- a/admin_frontend/src/context/AuthContext.js +++ b/admin_frontend/src/context/AuthContext.js @@ -18,7 +18,7 @@ export const AuthContextProvider = ({ children }) => { try { const data = await fetch(`${process.env.REACT_APP_SERVER_URL}${ENDPOINTS['adminLogin']}`, { method: "POST", - body: JSON.stringify({ WALLET_ADDRESS: accounts[0] }), + body: JSON.stringify({ walletAddress: accounts[0] }), }); const dataJson = await data.json(); if (!dataJson.error) { @@ -47,7 +47,7 @@ export const AuthContextProvider = ({ children }) => { const address = await initializeProvider(); const data = await fetch(`${process.env.REACT_APP_SERVER_URL}${ENDPOINTS['adminLogin']}`, { method: "POST", - body: JSON.stringify({ WALLET_ADDRESS: address }), + body: JSON.stringify({ walletAddress: address }), }); const dataJson = await data.json(); if (!dataJson.error) { diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts index 8203eca..d17ac5b 100644 --- a/backend/src/routes/admin.ts +++ b/backend/src/routes/admin.ts @@ -15,8 +15,8 @@ const adminRoutes: FastifyPluginAsync = async (server) => { try { const body: any = JSON.parse(request.body as string); if (!body) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); - if (!body.WALLET_ADDRESS) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); - if (ethers.utils.getAddress(body.WALLET_ADDRESS) === server.config.ADMIN_WALLET_ADDRESS) return reply.code(ReturnCode.SUCCESS).send({ error: null, message: "Successfully Logged in" }); + if (!body.walletAddress) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); + if (ethers.utils.getAddress(body.walletAddress) === server.config.ADMIN_WALLET_ADDRESS) return reply.code(ReturnCode.SUCCESS).send({ error: null, message: "Successfully Logged in" }); return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_USER }); } catch (err: any) { return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_USER }); @@ -178,11 +178,11 @@ const adminRoutes: FastifyPluginAsync = async (server) => { try { const body: any = JSON.parse(request.body as string); if (!body) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); - if (!body.WALLET_ADDRESS) { + if (!body.walletAddress) { return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); } - const apiKeyEntity = await server.apiKeyRepository.findOneByWalletAddress(body.WALLET_ADDRESS); + const apiKeyEntity = await server.apiKeyRepository.findOneByWalletAddress(body.walletAddress); if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); let supportedNetworks; diff --git a/frontend/src/components/Dashboard.jsx b/frontend/src/components/Dashboard.jsx index 3d1ea19..cee202d 100644 --- a/frontend/src/components/Dashboard.jsx +++ b/frontend/src/components/Dashboard.jsx @@ -139,7 +139,7 @@ const Dashboard = ({ logInType }) => { const data = await ( await fetch(`${process.env.REACT_APP_SERVER_URL}${ENDPOINTS['getSupportedNetworks']}`, { method: "POST", - body: JSON.stringify({ WALLET_ADDRESS: address }), + body: JSON.stringify({ walletAddress: address }), }) ).json(); const supportedNetworksChainIds = []; From 06e4617ec486af4fb89eea2d56b01068af1c0ed4 Mon Sep 17 00:00:00 2001 From: kanth Date: Fri, 14 Jun 2024 13:08:01 +0530 Subject: [PATCH 20/23] feat: PRO-2395 SponsorshipPolicy repository functions and package version upgrade for all 3 projects in arka --- admin_frontend/package.json | 2 +- backend/indexer/ponder-env.d.ts | 10 +- ...0611000002-create-sponsorship-policies.cjs | 4 +- backend/package.json | 2 +- backend/src/models/sponsorship-policy.ts | 7 +- .../sponsorship-policy-repository.ts | 227 ++++++++++++++++++ backend/src/types/sponsorship-policy-dto.ts | 2 +- frontend/package.json | 2 +- 8 files changed, 242 insertions(+), 14 deletions(-) create mode 100644 backend/src/repository/sponsorship-policy-repository.ts diff --git a/admin_frontend/package.json b/admin_frontend/package.json index 9aee4ce..b92ea9a 100644 --- a/admin_frontend/package.json +++ b/admin_frontend/package.json @@ -1,6 +1,6 @@ { "name": "admin_frontend", - "version": "1.2.6", + "version": "1.2.7", "private": true, "dependencies": { "@emotion/react": "11.11.3", diff --git a/backend/indexer/ponder-env.d.ts b/backend/indexer/ponder-env.d.ts index e5b1f32..01431bf 100644 --- a/backend/indexer/ponder-env.d.ts +++ b/backend/indexer/ponder-env.d.ts @@ -11,17 +11,17 @@ declare module "@/generated" { PonderApp, } from "@ponder/core"; - type Config = typeof import("./ponder.config.ts").default; + type ArkaConfig = typeof import("./ponder.config.ts").default; type Schema = typeof import("./ponder.schema.ts").default; - export const ponder: PonderApp; - export type EventNames = PonderEventNames; + export const ponder: PonderApp; + export type EventNames = PonderEventNames; export type Event = PonderEvent< - Config, + ArkaConfig, name >; export type Context = PonderContext< - Config, + ArkaConfig, Schema, name >; diff --git a/backend/migrations/20240611000002-create-sponsorship-policies.cjs b/backend/migrations/20240611000002-create-sponsorship-policies.cjs index f65ee33..1a09019 100644 --- a/backend/migrations/20240611000002-create-sponsorship-policies.cjs +++ b/backend/migrations/20240611000002-create-sponsorship-policies.cjs @@ -40,10 +40,10 @@ async function up({ context: queryInterface }) { defaultValue: false, field: 'IS_ENABLED' }, - isUniversal: { + isApplicableToAllNetworks: { type: Sequelize.BOOLEAN, defaultValue: false, - field: 'IS_UNIVERSAL' + field: 'IS_APPLICABLE_TO_ALL_NETWORKS' }, enabledChains: { type: Sequelize.ARRAY(Sequelize.INTEGER), diff --git a/backend/package.json b/backend/package.json index cfb0de3..3bd99ad 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "arka", - "version": "1.2.6", + "version": "1.2.7", "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/models/sponsorship-policy.ts b/backend/src/models/sponsorship-policy.ts index bffc487..55cb7c5 100644 --- a/backend/src/models/sponsorship-policy.ts +++ b/backend/src/models/sponsorship-policy.ts @@ -7,7 +7,7 @@ export class SponsorshipPolicy extends Model { public description!: string | null; public isPublic: boolean = false; public isEnabled: boolean = false; - public isUniversal!: boolean; + public isApplicableToAllNetworks!: boolean; public enabledChains?: number[]; public isPerpetual!: boolean; public startTime!: Date | null; @@ -53,6 +53,7 @@ export function initializeSponsorshipPolicyModel(sequelize: Sequelize, schema: s SponsorshipPolicy.init({ id: { type: DataTypes.INTEGER, + autoIncrement: true, primaryKey: true, field: 'ID' }, @@ -87,10 +88,10 @@ export function initializeSponsorshipPolicyModel(sequelize: Sequelize, schema: s defaultValue: false, field: 'IS_ENABLED' }, - isUniversal: { + isApplicableToAllNetworks: { type: DataTypes.BOOLEAN, defaultValue: false, - field: 'IS_UNIVERSAL' + field: 'IS_APPLICABLE_TO_ALL_NETWORKS' }, enabledChains: { type: DataTypes.ARRAY(DataTypes.INTEGER), diff --git a/backend/src/repository/sponsorship-policy-repository.ts b/backend/src/repository/sponsorship-policy-repository.ts new file mode 100644 index 0000000..3d44434 --- /dev/null +++ b/backend/src/repository/sponsorship-policy-repository.ts @@ -0,0 +1,227 @@ +import { Sequelize, Op } from 'sequelize'; +import { SponsorshipPolicy } from '../models/sponsorship-policy'; + +export class SponsorshipPolicyRepository { + private sequelize: Sequelize; + + constructor(sequelize: Sequelize) { + this.sequelize = sequelize; + } + + async findAll(): Promise { + const result = await this.sequelize.models.SponsorshipPolicy.findAll(); + return result.map(apiKey => apiKey.get() as SponsorshipPolicy); + } + + // findAllInADateRange must use the model fields startTime and endTime to filter the results + // user will pass the date range and the query must compare if the startTime and endTime are within the range + // if the policy is perpetual, then it should always be returned + async findAllInADateRange(startDate: Date, endDate: Date): Promise { + const result = await this.sequelize.models.SponsorshipPolicy.findAll({ + where: { + [Op.or]: [ + { + startTime: { + [Op.lte]: endDate + }, + endTime: { + [Op.gte]: startDate + } + }, + { + isPerpetual: true + } + ] + } + }); + return result.map(apiKey => apiKey as SponsorshipPolicy); + } + + async findAllEnabled(): Promise { + const result = await this.sequelize.models.SponsorshipPolicy.findAll({ where: { isEnabled: true } }); + return result.map(apiKey => apiKey.get() as SponsorshipPolicy); + } + + async findAllEnabledAndApplicable(): Promise { + const result = await this.sequelize.models.SponsorshipPolicy.findAll({ where: { isEnabled: true } }); + return result.map(apiKey => apiKey.get() as SponsorshipPolicy).filter(apiKey => apiKey.isApplicable); + } + + async findLatestEnabledAndApplicable(): Promise { + const result = await this.sequelize.models.SponsorshipPolicy.findOne({ where: { isEnabled: true }, order: [['createdAt', 'DESC']] }); + return result ? result.get() as SponsorshipPolicy : null; + } + + async findOneByWalletAddress(walletAddress: string): Promise { + const result = await this.sequelize.models.SponsorshipPolicy.findOne({ where: { walletAddress: walletAddress } }); + return result ? result.get() as SponsorshipPolicy : null; + } + + async findOneByPolicyName(name: string): Promise { + const result = await this.sequelize.models.SponsorshipPolicy.findOne({ where: { name: name } }); + return result ? result.get() as SponsorshipPolicy : null; + } + + async findOneById(id: number): Promise { + const result = await this.sequelize.models.SponsorshipPolicy.findOne({ where: { id: id } }); + return result ? result.get() as SponsorshipPolicy : null; + } + + async createSponsorshipPolicy(sponsorshipPolicy: SponsorshipPolicy): Promise { + this.validateSponsorshipPolicy(sponsorshipPolicy); + + const result = await this.sequelize.models.SponsorshipPolicy.create({ + walletAddress: sponsorshipPolicy.walletAddress, + name: sponsorshipPolicy.name, + description: sponsorshipPolicy.description, + isPublic: sponsorshipPolicy.isPublic, + isEnabled: sponsorshipPolicy.isEnabled, + isApplicableToAllNetworks: sponsorshipPolicy.isApplicableToAllNetworks, + enabledChains: sponsorshipPolicy.enabledChains, + isPerpetual: sponsorshipPolicy.isPerpetual, + startTime: sponsorshipPolicy.startTime, + endTime: sponsorshipPolicy.endTime, + globalMaximumApplicable: sponsorshipPolicy.globalMaximumApplicable, + globalMaximumUsd: sponsorshipPolicy.globalMaximumUsd, + globalMaximumNative: sponsorshipPolicy.globalMaximumNative, + globalMaximumOpCount: sponsorshipPolicy.globalMaximumOpCount, + perUserMaximumApplicable: sponsorshipPolicy.perUserMaximumApplicable, + perUserMaximumUsd: sponsorshipPolicy.perUserMaximumUsd, + perUserMaximumNative: sponsorshipPolicy.perUserMaximumNative, + perUserMaximumOpCount: sponsorshipPolicy.perUserMaximumOpCount, + perOpMaximumApplicable: sponsorshipPolicy.perOpMaximumApplicable, + perOpMaximumUsd: sponsorshipPolicy.perOpMaximumUsd, + perOpMaximumNative: sponsorshipPolicy.perOpMaximumNative, + addressAllowList: sponsorshipPolicy.addressAllowList, + addressBlockList: sponsorshipPolicy.addressBlockList + }); + + return result.get() as SponsorshipPolicy; + } + + async updateSponsorshipPolicy(sponsorshipPolicy: SponsorshipPolicy): Promise { + + // check if sponsorship policy exists (by primary key id) + const existingSponsorshipPolicy = await this.findOneById(sponsorshipPolicy.id as number); + + if (!existingSponsorshipPolicy) { + throw new Error('Sponsorship Policy not found'); + } + + this.validateSponsorshipPolicy(sponsorshipPolicy); + + existingSponsorshipPolicy.name = sponsorshipPolicy.name; + existingSponsorshipPolicy.description = sponsorshipPolicy.description; + existingSponsorshipPolicy.isApplicableToAllNetworks = sponsorshipPolicy.isApplicableToAllNetworks; + existingSponsorshipPolicy.isPerpetual = sponsorshipPolicy.isPerpetual; + // if marked as IsPerpetual, then set startTime and endTime to null + if (sponsorshipPolicy.isPerpetual) { + existingSponsorshipPolicy.startTime = null; + existingSponsorshipPolicy.endTime = null; + } else { + existingSponsorshipPolicy.startTime = sponsorshipPolicy.startTime; + existingSponsorshipPolicy.endTime = sponsorshipPolicy.endTime; + } + existingSponsorshipPolicy.globalMaximumApplicable = sponsorshipPolicy.globalMaximumApplicable; + existingSponsorshipPolicy.globalMaximumUsd = sponsorshipPolicy.globalMaximumUsd; + existingSponsorshipPolicy.globalMaximumNative = sponsorshipPolicy.globalMaximumNative; + existingSponsorshipPolicy.globalMaximumOpCount = sponsorshipPolicy.globalMaximumOpCount; + existingSponsorshipPolicy.perUserMaximumApplicable = sponsorshipPolicy.perUserMaximumApplicable; + existingSponsorshipPolicy.perUserMaximumNative = sponsorshipPolicy.perUserMaximumNative; + existingSponsorshipPolicy.perUserMaximumOpCount = sponsorshipPolicy.perUserMaximumOpCount; + existingSponsorshipPolicy.perUserMaximumUsd = sponsorshipPolicy.perUserMaximumUsd; + existingSponsorshipPolicy.perOpMaximumApplicable = sponsorshipPolicy.perOpMaximumApplicable; + existingSponsorshipPolicy.perOpMaximumNative = sponsorshipPolicy.perOpMaximumNative; + existingSponsorshipPolicy.perOpMaximumUsd = sponsorshipPolicy.perOpMaximumUsd; + existingSponsorshipPolicy.isPublic = sponsorshipPolicy.isPublic; + existingSponsorshipPolicy.addressAllowList = sponsorshipPolicy.addressAllowList; + existingSponsorshipPolicy.addressBlockList = sponsorshipPolicy.addressBlockList; + + const result = await existingSponsorshipPolicy.save(); + return result.get() as SponsorshipPolicy; + } + + validateSponsorshipPolicy(sponsorshipPolicy: SponsorshipPolicy) { + let errors: string[] = []; + + if (!sponsorshipPolicy.name || !sponsorshipPolicy.description) { + errors.push('Name and description are required fields'); + } + + if (!sponsorshipPolicy.isApplicableToAllNetworks) { + if (!sponsorshipPolicy.enabledChains || sponsorshipPolicy.enabledChains.length === 0) { + errors.push('Enabled chains are required'); + } + } + + if (!sponsorshipPolicy.isPerpetual) { + if (!sponsorshipPolicy.startTime || !sponsorshipPolicy.endTime) { + errors.push('Start and End time are required fields'); + } + + if (sponsorshipPolicy.startTime && sponsorshipPolicy.endTime) { + if (sponsorshipPolicy.startTime < new Date() || sponsorshipPolicy.endTime < new Date() || sponsorshipPolicy.endTime < sponsorshipPolicy.startTime) { + errors.push('Invalid start and end time'); + } + } + } + + if (sponsorshipPolicy.globalMaximumApplicable) { + if (!sponsorshipPolicy.globalMaximumUsd && !sponsorshipPolicy.globalMaximumNative && !sponsorshipPolicy.globalMaximumOpCount) { + errors.push('At least 1 Global maximum value is required'); + } + } + + if (sponsorshipPolicy.perUserMaximumApplicable) { + if (!sponsorshipPolicy.perUserMaximumUsd && !sponsorshipPolicy.perUserMaximumNative && !sponsorshipPolicy.perUserMaximumOpCount) { + errors.push('At least 1 Per User maximum value is required'); + } + } + + if (sponsorshipPolicy.perOpMaximumApplicable) { + if (!sponsorshipPolicy.perOpMaximumUsd && !sponsorshipPolicy.perOpMaximumNative) { + errors.push('At least 1 Per Op maximum value is required'); + } + } + + if (errors.length > 0) { + throw new Error(errors.join(', ')); + } + } + + async disableSponsorshipPolicy(id: number): Promise { + const existingSponsorshipPolicy = await this.findOneById(id); + + if (!existingSponsorshipPolicy) { + throw new Error('Sponsorship Policy not found'); + } + + existingSponsorshipPolicy.isEnabled = false; + await existingSponsorshipPolicy.save(); + } + + async enableSponsorshipPolicy(id: number): Promise { + const existingSponsorshipPolicy = await this.findOneById(id); + + if (!existingSponsorshipPolicy) { + throw new Error('Sponsorship Policy not found'); + } + + existingSponsorshipPolicy.isEnabled = true; + await existingSponsorshipPolicy.save(); + } + + async deleteSponsorshipPolicy(id: number): Promise { + const existingSponsorshipPolicy = await this.findOneById(id); + + if (!existingSponsorshipPolicy) { + throw new Error('Sponsorship Policy not found'); + } + + await existingSponsorshipPolicy.destroy(); + } + + async deleteAllSponsorshipPolicies(): Promise { + await this.sequelize.models.SponsorshipPolicy.destroy({ where: {} }); + } +} \ No newline at end of file diff --git a/backend/src/types/sponsorship-policy-dto.ts b/backend/src/types/sponsorship-policy-dto.ts index 3c3bc2f..9a25845 100644 --- a/backend/src/types/sponsorship-policy-dto.ts +++ b/backend/src/types/sponsorship-policy-dto.ts @@ -6,7 +6,7 @@ export interface SponsorshipPolicyDto { description: string; // Description of the sponsorship policy isPublic: boolean; // Flag to indicate if the policy is public isEnabled: boolean; // Flag to indicate if the policy is enabled - isUniversal: boolean; // Flag to indicate if the policy is universal + isApplicableToAllNetworks: boolean; // Flag to indicate if the policy is universal enabledChains?: number[]; // Array of enabled chain IDs isPerpetual: boolean; // Flag to indicate if the policy is perpetual startDate?: string; // Optional start date for the policy diff --git a/frontend/package.json b/frontend/package.json index 0f388ae..c6f86de 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "arka_frontend", - "version": "1.2.6", + "version": "1.2.7", "private": true, "dependencies": { "@babel/plugin-proposal-private-property-in-object": "7.21.11", From acb5e01dc58942961a2bbe39a4c4f3fc51bb02a5 Mon Sep 17 00:00:00 2001 From: kanth Date: Fri, 14 Jun 2024 22:55:16 +0530 Subject: [PATCH 21/23] fix: PRO-2395 fix imports in backend --- backend/Dockerfile | 2 +- backend/src/plugins/sequelizePlugin.ts | 10 +++++----- backend/src/routes/index.ts | 2 +- backend/src/routes/metadata.ts | 2 +- backend/src/server.ts | 6 +++--- backend/src/utils/common.ts | 1 - docker-compose.yml | 9 +++++++-- 7 files changed, 18 insertions(+), 14 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 52f00d8..6202c81 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -16,7 +16,7 @@ FROM node:18-alpine AS runner WORKDIR /usr/app ARG APP_ENV COPY --from=builder /app/build ./build -COPY ./src/migrations ./build/migrations +COPY ./migrations ./build/migrations COPY package.json ./ COPY --from=builder /app/config.json.default /usr/app/config.json RUN touch database.sqlite diff --git a/backend/src/plugins/sequelizePlugin.ts b/backend/src/plugins/sequelizePlugin.ts index 0649525..4dcd264 100644 --- a/backend/src/plugins/sequelizePlugin.ts +++ b/backend/src/plugins/sequelizePlugin.ts @@ -2,11 +2,11 @@ import fp from "fastify-plugin"; import { FastifyPluginAsync } from "fastify"; import { Sequelize } from 'sequelize'; import dotenv from 'dotenv'; -import { initializeAPIKeyModel } from '../models/api-key'; // Assuming path correctness -import { initializeSponsorshipPolicyModel } from '../models/sponsorship-policy'; -import { initializeArkaConfigModel } from "../models/arka-config"; -import { APIKeyRepository } from "../repository/api-key-repository"; -import { ArkaConfigRepository } from "../repository/arka-config-repository"; +import { initializeAPIKeyModel } from '../models/api-key.js'; +import { initializeSponsorshipPolicyModel } from '../models/sponsorship-policy.js'; +import { initializeArkaConfigModel } from "../models/arka-config.js"; +import { APIKeyRepository } from "../repository/api-key-repository.js"; +import { ArkaConfigRepository } from "../repository/arka-config-repository.js"; const pg = await import('pg'); const Client = pg.default.Client; diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 1c452c2..e80de18 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -11,7 +11,7 @@ import ErrorMessage from "../constants/ErrorMessage.js"; import ReturnCode from "../constants/ReturnCode.js"; import { decode } from "../utils/crypto.js"; import { printRequest, getNetworkConfig } from "../utils/common.js"; -import { APIKey } from "models/api-key.js"; +import { APIKey } from "../models/api-key.js"; const SUPPORTED_ENTRYPOINTS = { 'EPV_06' : "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", diff --git a/backend/src/routes/metadata.ts b/backend/src/routes/metadata.ts index 9ee7aef..93072fc 100644 --- a/backend/src/routes/metadata.ts +++ b/backend/src/routes/metadata.ts @@ -7,7 +7,7 @@ import ReturnCode from "../constants/ReturnCode.js"; import ErrorMessage from "../constants/ErrorMessage.js"; import { decode } from "../utils/crypto.js"; import { PAYMASTER_ADDRESS } from "../constants/Pimlico.js"; -import { APIKey } from "../models/api-key"; +import { APIKey } from "../models/api-key.js"; import * as EtherspotAbi from "../abi/EtherspotAbi.js"; const metadataRoutes: FastifyPluginAsync = async (server) => { diff --git a/backend/src/server.ts b/backend/src/server.ts index 6d6ebbe..975a133 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -17,10 +17,10 @@ import PimlicoAbi from './abi/PimlicoAbi.js'; import PythOracleAbi from './abi/PythOracleAbi.js'; import { getNetworkConfig } from './utils/common.js'; import { checkDeposit } from './utils/monitorTokenPaymaster.js'; -import { APIKey } from 'models/api-key.js'; +import { APIKey } from './models/api-key.js'; import { APIKeyRepository } from './repository/api-key-repository.js'; -import { ArkaConfig } from 'models/arka-config.js'; -import { ArkaConfigRepository } from 'repository/arka-config-repository.js'; +import { ArkaConfig } from './models/arka-config.js'; +import { ArkaConfigRepository } from './repository/arka-config-repository.js'; let server: FastifyInstance; diff --git a/backend/src/utils/common.ts b/backend/src/utils/common.ts index c093fd9..c4fc745 100644 --- a/backend/src/utils/common.ts +++ b/backend/src/utils/common.ts @@ -2,7 +2,6 @@ import { FastifyBaseLogger, FastifyRequest } from "fastify"; import { BigNumber, ethers } from "ethers"; import SupportedNetworks from "../../config.json" assert { type: "json" }; import { EtherscanResponse, getEtherscanFeeResponse } from "./interface.js"; -import { APIKey } from "models/api-key"; export function printRequest(methodName: string, request: FastifyRequest, log: FastifyBaseLogger) { log.info(methodName, "called: "); diff --git a/docker-compose.yml b/docker-compose.yml index 4b58bce..832910f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,13 +20,18 @@ services: - API_PORT=5050 - UNSAFE_MODE=false - SUPPORTED_NETWORKS= - - CRON_PRIVATE_KEY= + - CRON_PRIVATE_KEY="" - DEFAULT_INDEXER_ENDPOINT=http://localhost:3003 - - FEE_MARKUP= + - FEE_MARKUP=0 - MULTI_TOKEN_MARKUP=1150000 + - ADMIN_WALLET_ADDRESS="" - ETHERSCAN_GAS_ORACLES= - DEFAULT_API_KEY= - WEBHOOK_URL= + - DATABASE_URL="postgresql://arkauser:paymaster@localhost:5432/arkadev" + - DATABASE_SCHEMA_NAME=arka + - DATABASE_SSL_ENABLED=false + - DATABASE_SSL_REJECT_UNAUTHORIZED=false build: context: ./backend dockerfile: Dockerfile From ecbbbf09e02d2c0b97a72523029c43a8aa83a79c Mon Sep 17 00:00:00 2001 From: kanth Date: Sun, 16 Jun 2024 22:16:38 +0530 Subject: [PATCH 22/23] feat: PRO-2395 SponsorshipPolicy validations, db model updates and docker updates --- backend/Dockerfile | 1 - backend/README.md | 20 ++ ...0611000002-create-sponsorship-policies.cjs | 4 +- backend/package.json | 2 +- backend/src/constants/ErrorMessage.ts | 12 + backend/src/constants/ReturnCode.ts | 2 + backend/src/models/sponsorship-policy.ts | 30 +-- backend/src/plugins/config.ts | 6 + backend/src/plugins/db.ts | 4 +- backend/src/plugins/sequelizePlugin.ts | 7 +- .../sponsorship-policy-repository.ts | 244 ++++++++++++++++-- backend/src/routes/admin.ts | 2 +- .../src/routes/sponsorship-policy-route.ts | 110 ++++++++ backend/src/server.ts | 3 + backend/src/types/sponsorship-policy-dto.ts | 24 +- docker-compose.yml | 16 +- 16 files changed, 422 insertions(+), 65 deletions(-) create mode 100644 backend/src/routes/sponsorship-policy-route.ts diff --git a/backend/Dockerfile b/backend/Dockerfile index 6202c81..53e461e 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -19,7 +19,6 @@ COPY --from=builder /app/build ./build COPY ./migrations ./build/migrations COPY package.json ./ COPY --from=builder /app/config.json.default /usr/app/config.json -RUN touch database.sqlite RUN npm install USER root ENV NODE_ENV="production" diff --git a/backend/README.md b/backend/README.md index 8340009..9cf49c6 100644 --- a/backend/README.md +++ b/backend/README.md @@ -182,3 +182,23 @@ Parameters: - `/deposit` - This url accepts one parameter and returns the submitted transaction hash if successful. This url is used to deposit some funds to the entryPointAddress from the sponsor wallet 1. amount - The amount to be deposited in ETH +## Local Docker Networks + +1. Ensure the postgres docker instance is up and running + +2. Here we need to create a network and tag backend & postgres on same network + +```sh +docker network create arka-network +``` + +```sh +docker run --network arka-network --name local-setup-db-1 -d postgres +``` + +```sh +docker run --network arka-network --name arka-backend -d arka-backend +``` + + + diff --git a/backend/migrations/20240611000002-create-sponsorship-policies.cjs b/backend/migrations/20240611000002-create-sponsorship-policies.cjs index 1a09019..5805bbd 100644 --- a/backend/migrations/20240611000002-create-sponsorship-policies.cjs +++ b/backend/migrations/20240611000002-create-sponsorship-policies.cjs @@ -58,12 +58,12 @@ async function up({ context: queryInterface }) { startDate: { type: Sequelize.DATE, allowNull: true, - field: 'START_DATE' + field: 'START_TIME' }, endDate: { type: Sequelize.DATE, allowNull: true, - field: 'END_DATE' + field: 'END_TIME' }, globalMaxApplicable: { type: Sequelize.BOOLEAN, diff --git a/backend/package.json b/backend/package.json index 3bd99ad..e6afffa 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,7 +10,7 @@ "lint": "eslint .", "lint:fix": "eslint . --fix", "check:types": "tsc --noEmit", - "build": "esbuild `find src \\( -name '*.ts' \\)` --platform=node --outdir=build --resolve-extensions=.js && cp -r ./src/migrations ./build/", + "build": "esbuild `find src \\( -name '*.ts' \\)` --platform=node --outdir=build --resolve-extensions=.js && cp -r ./migrations ./build/", "build:docker:prod": "docker build . -t my-fastify-app --build-arg APP_ENV=production", "start": "node build", "dev": "tsx watch src | pino-pretty --colorize", diff --git a/backend/src/constants/ErrorMessage.ts b/backend/src/constants/ErrorMessage.ts index 74a04b2..545fe8f 100644 --- a/backend/src/constants/ErrorMessage.ts +++ b/backend/src/constants/ErrorMessage.ts @@ -1,9 +1,21 @@ export default { INVALID_DATA: 'Invalid data provided', + INVALID_SPONSORSHIP_POLICY: 'Invalid sponsorship policy data', + INVALID_SPONSORSHIP_POLICY_ID: 'Invalid sponsorship policy id', INVALID_API_KEY: 'Invalid Api Key', UNSUPPORTED_NETWORK: 'Unsupported network', UNSUPPORTED_NETWORK_TOKEN: 'Unsupported network/token', EMPTY_BODY: 'Empty Body received', + API_KEY_DOES_NOT_EXIST_FOR_THE_WALLET_ADDRESS: 'Api Key does not exist for the wallet address', + FAILED_TO_CREATE_SPONSORSHIP_POLICY: 'Failed to create sponsorship policy', + FAILED_TO_UPDATE_SPONSORSHIP_POLICY: 'Failed to update sponsorship policy', + SPONSORSHIP_POLICY_NOT_FOUND: 'Sponsorship policy not found', + SPONSORSHIP_POLICY_ALREADY_EXISTS: 'Sponsorship policy already exists', + SPONSORSHIP_POLICY_IS_DISABLED: 'Sponsorship policy is disabled', + FAILED_TO_DELETE_SPONSORSHIP_POLICY: 'Failed to delete sponsorship policy', + FAILED_TO_ENABLE_SPONSORSHIP_POLICY: 'Failed to enable sponsorship policy', + FAILED_TO_DISABLE_SPONSORSHIP_POLICY: 'Failed to disable sponsorship policy', + FAILED_TO_QUERY_SPONSORSHIP_POLICY: 'Failed to query sponsorship policy', FAILED_TO_PROCESS: 'Failed to process the request. Please try again or contact ARKA support team', INVALID_MODE: 'Invalid mode selected', DUPLICATE_RECORD: 'Duplicate record found', diff --git a/backend/src/constants/ReturnCode.ts b/backend/src/constants/ReturnCode.ts index bc7fc8d..863ed2f 100644 --- a/backend/src/constants/ReturnCode.ts +++ b/backend/src/constants/ReturnCode.ts @@ -1,4 +1,6 @@ export default { SUCCESS: 200, FAILURE: 400, + BAD_REQUEST: 400, + NOT_FOUND: 404, } diff --git a/backend/src/models/sponsorship-policy.ts b/backend/src/models/sponsorship-policy.ts index 55cb7c5..251c535 100644 --- a/backend/src/models/sponsorship-policy.ts +++ b/backend/src/models/sponsorship-policy.ts @@ -9,20 +9,20 @@ export class SponsorshipPolicy extends Model { public isEnabled: boolean = false; public isApplicableToAllNetworks!: boolean; public enabledChains?: number[]; - public isPerpetual!: boolean; - public startTime!: Date | null; - public endTime!: Date | null; + public isPerpetual: boolean = false; + public startTime: Date | null = null; + public endTime: Date | null = null; public globalMaximumApplicable: boolean = false; - public globalMaximumUsd!: number | null; - public globalMaximumNative!: number | null; - public globalMaximumOpCount!: number | null; + public globalMaximumUsd: number | null = null; + public globalMaximumNative: number | null = null; + public globalMaximumOpCount: number | null = null; public perUserMaximumApplicable: boolean = false; - public perUserMaximumUsd!: number | null; - public perUserMaximumNative!: number | null; - public perUserMaximumOpCount!: number | null; + public perUserMaximumUsd: number | null = null; + public perUserMaximumNative: number | null = null; + public perUserMaximumOpCount: number | null = null; public perOpMaximumApplicable: boolean = false; - public perOpMaximumUsd!: number | null; - public perOpMaximumNative!: number | null; + public perOpMaximumUsd: number | null = null; + public perOpMaximumNative: number | null = null; public addressAllowList: string[] | null = null; public addressBlockList: string[] | null = null; public readonly createdAt!: Date; @@ -103,15 +103,15 @@ export function initializeSponsorshipPolicyModel(sequelize: Sequelize, schema: s defaultValue: false, field: 'IS_PERPETUAL' }, - startDate: { + startTime: { type: DataTypes.DATE, allowNull: true, - field: 'START_DATE' + field: 'START_TIME' }, - endDate: { + endTime: { type: DataTypes.DATE, allowNull: true, - field: 'END_DATE' + field: 'END_TIME' }, globalMaximumApplicable: { type: DataTypes.BOOLEAN, diff --git a/backend/src/plugins/config.ts b/backend/src/plugins/config.ts index e09177d..9ed9f2c 100644 --- a/backend/src/plugins/config.ts +++ b/backend/src/plugins/config.ts @@ -38,6 +38,7 @@ export type ArkaConfig = Static; const configPlugin: FastifyPluginAsync = async (server) => { const validate = ajv.compile(ConfigSchema); + server.log.info("Validating .env file"); const valid = validate(process.env); if (!valid) { throw new Error( @@ -46,6 +47,8 @@ const configPlugin: FastifyPluginAsync = async (server) => { ); } + server.log.info("Configuring .env file"); + const config = { LOG_LEVEL: process.env.LOG_LEVEL ?? '', API_PORT: process.env.API_PORT ?? '', @@ -59,6 +62,9 @@ const configPlugin: FastifyPluginAsync = async (server) => { DATABASE_SCHEMA_NAME: process.env.DATABASE_SCHEMA_NAME ?? 'arka', } + server.log.info("Configured .env file"); + server.log.info(`config: ${JSON.stringify(config, null, 2)}`); + server.decorate("config", config); }; diff --git a/backend/src/plugins/db.ts b/backend/src/plugins/db.ts index 86647bb..2758b67 100644 --- a/backend/src/plugins/db.ts +++ b/backend/src/plugins/db.ts @@ -8,6 +8,8 @@ import { Umzug, SequelizeStorage } from 'umzug'; const databasePlugin: FastifyPluginAsync = async (server) => { + server.log.info(`Connecting to database... with URL: ${server.config.DATABASE_URL} and schemaName: ${server.config.DATABASE_SCHEMA_NAME}`); + const sequelize = new Sequelize(server.config.DATABASE_URL, { schema: server.config.DATABASE_SCHEMA_NAME, }); @@ -32,8 +34,6 @@ const databasePlugin: FastifyPluginAsync = async (server) => { console.error('Migration failed:', err) process.exitCode = 1 } - - //server.decorate('sequelize', sequelize); }; declare module "fastify" { diff --git a/backend/src/plugins/sequelizePlugin.ts b/backend/src/plugins/sequelizePlugin.ts index 4dcd264..c84ab85 100644 --- a/backend/src/plugins/sequelizePlugin.ts +++ b/backend/src/plugins/sequelizePlugin.ts @@ -7,6 +7,7 @@ import { initializeSponsorshipPolicyModel } from '../models/sponsorship-policy.j import { initializeArkaConfigModel } from "../models/arka-config.js"; import { APIKeyRepository } from "../repository/api-key-repository.js"; import { ArkaConfigRepository } from "../repository/arka-config-repository.js"; +import { SponsorshipPolicyRepository } from "../repository/sponsorship-policy-repository.js"; const pg = await import('pg'); const Client = pg.default.Client; @@ -43,9 +44,10 @@ const sequelizePlugin: FastifyPluginAsync = async (server) => { // Initialize models initializeArkaConfigModel(sequelize, server.config.DATABASE_SCHEMA_NAME); const initializedAPIKeyModel = initializeAPIKeyModel(sequelize, server.config.DATABASE_SCHEMA_NAME); - sequelize.models.APIKey = initializedAPIKeyModel; + //sequelize.models.APIKey = initializedAPIKeyModel; server.log.info(`Initialized APIKey model... ${sequelize.models.APIKey}`); initializeSponsorshipPolicyModel(sequelize, server.config.DATABASE_SCHEMA_NAME); + server.log.info('Initialized SponsorshipPolicy model...'); server.log.info('Initialized all models...'); @@ -55,6 +57,8 @@ const sequelizePlugin: FastifyPluginAsync = async (server) => { server.decorate('apiKeyRepository', apiKeyRepository); const arkaConfigRepository : ArkaConfigRepository = new ArkaConfigRepository(sequelize); server.decorate('arkaConfigRepository', arkaConfigRepository); + const sponsorshipPolicyRepository = new SponsorshipPolicyRepository(sequelize); + server.decorate('sponsorshipPolicyRepository', sponsorshipPolicyRepository); server.log.info('decorated fastify server with models...'); @@ -70,6 +74,7 @@ declare module "fastify" { sequelize: Sequelize; apiKeyRepository: APIKeyRepository; arkaConfigRepository: ArkaConfigRepository; + sponsorshipPolicyRepository: SponsorshipPolicyRepository; } } diff --git a/backend/src/repository/sponsorship-policy-repository.ts b/backend/src/repository/sponsorship-policy-repository.ts index 3d44434..eced7fb 100644 --- a/backend/src/repository/sponsorship-policy-repository.ts +++ b/backend/src/repository/sponsorship-policy-repository.ts @@ -1,5 +1,7 @@ import { Sequelize, Op } from 'sequelize'; import { SponsorshipPolicy } from '../models/sponsorship-policy'; +import { SponsorshipPolicyDto } from '../types/sponsorship-policy-dto'; +import { ethers } from 'ethers'; export class SponsorshipPolicyRepository { private sequelize: Sequelize; @@ -67,7 +69,7 @@ export class SponsorshipPolicyRepository { return result ? result.get() as SponsorshipPolicy : null; } - async createSponsorshipPolicy(sponsorshipPolicy: SponsorshipPolicy): Promise { + async createSponsorshipPolicy(sponsorshipPolicy: SponsorshipPolicyDto): Promise { this.validateSponsorshipPolicy(sponsorshipPolicy); const result = await this.sequelize.models.SponsorshipPolicy.create({ @@ -99,7 +101,7 @@ export class SponsorshipPolicyRepository { return result.get() as SponsorshipPolicy; } - async updateSponsorshipPolicy(sponsorshipPolicy: SponsorshipPolicy): Promise { + async updateSponsorshipPolicy(sponsorshipPolicy: SponsorshipPolicyDto): Promise { // check if sponsorship policy exists (by primary key id) const existingSponsorshipPolicy = await this.findOneById(sponsorshipPolicy.id as number); @@ -119,29 +121,110 @@ export class SponsorshipPolicyRepository { existingSponsorshipPolicy.startTime = null; existingSponsorshipPolicy.endTime = null; } else { - existingSponsorshipPolicy.startTime = sponsorshipPolicy.startTime; - existingSponsorshipPolicy.endTime = sponsorshipPolicy.endTime; + + if (!sponsorshipPolicy.startTime || sponsorshipPolicy.startTime == null) { + existingSponsorshipPolicy.startTime = null; + } else { + existingSponsorshipPolicy.startTime = sponsorshipPolicy.startTime; + } + + if (!sponsorshipPolicy.endTime || sponsorshipPolicy.endTime == null) { + existingSponsorshipPolicy.endTime = null; + } else { + existingSponsorshipPolicy.endTime = sponsorshipPolicy.endTime; + } } + existingSponsorshipPolicy.globalMaximumApplicable = sponsorshipPolicy.globalMaximumApplicable; - existingSponsorshipPolicy.globalMaximumUsd = sponsorshipPolicy.globalMaximumUsd; - existingSponsorshipPolicy.globalMaximumNative = sponsorshipPolicy.globalMaximumNative; - existingSponsorshipPolicy.globalMaximumOpCount = sponsorshipPolicy.globalMaximumOpCount; + + if (existingSponsorshipPolicy.globalMaximumApplicable) { + if (!sponsorshipPolicy.globalMaximumUsd || sponsorshipPolicy.globalMaximumUsd == null) { + existingSponsorshipPolicy.globalMaximumUsd = null; + } else { + existingSponsorshipPolicy.globalMaximumUsd = sponsorshipPolicy.globalMaximumUsd; + } + + if (!sponsorshipPolicy.globalMaximumNative || sponsorshipPolicy.globalMaximumNative == null) { + existingSponsorshipPolicy.globalMaximumNative = null; + } else { + existingSponsorshipPolicy.globalMaximumNative = sponsorshipPolicy.globalMaximumNative; + } + + if (!sponsorshipPolicy.globalMaximumOpCount || sponsorshipPolicy.globalMaximumOpCount == null) { + existingSponsorshipPolicy.globalMaximumOpCount = null; + } else { + existingSponsorshipPolicy.globalMaximumOpCount = sponsorshipPolicy.globalMaximumOpCount; + } + } else { + existingSponsorshipPolicy.globalMaximumUsd = null; + existingSponsorshipPolicy.globalMaximumNative = null; + existingSponsorshipPolicy.globalMaximumOpCount = null; + } + existingSponsorshipPolicy.perUserMaximumApplicable = sponsorshipPolicy.perUserMaximumApplicable; - existingSponsorshipPolicy.perUserMaximumNative = sponsorshipPolicy.perUserMaximumNative; - existingSponsorshipPolicy.perUserMaximumOpCount = sponsorshipPolicy.perUserMaximumOpCount; - existingSponsorshipPolicy.perUserMaximumUsd = sponsorshipPolicy.perUserMaximumUsd; + + if (existingSponsorshipPolicy.perUserMaximumApplicable) { + if (!sponsorshipPolicy.perUserMaximumUsd || sponsorshipPolicy.perUserMaximumUsd == null) { + existingSponsorshipPolicy.perUserMaximumUsd = null; + } else { + existingSponsorshipPolicy.perUserMaximumUsd = sponsorshipPolicy.perUserMaximumUsd; + } + + if (!sponsorshipPolicy.perUserMaximumNative || sponsorshipPolicy.perUserMaximumNative == null) { + existingSponsorshipPolicy.perUserMaximumNative = null; + } else { + existingSponsorshipPolicy.perUserMaximumNative = sponsorshipPolicy.perUserMaximumNative; + } + + if (!sponsorshipPolicy.perUserMaximumOpCount || sponsorshipPolicy.perUserMaximumOpCount == null) { + existingSponsorshipPolicy.perUserMaximumOpCount = null; + } else { + existingSponsorshipPolicy.perUserMaximumOpCount = sponsorshipPolicy.perUserMaximumOpCount; + } + } else { + existingSponsorshipPolicy.perUserMaximumUsd = null; + existingSponsorshipPolicy.perUserMaximumNative = null; + existingSponsorshipPolicy.perUserMaximumOpCount = null; + } + existingSponsorshipPolicy.perOpMaximumApplicable = sponsorshipPolicy.perOpMaximumApplicable; - existingSponsorshipPolicy.perOpMaximumNative = sponsorshipPolicy.perOpMaximumNative; - existingSponsorshipPolicy.perOpMaximumUsd = sponsorshipPolicy.perOpMaximumUsd; + + if (existingSponsorshipPolicy.perOpMaximumApplicable) { + if (!sponsorshipPolicy.perOpMaximumUsd || sponsorshipPolicy.perOpMaximumUsd == null) { + existingSponsorshipPolicy.perOpMaximumUsd = null; + } else { + existingSponsorshipPolicy.perOpMaximumUsd = sponsorshipPolicy.perOpMaximumUsd; + } + + if (!sponsorshipPolicy.perOpMaximumNative || sponsorshipPolicy.perOpMaximumNative == null) { + existingSponsorshipPolicy.perOpMaximumNative = null; + } else { + existingSponsorshipPolicy.perOpMaximumNative = sponsorshipPolicy.perOpMaximumNative; + } + } else { + existingSponsorshipPolicy.perOpMaximumUsd = null; + existingSponsorshipPolicy.perOpMaximumNative = null; + } + existingSponsorshipPolicy.isPublic = sponsorshipPolicy.isPublic; - existingSponsorshipPolicy.addressAllowList = sponsorshipPolicy.addressAllowList; - existingSponsorshipPolicy.addressBlockList = sponsorshipPolicy.addressBlockList; + + if (existingSponsorshipPolicy.addressAllowList && existingSponsorshipPolicy.addressAllowList.length > 0) { + existingSponsorshipPolicy.addressAllowList = sponsorshipPolicy.addressAllowList as string[]; + } else { + existingSponsorshipPolicy.addressAllowList = null; + } + + if (existingSponsorshipPolicy.addressBlockList && existingSponsorshipPolicy.addressBlockList.length > 0) { + existingSponsorshipPolicy.addressBlockList = sponsorshipPolicy.addressBlockList as string[]; + } else { + existingSponsorshipPolicy.addressBlockList = null; + } const result = await existingSponsorshipPolicy.save(); return result.get() as SponsorshipPolicy; } - validateSponsorshipPolicy(sponsorshipPolicy: SponsorshipPolicy) { + validateSponsorshipPolicy(sponsorshipPolicy: SponsorshipPolicyDto) { let errors: string[] = []; if (!sponsorshipPolicy.name || !sponsorshipPolicy.description) { @@ -159,9 +242,20 @@ export class SponsorshipPolicyRepository { errors.push('Start and End time are required fields'); } + const currentTime = new Date(); + if (sponsorshipPolicy.startTime && sponsorshipPolicy.endTime) { - if (sponsorshipPolicy.startTime < new Date() || sponsorshipPolicy.endTime < new Date() || sponsorshipPolicy.endTime < sponsorshipPolicy.startTime) { - errors.push('Invalid start and end time'); + const startTime = new Date(sponsorshipPolicy.startTime + 'Z'); + const endTime = new Date(sponsorshipPolicy.endTime + 'Z'); + + if (startTime.getTime() < currentTime.getTime()) { + errors.push(`Invalid start time. Provided start time is ${startTime.toISOString()} in GMT. The start time must be now or in the future. Current time is ${currentTime.toISOString()} in GMT.`); + } + if (endTime.getTime() < currentTime.getTime()) { + errors.push(`Invalid end time. Provided end time is ${endTime.toISOString()} in GMT. The end time must be in the future. Current time is ${currentTime.toISOString()} in GMT.`); + } + if (endTime.getTime() < startTime.getTime()) { + errors.push(`Invalid end time. Provided end time is ${endTime.toISOString()} in GMT and start time is ${startTime.toISOString()} in GMT. The end time must be greater than the start time.`); } } } @@ -170,18 +264,101 @@ export class SponsorshipPolicyRepository { if (!sponsorshipPolicy.globalMaximumUsd && !sponsorshipPolicy.globalMaximumNative && !sponsorshipPolicy.globalMaximumOpCount) { errors.push('At least 1 Global maximum value is required'); } + + const globalMaximumUsd = sponsorshipPolicy.globalMaximumUsd; + + if (globalMaximumUsd !== undefined && globalMaximumUsd !== null) { + const parts = globalMaximumUsd.toString().split('.'); + if (parts.length > 2 || parts[0].length > 6 || (parts[1] && parts[1].length > 4)) { + errors.push(`Invalid value for globalMaximumUsd. The value ${globalMaximumUsd} exceeds the maximum allowed precision of 10 total digits, with a maximum of 4 digits allowed after the decimal point.`); + } + } + + const globalMaximumNative = sponsorshipPolicy.globalMaximumNative; + + if (globalMaximumNative !== undefined && globalMaximumNative !== null) { + const parts = globalMaximumNative.toString().split('.'); + if (parts.length > 2 || parts[0].length > 4 || (parts[1] && parts[1].length > 18)) { + errors.push(`Invalid value for globalMaximumNative. The value ${globalMaximumNative} exceeds the maximum allowed precision of 22 total digits, with a maximum of 18 digits allowed after the decimal point.`); + } + } } if (sponsorshipPolicy.perUserMaximumApplicable) { if (!sponsorshipPolicy.perUserMaximumUsd && !sponsorshipPolicy.perUserMaximumNative && !sponsorshipPolicy.perUserMaximumOpCount) { errors.push('At least 1 Per User maximum value is required'); } + + const perUserMaximumUsd = sponsorshipPolicy.perUserMaximumUsd; + + if (perUserMaximumUsd !== undefined && perUserMaximumUsd !== null) { + const parts = perUserMaximumUsd.toString().split('.'); + if (parts.length > 2 || parts[0].length > 6 || (parts[1] && parts[1].length > 4)) { + errors.push(`Invalid value for perUserMaximumUsd. The value ${perUserMaximumUsd} exceeds the maximum allowed precision of 10 total digits, with a maximum of 4 digits allowed after the decimal point.`); + } + } + + const perUserMaximumNative = sponsorshipPolicy.perUserMaximumNative; + + if (perUserMaximumNative !== undefined && perUserMaximumNative !== null) { + const parts = perUserMaximumNative.toString().split('.'); + if (parts.length > 2 || parts[0].length > 4 || (parts[1] && parts[1].length > 18)) { + errors.push(`Invalid value for perUserMaximumNative. The value ${perUserMaximumNative} exceeds the maximum allowed precision of 22 total digits, with a maximum of 18 digits allowed after the decimal point.`); + } + } } if (sponsorshipPolicy.perOpMaximumApplicable) { if (!sponsorshipPolicy.perOpMaximumUsd && !sponsorshipPolicy.perOpMaximumNative) { errors.push('At least 1 Per Op maximum value is required'); } + + const perOpMaximumUsd = sponsorshipPolicy.perOpMaximumUsd; + + if (perOpMaximumUsd !== undefined && perOpMaximumUsd !== null) { + const parts = perOpMaximumUsd.toString().split('.'); + if (parts.length > 2 || parts[0].length > 6 || (parts[1] && parts[1].length > 4)) { + errors.push(`Invalid value for perOpMaximumUsd. The value ${perOpMaximumUsd} exceeds the maximum allowed precision of 10 total digits, with a maximum of 4 digits allowed after the decimal point.`); + } + } + + const perOpMaximumNative = sponsorshipPolicy.perOpMaximumNative; + + if (perOpMaximumNative !== undefined && perOpMaximumNative !== null) { + const parts = perOpMaximumNative.toString().split('.'); + if (parts.length > 2 || parts[0].length > 4 || (parts[1] && parts[1].length > 18)) { + errors.push(`Invalid value for perOpMaximumNative. The value ${perOpMaximumNative} exceeds the maximum allowed precision of 22 total digits, with a maximum of 18 digits allowed after the decimal point.`); + } + } + } + + // check if the addressAllowList and addressBlockList are valid addresses + if (sponsorshipPolicy.addressAllowList && sponsorshipPolicy.addressAllowList.length > 0) { + const invalidAddresses: string[] = []; + + sponsorshipPolicy.addressAllowList.forEach(address => { + if (!address || !ethers.utils.isAddress(address)) { + invalidAddresses.push(address); + } + }); + + if (invalidAddresses.length > 0) { + errors.push(`The following addresses in addressAllowList are invalid: ${invalidAddresses.join(', ')}`); + } + } + + if (sponsorshipPolicy.addressBlockList && sponsorshipPolicy.addressBlockList.length > 0) { + const invalidAddresses: string[] = []; + + sponsorshipPolicy.addressBlockList.forEach(address => { + if (!address || !ethers.utils.isAddress(address)) { + invalidAddresses.push(address); + } + }); + + if (invalidAddresses.length > 0) { + errors.push(`The following addresses in addressBlockList are invalid: ${invalidAddresses.join(', ')}`); + } } if (errors.length > 0) { @@ -196,6 +373,10 @@ export class SponsorshipPolicyRepository { throw new Error('Sponsorship Policy not found'); } + if (!existingSponsorshipPolicy.isEnabled) { + throw new Error('Cannot disable a policy which is already disabled'); + } + existingSponsorshipPolicy.isEnabled = false; await existingSponsorshipPolicy.save(); } @@ -207,21 +388,40 @@ export class SponsorshipPolicyRepository { throw new Error('Sponsorship Policy not found'); } + if (existingSponsorshipPolicy.isEnabled) { + throw new Error('Cannot enable a policy which is already enabled'); + } + existingSponsorshipPolicy.isEnabled = true; await existingSponsorshipPolicy.save(); } - async deleteSponsorshipPolicy(id: number): Promise { + async deleteSponsorshipPolicy(id: number): Promise { const existingSponsorshipPolicy = await this.findOneById(id); if (!existingSponsorshipPolicy) { - throw new Error('Sponsorship Policy not found'); + throw new Error(`Sponsorship Policy deletion failed as Policy doesnot exist with id: ${id}`); } - await existingSponsorshipPolicy.destroy(); + const deletedCount = await this.sequelize.models.SponsorshipPolicy.destroy({ + where + : { id: id } + }); + + if (deletedCount === 0) { + throw new Error(`SponsorshipPolicy deletion failed for id: ${id}`); + } + + return deletedCount; } - async deleteAllSponsorshipPolicies(): Promise { - await this.sequelize.models.SponsorshipPolicy.destroy({ where: {} }); + async deleteAllSponsorshipPolicies(): Promise<{ message: string }> { + try { + await this.sequelize.models.SponsorshipPolicy.destroy({ where: {} }); + return { message: 'Successfully deleted all policies' }; + } catch (err) { + console.error(err); + throw new Error('Failed to delete all policies'); + } } } \ No newline at end of file diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts index d17ac5b..ac6fe7f 100644 --- a/backend/src/routes/admin.ts +++ b/backend/src/routes/admin.ts @@ -16,7 +16,7 @@ const adminRoutes: FastifyPluginAsync = async (server) => { const body: any = JSON.parse(request.body as string); if (!body) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); if (!body.walletAddress) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); - if (ethers.utils.getAddress(body.walletAddress) === server.config.ADMIN_WALLET_ADDRESS) return reply.code(ReturnCode.SUCCESS).send({ error: null, message: "Successfully Logged in" }); + if (ethers.utils.getAddress(body.walletAddress) === ethers.utils.getAddress(server.config.ADMIN_WALLET_ADDRESS)) return reply.code(ReturnCode.SUCCESS).send({ error: null, message: "Successfully Logged in" }); return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_USER }); } catch (err: any) { return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_USER }); diff --git a/backend/src/routes/sponsorship-policy-route.ts b/backend/src/routes/sponsorship-policy-route.ts new file mode 100644 index 0000000..2862a19 --- /dev/null +++ b/backend/src/routes/sponsorship-policy-route.ts @@ -0,0 +1,110 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { FastifyPluginAsync, FastifyReply, FastifyRequest } from "fastify"; +import ErrorMessage from "../constants/ErrorMessage.js"; +import ReturnCode from "../constants/ReturnCode.js"; +import { SponsorshipPolicyDto } from "../types/sponsorship-policy-dto.js"; + +interface RouteParams { + id: string; +} + +const sponsorshipPolicyRoutes: FastifyPluginAsync = async (server) => { + + server.get("/getPolicies", async function (request, reply) { + try { + const result = await server.sponsorshipPolicyRepository.findAll(); + + if (!result) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.FAILED_TO_QUERY_SPONSORSHIP_POLICY }); + } + + return reply.code(ReturnCode.SUCCESS).send(result); + } catch (err: any) { + request.log.error(err); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_QUERY_SPONSORSHIP_POLICY }); + } + }) + + server.post("/addPolicy", async function (request, reply) { + try { + // parse the request body as JSON + const sponsorshipPolicyDto: SponsorshipPolicyDto = JSON.parse(JSON.stringify(request.body)) as SponsorshipPolicyDto; + if (!sponsorshipPolicyDto) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); + + // id is to be null + if (sponsorshipPolicyDto.id || sponsorshipPolicyDto.id as number > 0 || + !sponsorshipPolicyDto.walletAddress || + !sponsorshipPolicyDto.name) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_SPONSORSHIP_POLICY }); + } + + // verify if api key exists for the given wallet address + const apiKey = await server.apiKeyRepository.findOneByWalletAddress(sponsorshipPolicyDto.walletAddress); + + if (!apiKey) { + return reply.code(ReturnCode.FAILURE).send({ + error: ErrorMessage.API_KEY_DOES_NOT_EXIST_FOR_THE_WALLET_ADDRESS + }); + } + + const result = await server.sponsorshipPolicyRepository.createSponsorshipPolicy(sponsorshipPolicyDto); + if (!result) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.FAILED_TO_CREATE_SPONSORSHIP_POLICY }); + } + + return reply.code(ReturnCode.SUCCESS).send(result); + } catch (err: any) { + request.log.error(err); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_CREATE_SPONSORSHIP_POLICY }); + } + }) + + + server.delete<{ Params: RouteParams }>("/deletePolicy/:id", async (request, reply) => { + try { + const id = Number(request.params.id); + if (isNaN(id)) { + return reply.code(400).send({ error: ErrorMessage.INVALID_SPONSORSHIP_POLICY_ID }); + } + + const result = await server.sponsorshipPolicyRepository.deleteSponsorshipPolicy(id); + return reply.code(200).send({ message: `Successfully deleted policy with id ${id}` }); + } catch (err) { + request.log.error(err); + return reply.code(500).send({ error: ErrorMessage.FAILED_TO_DELETE_SPONSORSHIP_POLICY }); + } + }); + + + server.put<{ Body: SponsorshipPolicyDto }>("/updatePolicy", async (request, reply) => { + try { + const sponsorshipPolicyDto: SponsorshipPolicyDto = JSON.parse(JSON.stringify(request.body)) as SponsorshipPolicyDto; + const id = sponsorshipPolicyDto.id; + + if (!id || isNaN(id)) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_SPONSORSHIP_POLICY_ID }); + } + + const existingSponsorshipPolicy = await server.sponsorshipPolicyRepository.findOneById(id); + if (!existingSponsorshipPolicy) { + return reply.code(ReturnCode.NOT_FOUND).send({ error: ErrorMessage.SPONSORSHIP_POLICY_NOT_FOUND }); + } + + // cannot update a disabled policy + if (!existingSponsorshipPolicy.isEnabled) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.SPONSORSHIP_POLICY_IS_DISABLED }); + } + + const updatedPolicy = await server.sponsorshipPolicyRepository.updateSponsorshipPolicy(sponsorshipPolicyDto); + return reply.code(ReturnCode.SUCCESS).send(updatedPolicy); + } catch (err) { + request.log.error(err); + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.FAILED_TO_UPDATE_SPONSORSHIP_POLICY }); + } + }); + + + +}; + +export default sponsorshipPolicyRoutes; diff --git a/backend/src/server.ts b/backend/src/server.ts index 975a133..7eab9bf 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -21,6 +21,7 @@ import { APIKey } from './models/api-key.js'; import { APIKeyRepository } from './repository/api-key-repository.js'; import { ArkaConfig } from './models/arka-config.js'; import { ArkaConfigRepository } from './repository/arka-config-repository.js'; +import sponsorshipPolicyRoutes from './routes/sponsorship-policy-route.js'; let server: FastifyInstance; @@ -57,6 +58,8 @@ const initializeServer = async (): Promise => { await server.register(metadataRoutes); + await server.register(sponsorshipPolicyRoutes); + // Database await server.register(database); diff --git a/backend/src/types/sponsorship-policy-dto.ts b/backend/src/types/sponsorship-policy-dto.ts index 9a25845..b03a1a6 100644 --- a/backend/src/types/sponsorship-policy-dto.ts +++ b/backend/src/types/sponsorship-policy-dto.ts @@ -1,6 +1,6 @@ // DTO for receiving data in the POST request to create a sponsorship policy export interface SponsorshipPolicyDto { - id: number; // ID of the policy + id?: number; // ID of the policy walletAddress: string; // The wallet address associated with the API key name: string; // Name of the sponsorship policy description: string; // Description of the sponsorship policy @@ -9,21 +9,21 @@ export interface SponsorshipPolicyDto { isApplicableToAllNetworks: boolean; // Flag to indicate if the policy is universal enabledChains?: number[]; // Array of enabled chain IDs isPerpetual: boolean; // Flag to indicate if the policy is perpetual - startDate?: string; // Optional start date for the policy - endDate?: string; // Optional end date for the policy + startTime?: Date | null; // Optional start date for the policy + endTime?: Date | null; // Optional end date for the policy globalMaximumApplicable: boolean; // Flag to indicate if the global maximum is applicable - globalMaximumUsd?: number; // Optional global maximum USD limit - globalMaximumNative?: number; // Optional global maximum native limit - globalMaximumOpCount?: number; // Optional global maximum operation count + globalMaximumUsd?: number | null; // Optional global maximum USD limit + globalMaximumNative?: number | null; // Optional global maximum native limit + globalMaximumOpCount?: number | null; // Optional global maximum operation count perUserMaximumApplicable: boolean; // Flag to indicate if the per user maximum is applicable - perUserMaximumUsd?: number; // Optional per user maximum USD limit - perUserMaximumNative?: number; // Optional per user maximum native limit + perUserMaximumUsd?: number | null; // Optional per user maximum USD limit + perUserMaximumNative?: number | null; // Optional per user maximum native limit perUserMaximumOpCount?: number; // Optional per user maximum operation count perOpMaximumApplicable: boolean; // Flag to indicate if the per operation maximum is applicable - perOpMaximumUsd?: number; // Optional per operation maximum USD limit - perOpMaximumNative?: number; // Optional per operation maximum native limit - addressAllowList?: string[]; // Optional array of allowed addresses - addressBlockList?: string[]; // Optional array of blocked addresses + perOpMaximumUsd?: number | null; // Optional per operation maximum USD limit + perOpMaximumNative?: number | null; // Optional per operation maximum native limit + addressAllowList?: string[] | null; // Optional array of allowed addresses + addressBlockList?: string[] | null; // Optional array of blocked addresses isExpired: boolean; // Flag to indicate if the policy is expired isCurrent: boolean; // Flag to indicate if the policy is current isApplicable: boolean; // Flag to indicate if the policy is applicable diff --git a/docker-compose.yml b/docker-compose.yml index 832910f..23143a6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,16 +20,16 @@ services: - API_PORT=5050 - UNSAFE_MODE=false - SUPPORTED_NETWORKS= - - CRON_PRIVATE_KEY="" + - CRON_PRIVATE_KEY= - DEFAULT_INDEXER_ENDPOINT=http://localhost:3003 - FEE_MARKUP=0 - MULTI_TOKEN_MARKUP=1150000 - - ADMIN_WALLET_ADDRESS="" - - ETHERSCAN_GAS_ORACLES= - - DEFAULT_API_KEY= - - WEBHOOK_URL= - - DATABASE_URL="postgresql://arkauser:paymaster@localhost:5432/arkadev" - - DATABASE_SCHEMA_NAME=arka + - ADMIN_WALLET_ADDRESS= + - ETHERSCAN_GAS_ORACLES="" + - DEFAULT_API_KEY="" + - WEBHOOK_URL="" + - DATABASE_URL=postgresql://arkauser:paymaster@local-setup-db-1:5432/arkadev + - DATABASE_SCHEMA_NAME="arka" - DATABASE_SSL_ENABLED=false - DATABASE_SSL_REJECT_UNAUTHORIZED=false build: @@ -50,4 +50,4 @@ services: expose: - 3002 ports: - - "3002:3002" \ No newline at end of file + - "3002:3002" From a4d8a9be90574d79f734ebbd2f1050a984e3de65 Mon Sep 17 00:00:00 2001 From: kanth Date: Sun, 16 Jun 2024 22:21:36 +0530 Subject: [PATCH 23/23] doc: PRO-2395 docker documentation for backend updated in readme file --- backend/README.md | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/backend/README.md b/backend/README.md index 9cf49c6..205b992 100644 --- a/backend/README.md +++ b/backend/README.md @@ -186,18 +186,37 @@ Parameters: 1. Ensure the postgres docker instance is up and running -2. Here we need to create a network and tag backend & postgres on same network +```sh +cd backend/local-setup +``` + +2. Start `postgres` database instance + +```sh +docker-compose up -d +``` + +3. Here we need to create a network and tag backend & postgres on same network ```sh docker network create arka-network ``` ```sh -docker run --network arka-network --name local-setup-db-1 -d postgres +docker network connect arka-network local-setup-db-1 +``` + +```sh +docker network connect arka-network arka-backend-1 ``` +4. restart the docker backend instance + +- change to root directory i.e `arka` project directory +- restart the backend + ```sh -docker run --network arka-network --name arka-backend -d arka-backend +docker-compose up -d ```