diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5fcecb7 --- /dev/null +++ b/.env.example @@ -0,0 +1,21 @@ +LOG_LEVEL= +API_HOST= +API_PORT= +UNSAFE_MODE=true +SUPPORTED_NETWORKS= +CRON_PRIVATE_KEY= +DEFAULT_INDEXER_ENDPOINT= +FEE_MARKUP=0 +MULTI_TOKEN_MARKUP= +ADMIN_WALLET_ADDRESS= +ETHERSCAN_GAS_ORACLES="" +DEFAULT_API_KEY="" +WEBHOOK_URL= +HMAC_SECRET= +DATABASE_URL= +DATABASE_USER= +DATABASE_PASSWORD= +DATABASE_NAME= +DATABASE_SCHEMA_NAME=arka +DATABASE_SSL_ENABLED=false +DATABASE_SSL_REJECT_UNAUTHORIZED=false 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/README.md b/README.md index e1b213b..8cba859 100644 --- a/README.md +++ b/README.md @@ -93,9 +93,114 @@ To run your own instance of this, please [follow these steps.](https://github.co You can see a [list of available endpoints here](https://github.com/etherspot/arka/tree/master/backend#available-endpoints--). + +### Local - Setup + +#### environment for backend + +1. Start Docker postgres database + +```sh +cd local-setup +``` + +```sh +docker-compose up -d +``` + +- Verify if the postgres docker instance is up and running + +```sh +docker ps -a +``` + +```sh +docker logs --follow local-setup-db-1 +``` + +- It must show up logs similar to this + +``` +PostgreSQL init process complete; ready for start up. +2024-06-24 19:31:56.739 UTC [1] LOG: starting PostgreSQL 16.1 (Debian 16.1-1.pgdg120+1) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit +2024-06-24 19:31:56.740 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432 +2024-06-24 19:31:56.740 UTC [1] LOG: listening on IPv6 address "::", port 5432 +2024-06-24 19:31:56.741 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432" +2024-06-24 19:31:56.744 UTC [66] LOG: database system was shut down at 2024-06-24 19:31:56 UTC +2024-06-24 19:31:56.748 UTC [1] LOG: database system is ready to accept connections +``` + +2. create .env + +```sh +cp .env.example .env +``` + +3. populate these environment variables + +```sh +LOG_LEVEL= +API_HOST= +API_PORT= +UNSAFE_MODE=true +SUPPORTED_NETWORKS= +CRON_PRIVATE_KEY= +DEFAULT_INDEXER_ENDPOINT= +FEE_MARKUP=0 +MULTI_TOKEN_MARKUP= +ADMIN_WALLET_ADDRESS= +ETHERSCAN_GAS_ORACLES="" +DEFAULT_API_KEY="" +WEBHOOK_URL= +HMAC_SECRET= +DATABASE_URL= +DATABASE_USER= +DATABASE_PASSWORD= +DATABASE_NAME= +DATABASE_SCHEMA_NAME=arka +DATABASE_SSL_ENABLED=false +DATABASE_SSL_REJECT_UNAUTHORIZED=false +``` + +4. start docker instance for backend, admin_frontend, frontend + +```sh +docker-compose up --build -d +``` + +5. Here we need to create a network and tag backend & postgres on same network + +```sh +docker network create arka-network +``` + +6. add postgres instance to docker network + +```sh +docker network connect arka-network local-setup-db-1 +``` + +7. add arka backend docker instance to docker-network + +```sh +docker network connect arka-network arka-backend-1 +``` + +8. restart backend docker instance + +```sh +docker-compose up -d +``` + +9. verify backend logs + +```sh +docker logs arka-backend-1 +``` + ## 🖥 Arka Admin Frontend -This contains ability to change configuration that are available in sql. +This contains ability to change system configuration, apiKeys and apiKey-config that are available in database. ## 🖥 Arka Frontend 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/.env.example b/backend/.env.example new file mode 100644 index 0000000..9b1855a --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,22 @@ +LOG_LEVEL="debug" +API_HOST=0.0.0.0 +API_PORT=5050 +MULTI_TOKEN_MARKUP=1150000 +SUPPORTED_NETWORKS= +UNSAFE_MODE=true +CRON_PRIVATE_KEY="" +ADMIN_WALLET_ADDRESS="" +DEFAULT_INDEXER_ENDPOINT=http://localhost:3003 +FEE_MARKUP=10 +HMAC_SECRET="" + +# postgres database connection +DATABASE_URL="postgresql://arkauser:paymaster@localhost:5432/arkadev" +DATABASE_SCHEMA_NAME="arka" +DATABASE_SSL_ENABLED="false" +DATABASE_SSL_REJECT_UNAUTHORIZED="false" + +# postgres environment variables +DATABASE_USER="" +DATABASE_PASSWORD="" +DATABASE_NAME="" 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..32a8320 100644 --- a/backend/README.md +++ b/backend/README.md @@ -182,3 +182,8 @@ 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. Follow steps for local network setup in main readme.MD file in project root-directory +[project-readme-steps](../README.md) + 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/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..9cdb68c --- /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.TEXT, + 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..fafeec4 --- /dev/null +++ b/backend/migrations/20240611000002-create-sponsorship-policies.cjs @@ -0,0 +1,162 @@ +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' + }, + supportedEPVersions: { + type: Sequelize.ARRAY(Sequelize.TEXT), + allowNull: true, + field: 'SUPPORTED_EP_VERSIONS' + }, + 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..d8e0e9e 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,9 +10,10 @@ "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", + "migrate": "node scripts/migration.cjs", "dev": "tsx watch src | pino-pretty --colorize", "pretest": "npm run build", "test": "jest" @@ -32,6 +33,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 +46,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 +62,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 +76,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/scripts/hmac.cjs b/backend/scripts/hmac.cjs new file mode 100644 index 0000000..6f93e0f --- /dev/null +++ b/backend/scripts/hmac.cjs @@ -0,0 +1,5 @@ +const crypto = require('crypto'); + +const hmacSecret = crypto.randomBytes(32).toString('hex'); + +console.log(hmacSecret); \ No newline at end of file diff --git a/backend/scripts/migration.cjs b/backend/scripts/migration.cjs new file mode 100644 index 0000000..8e630f0 --- /dev/null +++ b/backend/scripts/migration.cjs @@ -0,0 +1,59 @@ +const { Sequelize } = require('sequelize'); +const path = require('path'); +const { Umzug, SequelizeStorage } = require('umzug'); +const dotenv = require('dotenv'); +dotenv.config({ path: path.resolve(__dirname, '../.env') }); + +// node backend/scripts/migration.cjs +async function runMigrations() { + let sequelize; + try { + const DATABASE_URL = process.env.DATABASE_URL; + const DATABASE_SCHEMA_NAME = process.env.DATABASE_SCHEMA_NAME; + const DATABASE_SSL_ENABLED = (process.env.DATABASE_SSL_ENABLED || '').toLowerCase() === 'true'; + const DATABASE_SSL_REJECT_UNAUTHORIZED = (process.env.DATABASE_SSL_REJECT_UNAUTHORIZED || '').toLowerCase() === 'true'; + + console.info(`Connecting to database... with URL: ${DATABASE_URL} and schemaName: ${DATABASE_SCHEMA_NAME}`); + + const sequelizeOptions = { + schema: DATABASE_SCHEMA_NAME, + }; + + if (DATABASE_SSL_ENABLED) { + sequelizeOptions.dialectOptions = { + ssl: { + require: DATABASE_SSL_ENABLED, + rejectUnauthorized: DATABASE_SSL_REJECT_UNAUTHORIZED + } + }; + } + + sequelize = new Sequelize(DATABASE_URL, sequelizeOptions); + + const migrationPath = path.join(__dirname, '../migrations/*.cjs'); + + const umzug = new Umzug({ + migrations: { glob: migrationPath }, + context: sequelize.getQueryInterface(), + storage: new SequelizeStorage({ sequelize }), + logger: console, + }); + + console.info('Running migrations...'); + await umzug.up(); + console.info('Migrations done.'); + process.exit(0); // Exit with a "success" code + } catch (err) { + console.error('Migration failed:', err); + process.exitCode = 1; + } finally { + console.info('Closing database connection...'); + if (sequelize) { + await sequelize.close(); + console.info('Database connection closed.'); + } + process.exit(process.exitCode || 0); + } +} + +runMigrations(); \ No newline at end of file diff --git a/backend/src/constants/ErrorMessage.ts b/backend/src/constants/ErrorMessage.ts index 74a04b2..f502334 100644 --- a/backend/src/constants/ErrorMessage.ts +++ b/backend/src/constants/ErrorMessage.ts @@ -1,9 +1,24 @@ 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_CHAINS_NOT_IN_SUBSET_OF_APIKEY_SUPPORTED_CHAINS: 'Sponsorship policy chains: {sponsorshipPolicyChains} are not in subset of ApiKey supported networks {apiKeyChains}', + SPONSORSHIP_POLICY_NOT_FOUND: 'Sponsorship policy not found', + ACTIVE_SPONSORSHIP_POLICY_NOT_FOUND: "Sponsorship policy not found for wallet address {walletAddress} with EP version {epVersion} and ChainId: ${chainId}", + SPONSORSHIP_POLICY_ALREADY_EXISTS: 'Sponsorship policy already exists', + NO_ACTIVE_SPONSORSHIP_POLICY_FOR_CURRENT_TIME: 'No active sponsorship policy for wallet address {walletAddress} with EP version {epVersion} and ChainId: ${chainId}', + 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', @@ -16,3 +31,15 @@ export default { UNSUPPORTED_METHOD: 'Unsupported method name received', UNSUPPORTED_ENTRYPOINT: 'Unsupported EntryPoint Address', } + +export function generateErrorMessage(template: string, values: { [key: string]: string | number }): string { + let message = template; + for (const key in values) { + if (!key || !values[key]) { + message = message.replace(`{${key}}`, 'N/A'); + } else { + message = message.replace(`{${key}}`, values[key].toString()); + } + } + return message; +} 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..a8b9f39 --- /dev/null +++ b/backend/src/models/sequelize-associations.ts @@ -0,0 +1,29 @@ +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' + // }); + + APIKey.hasMany(SponsorshipPolicy, { foreignKey: 'walletAddress', sourceKey: 'walletAddress' }); + SponsorshipPolicy.belongsTo(APIKey, { foreignKey: 'walletAddress', targetKey: 'walletAddress' }); + /** + * 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..701bfa8 --- /dev/null +++ b/backend/src/models/sponsorship-policy.ts @@ -0,0 +1,225 @@ +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 supportedEPVersions: string[] | null = null; + 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 { + if (this.isPerpetual) { + return false; + } + const currentTime = new Date(); + + return Boolean(this.endTime && this.endTime.getTime() <= currentTime.getTime()); + } + + public get isCurrent(): boolean { + const now = new Date(); + if (this.isPerpetual) { + return true; + } + + // If there is no start time, the policy is not current + if (!this.startTime) { + return false; + } + + if (this.startTime && this.endTime) { + const currentTime = new Date(); + const startTime = new Date(this.startTime + 'Z'); + const endTime = new Date(this.endTime + 'Z'); + if (startTime.getTime() > currentTime.getTime() || endTime.getTime() <= currentTime.getTime() || endTime.getTime() <= startTime.getTime()){ + return false; + } + } + + return true; + } + + 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' + }, + supportedEPVersions: { + type: DataTypes.ARRAY(DataTypes.STRING), + allowNull: false, + field: 'SUPPORTED_EP_VERSIONS', + }, + 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..09ee808 100644 --- a/backend/src/plugins/config.ts +++ b/backend/src/plugins/config.ts @@ -20,6 +20,10 @@ 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, + HMAC_SECRET: Type.String({ minLength: 1 }), }) ); @@ -31,18 +35,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,7 +57,11 @@ 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', + HMAC_SECRET: process.env.HMAC_SECRET ?? '', } server.decorate("config", config); @@ -58,7 +69,7 @@ const configPlugin: FastifyPluginAsync = async (server) => { declare module "fastify" { interface FastifyInstance { - config: Config; + config: ArkaConfig; } } diff --git a/backend/src/plugins/db.ts b/backend/src/plugins/db.ts deleted file mode 100644 index 2065a2c..0000000 --- a/backend/src/plugins/db.ts +++ /dev/null @@ -1,24 +0,0 @@ -import fp from "fastify-plugin"; -import { FastifyPluginAsync } from "fastify"; -import sqlite3 from 'sqlite3'; -import { Database, open } from "sqlite"; - -const databasePlugin: FastifyPluginAsync = async (server) => { - const db = await open({ - filename: './database.sqlite', - driver: sqlite3.Database, - }) - - await db.migrate({ - migrationsPath: './build/migrations' - }); - - server.decorate('sqlite', db); -}; - -declare module "fastify" { - interface FastifyInstance { - sqlite: Database; - } -} -export default fp(databasePlugin); diff --git a/backend/src/plugins/sequelizePlugin.ts b/backend/src/plugins/sequelizePlugin.ts new file mode 100644 index 0000000..27aed77 --- /dev/null +++ b/backend/src/plugins/sequelizePlugin.ts @@ -0,0 +1,84 @@ +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 sequelizeOptions: any = { + dialect: 'postgres', + protocol: 'postgres', + dialectOptions: { + searchPath: server.config.DATABASE_SCHEMA_NAME, + }, + }; + + if (server.config.DATABASE_SSL_ENABLED) { + sequelizeOptions.ssl = { + require: true, + rejectUnauthorized: true + }; + } + const sequelize = new Sequelize(server.config.DATABASE_URL, sequelizeOptions); + 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..08d3bf2 --- /dev/null +++ b/backend/src/repository/api-key-repository.ts @@ -0,0 +1,61 @@ +import { Sequelize } from 'sequelize'; +import { APIKey } from '../models/api-key.js'; +import { ApiKeyDto } from '../types/apikey-dto.js'; + +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..cec851b --- /dev/null +++ b/backend/src/repository/arka-config-repository.ts @@ -0,0 +1,68 @@ +import { Sequelize } from 'sequelize'; +import { ArkaConfig } from '../models/arka-config.js'; + +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..3a13226 --- /dev/null +++ b/backend/src/repository/sponsorship-policy-repository.ts @@ -0,0 +1,747 @@ +import { Sequelize, Op } from 'sequelize'; +import { SponsorshipPolicy } from '../models/sponsorship-policy.js'; +import { EPVersions, SponsorshipPolicyDto, getEPVersionString } from '../types/sponsorship-policy-dto.js'; +import { ethers } from 'ethers'; + +export class SponsorshipPolicyRepository { + private sequelize: Sequelize; + + constructor(sequelize: Sequelize) { + this.sequelize = sequelize; + } + + // findAll + // query must return all the policies + // if the policy is perpetual, then it should always be returned + 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); + } + + // findAllEnabled + // query must return all the policies that are enabled + // if the policy is perpetual, then it should always be returned + async findAllEnabled(): Promise { + const result = await this.sequelize.models.SponsorshipPolicy.findAll({ where: { isEnabled: true } }); + return result.map(apiKey => apiKey.get() as SponsorshipPolicy); + } + + // findAllEnabledAndApplicable + // query must return all the policies that are enabled and applicable + // if the policy is perpetual, then it should always be returned + 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); + } + + // findAllByWalletAddress + // user will pass wallet address + // the query must return all the policies that match the criteria + // if the policy is perpetual, then it should always be returned + async findAllByWalletAddress(walletAddress: string): Promise { + const result = await this.sequelize.models.SponsorshipPolicy.findAll({ + where: { walletAddress: walletAddress }, + order: [['createdAt', 'DESC']] + }); + return result ? result.map(item => item.get() as SponsorshipPolicy) : []; + } + + // findOneByWalletAddress + // user will pass wallet address + // the query must return the policy that matches the criteria + // if the policy is perpetual, then it should always be returned + async findOneByWalletAddress(walletAddress: string): Promise { + const result = await this.sequelize.models.SponsorshipPolicy.findOne({ + where: { + walletAddress: walletAddress, + isEnabled: true, + [Op.or]: [ + { isPerpetual: true }, + { + startTime: { + [Op.or]: [ + { [Op.lte]: Sequelize.literal(`CURRENT_TIMESTAMP AT TIME ZONE 'UTC'`) }, + { [Op.is]: null } + ] + }, + endTime: { + [Op.or]: [ + { [Op.gt]: Sequelize.literal(`CURRENT_TIMESTAMP AT TIME ZONE 'UTC'`) }, + { [Op.is]: null } + ] + } + } + ] + }, + order: [['createdAt', 'DESC']] + }); + return result ? result.get() as SponsorshipPolicy : null; + } + + // findAllByWalletAddressAndSupportedEPVersion + // user will pass wallet address and EP version + // the query must return all the policies that match the criteria + // if the policy is perpetual, then it should always be returned + async findAllByWalletAddressAndSupportedEPVersion(walletAddress: string, epVersion: EPVersions): Promise { + const result = await this.sequelize.models.SponsorshipPolicy.findAll({ + where: { + walletAddress: walletAddress, + isEnabled: true, + supportedEPVersions: { + [Op.contains]: Sequelize.literal(`ARRAY['${getEPVersionString(epVersion)}']::text[]`) + }, + [Op.or]: [ + { isPerpetual: true }, + { + startTime: { + [Op.or]: [ + { [Op.lte]: Sequelize.literal(`CURRENT_TIMESTAMP AT TIME ZONE 'UTC'`) }, + { [Op.is]: null } + ] + }, + endTime: { + [Op.or]: [ + { [Op.gt]: Sequelize.literal(`CURRENT_TIMESTAMP AT TIME ZONE 'UTC'`) }, + { [Op.is]: null } + ] + } + } + ] + }, + order: [['createdAt', 'DESC']] + }); + return result ? result.map(item => item.get() as SponsorshipPolicy) : []; + } + + // findOneByWalletAddressAndSupportedEPVersion + // user will pass wallet address and EP version + // the query must return the policy that matches the criteria + // if the policy is perpetual, then it should always be returned + async findOneByWalletAddressAndSupportedEPVersion(walletAddress: string, epVersion: EPVersions): Promise { + const result = await this.sequelize.models.SponsorshipPolicy.findOne({ + where: { + walletAddress: walletAddress, + isEnabled: true, + supportedEPVersions: { + [Op.contains]: Sequelize.literal(`ARRAY['${getEPVersionString(epVersion)}']::text[]`) + }, + [Op.or]: [ + { isPerpetual: true }, + { + startTime: { + [Op.or]: [ + { [Op.lte]: Sequelize.literal(`CURRENT_TIMESTAMP AT TIME ZONE 'UTC'`) }, + { [Op.is]: null } + ] + }, + endTime: { + [Op.or]: [ + { [Op.gt]: Sequelize.literal(`CURRENT_TIMESTAMP AT TIME ZONE 'UTC'`) }, + { [Op.is]: null } + ] + } + } + ] + }, + order: [['createdAt', 'DESC']] + }); + return result ? result.get() as SponsorshipPolicy : null; + } + + // findAllByWalletAddressAndChain + // user will pass wallet address and Supported ChainId + // the query must return all the policies that match the criteria + // if the policy is perpetual, then it should always be returned + async findAllByWalletAddressAndChain(walletAddress: string, chainId: number): Promise { + const result = await this.sequelize.models.SponsorshipPolicy.findAll({ + where: { + walletAddress: walletAddress, + isEnabled: true, + enabledChains: { + [Op.contains]: [chainId] + }, + [Op.or]: [ + { isPerpetual: true }, + { + startTime: { + [Op.or]: [ + { [Op.lte]: Sequelize.literal(`CURRENT_TIMESTAMP AT TIME ZONE 'UTC'`) }, + { [Op.is]: null } + ] + }, + endTime: { + [Op.or]: [ + { [Op.gt]: Sequelize.literal(`CURRENT_TIMESTAMP AT TIME ZONE 'UTC'`) }, + { [Op.is]: null } + ] + } + } + ] + }, + order: [['createdAt', 'DESC']] + }); + return result ? result.map(item => item.get() as SponsorshipPolicy) : []; + } + + // findOneByWalletAddressAndChain + // user will pass wallet address and ChainId + // the query must return the policy that matches the criteria + // if the policy is perpetual, then it should always be returned + async findOneByWalletAddressAndChain(walletAddress: string, chain: number): Promise { + const result = await this.sequelize.models.SponsorshipPolicy.findOne({ + where: { + walletAddress: walletAddress, + isEnabled: true, + enabledChains: { + [Op.contains]: [chain] + }, + [Op.or]: [ + { isPerpetual: true }, + { + startTime: { + [Op.or]: [ + { [Op.lte]: Sequelize.literal(`CURRENT_TIMESTAMP AT TIME ZONE 'UTC'`) }, + { [Op.is]: null } + ] + }, + endTime: { + [Op.or]: [ + { [Op.gt]: Sequelize.literal(`CURRENT_TIMESTAMP AT TIME ZONE 'UTC'`) }, + { [Op.is]: null } + ] + } + } + ] + }, + order: [['createdAt', 'DESC']] + }); + return result ? result.get() as SponsorshipPolicy : null; + } + + // findAllByWalletAddressAndSupportedEPVersionAndChain + // user will pass wallet address, EP version and chain ID + // the query must return all the policies that match the criteria + // if the policy is perpetual, then it should always be returned + async findAllByWalletAddressAndSupportedEPVersionAndChain(walletAddress: string, epVersion: EPVersions, chain: number): Promise { + const result = await this.sequelize.models.SponsorshipPolicy.findAll({ + where: { + walletAddress: walletAddress, + isEnabled: true, + supportedEPVersions: { + [Op.contains]: Sequelize.literal(`ARRAY['${getEPVersionString(epVersion)}']::text[]`) + }, + enabledChains: { + [Op.contains]: [chain] + }, + [Op.or]: [ + { isPerpetual: true }, + { + startTime: { + [Op.or]: [ + { [Op.lte]: Sequelize.literal(`CURRENT_TIMESTAMP AT TIME ZONE 'UTC'`) }, + { [Op.is]: null } + ] + }, + endTime: { + [Op.or]: [ + { [Op.gt]: Sequelize.literal(`CURRENT_TIMESTAMP AT TIME ZONE 'UTC'`) }, + { [Op.is]: null } + ] + } + } + ] + }, + order: [['createdAt', 'DESC']] + }); + return result ? result.map(item => item.get() as SponsorshipPolicy) : []; + } + + // findOneByWalletAddressAndSupportedEPVersionAndChain + // user will pass wallet address, EP version and chain ID + // the query must return the policy that matches the criteria + // if the policy is perpetual, then it should always be returned + async findOneByWalletAddressAndSupportedEPVersionAndChain(walletAddress: string, epVersion: EPVersions, chain: number): Promise { + const result = await this.sequelize.models.SponsorshipPolicy.findOne({ + where: { + walletAddress: walletAddress, + isEnabled: true, + supportedEPVersions: { + [Op.contains]: Sequelize.literal(`ARRAY['${getEPVersionString(epVersion)}']::text[]`) + }, + enabledChains: { + [Op.contains]: [chain] + }, + [Op.or]: [ + { isPerpetual: true }, + { + startTime: { + [Op.or]: [ + { [Op.lte]: Sequelize.literal(`CURRENT_TIMESTAMP AT TIME ZONE 'UTC'`) }, + { [Op.is]: null } + ] + }, + endTime: { + [Op.or]: [ + { [Op.gt]: Sequelize.literal(`CURRENT_TIMESTAMP AT TIME ZONE 'UTC'`) }, + { [Op.is]: null } + ] + } + } + ] + }, + order: [['createdAt', 'DESC']] + }); + return result ? result.get() as SponsorshipPolicy : null; + } + + + // findOneByPolicyName + // user will pass policy name + // the query must return the policy that matches the criteria + // if the policy is perpetual, then it should always be returned + async findOneByPolicyName(name: string): Promise { + const result = await this.sequelize.models.SponsorshipPolicy.findOne({ where: { name: name } }); + return result ? result.get() as SponsorshipPolicy : null; + } + + // findOneById + // user will pass policy ID + // the query must return the policy that matches the criteria + async findOneById(id: number): Promise { + const sponsorshipPolicy = await this.sequelize.models.SponsorshipPolicy.findOne({ where: { id: id } }) as SponsorshipPolicy; + if (!sponsorshipPolicy) { + return null; + } + + const dataValues = sponsorshipPolicy.get(); + return dataValues as SponsorshipPolicy; + } + + // createSponsorshipPolicy + // user will pass the policy details + // the query must create the policy and return the created policy + 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, + supportedEPVersions: sponsorshipPolicy.supportedEPVersions, + 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; + } + + // updateSponsorshipPolicy + // user will pass the policy details + // the query must update the policy and return the updated policy + 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; + existingSponsorshipPolicy.supportedEPVersions = sponsorshipPolicy.supportedEPVersions; + + // 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; + + // apply same logic to update the record + const result = await this.sequelize.models.SponsorshipPolicy.update({ + name: existingSponsorshipPolicy.name, + description: existingSponsorshipPolicy.description, + isApplicableToAllNetworks: existingSponsorshipPolicy.isApplicableToAllNetworks, + enabledChains: existingSponsorshipPolicy.enabledChains, + supportedEPVersions: existingSponsorshipPolicy.supportedEPVersions, + isPerpetual: existingSponsorshipPolicy.isPerpetual, + startTime: existingSponsorshipPolicy.startTime, + endTime: existingSponsorshipPolicy.endTime, + globalMaximumApplicable: existingSponsorshipPolicy.globalMaximumApplicable, + globalMaximumUsd: existingSponsorshipPolicy.globalMaximumUsd, + globalMaximumNative: existingSponsorshipPolicy.globalMaximumNative, + globalMaximumOpCount: existingSponsorshipPolicy.globalMaximumOpCount, + perUserMaximumApplicable: existingSponsorshipPolicy.perUserMaximumApplicable, + perUserMaximumUsd: existingSponsorshipPolicy.perUserMaximumUsd, + perUserMaximumNative: existingSponsorshipPolicy.perUserMaximumNative, + perUserMaximumOpCount: existingSponsorshipPolicy.perUserMaximumOpCount, + perOpMaximumApplicable: existingSponsorshipPolicy.perOpMaximumApplicable, + perOpMaximumUsd: existingSponsorshipPolicy.perOpMaximumUsd, + perOpMaximumNative: existingSponsorshipPolicy.perOpMaximumNative, + addressAllowList: existingSponsorshipPolicy.addressAllowList, + addressBlockList: existingSponsorshipPolicy.addressBlockList + }, { + where: { id: sponsorshipPolicy.id } + }); + + if (result[0] === 0) { + throw new Error(`SponsorshipPolicy update failed for id: ${sponsorshipPolicy.id}`); + } + + // return the updated record - fetch fresh from database + const updatedSponsorshipPolicy = await this.findOneById(sponsorshipPolicy.id as number); + return updatedSponsorshipPolicy 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.supportedEPVersions || + !sponsorshipPolicy.supportedEPVersions.every(version => Object.values(EPVersions).includes(version as EPVersions))) { + const enteredVersions = sponsorshipPolicy.supportedEPVersions ? sponsorshipPolicy.supportedEPVersions.join(', ') : 'none'; + errors.push(`Supported entry point versions are required and must be valid. You entered: ${enteredVersions}. Valid values are: ${Object.values(EPVersions).join(', ')}`); + } + + 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'); + } + + SponsorshipPolicy.update({ isEnabled: false }, { where: { id: id } }); + } + + 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'); + } + + SponsorshipPolicy.update({ isEnabled: true }, { where: { id: id } }); + } + + 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-routes.ts b/backend/src/routes/admin-routes.ts new file mode 100644 index 0000000..e2063f0 --- /dev/null +++ b/backend/src/routes/admin-routes.ts @@ -0,0 +1,203 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { FastifyPluginAsync } from "fastify"; +import { CronTime } from 'cron'; +import { ethers } from "ethers"; +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.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 }); + } + }) + + server.get("/getConfig", async function (request, reply) { + try { + 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); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); + } + }) + + server.post("/saveConfig", async function (request, reply) { + try { + const body: ArkaConfigUpdateData = JSON.parse(request.body as string); + if (!body) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); + if (Object.values(body).every(value => value)) { + try { + const result = await server.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 }); + } + } + catch (err: any) { + request.log.error(err); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); + } + }); + + server.post('/saveKey', async function (request, reply) { + try { + const body: any = JSON.parse(request.body as string) as ApiKeyDto; + if (!body) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); + if (!body.apiKey || !body.privateKey) + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); + + 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.privateKey); + const publicAddress = await wallet.getAddress(); + + // 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 }); + } + + await server.apiKeyRepository.create({ + apiKey: body.apiKey, + walletAddress: publicAddress, + privateKey: encode(body.privateKey, server.config.HMAC_SECRET), + 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); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); + } + }) + + server.post('/updateKey', async function (request, reply) { + try { + const body = JSON.parse(request.body as string) as ApiKeyDto; + if (!body) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); + if (!body.apiKey) + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); + 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 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); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); + } + }) + + server.get('/getKeys', async function (request, reply) { + try { + 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, server.config.HMAC_SECRET); + }); + 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 }); + } + }) + + server.post('/deleteKey', 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.apiKey) + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); + 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); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); + } + }) + + server.post('/getSupportedNetworks', async (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.walletAddress) { + 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 (!apiKeyEntity.supportedNetworks || apiKeyEntity.supportedNetworks == '') { + supportedNetworks = SupportedNetworks; + } + else { + const buffer = Buffer.from(apiKeyEntity.supportedNetworks as string, 'base64'); + supportedNetworks = JSON.parse(buffer.toString()) + } + return reply.code(ReturnCode.SUCCESS).send(supportedNetworks); + } catch (err: any) { + request.log.error(err); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); + } + }) +}; + +export default adminRoutes; diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts deleted file mode 100644 index 778a350..0000000 --- a/backend/src/routes/admin.ts +++ /dev/null @@ -1,237 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { FastifyPluginAsync } from "fastify"; -import { CronTime } from 'cron'; -import { ethers } from "ethers"; -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" }; - -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 }); - } catch (err: any) { - return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_USER }); - } - }) - - 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); - }) - }) - 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_PROCESS }); - } - }) - - server.post("/saveConfig", 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.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) - 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) { - request.log.error(err); - return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); - } - }); - - server.post('/saveKey', 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.API_KEY || !body.PRIVATE_KEY) - return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); - if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*-_&])[A-Za-z\d@$!%*-_&]{8,}$/.test(body.API_KEY)) - return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.API_KEY_VALIDATION_FAILED }) - const wallet = new ethers.Wallet(body.PRIVATE_KEY); - 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)}`) - 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); - }); - }); - return reply.code(ReturnCode.SUCCESS).send({ error: null, message: 'Successfully saved' }); - } catch (err: any) { - request.log.error(err); - return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); - } - }) - - server.post('/updateKey', 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.API_KEY) - return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); - if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*-_&])[A-Za-z\d@$!%*-_&]{8,}$/.test(body.API_KEY)) - return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.API_KEY_VALIDATION_FAILED }) - const result: any[] = await new Promise((resolve, reject) => { - server.sqlite.db.get("SELECT * FROM api_keys WHERE API_KEY=?", [body.API_KEY], (err: any, row: any) => { - if (err) reject(err); - resolve(row); - }) - }); - if (!result || result.length == 0) - return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.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); - }) - }); - return reply.code(ReturnCode.SUCCESS).send({ error: null, message: 'Successfully updated' }); - } catch (err: any) { - server.log.error(err); - return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); - } - }) - - 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) - }); - 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_PROCESS }); - } - }) - - server.post('/deleteKey', 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.API_KEY) - return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); - if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*-_&])[A-Za-z\d@$!%*-_&]{8,}$/.test(body.API_KEY)) - return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.API_KEY_VALIDATION_FAILED }) - await new Promise((resolve, reject) => { - server.sqlite.db.run("DELETE FROM api_keys WHERE API_KEY=?", [body.API_KEY], (err: any, rows: any) => { - if (err) reject(err); - resolve(rows); - }) - }) - return reply.code(ReturnCode.SUCCESS).send({ error: null, message: 'Successfully deleted' }); - } catch (err: any) { - request.log.error(err); - return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); - } - }) - - server.post('/getSupportedNetworks', async (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 }); - } - 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 }); - let supportedNetworks; - if (result.SUPPORTED_NETWORKS == '') supportedNetworks = SupportedNetworks; - else { - const buffer = Buffer.from(result.SUPPORTED_NETWORKS, 'base64'); - supportedNetworks = JSON.parse(buffer.toString()) - } - return reply.code(ReturnCode.SUCCESS).send(supportedNetworks); - } catch (err: any) { - request.log.error(err); - return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); - } - }) -}; - -export default adminRoutes; diff --git a/backend/src/routes/deposit-route.ts b/backend/src/routes/deposit-route.ts new file mode 100644 index 0000000..1acbcec --- /dev/null +++ b/backend/src/routes/deposit-route.ts @@ -0,0 +1,149 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Type } from "@sinclair/typebox"; +import { FastifyPluginAsync } from "fastify"; +import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; +import { Paymaster } from "../paymaster/index.js"; +import SupportedNetworks from "../../config.json" assert { type: "json" }; +import ErrorMessage from "../constants/ErrorMessage.js"; +import ReturnCode from "../constants/ReturnCode.js"; +import { decode } from "../utils/crypto.js"; +import { printRequest, getNetworkConfig } from "../utils/common.js"; +import { APIKey } from "../models/api-key.js"; + +const SUPPORTED_ENTRYPOINTS = { + 'EPV_06': "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", + 'EPV_07': "0x0000000071727De22E5E9d8BAf0edAc6f37da032" +} + +const depositRoutes: FastifyPluginAsync = async (server) => { + const paymaster = new Paymaster(server.config.FEE_MARKUP, server.config.MULTI_TOKEN_MARKUP); + + const prefixSecretId = 'arka_'; + + let client: SecretsManagerClient; + + const unsafeMode: boolean = process.env.UNSAFE_MODE == "true" ? true : false; + + if (!unsafeMode) { + client = new SecretsManagerClient(); + } + + const ResponseSchema = { + schema: { + response: { + 200: Type.Object({ + message: Type.String(), + }), + 400: Type.Object({ + error: Type.String(), + }), + } + } + } + + server.post("/deposit", + ResponseSchema, + async function (request, reply) { + try { + printRequest("/deposit", request, server.log); + const body: any = request.body; + const query: any = request.query; + const amount = body.params[0]; + const chainId = query['chainId'] ?? body.params[1]; + const api_key = query['apiKey'] ?? body.params[2]; + if (!api_key) + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + let privateKey = ''; + let supportedNetworks; + if (!unsafeMode) { + const AWSresponse = await client.send( + new GetSecretValueCommand({ + SecretId: prefixSecretId + api_key, + }) + ); + const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + privateKey = secrets['PRIVATE_KEY']; + supportedNetworks = secrets['SUPPORTED_NETWORKS']; + } else { + const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); + if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + privateKey = decode(apiKeyEntity.privateKey, server.config.HMAC_SECRET); + supportedNetworks = apiKeyEntity.supportedNetworks; + } + if ( + isNaN(amount) || + !chainId || + isNaN(chainId) + ) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); + } + if (server.config.SUPPORTED_NETWORKS == '' && !SupportedNetworks) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + } + const networkConfig = getNetworkConfig(chainId, supportedNetworks ?? '', SUPPORTED_ENTRYPOINTS.EPV_06); + if (!networkConfig) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + return await paymaster.deposit(amount, networkConfig.contracts.etherspotPaymasterAddress, networkConfig.bundler, privateKey, chainId, true, server.log); + } catch (err: any) { + request.log.error(err); + if (err.name == "ResourceNotFoundException") + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }) + } + } + ) + + server.post("/deposit/v2", + ResponseSchema, + async function (request, reply) { + try { + printRequest("/deposit/v2", request, server.log); + const body: any = request.body; + const query: any = request.query; + const amount = body.params[0]; + const chainId = query['chainId'] ?? body.params[1]; + const api_key = query['apiKey'] ?? body.params[2]; + if (!api_key) + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + let privateKey = ''; + let supportedNetworks; + if (!unsafeMode) { + const AWSresponse = await client.send( + new GetSecretValueCommand({ + SecretId: prefixSecretId + api_key, + }) + ); + const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + privateKey = secrets['PRIVATE_KEY']; + supportedNetworks = secrets['SUPPORTED_NETWORKS']; + } else { + const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); + if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + privateKey = decode(apiKeyEntity.privateKey, server.config.HMAC_SECRET); + supportedNetworks = apiKeyEntity.supportedNetworks; + } + if ( + isNaN(amount) || + !chainId || + isNaN(chainId) + ) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); + } + if (server.config.SUPPORTED_NETWORKS == '' && !SupportedNetworks) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + } + const networkConfig = getNetworkConfig(chainId, supportedNetworks ?? '', SUPPORTED_ENTRYPOINTS.EPV_07); + if (!networkConfig) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + return await paymaster.deposit(amount, networkConfig.contracts.etherspotPaymasterAddress, networkConfig.bundler, privateKey, chainId, false, server.log); + } catch (err: any) { + request.log.error(err); + if (err.name == "ResourceNotFoundException") + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }) + } + } + ) +}; + +export default depositRoutes; diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts deleted file mode 100644 index eeb84e6..0000000 --- a/backend/src/routes/index.ts +++ /dev/null @@ -1,1019 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { Type } from "@sinclair/typebox"; -import { FastifyPluginAsync } from "fastify"; -import { BigNumber, Wallet, ethers, providers } from "ethers"; -import { gql, request as GLRequest } from "graphql-request"; -import { - GetSecretValueCommand, - SecretsManagerClient, -} from "@aws-sdk/client-secrets-manager"; -import { Paymaster } from "../paymaster/index.js"; -import SupportedNetworks from "../../config.json" assert { type: "json" }; -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"; - -const SUPPORTED_ENTRYPOINTS = { - EPV_06: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", - EPV_07: "0x0000000071727De22E5E9d8BAf0edAc6f37da032", -}; - -const routes: FastifyPluginAsync = async (server) => { - const paymaster = new Paymaster( - server.config.FEE_MARKUP, - server.config.MULTI_TOKEN_MARKUP - ); - - const prefixSecretId = "arka_"; - - let client: SecretsManagerClient; - - const unsafeMode: boolean = process.env.UNSAFE_MODE == "true" ? true : false; - - if (!unsafeMode) { - client = new SecretsManagerClient(); - } - - const whitelistResponseSchema = { - schema: { - response: { - 200: Type.Object({ - message: Type.String(), - }), - 400: Type.Object({ - error: Type.String(), - }), - }, - }, - }; - - server.post("/", async function (request, reply) { - try { - printRequest("/", request, server.log); - const query: any = request.query; - const body: any = request.body; - if (!body) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.EMPTY_BODY }); - const userOp = body.params[0]; - const entryPoint = body.params[1]; - let context = body.params[2]; - let gasToken = context?.token ? context.token : null; - let mode = context?.mode ? String(context.mode) : "sponsor"; - let chainId = query["chainId"] ?? body.params[3]; - const api_key = query["apiKey"] ?? body.params[4]; - let sponsorDetails = false, - estimate = true; - if (body.method) { - switch (body.method) { - case "pm_getPaymasterData": { - estimate = false; - sponsorDetails = true; - } - case "pm_getPaymasterStubData": { - chainId = BigNumber.from(body.params[2]).toNumber(); - context = body.params[3]; - gasToken = context?.token ? context.token : null; - mode = context?.mode ? String(context.mode) : "sponsor"; - break; - } - case "pm_sponsorUserOperation": { - break; - } - default: { - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.UNSUPPORTED_METHOD }); - break; - } - } - } - 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 = []; - let multiTokenPaymasters = []; - let multiTokenOracles = []; - let privateKey = ""; - let supportedNetworks; - let noOfTxns; - let txnMode; - let indexerEndpoint; - let sponsorName = "", - sponsorImage = ""; - if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? "{}"); - if (!secrets["PRIVATE_KEY"]) { - server.log.info("Invalid Api Key provided"); - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - } - if (secrets["ERC20_PAYMASTERS"]) { - const buffer = Buffer.from(secrets["ERC20_PAYMASTERS"], "base64"); - customPaymasters = JSON.parse(buffer.toString()); - } - if (secrets["MULTI_TOKEN_PAYMASTERS"]) { - const buffer = Buffer.from( - secrets["MULTI_TOKEN_PAYMASTERS"], - "base64" - ); - multiTokenPaymasters = JSON.parse(buffer.toString()); - } - if (secrets["MULTI_TOKEN_ORACLES"]) { - const buffer = Buffer.from(secrets["MULTI_TOKEN_ORACLES"], "base64"); - multiTokenOracles = JSON.parse(buffer.toString()); - } - sponsorName = secrets["SPONSOR_NAME"]; - sponsorImage = secrets["LOGO_URL"]; - privateKey = secrets["PRIVATE_KEY"]; - supportedNetworks = secrets["SUPPORTED_NETWORKS"]; - noOfTxns = secrets["NO_OF_TRANSACTIONS_IN_A_MONTH"] ?? 10; - 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) { - 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"); - 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 (record["MULTI_TOKEN_ORACLES"]) { - const buffer = Buffer.from(record["MULTI_TOKEN_ORACLES"], "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; - } - - if (!userOp || !entryPoint || !chainId || !mode || isNaN(chainId)) { - server.log.info("Incomplete body data provided"); - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_DATA }); - } - - if (server.config.SUPPORTED_NETWORKS == "" && !SupportedNetworks) { - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); - } - - if ( - mode.toLowerCase() == "erc20" && - !(PAYMASTER_ADDRESS[chainId] && PAYMASTER_ADDRESS[chainId][gasToken]) && - !(customPaymasters[chainId] && customPaymasters[chainId][gasToken]) - ) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.UNSUPPORTED_NETWORK_TOKEN }); - - if (gasToken && ethers.utils.isAddress(gasToken)) - gasToken = ethers.utils.getAddress(gasToken); - - if ( - mode.toLowerCase() == "multitoken" && - !( - multiTokenPaymasters[chainId] && - multiTokenPaymasters[chainId][gasToken] - ) && - !(multiTokenOracles[chainId] && multiTokenOracles[chainId][gasToken]) - ) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.UNSUPPORTED_NETWORK_TOKEN }); - - const networkConfig = getNetworkConfig( - chainId, - supportedNetworks ?? "", - entryPoint - ); - if (!networkConfig) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); - - let result: any; - switch (mode.toLowerCase()) { - case "sponsor": { - const date = new Date(); - const provider = new providers.JsonRpcProvider(networkConfig.bundler); - const signer = new Wallet(privateKey, provider); - if (txnMode) { - const signerAddress = await signer.getAddress(); - const IndexerData = await getIndexerData( - signerAddress, - userOp.sender, - date.getMonth(), - date.getFullYear(), - noOfTxns, - indexerEndpoint - ); - if (IndexerData.length >= noOfTxns) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.QUOTA_EXCEEDED }); - } - const validUntil = context?.validUntil - ? new Date(context.validUntil) - : date; - const validAfter = context?.validAfter - ? new Date(context.validAfter) - : date; - const hex = ( - Number((validUntil.valueOf() / 1000).toFixed(0)) + 600 - ).toString(16); - const hex1 = ( - Number((validAfter.valueOf() / 1000).toFixed(0)) - 60 - ).toString(16); - let str = "0x"; - let str1 = "0x"; - for (let i = 0; i < 14 - hex.length; i++) { - str += "0"; - } - for (let i = 0; i < 14 - hex1.length; i++) { - str1 += "0"; - } - str += hex; - str1 += hex1; - if (entryPoint == SUPPORTED_ENTRYPOINTS.EPV_06) - result = await paymaster.signV06( - userOp, - str, - str1, - entryPoint, - networkConfig.contracts.etherspotPaymasterAddress, - networkConfig.bundler, - signer, - estimate, - server.log - ); - else - result = await paymaster.signV07( - userOp, - str, - str1, - entryPoint, - networkConfig.contracts.etherspotPaymasterAddress, - networkConfig.bundler, - signer, - estimate, - server.log - ); - break; - } - case "erc20": { - if (entryPoint !== SUPPORTED_ENTRYPOINTS.EPV_06) - throw new Error( - "Currently only 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 entryPoint address is supported" - ); - let paymasterAddress: string; - if (customPaymasters[chainId] && customPaymasters[chainId][gasToken]) - paymasterAddress = customPaymasters[chainId][gasToken]; - else paymasterAddress = PAYMASTER_ADDRESS[chainId][gasToken]; - result = await paymaster.pimlico( - userOp, - networkConfig.bundler, - entryPoint, - paymasterAddress, - server.log - ); - break; - } - case "multitoken": { - if (entryPoint !== SUPPORTED_ENTRYPOINTS.EPV_06) - throw new Error( - "Currently only 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 entryPoint address is supported" - ); - const date = new Date(); - const provider = new providers.JsonRpcProvider(networkConfig.bundler); - const signer = new Wallet(privateKey, provider); - const validUntil = context.validUntil - ? new Date(context.validUntil) - : date; - const validAfter = context.validAfter - ? new Date(context.validAfter) - : date; - const hex = ( - Number((validUntil.valueOf() / 1000).toFixed(0)) + 600 - ).toString(16); - const hex1 = ( - Number((validAfter.valueOf() / 1000).toFixed(0)) - 60 - ).toString(16); - let str = "0x"; - let str1 = "0x"; - for (let i = 0; i < 14 - hex.length; i++) { - str += "0"; - } - for (let i = 0; i < 14 - hex1.length; i++) { - str1 += "0"; - } - str += hex; - str1 += hex1; - if ( - !networkConfig.MultiTokenPaymasterOracleUsed || - !( - networkConfig.MultiTokenPaymasterOracleUsed == "orochi" || - networkConfig.MultiTokenPaymasterOracleUsed == "chainlink" - ) - ) - throw new Error("Oracle is not Defined/Invalid"); - result = await paymaster.signMultiTokenPaymaster( - userOp, - str, - str1, - entryPoint, - multiTokenPaymasters[chainId][gasToken], - gasToken, - multiTokenOracles[chainId][gasToken], - networkConfig.bundler, - signer, - networkConfig.MultiTokenPaymasterOracleUsed, - server.log - ); - break; - } - default: { - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_MODE }); - } - } - server.log.info(result, "Response sent: "); - if (sponsorDetails) - result.sponsor = { name: sponsorName, icon: sponsorImage }; - if (body.jsonrpc) - return reply - .code(ReturnCode.SUCCESS) - .send({ jsonrpc: body.jsonrpc, id: body.id, result, error: null }); - return reply.code(ReturnCode.SUCCESS).send(result); - } catch (err: any) { - request.log.error(err); - if (err.name == "ResourceNotFoundException") - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - return reply - .code(ReturnCode.FAILURE) - .send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); - } - }); - - server.post( - "/pimlicoAddress", - whitelistResponseSchema, - async function (request, reply) { - try { - printRequest("/pimlicoAddress", request, server.log); - const query: any = request.query; - const body: any = request.body; - const entryPoint = body.params[0]; - const context = body.params[1]; - const gasToken = context ? context.token : null; - const chainId = query["chainId"] ?? body.params[2]; - const api_key = query["apiKey"] ?? body.params[3]; - if (!api_key) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - let customPaymasters = []; - let privateKey = ""; - let supportedNetworks; - if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? "{}"); - if (!secrets["PRIVATE_KEY"]) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - if (secrets["ERC20_PAYMASTERS"]) { - const buffer = Buffer.from(secrets["ERC20_PAYMASTERS"], "base64"); - customPaymasters = JSON.parse(buffer.toString()); - } - 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"); - customPaymasters = JSON.parse(buffer.toString()); - } - privateKey = decode(record["PRIVATE_KEY"]); - supportedNetworks = record["SUPPORTED_NETWORKS"]; - } - if (!privateKey) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - if (!entryPoint || !gasToken || !chainId || isNaN(chainId)) { - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_DATA }); - } - if (server.config.SUPPORTED_NETWORKS == "" && !SupportedNetworks) { - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); - } - const networkConfig = getNetworkConfig( - chainId, - supportedNetworks ?? "", - entryPoint - ); - if (!networkConfig) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); - let result; - if (customPaymasters[chainId] && customPaymasters[chainId][gasToken]) - result = { message: customPaymasters[chainId][gasToken] }; - else { - if ( - !( - PAYMASTER_ADDRESS[chainId] && PAYMASTER_ADDRESS[chainId][gasToken] - ) - ) - return reply - .code(ReturnCode.FAILURE) - .send({ error: "Invalid network/token" }); - result = { message: PAYMASTER_ADDRESS[chainId][gasToken] }; - } - server.log.info(result, "Response sent: "); - if (body.jsonrpc) - return reply - .code(ReturnCode.SUCCESS) - .send({ - jsonrpc: body.jsonrpc, - id: body.id, - message: result.message, - error: null, - }); - return reply.code(ReturnCode.SUCCESS).send(result); - } catch (err: any) { - request.log.error(err); - if (err.name == "ResourceNotFoundException") - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - return reply - .code(ReturnCode.FAILURE) - .send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); - } - } - ); - - server.post("/whitelist", async function (request, reply) { - try { - printRequest("/whitelist", request, server.log); - const body: any = request.body; - const query: any = request.query; - const address = body.params[0]; - const chainId = query["chainId"] ?? body.params[1]; - const api_key = query["apiKey"] ?? body.params[2]; - if (!api_key) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - let privateKey = ""; - let supportedNetworks; - if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? "{}"); - if (!secrets["PRIVATE_KEY"]) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - privateKey = secrets["PRIVATE_KEY"]; - supportedNetworks = secrets["SUPPORTED_NETWORKS"]; - } else { - const 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"]; - } - if (!privateKey) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - if ( - !Array.isArray(address) || - address.length > 10 || - !chainId || - isNaN(chainId) - ) { - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_DATA }); - } - if (server.config.SUPPORTED_NETWORKS == "" && !SupportedNetworks) { - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); - } - const networkConfig = getNetworkConfig( - chainId, - supportedNetworks ?? "", - SUPPORTED_ENTRYPOINTS.EPV_06 - ); - if (!networkConfig) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); - const validAddresses = address.every(ethers.utils.isAddress); - if (!validAddresses) - return reply - .code(ReturnCode.FAILURE) - .send({ error: "Invalid Address passed" }); - const result = await paymaster.whitelistAddresses( - address, - networkConfig.contracts.etherspotPaymasterAddress, - networkConfig.bundler, - privateKey, - chainId, - server.log - ); - server.log.info(result, "Response sent: "); - if (body.jsonrpc) - return reply - .code(ReturnCode.SUCCESS) - .send({ jsonrpc: body.jsonrpc, id: body.id, result, error: null }); - return reply.code(ReturnCode.SUCCESS).send(result); - } catch (err: any) { - request.log.error(err); - if (err.name == "ResourceNotFoundException") - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - return reply - .code(ReturnCode.FAILURE) - .send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); - } - }); - - server.post("/removeWhitelist", async function (request, reply) { - try { - printRequest("/removeWhitelist", request, server.log); - const body: any = request.body; - const query: any = request.query; - const address = body.params[0]; - const chainId = query["chainId"] ?? body.params[1]; - const api_key = query["apiKey"] ?? body.params[2]; - if (!api_key) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - let privateKey = ""; - let supportedNetworks; - if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? "{}"); - if (!secrets["PRIVATE_KEY"]) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - privateKey = secrets["PRIVATE_KEY"]; - supportedNetworks = secrets["SUPPORTED_NETWORKS"]; - } else { - const 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"]; - } - if (!privateKey) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - if ( - !Array.isArray(address) || - address.length > 10 || - !chainId || - isNaN(chainId) - ) { - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_DATA }); - } - if (server.config.SUPPORTED_NETWORKS == "" && !SupportedNetworks) { - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); - } - const networkConfig = getNetworkConfig( - chainId, - supportedNetworks ?? "", - SUPPORTED_ENTRYPOINTS.EPV_06 - ); - if (!networkConfig) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); - const validAddresses = address.every(ethers.utils.isAddress); - if (!validAddresses) - return reply - .code(ReturnCode.FAILURE) - .send({ error: "Invalid Address passed" }); - const result = await paymaster.removeWhitelistAddress( - address, - networkConfig.contracts.etherspotPaymasterAddress, - networkConfig.bundler, - privateKey, - chainId, - server.log - ); - if (body.jsonrpc) - return reply - .code(ReturnCode.SUCCESS) - .send({ jsonrpc: body.jsonrpc, id: body.id, result, error: null }); - return reply.code(ReturnCode.SUCCESS).send(result); - } catch (err: any) { - request.log.error(err); - if (err.name == "ResourceNotFoundException") - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - return reply - .code(ReturnCode.FAILURE) - .send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); - } - }); - - server.post("/checkWhitelist", async function (request, reply) { - try { - printRequest("/checkWhitelist", request, server.log); - const body: any = request.body; - const query: any = request.query; - const accountAddress = body.params[0]; - const chainId = query["chainId"] ?? body.params[1]; - const api_key = query["apiKey"] ?? body.params[2]; - if (!api_key) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - let privateKey = ""; - let supportedNetworks; - if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? "{}"); - if (!secrets["PRIVATE_KEY"]) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - privateKey = secrets["PRIVATE_KEY"]; - supportedNetworks = secrets["SUPPORTED_NETWORKS"]; - } else { - const 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"]; - } - if (!privateKey) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - if ( - !accountAddress || - !ethers.utils.isAddress(accountAddress) || - !chainId || - isNaN(chainId) - ) { - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_DATA }); - } - if (server.config.SUPPORTED_NETWORKS == "" && !SupportedNetworks) { - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); - } - const networkConfig = getNetworkConfig( - chainId, - supportedNetworks ?? "", - SUPPORTED_ENTRYPOINTS.EPV_06 - ); - if (!networkConfig) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); - const response = await paymaster.checkWhitelistAddress( - accountAddress, - networkConfig.contracts.etherspotPaymasterAddress, - networkConfig.bundler, - privateKey, - server.log - ); - server.log.info(response, "Response sent: "); - if (body.jsonrpc) - return reply - .code(ReturnCode.SUCCESS) - .send({ - jsonrpc: body.jsonrpc, - id: body.id, - result: { - message: response === true ? "Already added" : "Not added yet", - }, - error: null, - }); - return reply - .code(ReturnCode.SUCCESS) - .send({ - message: response === true ? "Already added" : "Not added yet", - }); - } catch (err: any) { - request.log.error(err); - if (err.name == "ResourceNotFoundException") - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - return reply - .code(ReturnCode.FAILURE) - .send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); - } - }); - - server.post( - "/deposit", - whitelistResponseSchema, - async function (request, reply) { - try { - printRequest("/deposit", request, server.log); - const body: any = request.body; - const query: any = request.query; - const amount = body.params[0]; - const chainId = query["chainId"] ?? body.params[1]; - const api_key = query["apiKey"] ?? body.params[2]; - if (!api_key) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - let privateKey = ""; - let supportedNetworks; - if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? "{}"); - if (!secrets["PRIVATE_KEY"]) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - privateKey = secrets["PRIVATE_KEY"]; - supportedNetworks = secrets["SUPPORTED_NETWORKS"]; - } else { - const 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"]; - } - if (isNaN(amount) || !chainId || isNaN(chainId)) { - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_DATA }); - } - if (server.config.SUPPORTED_NETWORKS == "" && !SupportedNetworks) { - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); - } - const networkConfig = getNetworkConfig( - chainId, - supportedNetworks ?? "", - SUPPORTED_ENTRYPOINTS.EPV_06 - ); - if (!networkConfig) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); - return await paymaster.deposit( - amount, - networkConfig.contracts.etherspotPaymasterAddress, - networkConfig.bundler, - privateKey, - chainId, - true, - server.log - ); - } catch (err: any) { - request.log.error(err); - if (err.name == "ResourceNotFoundException") - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - return reply - .code(ReturnCode.FAILURE) - .send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); - } - } - ); - - server.post( - "/deposit/v2", - whitelistResponseSchema, - async function (request, reply) { - try { - printRequest("/deposit/v2", request, server.log); - const body: any = request.body; - const query: any = request.query; - const amount = body.params[0]; - const chainId = query["chainId"] ?? body.params[1]; - const api_key = query["apiKey"] ?? body.params[2]; - if (!api_key) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - let privateKey = ""; - let supportedNetworks; - if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? "{}"); - if (!secrets["PRIVATE_KEY"]) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - privateKey = secrets["PRIVATE_KEY"]; - supportedNetworks = secrets["SUPPORTED_NETWORKS"]; - } else { - const 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"]; - } - if (isNaN(amount) || !chainId || isNaN(chainId)) { - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_DATA }); - } - if (server.config.SUPPORTED_NETWORKS == "" && !SupportedNetworks) { - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); - } - const networkConfig = getNetworkConfig( - chainId, - supportedNetworks ?? "", - SUPPORTED_ENTRYPOINTS.EPV_07 - ); - if (!networkConfig) - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); - return await paymaster.deposit( - amount, - networkConfig.contracts.etherspotPaymasterAddress, - networkConfig.bundler, - privateKey, - chainId, - false, - server.log - ); - } catch (err: any) { - request.log.error(err); - if (err.name == "ResourceNotFoundException") - return reply - .code(ReturnCode.FAILURE) - .send({ error: ErrorMessage.INVALID_API_KEY }); - return reply - .code(ReturnCode.FAILURE) - .send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); - } - } - ); - - async function getIndexerData( - sponsor: string, - sender: string, - month: number, - year: number, - noOfTxns: number, - endpoint: string - ): Promise { - try { - const query = gql` - query { - paymasterEvents( - limit: ${noOfTxns} - where: {month: ${month}, year: ${year}, paymaster: "${sponsor}", sender: "${sender}"}) - { - items { - sender - paymaster - transactionHash - year - month - } - } - }`; - const apiResponse: any = await GLRequest(endpoint, query); - return apiResponse.paymasterEvents.items; - } catch (err) { - server.log.error(err); - return []; - } - } -}; - -export default routes; diff --git a/backend/src/routes/metadata.ts b/backend/src/routes/metadata-routes.ts similarity index 87% rename from backend/src/routes/metadata.ts rename to backend/src/routes/metadata-routes.ts index 30bc946..cefa106 100644 --- a/backend/src/routes/metadata.ts +++ b/backend/src/routes/metadata-routes.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, server.config.HMAC_SECRET); + 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/paymaster-routes.ts b/backend/src/routes/paymaster-routes.ts new file mode 100644 index 0000000..5c29e63 --- /dev/null +++ b/backend/src/routes/paymaster-routes.ts @@ -0,0 +1,323 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { FastifyPluginAsync } from "fastify"; +import { BigNumber, Wallet, ethers, providers } from "ethers"; +import { gql, request as GLRequest } from "graphql-request"; +import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; +import { Paymaster } from "../paymaster/index.js"; +import SupportedNetworks from "../../config.json" assert { type: "json" }; +import { PAYMASTER_ADDRESS } from "../constants/Pimlico.js"; +import ErrorMessage, { generateErrorMessage } from "../constants/ErrorMessage.js"; +import ReturnCode from "../constants/ReturnCode.js"; +import { decode } from "../utils/crypto.js"; +import { printRequest, getNetworkConfig } from "../utils/common.js"; +import { SponsorshipPolicy } from "../models/sponsorship-policy.js"; +import { DEFAULT_EP_VERSION, EPVersions, getEPVersion } from "../types/sponsorship-policy-dto.js"; + +const SUPPORTED_ENTRYPOINTS = { + 'EPV_06': "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", + 'EPV_07': "0x0000000071727De22E5E9d8BAf0edAc6f37da032" +} + +const paymasterRoutes: FastifyPluginAsync = async (server) => { + const paymaster = new Paymaster(server.config.FEE_MARKUP, server.config.MULTI_TOKEN_MARKUP); + + const prefixSecretId = 'arka_'; + + let client: SecretsManagerClient; + + const unsafeMode: boolean = process.env.UNSAFE_MODE == "true" ? true : false; + + if (!unsafeMode) { + client = new SecretsManagerClient(); + } + + server.post("/", + async function (request, reply) { + try { + printRequest("/", request, server.log); + const query: any = request.query; + const body: any = request.body; + if (!body) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); + const userOp = body.params[0]; + const entryPoint = body.params[1]; + let context = body.params[2]; + let gasToken = context?.token ? context.token : null; + let mode = context?.mode ? String(context.mode) : "sponsor"; + let chainId = query['chainId'] ?? body.params[3]; + const api_key = query['apiKey'] ?? body.params[4]; + let epVersion: EPVersions = DEFAULT_EP_VERSION; + + let sponsorDetails = false, estimate = true; + if (body.method) { + switch (body.method) { + case 'pm_getPaymasterData': { + estimate = false; + sponsorDetails = true; + } + case 'pm_getPaymasterStubData': { + chainId = BigNumber.from(body.params[2]).toNumber(); + context = body.params[3]; + gasToken = context?.token ? context.token : null; + mode = context?.mode ? String(context.mode) : "sponsor"; + break; + }; + case 'pm_sponsorUserOperation': { + break; + }; + default: { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_METHOD }); + } + } + } + if (!api_key) + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + if ((entryPoint != SUPPORTED_ENTRYPOINTS.EPV_06) && (entryPoint != SUPPORTED_ENTRYPOINTS.EPV_07)) + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_ENTRYPOINT }) + + if (entryPoint == SUPPORTED_ENTRYPOINTS.EPV_06) epVersion = EPVersions.EPV_06; + else epVersion = EPVersions.EPV_07; + + let customPaymasters = []; + let multiTokenPaymasters = []; + let multiTokenOracles = []; + let privateKey = ''; + let supportedNetworks; + let noOfTxns; + let txnMode; + let indexerEndpoint; + let sponsorName = '', sponsorImage = ''; + if (!unsafeMode) { + const AWSresponse = await client.send( + new GetSecretValueCommand({ + SecretId: prefixSecretId + api_key, + }) + ); + const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + if (!secrets['PRIVATE_KEY']) { + server.log.info("Invalid Api Key provided") + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + } + if (secrets['ERC20_PAYMASTERS']) { + const buffer = Buffer.from(secrets['ERC20_PAYMASTERS'], 'base64'); + customPaymasters = JSON.parse(buffer.toString()); + } + if (secrets['MULTI_TOKEN_PAYMASTERS']) { + const buffer = Buffer.from(secrets['MULTI_TOKEN_PAYMASTERS'], 'base64'); + multiTokenPaymasters = JSON.parse(buffer.toString()); + } + if (secrets['MULTI_TOKEN_ORACLES']) { + const buffer = Buffer.from(secrets['MULTI_TOKEN_ORACLES'], 'base64'); + multiTokenOracles = JSON.parse(buffer.toString()); + } + sponsorName = secrets['SPONSOR_NAME']; + sponsorImage = secrets['LOGO_URL']; + privateKey = secrets['PRIVATE_KEY']; + supportedNetworks = secrets['SUPPORTED_NETWORKS']; + noOfTxns = secrets['NO_OF_TRANSACTIONS_IN_A_MONTH'] ?? 10; + txnMode = secrets['TRANSACTION_LIMIT'] ?? 0; + indexerEndpoint = secrets['INDEXER_ENDPOINT'] ?? process.env.DEFAULT_INDEXER_ENDPOINT; + } else { + + //validate api_key + if (!api_key) { + server.log.error("Invalid Api Key provided") + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + } + + const apiKeyEntity = await server.apiKeyRepository.findOneByApiKey(api_key); + + if (!apiKeyEntity) { + server.log.error("Invalid Api Key provided") + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + } + + if (apiKeyEntity.erc20Paymasters) { + const buffer = Buffer.from(apiKeyEntity.erc20Paymasters, 'base64'); + customPaymasters = JSON.parse(buffer.toString()); + } + + if (apiKeyEntity.multiTokenPaymasters) { + const buffer = Buffer.from(apiKeyEntity.multiTokenPaymasters, 'base64'); + multiTokenPaymasters = JSON.parse(buffer.toString()); + } + if (apiKeyEntity.multiTokenOracles) { + const buffer = Buffer.from(apiKeyEntity.multiTokenOracles, 'base64'); + multiTokenOracles = JSON.parse(buffer.toString()); + } + sponsorName = apiKeyEntity.sponsorName ? apiKeyEntity.sponsorName : ''; + sponsorImage = apiKeyEntity.logoUrl ? apiKeyEntity.logoUrl : ''; + privateKey = decode(apiKeyEntity.privateKey, server.config.HMAC_SECRET); + supportedNetworks = apiKeyEntity.supportedNetworks; + noOfTxns = apiKeyEntity.noOfTransactionsInAMonth; + txnMode = apiKeyEntity.transactionLimit; + indexerEndpoint = apiKeyEntity.indexerEndpoint ?? process.env.DEFAULT_INDEXER_ENDPOINT; + } + + if ( + !userOp || + !entryPoint || + !chainId || + !mode || + isNaN(chainId) + ) { + server.log.error("Incomplete body data provided") + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); + } + + if (server.config.SUPPORTED_NETWORKS == '' && !SupportedNetworks) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + } + + if ( + mode.toLowerCase() == 'erc20' && + !(PAYMASTER_ADDRESS[chainId] && PAYMASTER_ADDRESS[chainId][gasToken]) && + !(customPaymasters[chainId] && customPaymasters[chainId][gasToken]) + ) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK_TOKEN }) + + if (gasToken && ethers.utils.isAddress(gasToken)) gasToken = ethers.utils.getAddress(gasToken) + + if (mode.toLowerCase() == 'multitoken' && + !(multiTokenPaymasters[chainId] && multiTokenPaymasters[chainId][gasToken]) && + !(multiTokenOracles[chainId] && multiTokenOracles[chainId][gasToken]) + ) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK_TOKEN }) + + const networkConfig = getNetworkConfig(chainId, supportedNetworks ?? '', entryPoint); + if (!networkConfig) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + + let result: any; + switch (mode.toLowerCase()) { + case 'sponsor': { + const date = new Date(); + const provider = new providers.JsonRpcProvider(networkConfig.bundler); + const signer = new Wallet(privateKey, provider) + + // get chainid from provider + const chainId = await provider.getNetwork(); + + // get wallet_address from api_key + const apiKeyData = await server.apiKeyRepository.findOneByApiKey(api_key); + if (!apiKeyData) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); + + // get sponsorshipPolicy for the user from walletAddress and entrypoint version + const sponsorshipPolicy: SponsorshipPolicy | null = await server.sponsorshipPolicyRepository.findOneByWalletAddressAndSupportedEPVersion(apiKeyData?.walletAddress, getEPVersion(epVersion), chainId.chainId); + if (!sponsorshipPolicy) { + const errorMessage: string = generateErrorMessage(ErrorMessage.ACTIVE_SPONSORSHIP_POLICY_NOT_FOUND, { walletAddress: apiKeyData?.walletAddress, epVersion: epVersion, chainId: chainId.chainId }); + return reply.code(ReturnCode.FAILURE).send({ error: errorMessage }); + } + + if (!Object.assign(new SponsorshipPolicy(), sponsorshipPolicy).isApplicable) { + const errorMessage: string = generateErrorMessage(ErrorMessage.NO_ACTIVE_SPONSORSHIP_POLICY_FOR_CURRENT_TIME, { walletAddress: apiKeyData?.walletAddress, epVersion: epVersion, chainId: chainId.chainId }); + return reply.code(ReturnCode.FAILURE).send({ error: errorMessage }); + } + + // get supported networks from sponsorshipPolicy + const supportedNetworks: number[] | undefined | null = sponsorshipPolicy.enabledChains; + if (!supportedNetworks || !supportedNetworks.includes(chainId.chainId)) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + + // is chainId exists in supportedNetworks + if (!supportedNetworks.includes(chainId.chainId)) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + + if (txnMode) { + const signerAddress = await signer.getAddress(); + const IndexerData = await getIndexerData(signerAddress, userOp.sender, date.getMonth(), date.getFullYear(), noOfTxns, indexerEndpoint); + if (IndexerData.length >= noOfTxns) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.QUOTA_EXCEEDED }) + } + const validUntil = context?.validUntil ? new Date(context.validUntil) : date; + const validAfter = context?.validAfter ? new Date(context.validAfter) : date; + const hex = (Number((validUntil.valueOf() / 1000).toFixed(0)) + 600).toString(16); + const hex1 = (Number((validAfter.valueOf() / 1000).toFixed(0)) - 60).toString(16); + let str = '0x' + let str1 = '0x' + for (let i = 0; i < 14 - hex.length; i++) { + str += '0'; + } + for (let i = 0; i < 14 - hex1.length; i++) { + str1 += '0'; + } + str += hex; + str1 += hex1; + if (entryPoint == SUPPORTED_ENTRYPOINTS.EPV_06) + result = await paymaster.signV06(userOp, str, str1, entryPoint, networkConfig.contracts.etherspotPaymasterAddress, networkConfig.bundler, signer, estimate, server.log); + else result = await paymaster.signV07(userOp, str, str1, entryPoint, networkConfig.contracts.etherspotPaymasterAddress, networkConfig.bundler, signer, estimate, server.log); + break; + } + case 'erc20': { + if (entryPoint !== SUPPORTED_ENTRYPOINTS.EPV_06) + throw new Error('Currently only 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 entryPoint address is supported') + let paymasterAddress: string; + if (customPaymasters[chainId] && customPaymasters[chainId][gasToken]) paymasterAddress = customPaymasters[chainId][gasToken]; + else paymasterAddress = PAYMASTER_ADDRESS[chainId][gasToken] + result = await paymaster.pimlico(userOp, networkConfig.bundler, entryPoint, paymasterAddress, server.log); + break; + } + case 'multitoken': { + if (entryPoint !== SUPPORTED_ENTRYPOINTS.EPV_06) + throw new Error('Currently only 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 entryPoint address is supported') + const date = new Date(); + const provider = new providers.JsonRpcProvider(networkConfig.bundler); + const signer = new Wallet(privateKey, provider) + const validUntil = context.validUntil ? new Date(context.validUntil) : date; + const validAfter = context.validAfter ? new Date(context.validAfter) : date; + const hex = (Number((validUntil.valueOf() / 1000).toFixed(0)) + 600).toString(16); + const hex1 = (Number((validAfter.valueOf() / 1000).toFixed(0)) - 60).toString(16); + let str = '0x' + let str1 = '0x' + for (let i = 0; i < 14 - hex.length; i++) { + str += '0'; + } + for (let i = 0; i < 14 - hex1.length; i++) { + str1 += '0'; + } + str += hex; + str1 += hex1; + if (!networkConfig.MultiTokenPaymasterOracleUsed || + !(networkConfig.MultiTokenPaymasterOracleUsed == "orochi" || networkConfig.MultiTokenPaymasterOracleUsed == "chainlink")) + throw new Error("Oracle is not Defined/Invalid"); + result = await paymaster.signMultiTokenPaymaster(userOp, str, str1, entryPoint, multiTokenPaymasters[chainId][gasToken], gasToken, multiTokenOracles[chainId][gasToken], networkConfig.bundler, signer, networkConfig.MultiTokenPaymasterOracleUsed, server.log); + break; + } + default: { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_MODE }); + } + } + server.log.info(result, 'Response sent: '); + if (sponsorDetails) result.sponsor = { name: sponsorName, icon: sponsorImage }; + if (body.jsonrpc) + return reply.code(ReturnCode.SUCCESS).send({ jsonrpc: body.jsonrpc, id: body.id, result, error: null }) + return reply.code(ReturnCode.SUCCESS).send(result); + } catch (err: any) { + request.log.error(err); + if (err.name == "ResourceNotFoundException") + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); + } + } + ); + + async function getIndexerData(sponsor: string, sender: string, month: number, year: number, noOfTxns: number, endpoint: string): Promise { + try { + const query = gql` + query { + paymasterEvents( + limit: ${noOfTxns} + where: {month: ${month}, year: ${year}, paymaster: "${sponsor}", sender: "${sender}"}) + { + items { + sender + paymaster + transactionHash + year + month + } + } + }`; + const apiResponse: any = await GLRequest(endpoint, query); + return apiResponse.paymasterEvents.items; + } catch (err) { + server.log.error(err); + return []; + } + } +}; + +export default paymasterRoutes; diff --git a/backend/src/routes/pimlico-routes.ts b/backend/src/routes/pimlico-routes.ts new file mode 100644 index 0000000..35f9654 --- /dev/null +++ b/backend/src/routes/pimlico-routes.ts @@ -0,0 +1,117 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Type } from "@sinclair/typebox"; +import { FastifyPluginAsync } from "fastify"; +import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; +import SupportedNetworks from "../../config.json" assert { type: "json" }; +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 } from "../utils/common.js"; +import { APIKey } from "../models/api-key.js"; + +const pimlicoRoutes: FastifyPluginAsync = async (server) => { + + const prefixSecretId = 'arka_'; + + let client: SecretsManagerClient; + + const unsafeMode: boolean = process.env.UNSAFE_MODE == "true" ? true : false; + + if (!unsafeMode) { + client = new SecretsManagerClient(); + } + + const ResponseSchema = { + schema: { + response: { + 200: Type.Object({ + message: Type.String(), + }), + 400: Type.Object({ + error: Type.String(), + }), + } + } + } + + server.post("/pimlicoAddress", + ResponseSchema, + async function (request, reply) { + try { + printRequest("/pimlicoAddress", request, server.log); + const query: any = request.query; + const body: any = request.body; + const entryPoint = body.params[0]; + const context = body.params[1]; + const gasToken = context ? context.token : null; + const chainId = query['chainId'] ?? body.params[2]; + const api_key = query['apiKey'] ?? body.params[3]; + if (!api_key) + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + let customPaymasters = []; + let privateKey = ''; + let supportedNetworks; + if (!unsafeMode) { + const AWSresponse = await client.send( + new GetSecretValueCommand({ + SecretId: prefixSecretId + api_key, + }) + ); + const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + if (secrets['ERC20_PAYMASTERS']) { + const buffer = Buffer.from(secrets['ERC20_PAYMASTERS'], 'base64'); + customPaymasters = JSON.parse(buffer.toString()); + } + privateKey = secrets['PRIVATE_KEY']; + supportedNetworks = secrets['SUPPORTED_NETWORKS']; + } else { + 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(apiKeyEntity.privateKey, server.config.HMAC_SECRET); + + supportedNetworks = apiKeyEntity.supportedNetworks; + } + if (!privateKey) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + if ( + !entryPoint || + !gasToken || + !chainId || + isNaN(chainId) + ) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); + } + if (server.config.SUPPORTED_NETWORKS == '' && !SupportedNetworks) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + } + const networkConfig = getNetworkConfig(chainId, supportedNetworks ?? '', entryPoint); + if (!networkConfig) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + let result; + if (customPaymasters[chainId] && customPaymasters[chainId][gasToken]) result = { message: customPaymasters[chainId][gasToken] } + else { + if (!(PAYMASTER_ADDRESS[chainId] && PAYMASTER_ADDRESS[chainId][gasToken])) return reply.code(ReturnCode.FAILURE).send({ error: "Invalid network/token" }) + result = { message: PAYMASTER_ADDRESS[chainId][gasToken] } + } + server.log.info(result, 'PimlicoAddress Response sent: '); + if (body.jsonrpc) + return reply.code(ReturnCode.SUCCESS).send({ jsonrpc: body.jsonrpc, id: body.id, message: result.message, error: null }) + return reply.code(ReturnCode.SUCCESS).send(result); + } catch (err: any) { + request.log.error(err); + if (err.name == "ResourceNotFoundException") + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }); + } + } + ) + +}; + +export default pimlicoRoutes; diff --git a/backend/src/routes/sponsorship-policy-routes.ts b/backend/src/routes/sponsorship-policy-routes.ts new file mode 100644 index 0000000..5bf15e9 --- /dev/null +++ b/backend/src/routes/sponsorship-policy-routes.ts @@ -0,0 +1,622 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { FastifyPluginAsync } from "fastify"; +import ErrorMessage, { generateErrorMessage } from "../constants/ErrorMessage.js"; +import ReturnCode from "../constants/ReturnCode.js"; +import { SponsorshipPolicyDto, getEPVersion } from "../types/sponsorship-policy-dto.js"; +import { SponsorshipPolicy } from "../models/sponsorship-policy.js"; +import { getChainIdsFromSupportedNetworks } from "../utils/common.js"; + +interface RouteParams { + id?: string; + apiKey?: string; + walletAddress?: string; + epVersion?: string; + chainId?: number; +} + +const sponsorshipPolicyRoutes: FastifyPluginAsync = async (server) => { + + // Get all policies + server.get("/policy", async (request, reply) => { + try { + const policies: SponsorshipPolicy[] = await server.sponsorshipPolicyRepository.findAll(); + if (!policies.length) return reply.code(ReturnCode.NOT_FOUND).send({ error: ErrorMessage.SPONSORSHIP_POLICY_NOT_FOUND }); + + // Sort policies by createdTime + policies.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + + return reply.code(ReturnCode.SUCCESS).send(policies); + } catch (err: any) { + request.log.error(err); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_QUERY_SPONSORSHIP_POLICY }); + } + }); + + // get a Policy by id + server.get<{ Params: RouteParams }>("/policy/:id", async (request, reply) => { + try { + const id = Number(request.params.id); + if (isNaN(id)) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_SPONSORSHIP_POLICY_ID }); + } + + const result = await server.sponsorshipPolicyRepository.findOneById(id); + if (!result) { + return reply.code(ReturnCode.NOT_FOUND).send({ error: ErrorMessage.SPONSORSHIP_POLICY_NOT_FOUND }); + } + + 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 }); + } + }) + + // find all by apiKey + server.get<{ Params: RouteParams }>("/policy/api-key/:apiKey", async (request, reply) => { + try { + const apiKey = request + .params.apiKey; + console.log(apiKey); + + if (!apiKey) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_DATA }); + } + + // get wallet_address from api_key + const apiKeyData = await server.apiKeyRepository.findOneByApiKey(apiKey); + if (!apiKeyData) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); + + // get all sponsorshipPolicies for the user from walletAddress and entrypoint version + const walletAddress = apiKeyData.walletAddress; + + if (!walletAddress) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_DATA }); + } + + const result = await server.sponsorshipPolicyRepository.findAllByWalletAddress(walletAddress); + if (!result.length) { + return reply.code(ReturnCode.NOT_FOUND).send({ error: ErrorMessage.SPONSORSHIP_POLICY_NOT_FOUND }); + } + + 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 }); + } + }) + + // find all by walletAddress + server.get<{ Params: RouteParams }>("/policy/wallet-address/:walletAddress", async (request, reply) => { + try { + const walletAddress = request.params.walletAddress; + + if (!walletAddress) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_DATA }); + } + + const result = await server.sponsorshipPolicyRepository.findAllByWalletAddress(walletAddress); + if (!result.length) { + return reply.code(ReturnCode.NOT_FOUND).send({ error: ErrorMessage.SPONSORSHIP_POLICY_NOT_FOUND }); + } + + 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 }); + } + }) + + // find all by apiKey And EPVersion + server.get<{ Params: RouteParams }>("/policy/api-key/:apiKey/ep-version/:epVersion", async (request, reply) => { + try { + const apiKey = request + .params.apiKey; + const epVersion = request.params.epVersion; + + if (!apiKey || !epVersion) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_DATA }); + } + + // get wallet_address from api_key + const apiKeyData = await server.apiKeyRepository.findOneByApiKey(apiKey); + if (!apiKeyData) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); + + // get all sponsorshipPolicies for the user from walletAddress and entrypoint version + const walletAddress = apiKeyData.walletAddress; + + if (!walletAddress || !epVersion) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_DATA }); + } + + const result = await server.sponsorshipPolicyRepository.findAllByWalletAddressAndSupportedEPVersion(walletAddress, getEPVersion(epVersion)); + if (!result.length) { + return reply.code(ReturnCode.NOT_FOUND).send({ error: ErrorMessage.SPONSORSHIP_POLICY_NOT_FOUND }); + } + + 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 }); + } + }) + + // find all by walletAddress And EPVersion + server.get<{ Params: RouteParams }>("/policy/wallet-address/:walletAddress/ep-version/:epVersion", async (request, reply) => { + try { + const walletAddress = request.params.walletAddress; + const epVersion = request.params.epVersion; + + if (!walletAddress || !epVersion) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_DATA }); + } + + const result = await server.sponsorshipPolicyRepository.findAllByWalletAddressAndSupportedEPVersion(walletAddress, getEPVersion(epVersion)); + if (!result.length) { + return reply.code(ReturnCode.NOT_FOUND).send({ error: ErrorMessage.SPONSORSHIP_POLICY_NOT_FOUND }); + } + + 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 }); + } + }) + + // find all by apiKey And chainId + server.get<{ Params: RouteParams }>("/policy/api-key/:apiKey/chain-id/:chainId", async (request, reply) => { + try { + const apiKey = request + .params.apiKey; + const chainId = request.params.chainId; + + if (!apiKey || !chainId) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_DATA }); + } + + // get wallet_address from api_key + const apiKeyData = await server.apiKeyRepository.findOneByApiKey(apiKey); + if (!apiKeyData) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); + + // get all sponsorshipPolicies for the user from walletAddress and entrypoint version + const walletAddress = apiKeyData.walletAddress; + + if (!walletAddress || !chainId) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_DATA }); + } + + const result = await server.sponsorshipPolicyRepository.findAllByWalletAddressAndChain(walletAddress, chainId); + if (!result.length) { + return reply.code(ReturnCode.NOT_FOUND).send({ error: ErrorMessage.SPONSORSHIP_POLICY_NOT_FOUND }); + } + + 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 }); + } + }) + + // Get all policies for a given apiKey, epVersion, and chainId + server.get<{ Params: RouteParams }>("/policy/api-key/:apiKey/ep-version/:epVersion/chain-id/:chainId", async (request, reply) => { + try { + const apiKey = request.params.apiKey; + const epVersion = request.params.epVersion; + const chainId = Number(request.params.chainId); + + if (!apiKey || !epVersion) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_DATA }); + } + + // get wallet_address from api_key + const apiKeyData = await server.apiKeyRepository.findOneByApiKey(apiKey); + if (!apiKeyData) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); + + const result = await server.sponsorshipPolicyRepository.findAllByWalletAddressAndSupportedEPVersionAndChain(apiKeyData.walletAddress, getEPVersion(epVersion), chainId); + if (!result.length) { + return reply.code(ReturnCode.NOT_FOUND).send({ error: ErrorMessage.SPONSORSHIP_POLICY_NOT_FOUND }); + } + + 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 }); + } + }) + + // Get all policies for a given walletAddress, epVersion, and chainId + server.get<{ Params: RouteParams }>("/policy/wallet-address/:walletAddress/ep-version/:epVersion/chain-id/:chainId", async (request, reply) => { + try { + const walletAddress = request.params.walletAddress; + const epVersion = request.params.epVersion; + const chainId = Number(request.params.chainId); + + if (!walletAddress || !epVersion || chainId === undefined || isNaN(chainId)) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_DATA }); + } + + const result = await server.sponsorshipPolicyRepository.findAllByWalletAddressAndSupportedEPVersionAndChain(walletAddress, getEPVersion(epVersion), chainId); + if (!result.length) { + return reply.code(ReturnCode.NOT_FOUND).send({ error: ErrorMessage.SPONSORSHIP_POLICY_NOT_FOUND }); + } + + 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 }); + } + }) + + // find latest by apiKey + server.get<{ Params: RouteParams }>("/policy/api-key/:apiKey/latest", async (request, reply) => { + try { + const apiKey = request + .params.apiKey; + + if (!apiKey) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_DATA }); + } + + // get wallet_address from api_key + const apiKeyData = await server.apiKeyRepository.findOneByApiKey(apiKey); + if (!apiKeyData) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); + + // get all sponsorshipPolicies for the user from walletAddress and entrypoint version + const walletAddress = apiKeyData.walletAddress; + + if (!walletAddress) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_DATA }); + } + + const result = await server.sponsorshipPolicyRepository.findOneByWalletAddress(walletAddress); + if (!result) { + return reply.code(ReturnCode.NOT_FOUND).send({ error: ErrorMessage.SPONSORSHIP_POLICY_NOT_FOUND }); + } + + 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 }); + } + }) + + // find latest by walletAddress + server.get<{ Params: RouteParams }>("/policy/wallet-address/:walletAddress/latest", async (request, reply) => { + try { + const walletAddress = request.params.walletAddress; + + if (!walletAddress) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_DATA }); + } + + const result = await server.sponsorshipPolicyRepository.findOneByWalletAddress(walletAddress); + if (!result) { + return reply.code(ReturnCode.NOT_FOUND).send({ error: ErrorMessage.SPONSORSHIP_POLICY_NOT_FOUND }); + } + + 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 }); + } + }) + + // find latest by apiKey And chainId + server.get<{ Params: RouteParams }>("/policy/api-key/:apiKey/chain-id/:chainId/latest", async (request, reply) => { + try { + const apiKey = request + .params.apiKey; + const chainId = request.params.chainId; + + if (!apiKey || !chainId) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_DATA }); + } + + // get wallet_address from api_key + const apiKeyData = await server.apiKeyRepository.findOneByApiKey(apiKey); + if (!apiKeyData) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); + + // get all sponsorshipPolicies for the user from walletAddress and entrypoint version + const walletAddress = apiKeyData.walletAddress; + + if (!walletAddress || !chainId) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_DATA }); + } + + const result = await server.sponsorshipPolicyRepository.findOneByWalletAddressAndChain(walletAddress, chainId); + if (!result) { + return reply.code(ReturnCode.NOT_FOUND).send({ error: ErrorMessage.SPONSORSHIP_POLICY_NOT_FOUND }); + } + + 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 }); + } + }) + + // find latest by walletAddress And chainId + server.get<{ Params: RouteParams }>("/policy/wallet-address/:walletAddress/chain-id/:chainId/latest", async (request, reply) => { + try { + const walletAddress = request.params.walletAddress; + const chainId = request.params.chainId; + + if (!walletAddress || !chainId) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_DATA }); + } + + const result = await server.sponsorshipPolicyRepository.findOneByWalletAddressAndChain(walletAddress, chainId); + if (!result) { + return reply.code(ReturnCode.NOT_FOUND).send({ error: ErrorMessage.SPONSORSHIP_POLICY_NOT_FOUND }); + } + + 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 }); + } + }) + + // find latest by apiKey and EPVersion + server.get<{ Params: RouteParams }>("/policy/api-key/:apiKey/ep-version/:epVersion/latest", async (request, reply) => { + try { + const apiKey = request.params.apiKey; + const epVersion = request.params.epVersion; + + if (!apiKey || !epVersion) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_DATA }); + } + + // get wallet_address from api_key + const apiKeyData = await server.apiKeyRepository.findOneByApiKey(apiKey); + if (!apiKeyData) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); + + // get sponsorshipPolicy for the user from walletAddress and entrypoint version + const sponsorshipPolicy: SponsorshipPolicy | null = await server.sponsorshipPolicyRepository.findOneByWalletAddressAndSupportedEPVersion(apiKeyData?.walletAddress, getEPVersion(epVersion)); + if (!sponsorshipPolicy) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.SPONSORSHIP_POLICY_NOT_FOUND }); + if (!Object.assign(new SponsorshipPolicy(), sponsorshipPolicy).isApplicable) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.NO_ACTIVE_SPONSORSHIP_POLICY_FOR_CURRENT_TIME }); + + return reply.code(ReturnCode.SUCCESS).send(sponsorshipPolicy); + } catch (err: any) { + request.log.error(err); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_QUERY_SPONSORSHIP_POLICY }); + } + }) + + // find latest By WalletAddress And EPVersion + server.get<{ Params: RouteParams }>("/policy/wallet-address/:walletAddress/ep-version/:epVersion/latest", async (request, reply) => { + try { + const walletAddress = request.params.walletAddress; + const epVersion = request.params.epVersion; + + if (!walletAddress || !epVersion) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_DATA }); + } + + const result = await server.sponsorshipPolicyRepository.findOneByWalletAddressAndSupportedEPVersion(walletAddress, getEPVersion(epVersion)); + if (!result) { + return reply.code(ReturnCode.NOT_FOUND).send({ error: ErrorMessage.SPONSORSHIP_POLICY_NOT_FOUND }); + } + + 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 }); + } + }) + + // find latest policy for a given apiKey, epVersion, and chainId + server.get<{ Params: RouteParams }>("/policy/api-key/:apiKey/ep-version/:epVersion/chain-id/:chainId/latest", async (request, reply) => { + try { + const apiKey = request.params.apiKey; + const epVersion = request.params.epVersion; + const chainId = Number(request.params.chainId); + + if (!apiKey || !epVersion) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_DATA }); + } + + // get wallet_address from api_key + const apiKeyData = await server.apiKeyRepository.findOneByApiKey(apiKey); + if (!apiKeyData) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); + + // get sponsorshipPolicy for the user from walletAddress and entrypoint version + const sponsorshipPolicy: SponsorshipPolicy | null = await server.sponsorshipPolicyRepository.findOneByWalletAddressAndSupportedEPVersionAndChain(apiKeyData?.walletAddress, getEPVersion(epVersion), chainId); + if (!sponsorshipPolicy) { + const errorMessage: string = generateErrorMessage(ErrorMessage.ACTIVE_SPONSORSHIP_POLICY_NOT_FOUND, { walletAddress: apiKeyData?.walletAddress, epVersion: epVersion, chainId: chainId }); + return reply.code(ReturnCode.FAILURE).send({ error: errorMessage }); + } + + if (!Object.assign(new SponsorshipPolicy(), sponsorshipPolicy).isApplicable) { + const errorMessage: string = generateErrorMessage(ErrorMessage.NO_ACTIVE_SPONSORSHIP_POLICY_FOR_CURRENT_TIME, { walletAddress: apiKeyData?.walletAddress, epVersion: epVersion, chainId: chainId }); + return reply.code(ReturnCode.FAILURE).send({ error: errorMessage }); + } + + return reply.code(ReturnCode.SUCCESS).send(sponsorshipPolicy); + } catch (err: any) { + request.log.error(err); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_QUERY_SPONSORSHIP_POLICY }); + } + }) + + // find latest policy for a given walletAddress, epVersion, and chainId + server.get<{ Params: RouteParams }>("/policy/wallet-address/:walletAddress/ep-version/:epVersion/chain-id/:chainId/latest", async (request, reply) => { + try { + const walletAddress = request.params.walletAddress; + const epVersion = request.params.epVersion; + const chainId = Number(request.params.chainId); + + if (!walletAddress || !epVersion || chainId === undefined || isNaN(chainId)) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_DATA }); + } + + // get sponsorshipPolicy for the user from walletAddress and entrypoint version + const sponsorshipPolicy: SponsorshipPolicy | null = await server.sponsorshipPolicyRepository.findOneByWalletAddressAndSupportedEPVersion(walletAddress, getEPVersion(epVersion)); + if (!sponsorshipPolicy) { + const errorMessage: string = generateErrorMessage(ErrorMessage.ACTIVE_SPONSORSHIP_POLICY_NOT_FOUND, { walletAddress: walletAddress, epVersion: epVersion, chainId: chainId }); + return reply.code(ReturnCode.FAILURE).send({ error: errorMessage }); + } + + if (!Object.assign(new SponsorshipPolicy(), sponsorshipPolicy).isApplicable) { + const errorMessage: string = generateErrorMessage(ErrorMessage.NO_ACTIVE_SPONSORSHIP_POLICY_FOR_CURRENT_TIME, { walletAddress: walletAddress, epVersion: epVersion, chainId: chainId }); + return reply.code(ReturnCode.FAILURE).send({ error: errorMessage }); + } + + return reply.code(ReturnCode.SUCCESS).send(sponsorshipPolicy); + } catch (err: any) { + request.log.error(err); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_QUERY_SPONSORSHIP_POLICY }); + } + }) + + // create a new policy + server.post("/add-policy", 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 + }); + } + + // apiKey has supportedNetworks and validate if the enabledChains array in SponsorshipPolicyDto is a subset of supportedNetworks + const supportedNetworks = apiKey.supportedNetworks; + if (!supportedNetworks) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + } + const apiKeySupportedChains: number[] = getChainIdsFromSupportedNetworks(supportedNetworks as string); + const sponsorshipPolicySupportedChains = sponsorshipPolicyDto.enabledChains; + + if (sponsorshipPolicySupportedChains && sponsorshipPolicySupportedChains.length > 0) { + + if (!sponsorshipPolicySupportedChains.every((chainId: number) => sponsorshipPolicySupportedChains.includes(chainId))) { + + //generate a comma separate string of sponsorshipPolicySupportedChains + const sponsorshipPolicySupportedChainsCSV: string = sponsorshipPolicySupportedChains.join(','); + + //generate a comma separated string of apiKeySupportedChains + const apiKeySupportedChainsCSV: string = apiKeySupportedChains.join(','); + const errorMessage: string = generateErrorMessage(ErrorMessage.SPONSORSHIP_POLICY_CHAINS_NOT_IN_SUBSET_OF_APIKEY_SUPPORTED_CHAINS, { sponsorshipPolicyChains: sponsorshipPolicySupportedChainsCSV, apiKeyChains: apiKeySupportedChainsCSV }); + + return reply.code(ReturnCode.FAILURE).send({ error: errorMessage }); + } + } + + // TODO this needs to be replaced where user should enable after creation + // TODO default policy should be disabled + sponsorshipPolicyDto.isEnabled = true; + + 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 }); + } + }) + + // delete a policy + server.delete<{ Params: RouteParams }>("/delete-policy/: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 }); + } + }); + + // update a policy + server.put<{ Body: SponsorshipPolicyDto }>("/update-policy", 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 }); + } + }); + + // enable policy + server.put<{ Params: RouteParams }>("/enable-policy/:id", async (request, reply) => { + try { + const id = Number(request.params.id); + if (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 }); + } + + const updatedPolicy = await server.sponsorshipPolicyRepository.enableSponsorshipPolicy(id); + return reply.code(ReturnCode.SUCCESS).send(updatedPolicy); + } catch (err) { + request.log.error(err); + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.FAILED_TO_ENABLE_SPONSORSHIP_POLICY }); + } + }); + + // disable policy + server.put<{ Params: RouteParams }>("/disable-policy/:id", async (request, reply) => { + try { + const id = Number(request.params.id); + if (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 }); + } + + const updatedPolicy = await server.sponsorshipPolicyRepository.disableSponsorshipPolicy(id); + return reply.code(ReturnCode.SUCCESS).send(updatedPolicy); + } catch (err) { + request.log.error(err); + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.FAILED_TO_DISABLE_SPONSORSHIP_POLICY }); + } + }); +}; + +export default sponsorshipPolicyRoutes; diff --git a/backend/src/routes/whitelist-routes.ts b/backend/src/routes/whitelist-routes.ts new file mode 100644 index 0000000..924dd27 --- /dev/null +++ b/backend/src/routes/whitelist-routes.ts @@ -0,0 +1,204 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { FastifyPluginAsync } from "fastify"; +import { ethers } from "ethers"; +import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; +import { Paymaster } from "../paymaster/index.js"; +import SupportedNetworks from "../../config.json" assert { type: "json" }; +import ErrorMessage from "../constants/ErrorMessage.js"; +import ReturnCode from "../constants/ReturnCode.js"; +import { decode } from "../utils/crypto.js"; +import { printRequest, getNetworkConfig } from "../utils/common.js"; +import { APIKey } from "../models/api-key.js"; + +const SUPPORTED_ENTRYPOINTS = { + 'EPV_06': "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", + 'EPV_07': "0x0000000071727De22E5E9d8BAf0edAc6f37da032" +} + +const whitelistRoutes: FastifyPluginAsync = async (server) => { + const paymaster = new Paymaster(server.config.FEE_MARKUP, server.config.MULTI_TOKEN_MARKUP); + + const prefixSecretId = 'arka_'; + + let client: SecretsManagerClient; + + const unsafeMode: boolean = process.env.UNSAFE_MODE == "true" ? true : false; + + if (!unsafeMode) { + client = new SecretsManagerClient(); + } + + server.post("/whitelist", + async function (request, reply) { + try { + printRequest("/whitelist", request, server.log); + const body: any = request.body; + const query: any = request.query; + const address = body.params[0]; + const chainId = query['chainId'] ?? body.params[1]; + const api_key = query['apiKey'] ?? body.params[2]; + if (!api_key) + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + let privateKey = ''; + let supportedNetworks; + if (!unsafeMode) { + const AWSresponse = await client.send( + new GetSecretValueCommand({ + SecretId: prefixSecretId + api_key, + }) + ); + const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + privateKey = secrets['PRIVATE_KEY']; + supportedNetworks = secrets['SUPPORTED_NETWORKS']; + } else { + const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); + if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + privateKey = decode(apiKeyEntity.privateKey, server.config.HMAC_SECRET); + supportedNetworks = apiKeyEntity.supportedNetworks; + } + if (!privateKey) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + if ( + !Array.isArray(address) || + address.length > 10 || + !chainId || + isNaN(chainId) + ) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); + } + if (server.config.SUPPORTED_NETWORKS == '' && !SupportedNetworks) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + } + const networkConfig = getNetworkConfig(chainId, supportedNetworks ?? '', SUPPORTED_ENTRYPOINTS.EPV_06); + if (!networkConfig) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + const validAddresses = address.every(ethers.utils.isAddress); + if (!validAddresses) return reply.code(ReturnCode.FAILURE).send({ error: "Invalid Address passed" }); + const result = await paymaster.whitelistAddresses(address, networkConfig.contracts.etherspotPaymasterAddress, networkConfig.bundler, privateKey, chainId, server.log); + server.log.info(result, 'Response sent: '); + if (body.jsonrpc) + return reply.code(ReturnCode.SUCCESS).send({ jsonrpc: body.jsonrpc, id: body.id, result, error: null }) + return reply.code(ReturnCode.SUCCESS).send(result); + } catch (err: any) { + request.log.error(err); + if (err.name == "ResourceNotFoundException") + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }) + } + } + ) + + server.post("/removeWhitelist", async function (request, reply) { + try { + printRequest("/removeWhitelist", request, server.log); + const body: any = request.body; + const query: any = request.query; + const address = body.params[0]; + const chainId = query['chainId'] ?? body.params[1]; + const api_key = query['apiKey'] ?? body.params[2]; + if (!api_key) + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + let privateKey = ''; + let supportedNetworks; + if (!unsafeMode) { + const AWSresponse = await client.send( + new GetSecretValueCommand({ + SecretId: prefixSecretId + api_key, + }) + ); + const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + privateKey = secrets['PRIVATE_KEY']; + supportedNetworks = secrets['SUPPORTED_NETWORKS']; + } else { + const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); + if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + privateKey = decode(apiKeyEntity.privateKey, server.config.HMAC_SECRET); + supportedNetworks = apiKeyEntity.supportedNetworks; + } + if (!privateKey) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + if ( + !Array.isArray(address) || + address.length > 10 || + !chainId || + isNaN(chainId) + ) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); + } + if (server.config.SUPPORTED_NETWORKS == '' && !SupportedNetworks) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + } + const networkConfig = getNetworkConfig(chainId, supportedNetworks ?? '', SUPPORTED_ENTRYPOINTS.EPV_06); + if (!networkConfig) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + const validAddresses = address.every(ethers.utils.isAddress); + if (!validAddresses) return reply.code(ReturnCode.FAILURE).send({ error: "Invalid Address passed" }); + const result = await paymaster.removeWhitelistAddress(address, networkConfig.contracts.etherspotPaymasterAddress, networkConfig.bundler, privateKey, chainId, server.log); + if (body.jsonrpc) + return reply.code(ReturnCode.SUCCESS).send({ jsonrpc: body.jsonrpc, id: body.id, result, error: null }) + return reply.code(ReturnCode.SUCCESS).send(result); + } catch (err: any) { + request.log.error(err); + if (err.name == "ResourceNotFoundException") + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }) + } + }) + + server.post("/checkWhitelist", + async function (request, reply) { + try { + printRequest("/checkWhitelist", request, server.log); + const body: any = request.body; + const query: any = request.query; + const accountAddress = body.params[0]; + const chainId = query['chainId'] ?? body.params[1]; + const api_key = query['apiKey'] ?? body.params[2]; + if (!api_key) + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + let privateKey = ''; + let supportedNetworks; + if (!unsafeMode) { + const AWSresponse = await client.send( + new GetSecretValueCommand({ + SecretId: prefixSecretId + api_key, + }) + ); + const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + privateKey = secrets['PRIVATE_KEY']; + supportedNetworks = secrets['SUPPORTED_NETWORKS']; + } else { + const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); + if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + privateKey = decode(apiKeyEntity.privateKey, server.config.HMAC_SECRET); + supportedNetworks = apiKeyEntity.supportedNetworks; + } + if (!privateKey) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) + if ( + !accountAddress || + !ethers.utils.isAddress(accountAddress) || + !chainId || + isNaN(chainId) + ) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); + } + if (server.config.SUPPORTED_NETWORKS == '' && !SupportedNetworks) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + } + const networkConfig = getNetworkConfig(chainId, supportedNetworks ?? '', SUPPORTED_ENTRYPOINTS.EPV_06); + if (!networkConfig) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_NETWORK }); + const response = await paymaster.checkWhitelistAddress(accountAddress, networkConfig.contracts.etherspotPaymasterAddress, networkConfig.bundler, privateKey, server.log); + server.log.info(response, 'Response sent: '); + if (body.jsonrpc) + return reply.code(ReturnCode.SUCCESS).send({ jsonrpc: body.jsonrpc, id: body.id, result: { message: response === true ? 'Already added' : 'Not added yet' }, error: null }) + return reply.code(ReturnCode.SUCCESS).send({ message: response === true ? 'Already added' : 'Not added yet' }); + } catch (err: any) { + request.log.error(err); + if (err.name == "ResourceNotFoundException") + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS }) + } + } + ) +}; + +export default whitelistRoutes; diff --git a/backend/src/server.ts b/backend/src/server.ts index 07e5615..f532946 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -6,16 +6,24 @@ import fastifyCron from 'fastify-cron'; 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'; -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 adminRoutes from './routes/admin-routes.js'; +import depositRoutes from './routes/deposit-route.js'; +import metadataRoutes from './routes/metadata-routes.js'; +import paymasterRoutes from './routes/paymaster-routes.js'; +import pimlicoRoutes from './routes/pimlico-routes.js'; +import whitelistRoutes from './routes/whitelist-routes.js'; +import sponsorshipPolicyRoutes from './routes/sponsorship-policy-routes.js'; let server: FastifyInstance; @@ -46,21 +54,31 @@ const initializeServer = async (): Promise => { logLevel: "warn" }); - await server.register(routes); + await server.register(paymasterRoutes); await server.register(adminRoutes); await server.register(metadataRoutes); - // Database - await server.register(database); + await server.register(depositRoutes); - const ConfigData: any = await new Promise(resolve => { - server.sqlite.db.get("SELECT * FROM config", (err, row) => { - if (err) resolve(null); - resolve(row); - }); - }); + await server.register(pimlicoRoutes); + + await server.register(whitelistRoutes); + + await server.register(sponsorshipPolicyRoutes); + + // 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,34 +86,35 @@ 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()); Object.keys(DEPLOYED_ERC20_PAYMASTERS).forEach(async (chain) => { - const networkConfig = getNetworkConfig(chain, ''); + //EP-v6 entrypoint address + const networkConfig = getNetworkConfig(chain, '', "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"); if (networkConfig) { const deployedPaymasters: string[] = DEPLOYED_ERC20_PAYMASTERS[chain]; const provider = new providers.JsonRpcProvider(networkConfig.bundler); 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 +131,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 +142,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,19 +210,21 @@ 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, ''); + const networkConfig = getNetworkConfig(chainId, '', "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"); if (networkConfig) { for (const symbol in customPaymasters[chainId]) { checkDeposit(customPaymasters[chainId][symbol], networkConfig.bundler, process.env.WEBHOOK_URL, networkConfig.thresholdValue ?? '0.001', Number(chainId), server.log) 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..c40f4c9 --- /dev/null +++ b/backend/src/types/sponsorship-policy-dto.ts @@ -0,0 +1,65 @@ +// 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 + supportedEPVersions: string[]; // Array of supported entry point versions (EPV_06, EPV_07) + 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 +} + +export enum EPVersions { + EPV_06 = 'EPV_06', + EPV_07 = 'EPV_07', + // Add more versions here as needed +} + +export const DEFAULT_EP_VERSION : EPVersions = EPVersions.EPV_06; + +// a function which takes string value and return EPVersions enum value +export function getEPVersion(value: string): EPVersions { + switch (value) { + case 'EPV_06': + return EPVersions.EPV_06; + case 'EPV_07': + return EPVersions.EPV_07; + default: + throw new Error('Unsupported EP version'); + } +} + +// a function which takes EPVersions enum value and return string value +export function getEPVersionString(value: EPVersions): string { + switch (value) { + case EPVersions.EPV_06: + return 'EPV_06'; + case EPVersions.EPV_07: + return 'EPV_07'; + default: + throw new Error('Unsupported EP version'); + } +} \ No newline at end of file diff --git a/backend/src/utils/common.ts b/backend/src/utils/common.ts index bfc2624..5a529f9 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,19 +18,16 @@ 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 function decodeSupportedNetworks(supportedNetworksForDecode: string) { + const buffer = Buffer.from(supportedNetworksForDecode, "base64"); + return JSON.parse(buffer.toString()); +} + +export function getChainIdsFromSupportedNetworks(supportedNetworksForDecode: string) { + const decodedSupportedNetworks = decodeSupportedNetworks(supportedNetworksForDecode); + if(!decodedSupportedNetworks) + return []; + return decodedSupportedNetworks.map((chain: any) => chain.chainId); } export async function getEtherscanFee(chainId: number, log?: FastifyBaseLogger): Promise { @@ -40,17 +36,14 @@ export async function getEtherscanFee(chainId: number, log?: FastifyBaseLogger): if (etherscanUrlsBase64) { const buffer = Buffer.from(etherscanUrlsBase64, 'base64'); const etherscanUrls = JSON.parse(buffer.toString()); - console.log('etherscanUrl: ', etherscanUrls[chainId]); - if (etherscanUrls[chainId]) { const data = await fetch(etherscanUrls[chainId]); const response: EtherscanResponse = await data.json(); - console.log('Etherscan Response: ', response); if (response.result && typeof response.result === "object" && response.status === "1") { - console.log('setting maxFeePerGas and maxPriorityFeePerGas as received') + if(log) log.info('setting maxFeePerGas and maxPriorityFeePerGas as received') const maxFeePerGas = ethers.utils.parseUnits(response.result.suggestBaseFee, 'gwei') const fastGasPrice = ethers.utils.parseUnits(response.result.FastGasPrice, 'gwei') - return { + return { maxPriorityFeePerGas: fastGasPrice.sub(maxFeePerGas), maxFeePerGas, gasPrice: maxFeePerGas, @@ -58,7 +51,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/backend/src/utils/crypto.ts b/backend/src/utils/crypto.ts index c834b28..297f1a8 100644 --- a/backend/src/utils/crypto.ts +++ b/backend/src/utils/crypto.ts @@ -1,27 +1,24 @@ import crypto, { BinaryToTextEncoding } from 'crypto'; -import getMAC from 'getmac'; -const secret = getMAC(); - -function createDigest(encodedData: string, format: BinaryToTextEncoding) { +function createDigest(encodedData: string, format: BinaryToTextEncoding, hmacSecret: string) { return crypto - .createHmac('sha256', secret) + .createHmac('sha256', hmacSecret) .update(encodedData) .digest(format); } -export function encode(sourceData: string) { +export function encode(sourceData: string, hmacSecret: string) { const json = JSON.stringify(sourceData); const encodedData = Buffer.from(json).toString('base64'); - return `${encodedData}!${createDigest(encodedData, 'base64')}`; + return `${encodedData}!${createDigest(encodedData, 'base64', hmacSecret)}`; } -export function decode(value: string) { +export function decode(value: string, hmacSecret: string) { const [encodedData, sourceDigest] = value.split('!'); if (!encodedData || !sourceDigest) throw new Error('invalid value(s)'); const json = Buffer.from(encodedData, 'base64').toString('utf8'); const decodedData = JSON.parse(json); - const checkDigest = crypto.createHmac('sha256', secret).update(encodedData).digest(); + const checkDigest = crypto.createHmac('sha256', hmacSecret).update(encodedData).digest(); const digestsEqual = crypto.timingSafeEqual( Buffer.from(sourceDigest, 'base64'), checkDigest diff --git a/docker-compose.yml b/docker-compose.yml index 4b58bce..3636a75 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,18 +15,24 @@ services: backend: environment: - - LOG_LEVEL=debug - - API_HOST=0.0.0.0 - - API_PORT=5050 - - UNSAFE_MODE=false - - SUPPORTED_NETWORKS= - - CRON_PRIVATE_KEY= - - DEFAULT_INDEXER_ENDPOINT=http://localhost:3003 - - FEE_MARKUP= - - MULTI_TOKEN_MARKUP=1150000 - - ETHERSCAN_GAS_ORACLES= - - DEFAULT_API_KEY= - - WEBHOOK_URL= + - LOG_LEVEL=${LOG_LEVEL} + - API_HOST=${API_HOST} + - API_PORT=${API_PORT} + - UNSAFE_MODE=${UNSAFE_MODE} + - SUPPORTED_NETWORKS=${SUPPORTED_NETWORKS} + - CRON_PRIVATE_KEY=${CRON_PRIVATE_KEY} + - DEFAULT_INDEXER_ENDPOINT=${DEFAULT_INDEXER_ENDPOINT} + - FEE_MARKUP=${FEE_MARKUP} + - MULTI_TOKEN_MARKUP=${MULTI_TOKEN_MARKUP} + - ETHERSCAN_GAS_ORACLES=${ETHERSCAN_GAS_ORACLES} + - DEFAULT_API_KEY=${DEFAULT_API_KEY} + - WEBHOOK_URL=${WEBHOOK_URL} + - ADMIN_WALLET_ADDRESS=${ADMIN_WALLET_ADDRESS} + - HMAC_SECRET=${HMAC_SECRET} + - DATABASE_URL=${DATABASE_URL} + - DATABASE_SCHEMA_NAME=${DATABASE_SCHEMA_NAME} + - DATABASE_SSL_ENABLED=${DATABASE_SSL_ENABLED} + - DATABASE_SSL_REJECT_UNAUTHORIZED=${DATABASE_SSL_REJECT_UNAUTHORIZED} build: context: ./backend dockerfile: Dockerfile @@ -45,4 +51,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 = []; diff --git a/local-setup/docker-compose.yml b/local-setup/docker-compose.yml new file mode 100644 index 0000000..680f7a0 --- /dev/null +++ b/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/local-setup/init.sql b/local-setup/init.sql new file mode 100644 index 0000000..521f216 --- /dev/null +++ b/local-setup/init.sql @@ -0,0 +1 @@ +CREATE SCHEMA IF NOT EXISTS arka AUTHORIZATION arkauser; \ No newline at end of file