diff --git a/backend/Dockerfile b/backend/Dockerfile index 6202c81..53e461e 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -19,7 +19,6 @@ COPY --from=builder /app/build ./build COPY ./migrations ./build/migrations COPY package.json ./ COPY --from=builder /app/config.json.default /usr/app/config.json -RUN touch database.sqlite RUN npm install USER root ENV NODE_ENV="production" diff --git a/backend/README.md b/backend/README.md index 8340009..9cf49c6 100644 --- a/backend/README.md +++ b/backend/README.md @@ -182,3 +182,23 @@ Parameters: - `/deposit` - This url accepts one parameter and returns the submitted transaction hash if successful. This url is used to deposit some funds to the entryPointAddress from the sponsor wallet 1. amount - The amount to be deposited in ETH +## Local Docker Networks + +1. Ensure the postgres docker instance is up and running + +2. Here we need to create a network and tag backend & postgres on same network + +```sh +docker network create arka-network +``` + +```sh +docker run --network arka-network --name local-setup-db-1 -d postgres +``` + +```sh +docker run --network arka-network --name arka-backend -d arka-backend +``` + + + diff --git a/backend/migrations/20240611000002-create-sponsorship-policies.cjs b/backend/migrations/20240611000002-create-sponsorship-policies.cjs index 1a09019..5805bbd 100644 --- a/backend/migrations/20240611000002-create-sponsorship-policies.cjs +++ b/backend/migrations/20240611000002-create-sponsorship-policies.cjs @@ -58,12 +58,12 @@ async function up({ context: queryInterface }) { startDate: { type: Sequelize.DATE, allowNull: true, - field: 'START_DATE' + field: 'START_TIME' }, endDate: { type: Sequelize.DATE, allowNull: true, - field: 'END_DATE' + field: 'END_TIME' }, globalMaxApplicable: { type: Sequelize.BOOLEAN, diff --git a/backend/package.json b/backend/package.json index 3bd99ad..e6afffa 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,7 +10,7 @@ "lint": "eslint .", "lint:fix": "eslint . --fix", "check:types": "tsc --noEmit", - "build": "esbuild `find src \\( -name '*.ts' \\)` --platform=node --outdir=build --resolve-extensions=.js && cp -r ./src/migrations ./build/", + "build": "esbuild `find src \\( -name '*.ts' \\)` --platform=node --outdir=build --resolve-extensions=.js && cp -r ./migrations ./build/", "build:docker:prod": "docker build . -t my-fastify-app --build-arg APP_ENV=production", "start": "node build", "dev": "tsx watch src | pino-pretty --colorize", diff --git a/backend/src/constants/ErrorMessage.ts b/backend/src/constants/ErrorMessage.ts index 74a04b2..545fe8f 100644 --- a/backend/src/constants/ErrorMessage.ts +++ b/backend/src/constants/ErrorMessage.ts @@ -1,9 +1,21 @@ export default { INVALID_DATA: 'Invalid data provided', + INVALID_SPONSORSHIP_POLICY: 'Invalid sponsorship policy data', + INVALID_SPONSORSHIP_POLICY_ID: 'Invalid sponsorship policy id', INVALID_API_KEY: 'Invalid Api Key', UNSUPPORTED_NETWORK: 'Unsupported network', UNSUPPORTED_NETWORK_TOKEN: 'Unsupported network/token', EMPTY_BODY: 'Empty Body received', + API_KEY_DOES_NOT_EXIST_FOR_THE_WALLET_ADDRESS: 'Api Key does not exist for the wallet address', + FAILED_TO_CREATE_SPONSORSHIP_POLICY: 'Failed to create sponsorship policy', + FAILED_TO_UPDATE_SPONSORSHIP_POLICY: 'Failed to update sponsorship policy', + SPONSORSHIP_POLICY_NOT_FOUND: 'Sponsorship policy not found', + SPONSORSHIP_POLICY_ALREADY_EXISTS: 'Sponsorship policy already exists', + SPONSORSHIP_POLICY_IS_DISABLED: 'Sponsorship policy is disabled', + FAILED_TO_DELETE_SPONSORSHIP_POLICY: 'Failed to delete sponsorship policy', + FAILED_TO_ENABLE_SPONSORSHIP_POLICY: 'Failed to enable sponsorship policy', + FAILED_TO_DISABLE_SPONSORSHIP_POLICY: 'Failed to disable sponsorship policy', + FAILED_TO_QUERY_SPONSORSHIP_POLICY: 'Failed to query sponsorship policy', FAILED_TO_PROCESS: 'Failed to process the request. Please try again or contact ARKA support team', INVALID_MODE: 'Invalid mode selected', DUPLICATE_RECORD: 'Duplicate record found', diff --git a/backend/src/constants/ReturnCode.ts b/backend/src/constants/ReturnCode.ts index bc7fc8d..863ed2f 100644 --- a/backend/src/constants/ReturnCode.ts +++ b/backend/src/constants/ReturnCode.ts @@ -1,4 +1,6 @@ export default { SUCCESS: 200, FAILURE: 400, + BAD_REQUEST: 400, + NOT_FOUND: 404, } diff --git a/backend/src/models/sponsorship-policy.ts b/backend/src/models/sponsorship-policy.ts index 55cb7c5..251c535 100644 --- a/backend/src/models/sponsorship-policy.ts +++ b/backend/src/models/sponsorship-policy.ts @@ -9,20 +9,20 @@ export class SponsorshipPolicy extends Model { public isEnabled: boolean = false; public isApplicableToAllNetworks!: boolean; public enabledChains?: number[]; - public isPerpetual!: boolean; - public startTime!: Date | null; - public endTime!: Date | null; + public isPerpetual: boolean = false; + public startTime: Date | null = null; + public endTime: Date | null = null; public globalMaximumApplicable: boolean = false; - public globalMaximumUsd!: number | null; - public globalMaximumNative!: number | null; - public globalMaximumOpCount!: number | null; + public globalMaximumUsd: number | null = null; + public globalMaximumNative: number | null = null; + public globalMaximumOpCount: number | null = null; public perUserMaximumApplicable: boolean = false; - public perUserMaximumUsd!: number | null; - public perUserMaximumNative!: number | null; - public perUserMaximumOpCount!: number | null; + public perUserMaximumUsd: number | null = null; + public perUserMaximumNative: number | null = null; + public perUserMaximumOpCount: number | null = null; public perOpMaximumApplicable: boolean = false; - public perOpMaximumUsd!: number | null; - public perOpMaximumNative!: number | null; + public perOpMaximumUsd: number | null = null; + public perOpMaximumNative: number | null = null; public addressAllowList: string[] | null = null; public addressBlockList: string[] | null = null; public readonly createdAt!: Date; @@ -103,15 +103,15 @@ export function initializeSponsorshipPolicyModel(sequelize: Sequelize, schema: s defaultValue: false, field: 'IS_PERPETUAL' }, - startDate: { + startTime: { type: DataTypes.DATE, allowNull: true, - field: 'START_DATE' + field: 'START_TIME' }, - endDate: { + endTime: { type: DataTypes.DATE, allowNull: true, - field: 'END_DATE' + field: 'END_TIME' }, globalMaximumApplicable: { type: DataTypes.BOOLEAN, diff --git a/backend/src/plugins/config.ts b/backend/src/plugins/config.ts index e09177d..9ed9f2c 100644 --- a/backend/src/plugins/config.ts +++ b/backend/src/plugins/config.ts @@ -38,6 +38,7 @@ export type ArkaConfig = Static; const configPlugin: FastifyPluginAsync = async (server) => { const validate = ajv.compile(ConfigSchema); + server.log.info("Validating .env file"); const valid = validate(process.env); if (!valid) { throw new Error( @@ -46,6 +47,8 @@ const configPlugin: FastifyPluginAsync = async (server) => { ); } + server.log.info("Configuring .env file"); + const config = { LOG_LEVEL: process.env.LOG_LEVEL ?? '', API_PORT: process.env.API_PORT ?? '', @@ -59,6 +62,9 @@ const configPlugin: FastifyPluginAsync = async (server) => { DATABASE_SCHEMA_NAME: process.env.DATABASE_SCHEMA_NAME ?? 'arka', } + server.log.info("Configured .env file"); + server.log.info(`config: ${JSON.stringify(config, null, 2)}`); + server.decorate("config", config); }; diff --git a/backend/src/plugins/db.ts b/backend/src/plugins/db.ts index 86647bb..2758b67 100644 --- a/backend/src/plugins/db.ts +++ b/backend/src/plugins/db.ts @@ -8,6 +8,8 @@ import { Umzug, SequelizeStorage } from 'umzug'; const databasePlugin: FastifyPluginAsync = async (server) => { + server.log.info(`Connecting to database... with URL: ${server.config.DATABASE_URL} and schemaName: ${server.config.DATABASE_SCHEMA_NAME}`); + const sequelize = new Sequelize(server.config.DATABASE_URL, { schema: server.config.DATABASE_SCHEMA_NAME, }); @@ -32,8 +34,6 @@ const databasePlugin: FastifyPluginAsync = async (server) => { console.error('Migration failed:', err) process.exitCode = 1 } - - //server.decorate('sequelize', sequelize); }; declare module "fastify" { diff --git a/backend/src/plugins/sequelizePlugin.ts b/backend/src/plugins/sequelizePlugin.ts index 4dcd264..c84ab85 100644 --- a/backend/src/plugins/sequelizePlugin.ts +++ b/backend/src/plugins/sequelizePlugin.ts @@ -7,6 +7,7 @@ import { initializeSponsorshipPolicyModel } from '../models/sponsorship-policy.j import { initializeArkaConfigModel } from "../models/arka-config.js"; import { APIKeyRepository } from "../repository/api-key-repository.js"; import { ArkaConfigRepository } from "../repository/arka-config-repository.js"; +import { SponsorshipPolicyRepository } from "../repository/sponsorship-policy-repository.js"; const pg = await import('pg'); const Client = pg.default.Client; @@ -43,9 +44,10 @@ const sequelizePlugin: FastifyPluginAsync = async (server) => { // Initialize models initializeArkaConfigModel(sequelize, server.config.DATABASE_SCHEMA_NAME); const initializedAPIKeyModel = initializeAPIKeyModel(sequelize, server.config.DATABASE_SCHEMA_NAME); - sequelize.models.APIKey = initializedAPIKeyModel; + //sequelize.models.APIKey = initializedAPIKeyModel; server.log.info(`Initialized APIKey model... ${sequelize.models.APIKey}`); initializeSponsorshipPolicyModel(sequelize, server.config.DATABASE_SCHEMA_NAME); + server.log.info('Initialized SponsorshipPolicy model...'); server.log.info('Initialized all models...'); @@ -55,6 +57,8 @@ const sequelizePlugin: FastifyPluginAsync = async (server) => { server.decorate('apiKeyRepository', apiKeyRepository); const arkaConfigRepository : ArkaConfigRepository = new ArkaConfigRepository(sequelize); server.decorate('arkaConfigRepository', arkaConfigRepository); + const sponsorshipPolicyRepository = new SponsorshipPolicyRepository(sequelize); + server.decorate('sponsorshipPolicyRepository', sponsorshipPolicyRepository); server.log.info('decorated fastify server with models...'); @@ -70,6 +74,7 @@ declare module "fastify" { sequelize: Sequelize; apiKeyRepository: APIKeyRepository; arkaConfigRepository: ArkaConfigRepository; + sponsorshipPolicyRepository: SponsorshipPolicyRepository; } } diff --git a/backend/src/repository/sponsorship-policy-repository.ts b/backend/src/repository/sponsorship-policy-repository.ts index 3d44434..eced7fb 100644 --- a/backend/src/repository/sponsorship-policy-repository.ts +++ b/backend/src/repository/sponsorship-policy-repository.ts @@ -1,5 +1,7 @@ import { Sequelize, Op } from 'sequelize'; import { SponsorshipPolicy } from '../models/sponsorship-policy'; +import { SponsorshipPolicyDto } from '../types/sponsorship-policy-dto'; +import { ethers } from 'ethers'; export class SponsorshipPolicyRepository { private sequelize: Sequelize; @@ -67,7 +69,7 @@ export class SponsorshipPolicyRepository { return result ? result.get() as SponsorshipPolicy : null; } - async createSponsorshipPolicy(sponsorshipPolicy: SponsorshipPolicy): Promise { + async createSponsorshipPolicy(sponsorshipPolicy: SponsorshipPolicyDto): Promise { this.validateSponsorshipPolicy(sponsorshipPolicy); const result = await this.sequelize.models.SponsorshipPolicy.create({ @@ -99,7 +101,7 @@ export class SponsorshipPolicyRepository { return result.get() as SponsorshipPolicy; } - async updateSponsorshipPolicy(sponsorshipPolicy: SponsorshipPolicy): Promise { + async updateSponsorshipPolicy(sponsorshipPolicy: SponsorshipPolicyDto): Promise { // check if sponsorship policy exists (by primary key id) const existingSponsorshipPolicy = await this.findOneById(sponsorshipPolicy.id as number); @@ -119,29 +121,110 @@ export class SponsorshipPolicyRepository { existingSponsorshipPolicy.startTime = null; existingSponsorshipPolicy.endTime = null; } else { - existingSponsorshipPolicy.startTime = sponsorshipPolicy.startTime; - existingSponsorshipPolicy.endTime = sponsorshipPolicy.endTime; + + if (!sponsorshipPolicy.startTime || sponsorshipPolicy.startTime == null) { + existingSponsorshipPolicy.startTime = null; + } else { + existingSponsorshipPolicy.startTime = sponsorshipPolicy.startTime; + } + + if (!sponsorshipPolicy.endTime || sponsorshipPolicy.endTime == null) { + existingSponsorshipPolicy.endTime = null; + } else { + existingSponsorshipPolicy.endTime = sponsorshipPolicy.endTime; + } } + existingSponsorshipPolicy.globalMaximumApplicable = sponsorshipPolicy.globalMaximumApplicable; - existingSponsorshipPolicy.globalMaximumUsd = sponsorshipPolicy.globalMaximumUsd; - existingSponsorshipPolicy.globalMaximumNative = sponsorshipPolicy.globalMaximumNative; - existingSponsorshipPolicy.globalMaximumOpCount = sponsorshipPolicy.globalMaximumOpCount; + + if (existingSponsorshipPolicy.globalMaximumApplicable) { + if (!sponsorshipPolicy.globalMaximumUsd || sponsorshipPolicy.globalMaximumUsd == null) { + existingSponsorshipPolicy.globalMaximumUsd = null; + } else { + existingSponsorshipPolicy.globalMaximumUsd = sponsorshipPolicy.globalMaximumUsd; + } + + if (!sponsorshipPolicy.globalMaximumNative || sponsorshipPolicy.globalMaximumNative == null) { + existingSponsorshipPolicy.globalMaximumNative = null; + } else { + existingSponsorshipPolicy.globalMaximumNative = sponsorshipPolicy.globalMaximumNative; + } + + if (!sponsorshipPolicy.globalMaximumOpCount || sponsorshipPolicy.globalMaximumOpCount == null) { + existingSponsorshipPolicy.globalMaximumOpCount = null; + } else { + existingSponsorshipPolicy.globalMaximumOpCount = sponsorshipPolicy.globalMaximumOpCount; + } + } else { + existingSponsorshipPolicy.globalMaximumUsd = null; + existingSponsorshipPolicy.globalMaximumNative = null; + existingSponsorshipPolicy.globalMaximumOpCount = null; + } + existingSponsorshipPolicy.perUserMaximumApplicable = sponsorshipPolicy.perUserMaximumApplicable; - existingSponsorshipPolicy.perUserMaximumNative = sponsorshipPolicy.perUserMaximumNative; - existingSponsorshipPolicy.perUserMaximumOpCount = sponsorshipPolicy.perUserMaximumOpCount; - existingSponsorshipPolicy.perUserMaximumUsd = sponsorshipPolicy.perUserMaximumUsd; + + if (existingSponsorshipPolicy.perUserMaximumApplicable) { + if (!sponsorshipPolicy.perUserMaximumUsd || sponsorshipPolicy.perUserMaximumUsd == null) { + existingSponsorshipPolicy.perUserMaximumUsd = null; + } else { + existingSponsorshipPolicy.perUserMaximumUsd = sponsorshipPolicy.perUserMaximumUsd; + } + + if (!sponsorshipPolicy.perUserMaximumNative || sponsorshipPolicy.perUserMaximumNative == null) { + existingSponsorshipPolicy.perUserMaximumNative = null; + } else { + existingSponsorshipPolicy.perUserMaximumNative = sponsorshipPolicy.perUserMaximumNative; + } + + if (!sponsorshipPolicy.perUserMaximumOpCount || sponsorshipPolicy.perUserMaximumOpCount == null) { + existingSponsorshipPolicy.perUserMaximumOpCount = null; + } else { + existingSponsorshipPolicy.perUserMaximumOpCount = sponsorshipPolicy.perUserMaximumOpCount; + } + } else { + existingSponsorshipPolicy.perUserMaximumUsd = null; + existingSponsorshipPolicy.perUserMaximumNative = null; + existingSponsorshipPolicy.perUserMaximumOpCount = null; + } + existingSponsorshipPolicy.perOpMaximumApplicable = sponsorshipPolicy.perOpMaximumApplicable; - existingSponsorshipPolicy.perOpMaximumNative = sponsorshipPolicy.perOpMaximumNative; - existingSponsorshipPolicy.perOpMaximumUsd = sponsorshipPolicy.perOpMaximumUsd; + + if (existingSponsorshipPolicy.perOpMaximumApplicable) { + if (!sponsorshipPolicy.perOpMaximumUsd || sponsorshipPolicy.perOpMaximumUsd == null) { + existingSponsorshipPolicy.perOpMaximumUsd = null; + } else { + existingSponsorshipPolicy.perOpMaximumUsd = sponsorshipPolicy.perOpMaximumUsd; + } + + if (!sponsorshipPolicy.perOpMaximumNative || sponsorshipPolicy.perOpMaximumNative == null) { + existingSponsorshipPolicy.perOpMaximumNative = null; + } else { + existingSponsorshipPolicy.perOpMaximumNative = sponsorshipPolicy.perOpMaximumNative; + } + } else { + existingSponsorshipPolicy.perOpMaximumUsd = null; + existingSponsorshipPolicy.perOpMaximumNative = null; + } + existingSponsorshipPolicy.isPublic = sponsorshipPolicy.isPublic; - existingSponsorshipPolicy.addressAllowList = sponsorshipPolicy.addressAllowList; - existingSponsorshipPolicy.addressBlockList = sponsorshipPolicy.addressBlockList; + + if (existingSponsorshipPolicy.addressAllowList && existingSponsorshipPolicy.addressAllowList.length > 0) { + existingSponsorshipPolicy.addressAllowList = sponsorshipPolicy.addressAllowList as string[]; + } else { + existingSponsorshipPolicy.addressAllowList = null; + } + + if (existingSponsorshipPolicy.addressBlockList && existingSponsorshipPolicy.addressBlockList.length > 0) { + existingSponsorshipPolicy.addressBlockList = sponsorshipPolicy.addressBlockList as string[]; + } else { + existingSponsorshipPolicy.addressBlockList = null; + } const result = await existingSponsorshipPolicy.save(); return result.get() as SponsorshipPolicy; } - validateSponsorshipPolicy(sponsorshipPolicy: SponsorshipPolicy) { + validateSponsorshipPolicy(sponsorshipPolicy: SponsorshipPolicyDto) { let errors: string[] = []; if (!sponsorshipPolicy.name || !sponsorshipPolicy.description) { @@ -159,9 +242,20 @@ export class SponsorshipPolicyRepository { errors.push('Start and End time are required fields'); } + const currentTime = new Date(); + if (sponsorshipPolicy.startTime && sponsorshipPolicy.endTime) { - if (sponsorshipPolicy.startTime < new Date() || sponsorshipPolicy.endTime < new Date() || sponsorshipPolicy.endTime < sponsorshipPolicy.startTime) { - errors.push('Invalid start and end time'); + const startTime = new Date(sponsorshipPolicy.startTime + 'Z'); + const endTime = new Date(sponsorshipPolicy.endTime + 'Z'); + + if (startTime.getTime() < currentTime.getTime()) { + errors.push(`Invalid start time. Provided start time is ${startTime.toISOString()} in GMT. The start time must be now or in the future. Current time is ${currentTime.toISOString()} in GMT.`); + } + if (endTime.getTime() < currentTime.getTime()) { + errors.push(`Invalid end time. Provided end time is ${endTime.toISOString()} in GMT. The end time must be in the future. Current time is ${currentTime.toISOString()} in GMT.`); + } + if (endTime.getTime() < startTime.getTime()) { + errors.push(`Invalid end time. Provided end time is ${endTime.toISOString()} in GMT and start time is ${startTime.toISOString()} in GMT. The end time must be greater than the start time.`); } } } @@ -170,18 +264,101 @@ export class SponsorshipPolicyRepository { if (!sponsorshipPolicy.globalMaximumUsd && !sponsorshipPolicy.globalMaximumNative && !sponsorshipPolicy.globalMaximumOpCount) { errors.push('At least 1 Global maximum value is required'); } + + const globalMaximumUsd = sponsorshipPolicy.globalMaximumUsd; + + if (globalMaximumUsd !== undefined && globalMaximumUsd !== null) { + const parts = globalMaximumUsd.toString().split('.'); + if (parts.length > 2 || parts[0].length > 6 || (parts[1] && parts[1].length > 4)) { + errors.push(`Invalid value for globalMaximumUsd. The value ${globalMaximumUsd} exceeds the maximum allowed precision of 10 total digits, with a maximum of 4 digits allowed after the decimal point.`); + } + } + + const globalMaximumNative = sponsorshipPolicy.globalMaximumNative; + + if (globalMaximumNative !== undefined && globalMaximumNative !== null) { + const parts = globalMaximumNative.toString().split('.'); + if (parts.length > 2 || parts[0].length > 4 || (parts[1] && parts[1].length > 18)) { + errors.push(`Invalid value for globalMaximumNative. The value ${globalMaximumNative} exceeds the maximum allowed precision of 22 total digits, with a maximum of 18 digits allowed after the decimal point.`); + } + } } if (sponsorshipPolicy.perUserMaximumApplicable) { if (!sponsorshipPolicy.perUserMaximumUsd && !sponsorshipPolicy.perUserMaximumNative && !sponsorshipPolicy.perUserMaximumOpCount) { errors.push('At least 1 Per User maximum value is required'); } + + const perUserMaximumUsd = sponsorshipPolicy.perUserMaximumUsd; + + if (perUserMaximumUsd !== undefined && perUserMaximumUsd !== null) { + const parts = perUserMaximumUsd.toString().split('.'); + if (parts.length > 2 || parts[0].length > 6 || (parts[1] && parts[1].length > 4)) { + errors.push(`Invalid value for perUserMaximumUsd. The value ${perUserMaximumUsd} exceeds the maximum allowed precision of 10 total digits, with a maximum of 4 digits allowed after the decimal point.`); + } + } + + const perUserMaximumNative = sponsorshipPolicy.perUserMaximumNative; + + if (perUserMaximumNative !== undefined && perUserMaximumNative !== null) { + const parts = perUserMaximumNative.toString().split('.'); + if (parts.length > 2 || parts[0].length > 4 || (parts[1] && parts[1].length > 18)) { + errors.push(`Invalid value for perUserMaximumNative. The value ${perUserMaximumNative} exceeds the maximum allowed precision of 22 total digits, with a maximum of 18 digits allowed after the decimal point.`); + } + } } if (sponsorshipPolicy.perOpMaximumApplicable) { if (!sponsorshipPolicy.perOpMaximumUsd && !sponsorshipPolicy.perOpMaximumNative) { errors.push('At least 1 Per Op maximum value is required'); } + + const perOpMaximumUsd = sponsorshipPolicy.perOpMaximumUsd; + + if (perOpMaximumUsd !== undefined && perOpMaximumUsd !== null) { + const parts = perOpMaximumUsd.toString().split('.'); + if (parts.length > 2 || parts[0].length > 6 || (parts[1] && parts[1].length > 4)) { + errors.push(`Invalid value for perOpMaximumUsd. The value ${perOpMaximumUsd} exceeds the maximum allowed precision of 10 total digits, with a maximum of 4 digits allowed after the decimal point.`); + } + } + + const perOpMaximumNative = sponsorshipPolicy.perOpMaximumNative; + + if (perOpMaximumNative !== undefined && perOpMaximumNative !== null) { + const parts = perOpMaximumNative.toString().split('.'); + if (parts.length > 2 || parts[0].length > 4 || (parts[1] && parts[1].length > 18)) { + errors.push(`Invalid value for perOpMaximumNative. The value ${perOpMaximumNative} exceeds the maximum allowed precision of 22 total digits, with a maximum of 18 digits allowed after the decimal point.`); + } + } + } + + // check if the addressAllowList and addressBlockList are valid addresses + if (sponsorshipPolicy.addressAllowList && sponsorshipPolicy.addressAllowList.length > 0) { + const invalidAddresses: string[] = []; + + sponsorshipPolicy.addressAllowList.forEach(address => { + if (!address || !ethers.utils.isAddress(address)) { + invalidAddresses.push(address); + } + }); + + if (invalidAddresses.length > 0) { + errors.push(`The following addresses in addressAllowList are invalid: ${invalidAddresses.join(', ')}`); + } + } + + if (sponsorshipPolicy.addressBlockList && sponsorshipPolicy.addressBlockList.length > 0) { + const invalidAddresses: string[] = []; + + sponsorshipPolicy.addressBlockList.forEach(address => { + if (!address || !ethers.utils.isAddress(address)) { + invalidAddresses.push(address); + } + }); + + if (invalidAddresses.length > 0) { + errors.push(`The following addresses in addressBlockList are invalid: ${invalidAddresses.join(', ')}`); + } } if (errors.length > 0) { @@ -196,6 +373,10 @@ export class SponsorshipPolicyRepository { throw new Error('Sponsorship Policy not found'); } + if (!existingSponsorshipPolicy.isEnabled) { + throw new Error('Cannot disable a policy which is already disabled'); + } + existingSponsorshipPolicy.isEnabled = false; await existingSponsorshipPolicy.save(); } @@ -207,21 +388,40 @@ export class SponsorshipPolicyRepository { throw new Error('Sponsorship Policy not found'); } + if (existingSponsorshipPolicy.isEnabled) { + throw new Error('Cannot enable a policy which is already enabled'); + } + existingSponsorshipPolicy.isEnabled = true; await existingSponsorshipPolicy.save(); } - async deleteSponsorshipPolicy(id: number): Promise { + async deleteSponsorshipPolicy(id: number): Promise { const existingSponsorshipPolicy = await this.findOneById(id); if (!existingSponsorshipPolicy) { - throw new Error('Sponsorship Policy not found'); + throw new Error(`Sponsorship Policy deletion failed as Policy doesnot exist with id: ${id}`); } - await existingSponsorshipPolicy.destroy(); + const deletedCount = await this.sequelize.models.SponsorshipPolicy.destroy({ + where + : { id: id } + }); + + if (deletedCount === 0) { + throw new Error(`SponsorshipPolicy deletion failed for id: ${id}`); + } + + return deletedCount; } - async deleteAllSponsorshipPolicies(): Promise { - await this.sequelize.models.SponsorshipPolicy.destroy({ where: {} }); + async deleteAllSponsorshipPolicies(): Promise<{ message: string }> { + try { + await this.sequelize.models.SponsorshipPolicy.destroy({ where: {} }); + return { message: 'Successfully deleted all policies' }; + } catch (err) { + console.error(err); + throw new Error('Failed to delete all policies'); + } } } \ No newline at end of file diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts index d17ac5b..ac6fe7f 100644 --- a/backend/src/routes/admin.ts +++ b/backend/src/routes/admin.ts @@ -16,7 +16,7 @@ const adminRoutes: FastifyPluginAsync = async (server) => { const body: any = JSON.parse(request.body as string); if (!body) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); if (!body.walletAddress) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_DATA }); - if (ethers.utils.getAddress(body.walletAddress) === server.config.ADMIN_WALLET_ADDRESS) return reply.code(ReturnCode.SUCCESS).send({ error: null, message: "Successfully Logged in" }); + if (ethers.utils.getAddress(body.walletAddress) === ethers.utils.getAddress(server.config.ADMIN_WALLET_ADDRESS)) return reply.code(ReturnCode.SUCCESS).send({ error: null, message: "Successfully Logged in" }); return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_USER }); } catch (err: any) { return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_USER }); diff --git a/backend/src/routes/sponsorship-policy-route.ts b/backend/src/routes/sponsorship-policy-route.ts new file mode 100644 index 0000000..2862a19 --- /dev/null +++ b/backend/src/routes/sponsorship-policy-route.ts @@ -0,0 +1,110 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { FastifyPluginAsync, FastifyReply, FastifyRequest } from "fastify"; +import ErrorMessage from "../constants/ErrorMessage.js"; +import ReturnCode from "../constants/ReturnCode.js"; +import { SponsorshipPolicyDto } from "../types/sponsorship-policy-dto.js"; + +interface RouteParams { + id: string; +} + +const sponsorshipPolicyRoutes: FastifyPluginAsync = async (server) => { + + server.get("/getPolicies", async function (request, reply) { + try { + const result = await server.sponsorshipPolicyRepository.findAll(); + + if (!result) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.FAILED_TO_QUERY_SPONSORSHIP_POLICY }); + } + + return reply.code(ReturnCode.SUCCESS).send(result); + } catch (err: any) { + request.log.error(err); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_QUERY_SPONSORSHIP_POLICY }); + } + }) + + server.post("/addPolicy", async function (request, reply) { + try { + // parse the request body as JSON + const sponsorshipPolicyDto: SponsorshipPolicyDto = JSON.parse(JSON.stringify(request.body)) as SponsorshipPolicyDto; + if (!sponsorshipPolicyDto) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.EMPTY_BODY }); + + // id is to be null + if (sponsorshipPolicyDto.id || sponsorshipPolicyDto.id as number > 0 || + !sponsorshipPolicyDto.walletAddress || + !sponsorshipPolicyDto.name) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_SPONSORSHIP_POLICY }); + } + + // verify if api key exists for the given wallet address + const apiKey = await server.apiKeyRepository.findOneByWalletAddress(sponsorshipPolicyDto.walletAddress); + + if (!apiKey) { + return reply.code(ReturnCode.FAILURE).send({ + error: ErrorMessage.API_KEY_DOES_NOT_EXIST_FOR_THE_WALLET_ADDRESS + }); + } + + const result = await server.sponsorshipPolicyRepository.createSponsorshipPolicy(sponsorshipPolicyDto); + if (!result) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.FAILED_TO_CREATE_SPONSORSHIP_POLICY }); + } + + return reply.code(ReturnCode.SUCCESS).send(result); + } catch (err: any) { + request.log.error(err); + return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_CREATE_SPONSORSHIP_POLICY }); + } + }) + + + server.delete<{ Params: RouteParams }>("/deletePolicy/:id", async (request, reply) => { + try { + const id = Number(request.params.id); + if (isNaN(id)) { + return reply.code(400).send({ error: ErrorMessage.INVALID_SPONSORSHIP_POLICY_ID }); + } + + const result = await server.sponsorshipPolicyRepository.deleteSponsorshipPolicy(id); + return reply.code(200).send({ message: `Successfully deleted policy with id ${id}` }); + } catch (err) { + request.log.error(err); + return reply.code(500).send({ error: ErrorMessage.FAILED_TO_DELETE_SPONSORSHIP_POLICY }); + } + }); + + + server.put<{ Body: SponsorshipPolicyDto }>("/updatePolicy", async (request, reply) => { + try { + const sponsorshipPolicyDto: SponsorshipPolicyDto = JSON.parse(JSON.stringify(request.body)) as SponsorshipPolicyDto; + const id = sponsorshipPolicyDto.id; + + if (!id || isNaN(id)) { + return reply.code(ReturnCode.BAD_REQUEST).send({ error: ErrorMessage.INVALID_SPONSORSHIP_POLICY_ID }); + } + + const existingSponsorshipPolicy = await server.sponsorshipPolicyRepository.findOneById(id); + if (!existingSponsorshipPolicy) { + return reply.code(ReturnCode.NOT_FOUND).send({ error: ErrorMessage.SPONSORSHIP_POLICY_NOT_FOUND }); + } + + // cannot update a disabled policy + if (!existingSponsorshipPolicy.isEnabled) { + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.SPONSORSHIP_POLICY_IS_DISABLED }); + } + + const updatedPolicy = await server.sponsorshipPolicyRepository.updateSponsorshipPolicy(sponsorshipPolicyDto); + return reply.code(ReturnCode.SUCCESS).send(updatedPolicy); + } catch (err) { + request.log.error(err); + return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.FAILED_TO_UPDATE_SPONSORSHIP_POLICY }); + } + }); + + + +}; + +export default sponsorshipPolicyRoutes; diff --git a/backend/src/server.ts b/backend/src/server.ts index 975a133..7eab9bf 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -21,6 +21,7 @@ import { APIKey } from './models/api-key.js'; import { APIKeyRepository } from './repository/api-key-repository.js'; import { ArkaConfig } from './models/arka-config.js'; import { ArkaConfigRepository } from './repository/arka-config-repository.js'; +import sponsorshipPolicyRoutes from './routes/sponsorship-policy-route.js'; let server: FastifyInstance; @@ -57,6 +58,8 @@ const initializeServer = async (): Promise => { await server.register(metadataRoutes); + await server.register(sponsorshipPolicyRoutes); + // Database await server.register(database); diff --git a/backend/src/types/sponsorship-policy-dto.ts b/backend/src/types/sponsorship-policy-dto.ts index 9a25845..b03a1a6 100644 --- a/backend/src/types/sponsorship-policy-dto.ts +++ b/backend/src/types/sponsorship-policy-dto.ts @@ -1,6 +1,6 @@ // DTO for receiving data in the POST request to create a sponsorship policy export interface SponsorshipPolicyDto { - id: number; // ID of the policy + id?: number; // ID of the policy walletAddress: string; // The wallet address associated with the API key name: string; // Name of the sponsorship policy description: string; // Description of the sponsorship policy @@ -9,21 +9,21 @@ export interface SponsorshipPolicyDto { isApplicableToAllNetworks: boolean; // Flag to indicate if the policy is universal enabledChains?: number[]; // Array of enabled chain IDs isPerpetual: boolean; // Flag to indicate if the policy is perpetual - startDate?: string; // Optional start date for the policy - endDate?: string; // Optional end date for the policy + startTime?: Date | null; // Optional start date for the policy + endTime?: Date | null; // Optional end date for the policy globalMaximumApplicable: boolean; // Flag to indicate if the global maximum is applicable - globalMaximumUsd?: number; // Optional global maximum USD limit - globalMaximumNative?: number; // Optional global maximum native limit - globalMaximumOpCount?: number; // Optional global maximum operation count + globalMaximumUsd?: number | null; // Optional global maximum USD limit + globalMaximumNative?: number | null; // Optional global maximum native limit + globalMaximumOpCount?: number | null; // Optional global maximum operation count perUserMaximumApplicable: boolean; // Flag to indicate if the per user maximum is applicable - perUserMaximumUsd?: number; // Optional per user maximum USD limit - perUserMaximumNative?: number; // Optional per user maximum native limit + perUserMaximumUsd?: number | null; // Optional per user maximum USD limit + perUserMaximumNative?: number | null; // Optional per user maximum native limit perUserMaximumOpCount?: number; // Optional per user maximum operation count perOpMaximumApplicable: boolean; // Flag to indicate if the per operation maximum is applicable - perOpMaximumUsd?: number; // Optional per operation maximum USD limit - perOpMaximumNative?: number; // Optional per operation maximum native limit - addressAllowList?: string[]; // Optional array of allowed addresses - addressBlockList?: string[]; // Optional array of blocked addresses + perOpMaximumUsd?: number | null; // Optional per operation maximum USD limit + perOpMaximumNative?: number | null; // Optional per operation maximum native limit + addressAllowList?: string[] | null; // Optional array of allowed addresses + addressBlockList?: string[] | null; // Optional array of blocked addresses isExpired: boolean; // Flag to indicate if the policy is expired isCurrent: boolean; // Flag to indicate if the policy is current isApplicable: boolean; // Flag to indicate if the policy is applicable diff --git a/docker-compose.yml b/docker-compose.yml index 832910f..23143a6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,16 +20,16 @@ services: - API_PORT=5050 - UNSAFE_MODE=false - SUPPORTED_NETWORKS= - - CRON_PRIVATE_KEY="" + - CRON_PRIVATE_KEY= - DEFAULT_INDEXER_ENDPOINT=http://localhost:3003 - FEE_MARKUP=0 - MULTI_TOKEN_MARKUP=1150000 - - ADMIN_WALLET_ADDRESS="" - - ETHERSCAN_GAS_ORACLES= - - DEFAULT_API_KEY= - - WEBHOOK_URL= - - DATABASE_URL="postgresql://arkauser:paymaster@localhost:5432/arkadev" - - DATABASE_SCHEMA_NAME=arka + - ADMIN_WALLET_ADDRESS= + - ETHERSCAN_GAS_ORACLES="" + - DEFAULT_API_KEY="" + - WEBHOOK_URL="" + - DATABASE_URL=postgresql://arkauser:paymaster@local-setup-db-1:5432/arkadev + - DATABASE_SCHEMA_NAME="arka" - DATABASE_SSL_ENABLED=false - DATABASE_SSL_REJECT_UNAUTHORIZED=false build: @@ -50,4 +50,4 @@ services: expose: - 3002 ports: - - "3002:3002" \ No newline at end of file + - "3002:3002"