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 532eddc..d696e0a 100644 --- a/admin_frontend/.gitignore +++ b/admin_frontend/.gitignore @@ -21,5 +21,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* - -.env \ No newline at end of file +yarn.lock +.env 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/admin_frontend/src/components/ApiKeys.jsx b/admin_frontend/src/components/ApiKeys.jsx index 2bfd197..b1ebdb3 100644 --- a/admin_frontend/src/components/ApiKeys.jsx +++ b/admin_frontend/src/components/ApiKeys.jsx @@ -97,15 +97,15 @@ 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; + element.erc20Paymasters = parsedErc20Paymasters; } return element; }); @@ -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"]}`, @@ -149,7 +149,7 @@ const ApiKeysPage = () => { body: JSON.stringify(requestData), } ); - const dataJson = data.json(); + const dataJson = await data.json(); if (!dataJson.error) { toast.success("Saved Successfully"); setApiKey(""); @@ -157,7 +157,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")) { @@ -174,7 +174,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(); @@ -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/admin_frontend/src/components/Dashboard.jsx b/admin_frontend/src/components/Dashboard.jsx index 2d63dad..b46c969 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); @@ -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); @@ -135,13 +135,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 +202,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 +232,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 +291,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 +322,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 +350,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 +381,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/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/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 2676159..a357ad8 100644 --- a/admin_frontend/src/modals/AddSupportedNetworksModal.jsx +++ b/admin_frontend/src/modals/AddSupportedNetworksModal.jsx @@ -180,7 +180,7 @@ const AddSupportedNetworksModal = ({ - {supportedNetworks.map((network, index) => { + {Array.isArray(supportedNetworks) && supportedNetworks.map((network, index) => { return ( {network.chainId} 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/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/Dockerfile b/backend/Dockerfile index 52f00d8..53e461e 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -16,10 +16,9 @@ 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 RUN npm install USER root ENV NODE_ENV="production" diff --git a/backend/README.md b/backend/README.md index 8340009..205b992 100644 --- a/backend/README.md +++ b/backend/README.md @@ -182,3 +182,42 @@ 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 + +```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 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-compose up -d +``` + + + diff --git a/backend/docs/sponsorship-policies-db-design.png b/backend/docs/sponsorship-policies-db-design.png new file mode 100644 index 0000000..a11c72a Binary files /dev/null and b/backend/docs/sponsorship-policies-db-design.png differ 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) 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/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/migrations/20240611000000-create-arka-config.cjs b/backend/migrations/20240611000000-create-arka-config.cjs new file mode 100644 index 0000000..02e86e2 --- /dev/null +++ b/backend/migrations/20240611000000-create-arka-config.cjs @@ -0,0 +1,68 @@ +const { Sequelize } = require('sequelize') + +async function up({ context: queryInterface }) { + await queryInterface.createTable('arka_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: 'arka_config', + schema: process.env.DATABASE_SCHEMA_NAME + }) +} + +module.exports = { up, down } diff --git a/backend/migrations/20240611000001-create-api-keys.cjs b/backend/migrations/20240611000001-create-api-keys.cjs new file mode 100644 index 0000000..32692d5 --- /dev/null +++ b/backend/migrations/20240611000001-create-api-keys.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-policies.cjs b/backend/migrations/20240611000002-create-sponsorship-policies.cjs new file mode 100644 index 0000000..5805bbd --- /dev/null +++ b/backend/migrations/20240611000002-create-sponsorship-policies.cjs @@ -0,0 +1,157 @@ +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.TEXT, + allowNull: false, + field: 'WALLET_ADDRESS', + references: { + model: 'api_keys', + key: 'WALLET_ADDRESS' + }, + onDelete: 'CASCADE', + onUpdate: 'CASCADE' + }, + name: { + type: Sequelize.TEXT, + allowNull: false, + field: 'NAME' + }, + description: { + type: Sequelize.TEXT, + allowNull: true, + field: 'DESCRIPTION' + }, + isPublic: { + type: Sequelize.BOOLEAN, + defaultValue: false, + field: 'IS_PUBLIC' + }, + isEnabled: { + type: Sequelize.BOOLEAN, + defaultValue: false, + field: 'IS_ENABLED' + }, + isApplicableToAllNetworks: { + type: Sequelize.BOOLEAN, + defaultValue: false, + field: 'IS_APPLICABLE_TO_ALL_NETWORKS' + }, + 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, + field: 'START_TIME' + }, + endDate: { + type: Sequelize.DATE, + allowNull: true, + field: 'END_TIME' + }, + 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, + field: 'GLOBAL_MAX_USD' + }, + 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' + }, + 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, + 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' + }, + 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, + 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' + }, + addressAllowList: { + type: Sequelize.ARRAY(Sequelize.STRING), + allowNull: true, + field: 'ADDRESS_ALLOW_LIST' + }, + addressBlockList: { + type: Sequelize.ARRAY(Sequelize.STRING), + allowNull: true, + field: 'ADDRESS_BLOCK_LIST' + }, + 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/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/package.json b/backend/package.json index d80412a..e6afffa 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": { @@ -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", @@ -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,15 @@ "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", + "umzug": "^3.8.1", "viem": "2.7.6" }, "devDependencies": { @@ -53,6 +61,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 +75,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/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/index.ts b/backend/src/index.ts index 96915d9..a5b924a 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -15,8 +15,8 @@ setTimeout(async () => { for (const signal of ['SIGINT', 'SIGTERM']) { process.on(signal, () => server.close().then((err) => { - server.sqlite.close(); - console.log(`close application on ${signal}`); + server.sequelize.close(); + server.log.info(`close application on ${signal}`); process.exit(err ? 1 : 0); }), ); diff --git a/backend/src/migrations/001.default.sql b/backend/src/migrations/001.default.sql deleted file mode 100644 index b808ab9..0000000 --- a/backend/src/migrations/001.default.sql +++ /dev/null @@ -1,42 +0,0 @@ --------------------------------------------------------------------------------- --- Up --------------------------------------------------------------------------------- - -CREATE TABLE IF NOT EXISTS config ( - id INTEGER 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 diff --git a/backend/src/migrations/002.apiKeys.sql b/backend/src/migrations/002.apiKeys.sql deleted file mode 100644 index fd83ae8..0000000 --- a/backend/src/migrations/002.apiKeys.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, - 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, - TRANSACTION_LIMIT INT NOT NULL, - NO_OF_TRANSACTIONS_IN_A_MONTH int, - INDEXER_ENDPOINT varchar -); - --------------------------------------------------------------------------------- --- Down --------------------------------------------------------------------------------- - -DROP TABLE IF EXISTS api_keys diff --git a/backend/src/models/api-key.ts b/backend/src/models/api-key.ts new file mode 100644 index 0000000..e291f9d --- /dev/null +++ b/backend/src/models/api-key.ts @@ -0,0 +1,106 @@ +import { Sequelize, DataTypes, Model } from 'sequelize'; + +export class APIKey extends Model { + 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) { + const initializedAPIKeyModel = APIKey.init({ + apiKey: { + type: DataTypes.TEXT, + allowNull: false, + primaryKey: true, + field: 'API_KEY' + }, + walletAddress: { + type: DataTypes.TEXT, + allowNull: false, + unique: true, + 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' + }, + 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, + }); + + return initializedAPIKeyModel; +} \ No newline at end of file diff --git a/backend/src/models/arka-config.ts b/backend/src/models/arka-config.ts new file mode 100644 index 0000000..33828a8 --- /dev/null +++ b/backend/src/models/arka-config.ts @@ -0,0 +1,96 @@ +import { Sequelize, DataTypes, Model } from 'sequelize'; + +export class ArkaConfig 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 initializeArkaConfigModel = (sequelize: Sequelize, schema: string) => { + ArkaConfig.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: 'arka_config', + modelName: 'ArkaConfig', + timestamps: true, + createdAt: 'createdAt', + updatedAt: 'updatedAt', + freezeTableName: true, + schema: schema, + }); +}; + +export { initializeArkaConfigModel }; \ No newline at end of file diff --git a/backend/src/models/sequelize-associations.ts b/backend/src/models/sequelize-associations.ts new file mode 100644 index 0000000..1ff9e74 --- /dev/null +++ b/backend/src/models/sequelize-associations.ts @@ -0,0 +1,27 @@ +import { APIKey } from './api-key'; +import { SponsorshipPolicy } from './sponsorship-policy'; + +export function setupAssociations() { + + /** + * APIKey to SponsorshipPolicy + * 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, { + foreignKey: 'walletAddress', + 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 + }); +} diff --git a/backend/src/models/sponsorship-policy.ts b/backend/src/models/sponsorship-policy.ts new file mode 100644 index 0000000..251c535 --- /dev/null +++ b/backend/src/models/sponsorship-policy.ts @@ -0,0 +1,203 @@ +import { Sequelize, DataTypes, Model } from 'sequelize'; + +export class SponsorshipPolicy extends Model { + public id!: number; + public walletAddress!: string; + public name!: string; + public description!: string | null; + public isPublic: boolean = false; + public isEnabled: boolean = false; + public isApplicableToAllNetworks!: boolean; + public enabledChains?: number[]; + public isPerpetual: boolean = false; + public startTime: Date | null = null; + public endTime: Date | null = null; + public globalMaximumApplicable: boolean = false; + public globalMaximumUsd: number | null = null; + public globalMaximumNative: number | null = null; + public globalMaximumOpCount: number | null = null; + public perUserMaximumApplicable: boolean = false; + public perUserMaximumUsd: number | null = null; + public perUserMaximumNative: number | null = null; + public perUserMaximumOpCount: number | null = null; + public perOpMaximumApplicable: boolean = false; + public perOpMaximumUsd: number | null = null; + public perOpMaximumNative: number | null = null; + public addressAllowList: string[] | null = null; + public addressBlockList: string[] | null = 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) { + SponsorshipPolicy.init({ + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + field: '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.TEXT, + allowNull: false, + field: 'NAME' + }, + description: { + type: DataTypes.TEXT, + allowNull: true, + field: 'DESCRIPTION' + }, + isPublic: { + type: DataTypes.BOOLEAN, + defaultValue: false, + field: 'IS_PUBLIC' + }, + isEnabled: { + type: DataTypes.BOOLEAN, + defaultValue: false, + field: 'IS_ENABLED' + }, + isApplicableToAllNetworks: { + type: DataTypes.BOOLEAN, + defaultValue: false, + field: 'IS_APPLICABLE_TO_ALL_NETWORKS' + }, + enabledChains: { + type: DataTypes.ARRAY(DataTypes.INTEGER), + allowNull: true, + field: 'ENABLED_CHAINS' + }, + isPerpetual: { + type: DataTypes.BOOLEAN, + defaultValue: false, + field: 'IS_PERPETUAL' + }, + startTime: { + type: DataTypes.DATE, + allowNull: true, + field: 'START_TIME' + }, + endTime: { + type: DataTypes.DATE, + allowNull: true, + field: 'END_TIME' + }, + 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, + field: 'GLOBAL_MAX_USD' + }, + 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' + }, + 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, + 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' + }, + 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, + 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' + }, + addressAllowList: { + type: DataTypes.ARRAY(DataTypes.STRING), + allowNull: true, + field: 'ADDRESS_ALLOW_LIST' + }, + addressBlockList: { + type: DataTypes.ARRAY(DataTypes.STRING), + allowNull: true, + field: 'ADDRESS_BLOCK_LIST' + }, + 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_policies', + modelName: 'SponsorshipPolicy', + timestamps: true, + createdAt: 'createdAt', + updatedAt: 'updatedAt', + freezeTableName: true, + schema: schema, + }); +} \ 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/config.ts b/backend/src/plugins/config.ts index 990be71..9ed9f2c 100644 --- a/backend/src/plugins/config.ts +++ b/backend/src/plugins/config.ts @@ -20,6 +20,9 @@ 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, + DATABASE_SCHEMA_NAME: Type.String() || undefined, }) ); @@ -31,18 +34,21 @@ const ajv = new Ajv({ allowUnionTypes: true, }); -export type Config = Static; +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( ".env file validation failed - " + - JSON.stringify(validate.errors, null, 2) + JSON.stringify(validate.errors, null, 2) ); } + server.log.info("Configuring .env file"); + const config = { LOG_LEVEL: process.env.LOG_LEVEL ?? '', API_PORT: process.env.API_PORT ?? '', @@ -50,15 +56,21 @@ 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', + 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); }; declare module "fastify" { interface FastifyInstance { - config: Config; + config: ArkaConfig; } } diff --git a/backend/src/plugins/db.ts b/backend/src/plugins/db.ts index 2065a2c..2758b67 100644 --- a/backend/src/plugins/db.ts +++ b/backend/src/plugins/db.ts @@ -1,24 +1,45 @@ import fp from "fastify-plugin"; import { FastifyPluginAsync } from "fastify"; -import sqlite3 from 'sqlite3'; -import { Database, open } from "sqlite"; +import { Sequelize } from 'sequelize'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +import { Umzug, SequelizeStorage } from 'umzug'; const databasePlugin: FastifyPluginAsync = async (server) => { - const db = await open({ - filename: './database.sqlite', - driver: sqlite3.Database, - }) - await db.migrate({ - migrationsPath: './build/migrations' + 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, }); - server.decorate('sqlite', db); + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + + const migrationPath = path.join(__dirname, '../../migrations/*.cjs'); + + const umzug = new Umzug({ + migrations: {glob: migrationPath}, + context: sequelize.getQueryInterface(), + storage: new SequelizeStorage({sequelize}), + logger: console, + }) + + try { + server.log.info('Running migrations...') + await umzug.up(); + server.log.info('Migrations done.') + } catch (err) { + console.error('Migration failed:', err) + process.exitCode = 1 + } }; declare module "fastify" { interface FastifyInstance { - sqlite: Database; + sequelize: Sequelize; } } -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 new file mode 100644 index 0000000..c84ab85 --- /dev/null +++ b/backend/src/plugins/sequelizePlugin.ts @@ -0,0 +1,81 @@ +import fp from "fastify-plugin"; +import { FastifyPluginAsync } from "fastify"; +import { Sequelize } from 'sequelize'; +import dotenv from 'dotenv'; +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"; +import { SponsorshipPolicyRepository } from "../repository/sponsorship-policy-repository.js"; +const pg = await import('pg'); +const Client = pg.default.Client; + +dotenv.config(); + +const sequelizePlugin: FastifyPluginAsync = async (server) => { + + try { + const client: InstanceType = new Client({ + connectionString: server.config.DATABASE_URL + }); + await client.connect(); + server.log.info('Connected to database'); + } catch (err) { + console.error(err); + } + + const sequelize = new Sequelize(server.config.DATABASE_URL, { + dialect: 'postgres', + protocol: 'postgres', + dialectOptions: { + searchPath: server.config.DATABASE_SCHEMA_NAME, + // ssl: { + // require: false, + // rejectUnauthorized: false + // } + }, + }); + + await sequelize.authenticate(); + + server.log.info(`Initializing models... with schema name: ${server.config.DATABASE_SCHEMA_NAME}`); + + // Initialize models + 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); + server.log.info('Initialized SponsorshipPolicy model...'); + + server.log.info('Initialized all models...'); + + server.decorate('sequelize', sequelize); + + const apiKeyRepository : APIKeyRepository = new APIKeyRepository(sequelize); + 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...'); + + server.addHook('onClose', (instance, done) => { + instance.sequelize.close().then(() => done(), done); + }); + + server.log.info('added hooks...'); +}; + +declare module "fastify" { + interface FastifyInstance { + sequelize: Sequelize; + apiKeyRepository: APIKeyRepository; + arkaConfigRepository: ArkaConfigRepository; + sponsorshipPolicyRepository: SponsorshipPolicyRepository; + } +} + +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/repository/api-key-repository.ts b/backend/src/repository/api-key-repository.ts new file mode 100644 index 0000000..65b0ea9 --- /dev/null +++ b/backend/src/repository/api-key-repository.ts @@ -0,0 +1,61 @@ +import { Sequelize } from 'sequelize'; +import { APIKey } from '../models/api-key'; +import { ApiKeyDto } from '../types/apikey-dto'; + +export class APIKeyRepository { + private sequelize: Sequelize; + + constructor(sequelize: Sequelize) { + this.sequelize = sequelize; + } + + 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 { + 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 { + 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/repository/arka-config-repository.ts b/backend/src/repository/arka-config-repository.ts new file mode 100644 index 0000000..b9ecb4e --- /dev/null +++ b/backend/src/repository/arka-config-repository.ts @@ -0,0 +1,68 @@ +import { Sequelize } from 'sequelize'; +import { ArkaConfig } from '../models/arka-config'; + +export class ArkaConfigRepository { + private sequelize: Sequelize; + + constructor(sequelize: Sequelize) { + this.sequelize = sequelize; + } + + 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.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.ArkaConfig.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.ArkaConfig.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.ArkaConfig.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/repository/sponsorship-policy-repository.ts b/backend/src/repository/sponsorship-policy-repository.ts new file mode 100644 index 0000000..eced7fb --- /dev/null +++ b/backend/src/repository/sponsorship-policy-repository.ts @@ -0,0 +1,427 @@ +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; + + 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: SponsorshipPolicyDto): 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: SponsorshipPolicyDto): 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 { + + 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; + + 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; + + 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; + + 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; + + 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: SponsorshipPolicyDto) { + 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'); + } + + const currentTime = new Date(); + + if (sponsorshipPolicy.startTime && sponsorshipPolicy.endTime) { + 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.`); + } + } + } + + if (sponsorshipPolicy.globalMaximumApplicable) { + 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) { + 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'); + } + + if (!existingSponsorshipPolicy.isEnabled) { + throw new Error('Cannot disable a policy which is already disabled'); + } + + 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'); + } + + 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 { + const existingSponsorshipPolicy = await this.findOneById(id); + + if (!existingSponsorshipPolicy) { + throw new Error(`Sponsorship Policy deletion failed as Policy doesnot exist with id: ${id}`); + } + + 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<{ 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 778a350..ac6fe7f 100644 --- a/backend/src/routes/admin.ts +++ b/backend/src/routes/admin.ts @@ -6,15 +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 { 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) => { - 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 }); - if (ethers.utils.getAddress(body.WALLET_ADDRESS) === 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 }); + if (!body.walletAddress) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); + 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 }); } @@ -22,12 +25,12 @@ 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 = await server.arkaConfigRepository.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); @@ -37,32 +40,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: ArkaConfigUpdateData = 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.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); + 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 }); } @@ -70,57 +67,41 @@ const adminRoutes: FastifyPluginAsync = async (server) => { server.post('/saveKey', async function (request, reply) { try { - const body: any = JSON.parse(request.body as string); + 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.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){ - request.log.error(`Duplicate record found: ${JSON.stringify(result)}`) + request.log.info(`Public address is: ${publicAddress}`); + + // Use Sequelize to find the API key + 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 }); } - const privateKey = body.PRIVATE_KEY; - 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); - }); + + await server.apiKeyRepository.create({ + apiKey: body.apiKey, + walletAddress: publicAddress, + privateKey: encode(body.privateKey), + 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); @@ -130,36 +111,25 @@ 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.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)) - 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) + 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.apiKeyRepository.findOneByApiKey(body.apiKey); + 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) { - request.log.error(`Error while saving APIKeys: ${err}`) - reject(err); - } - resolve(row); - }) + + await apiKeyInstance.update({ + 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' }); } catch (err: any) { server.log.error(err); @@ -169,16 +139,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 apiKeys = await server.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,16 +156,17 @@ 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)) - 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); - }) - }) + 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.apiKeyRepository.findOneByApiKey(body.apiKey); + if (!apiKeyInstance) + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.RECORD_NOT_FOUND }); + + await server.apiKeyRepository.delete(body.apiKey); + return reply.code(ReturnCode.SUCCESS).send({ error: null, message: 'Successfully deleted' }); } catch (err: any) { request.log.error(err); @@ -210,20 +178,19 @@ 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 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.walletAddress); + 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/routes/index.ts b/backend/src/routes/index.ts index 2e467a1..e80de18 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -10,7 +10,8 @@ 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 { APIKey } from "../models/api-key.js"; const SUPPORTED_ENTRYPOINTS = { 'EPV_06' : "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", @@ -83,7 +84,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 = []; @@ -126,30 +126,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 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 }) } - 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 +302,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.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) { + 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 +372,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 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; } if (!privateKey) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if ( @@ -424,10 +430,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 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; } if (!privateKey) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if ( @@ -482,10 +488,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 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; } if (!privateKey) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if ( @@ -541,10 +547,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 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; } if ( isNaN(amount) || @@ -594,10 +600,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 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; } if ( isNaN(amount) || diff --git a/backend/src/routes/metadata.ts b/backend/src/routes/metadata.ts index 30bc946..93072fc 100644 --- a/backend/src/routes/metadata.ts +++ b/backend/src/routes/metadata.ts @@ -2,11 +2,12 @@ import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-sec import { FastifyPluginAsync } from "fastify"; import { Contract, 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/api-key.js"; import * as EtherspotAbi from "../abi/EtherspotAbi.js"; const metadataRoutes: FastifyPluginAsync = async (server) => { @@ -34,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 = ''; @@ -62,23 +62,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 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 }) } - 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/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 07e5615..7eab9bf 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,13 @@ 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 } 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; @@ -52,15 +58,22 @@ const initializeServer = async (): Promise => { await server.register(metadataRoutes); + await server.register(sponsorshipPolicyRoutes); + // Database await server.register(database); - const ConfigData: any = await new Promise(resolve => { - server.sqlite.db.get("SELECT * FROM config", (err, row) => { - if (err) resolve(null); - resolve(row); - }); - }); + // Register the sequelizePlugin + await server.register(sequelizePlugin); + + // Synchronize all models + await server.sequelize.sync(); + + server.log.info('registered sequelizePlugin...') + + const arkaConfigRepository = new ArkaConfigRepository(server.sequelize); + const configDatas = await arkaConfigRepository.findAll(); + const configData: ArkaConfig | null = configDatas.length > 0 ? configDatas[0] : null; await server.register(fastifyCron, { jobs: [ @@ -68,14 +81,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()); @@ -87,15 +100,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(); @@ -112,8 +125,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'); @@ -123,7 +136,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(); @@ -191,17 +204,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/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/arka-config-dto.ts b/backend/src/types/arka-config-dto.ts new file mode 100644 index 0000000..72947be --- /dev/null +++ b/backend/src/types/arka-config-dto.ts @@ -0,0 +1,13 @@ + +export interface ArkaConfigUpdateData { + deployedErc20Paymasters: string; + pythMainnetUrl: string; + pythTestnetUrl: string; + pythTestnetChainIds: string; + pythMainnetChainIds: string; + cronTime: string; + customChainlinkDeployed: string; + coingeckoIds: string; + coingeckoApiUrl: 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..b03a1a6 --- /dev/null +++ b/backend/src/types/sponsorship-policy-dto.ts @@ -0,0 +1,32 @@ +// DTO for receiving data in the POST request to create a sponsorship policy +export interface SponsorshipPolicyDto { + 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 + isPublic: boolean; // Flag to indicate if the policy is public + isEnabled: boolean; // Flag to indicate if the policy is enabled + 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 + 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 | 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 | 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 | 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 + createdAt: Date; // Date the policy was created + updatedAt: Date; // Date the policy was last updated +} diff --git a/backend/src/utils/common.ts b/backend/src/utils/common.ts index bfc2624..c4fc745 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"; @@ -19,38 +18,20 @@ 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; 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 { + return { maxPriorityFeePerGas: fastGasPrice.sub(maxFeePerGas), maxFeePerGas, gasPrice: maxFeePerGas, @@ -58,7 +39,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, diff --git a/docker-compose.yml b/docker-compose.yml index 4b58bce..23143a6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,11 +22,16 @@ services: - SUPPORTED_NETWORKS= - CRON_PRIVATE_KEY= - DEFAULT_INDEXER_ENDPOINT=http://localhost:3003 - - FEE_MARKUP= + - FEE_MARKUP=0 - MULTI_TOKEN_MARKUP=1150000 - - ETHERSCAN_GAS_ORACLES= - - DEFAULT_API_KEY= - - WEBHOOK_URL= + - 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: context: ./backend dockerfile: Dockerfile @@ -45,4 +50,4 @@ services: expose: - 3002 ports: - - "3002:3002" \ No newline at end of file + - "3002:3002" 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/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", 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 = [];