From 6e043eb9ad6b4326fd3b2d412deefc93aeabf675 Mon Sep 17 00:00:00 2001 From: crisis Date: Mon, 4 Nov 2024 11:18:28 -0300 Subject: [PATCH] feat: generalize segmentation rules --- apps/api/prisma/schema.prisma | 33 +++++++- apps/api/src/index.ts | 4 +- apps/api/src/routes/gas-sponsorship.ts | 110 ------------------------- apps/api/src/routes/partners.ts | 87 +++++++++++++++++++ apps/api/src/utils/segmentation.ts | 63 ++++++++++++++ 5 files changed, 182 insertions(+), 115 deletions(-) delete mode 100644 apps/api/src/routes/gas-sponsorship.ts create mode 100644 apps/api/src/routes/partners.ts create mode 100644 apps/api/src/utils/segmentation.ts diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index d1180dd4..84c272ea 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -6,7 +6,7 @@ generator client { datasource db { provider = "postgresql" url = env("DATABASE_URL") - schemas = ["public", "smart_account"] + schemas = ["public", "smart_account", "treasure_account"] } // Partner within the Treasure ecosystem @@ -255,16 +255,25 @@ model TransactionErrorLog { view Transaction { id String @id + chain String chainId Int @map("chain_id") blockNumber Decimal @map("block_number") @db.Decimal blockTimestamp DateTime @map("block_timestamp") transactionHash String @map("transaction_hash") fromAddress String @map("from_address") toAddress String @map("to_address") - value Decimal? @db.Decimal + + value Decimal? @db.Decimal + transactionIndex Decimal? @map("transaction_index") @db.Decimal + receiptEffectiveGasPrice Decimal? @map("receipt_effective_gas_price") @db.Decimal + receiptStatus Decimal? @map("receipt_status") @db.Decimal + receiptGasUsed Decimal? @map("receipt_gas_used") @db.Decimal + gasUsed Decimal? @map("gas_used") @db.Decimal + receiptCumulativeGasUsed Decimal? @map("receipt_cumulative_gas_used") @db.Decimal + gas Decimal? @db.Decimal @@map("transactions") - @@schema("smart_account") + @@schema("treasure_account") } // Ignored models and views added here for diff parity @@ -285,3 +294,21 @@ model RawTreasureAccount { @@ignore @@schema("smart_account") } + +model RawTransactionTreasureAccount { + key String @id @map("_key") + body Json + + @@map("transactions_jsonb") + @@ignore + @@schema("treasure_account") +} + +model RawTreasureAccountTreasureAccount { + key String @id @map("_key") + body Json + + @@map("treasure_accounts_jsonb") + @@ignore + @@schema("treasure_account") +} diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index add2bc55..19ade550 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -23,9 +23,9 @@ import { withCors } from "./middleware/cors"; import { withErrorHandler } from "./middleware/error"; import { withSwagger } from "./middleware/swagger"; import { authRoutes } from "./routes/auth"; -import { gasSponsorshipRoutes } from "./routes/gas-sponsorship"; import { harvestersRoutes } from "./routes/harvesters"; import { magicswapRoutes } from "./routes/magicswap"; +import { partnersRoutes } from "./routes/partners"; import { projectsRoutes } from "./routes/projects"; import { transactionsRoutes } from "./routes/transactions"; import { usersRoutes } from "./routes/users"; @@ -126,7 +126,7 @@ const main = async () => { app.register(transactionsRoutes(ctx)); app.register(harvestersRoutes(ctx)); app.register(magicswapRoutes(ctx)); - app.register(gasSponsorshipRoutes(ctx)); + app.register(partnersRoutes(ctx)); app.get("/healthcheck", async (_, reply) => { try { diff --git a/apps/api/src/routes/gas-sponsorship.ts b/apps/api/src/routes/gas-sponsorship.ts deleted file mode 100644 index 2cfe4f75..00000000 --- a/apps/api/src/routes/gas-sponsorship.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type { FastifyPluginAsync } from "fastify"; - -import type { ErrorReply } from "../schema"; -import { - type ValidateBody, - type ValidateParams, - type ValidateReply, - validateBodySchema, - validateReplySchema, -} from "../schema/gas-sponsorship"; -import type { TdkApiContext } from "../types"; - -// TODO: tweak data here based on final resolutions. -// TODO: move this to the DB so it can be updated without code changes (through some sort of dashboard) -const sponsorshipRules = { - partner: { - fullySponsoredIds: ["zeeverse", "smols"], - }, - user: { - maxTransactionsPerMonth: 100, - maxGasPerMonth: 0, - }, -}; - -const validateRules = ({ - partnerId, - monthlyTransactions, - monthlyGas, -}: { - partnerId: string; - monthlyTransactions: number; - monthlyGas: number; -}) => { - const { partner, user } = sponsorshipRules; - - const isPartnerFullySponsored = partner.fullySponsoredIds.includes(partnerId); - const hasExceededTransactions = - monthlyTransactions > user.maxTransactionsPerMonth; - const hasExceededGas = monthlyGas > user.maxGasPerMonth; - - let reason = "does not meet the criteria"; - - switch (true) { - case isPartnerFullySponsored: - reason = "partner is fully sponsored"; - break; - case hasExceededTransactions: - reason = "exceeded the maximum number of transactions per month"; - break; - case hasExceededGas: - reason = "exceeded the maximum gas per month"; - break; - - default: - break; - } - - const isAllowed = - isPartnerFullySponsored || !hasExceededTransactions || !hasExceededGas; - - return { - isAllowed, - reason, - }; -}; - -export const gasSponsorshipRoutes = - ({ db }: TdkApiContext): FastifyPluginAsync => - async (app) => { - app.post<{ - Params: ValidateParams; - Body: ValidateBody; - Reply: ValidateReply | ErrorReply; - }>( - "/gas-sponsorship/:partnerId/validate", - { - schema: { - body: validateBodySchema, - response: { - 200: validateReplySchema, - }, - }, - }, - async (req, reply) => { - const { body, params } = req; - - const oneMonthAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); - - const last24hTransactions = await db.transaction.count({ - where: { - fromAddress: body.userOp.sender, - blockTimestamp: { - gte: oneMonthAgo, - }, - }, - }); - - const { isAllowed, reason } = validateRules({ - partnerId: params.partnerId, - monthlyTransactions: last24hTransactions, - monthlyGas: 1, // TODO: calculate the total gas used in the last 24 hours - }); - - reply.send({ - isAllowed, - reason, - }); - }, - ); - }; diff --git a/apps/api/src/routes/partners.ts b/apps/api/src/routes/partners.ts new file mode 100644 index 00000000..4c18993c --- /dev/null +++ b/apps/api/src/routes/partners.ts @@ -0,0 +1,87 @@ +import type { FastifyPluginAsync } from "fastify"; + +import type { ErrorReply } from "../schema"; +import { + type ValidateBody, + type ValidateParams, + type ValidateReply, + validateBodySchema, + validateReplySchema, +} from "../schema/gas-sponsorship"; +import type { TdkApiContext } from "../types"; +import { validateSegmentationRules } from "../utils/segmentation"; + +export const partnersRoutes = + ({ db }: TdkApiContext): FastifyPluginAsync => + async (app) => { + app.post<{ + Params: ValidateParams; + Body: ValidateBody; + Reply: ValidateReply | ErrorReply; + }>( + "/partners/:partnerId/gas-sponsorship", + { + schema: { + body: validateBodySchema, + response: { + 200: validateReplySchema, + }, + }, + }, + async (req, reply) => { + const { body, params } = req; + + const oneMonthAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); + + const lastMonthTransactions = await db.transaction.count({ + where: { + fromAddress: body.userOp.sender, + blockTimestamp: { + gte: oneMonthAgo, + }, + }, + }); + + const { + _sum: { gas: lastMonthGasRaw }, + } = await db.transaction.aggregate({ + where: { + fromAddress: body.userOp.sender, + blockTimestamp: { + gte: oneMonthAgo, + }, + }, + _sum: { + gas: true, + }, + }); + + const lastMonthGas = + lastMonthGasRaw?.toNumber() ?? Number.MAX_SAFE_INTEGER; + + const { isAllowed, reason } = validateSegmentationRules( + { + partner: { + ids: ["zeeverse", "smols"], + }, + user: { + maxTransactions: 100, + maxGas: 1000, + }, + }, + { + partner: { id: params.partnerId }, + user: { + transactionsCount: lastMonthTransactions, + gas: lastMonthGas, + }, + }, + ); + + reply.send({ + isAllowed, + reason, + }); + }, + ); + }; diff --git a/apps/api/src/utils/segmentation.ts b/apps/api/src/utils/segmentation.ts new file mode 100644 index 00000000..dc611220 --- /dev/null +++ b/apps/api/src/utils/segmentation.ts @@ -0,0 +1,63 @@ +type Segment = { + partner: { + ids: string[]; + }; + user: { + maxTransactions: number; + maxGas: number; + }; +}; + +type SegmentData = { + partner: { + id: string; + }; + user: { + transactionsCount: number; + gas: number; + }; +}; + +export const validateSegmentationRules = ( + segment: Segment, + data: SegmentData, +) => { + const { + partner: { id }, + user: { transactionsCount, gas }, + } = data; + + const isValidPartner = segment.partner.ids.includes(id); + const hasNotExceededTransactions = + transactionsCount <= segment.user.maxTransactions; + const hasNotExceededGas = gas <= segment.user.maxGas; + + const rules = [ + { + condition: isValidPartner, + reason: "partner is valid", + }, + { + condition: hasNotExceededTransactions, + reason: "not exceeded the maximum number of transactions", + }, + { + condition: hasNotExceededGas, + reason: "not exceeded the maximum gas", + }, + ]; + + const approvedRule = rules.find((rule) => rule.condition); + + if (approvedRule) { + return { + isAllowed: true, + reason: approvedRule.reason, + }; + } + + return { + isAllowed: false, + reason: "does not meet any rule", + }; +};