From 4935326c47935de0c0c09c76819df9a629c26155 Mon Sep 17 00:00:00 2001 From: Yaroslav Grishajev Date: Thu, 31 Oct 2024 17:37:02 +0100 Subject: [PATCH] feat(deployment): implements custodial deployments top up data collection refs #39 --- apps/api/env/.env.functional.test | 4 +- apps/api/env/.env.unit.test | 25 +++ apps/api/jest.config.js | 3 +- apps/api/package.json | 1 + apps/api/src/billing/providers/index.ts | 1 - .../providers/signing-client.provider.ts | 2 +- .../providers/http-sdk.provider.ts | 4 +- apps/api/src/core/providers/index.ts | 1 + .../src/deployment/config/config.provider.ts | 11 ++ apps/api/src/deployment/config/env.config.ts | 8 + apps/api/src/deployment/config/index.ts | 1 + .../deployment/deployment.controller.ts | 4 +- .../repositories/lease/lease.repository.ts | 13 +- ...p-up-custodial-deployments.service.spec.ts | 172 ++++++++++++++++++ .../top-up-custodial-deployments.service.ts | 142 +++++++++++++++ .../top-up-deployments.service.ts | 135 -------------- .../test/functional/anonymous-user.spec.ts | 3 +- .../test/functional/create-deployment.spec.ts | 5 +- apps/api/test/functional/nodes-v1.spec.ts | 3 +- .../functional/sign-and-broadcast-tx.spec.ts | 5 +- apps/api/test/functional/start-trial.spec.ts | 3 +- apps/api/test/functional/user-init.spec.ts | 5 +- .../test/functional/wallets-refill.spec.ts | 5 +- apps/api/test/seeders/akash-address.seeder.ts | 7 + apps/api/test/seeders/balance.seeder.ts | 17 ++ apps/api/test/seeders/denom.seeder.ts | 8 + .../test/seeders/deployment-grant.seeder.ts | 28 +++ .../seeders/draining-deployment.seeder.ts | 21 +++ .../test/seeders/fees-authorization.seeder.ts | 36 ++++ .../{node-seeder.ts => node.seeder.ts} | 0 apps/api/test/setup-unit-env.ts | 6 + apps/api/test/setup-unit-tests.ts | 1 + apps/api/tsconfig.json | 2 +- package-lock.json | 19 +- packages/dev-config/.eslintrc.base.js | 2 +- .../src/allowance/allowance-http.service.ts | 17 +- .../src/balance/balance-http.service.ts | 2 +- .../http-sdk/src/block/block-http.service.ts | 22 +++ packages/http-sdk/src/index.ts | 1 + 39 files changed, 574 insertions(+), 171 deletions(-) create mode 100644 apps/api/env/.env.unit.test rename apps/api/src/{billing => core}/providers/http-sdk.provider.ts (52%) create mode 100644 apps/api/src/deployment/config/config.provider.ts create mode 100644 apps/api/src/deployment/config/env.config.ts create mode 100644 apps/api/src/deployment/config/index.ts create mode 100644 apps/api/src/deployment/services/top-up-custodial-deployments/top-up-custodial-deployments.service.spec.ts create mode 100644 apps/api/src/deployment/services/top-up-custodial-deployments/top-up-custodial-deployments.service.ts create mode 100644 apps/api/test/seeders/akash-address.seeder.ts create mode 100644 apps/api/test/seeders/balance.seeder.ts create mode 100644 apps/api/test/seeders/denom.seeder.ts create mode 100644 apps/api/test/seeders/deployment-grant.seeder.ts create mode 100644 apps/api/test/seeders/draining-deployment.seeder.ts create mode 100644 apps/api/test/seeders/fees-authorization.seeder.ts rename apps/api/test/seeders/{node-seeder.ts => node.seeder.ts} (100%) create mode 100644 apps/api/test/setup-unit-env.ts create mode 100644 packages/http-sdk/src/block/block-http.service.ts diff --git a/apps/api/env/.env.functional.test b/apps/api/env/.env.functional.test index e742ff368..f49bbae97 100644 --- a/apps/api/env/.env.functional.test +++ b/apps/api/env/.env.functional.test @@ -2,8 +2,8 @@ AKASH_SANDBOX_DATABASE_CS=postgres://postgres:password@localhost:5432/console-ak USER_DATABASE_CS=postgres://postgres:password@localhost:5432/console-users POSTGRES_DB_URI=postgres://postgres:password@localhost:5432/console-users MASTER_WALLET_MNEMONIC="motion isolate mother convince snack twenty tumble boost elbow bundle modify balcony" -UAKT_TOP_UP_MASTER_WALLET_MNEMONIC="motion isolate mother convince snack twenty tumble boost elbow bundle modify balcony" -USDC_TOP_UP_MASTER_WALLET_MNEMONIC="motion isolate mother convince snack twenty tumble boost elbow bundle modify balcony" +UAKT_TOP_UP_MASTER_WALLET_MNEMONIC="since bread kind field rookie stairs elephant tent horror rice gain tongue collect goose rural garment cover client biology toe ability boat afford mind" +USDC_TOP_UP_MASTER_WALLET_MNEMONIC="leaf brush weapon puppy depart hockey walnut hospital orphan require unfair hunt ribbon toe cereal eagle hour door awesome dress mouse when phone return" NETWORK=sandbox RPC_NODE_ENDPOINT=https://rpc.sandbox-01.aksh.pw:443 TRIAL_DEPLOYMENT_ALLOWANCE_AMOUNT=20000000 diff --git a/apps/api/env/.env.unit.test b/apps/api/env/.env.unit.test new file mode 100644 index 000000000..2f22dae59 --- /dev/null +++ b/apps/api/env/.env.unit.test @@ -0,0 +1,25 @@ +AKASH_SANDBOX_DATABASE_CS=postgres://postgres:password@localhost:5432/console-akash-sandbox +USER_DATABASE_CS=postgres://postgres:password@localhost:5432/console-users +POSTGRES_DB_URI=postgres://postgres:password@localhost:5432/console-users +MASTER_WALLET_MNEMONIC="motion isolate mother convince snack twenty tumble boost elbow bundle modify balcony" +UAKT_TOP_UP_MASTER_WALLET_MNEMONIC="since bread kind field rookie stairs elephant tent horror rice gain tongue collect goose rural garment cover client biology toe ability boat afford mind" +USDC_TOP_UP_MASTER_WALLET_MNEMONIC="leaf brush weapon puppy depart hockey walnut hospital orphan require unfair hunt ribbon toe cereal eagle hour door awesome dress mouse when phone return" +NETWORK=sandbox +RPC_NODE_ENDPOINT=https://rpc.sandbox-01.aksh.pw:443 +TRIAL_DEPLOYMENT_ALLOWANCE_AMOUNT=20000000 +DEPLOYMENT_ALLOWANCE_REFILL_AMOUNT=20000000 +DEPLOYMENT_ALLOWANCE_REFILL_THRESHOLD=2000000 +TRIAL_FEES_ALLOWANCE_AMOUNT=5000000 +FEE_ALLOWANCE_REFILL_AMOUNT=5000000 +FEE_ALLOWANCE_REFILL_THRESHOLD=500000 +DEPLOYMENT_GRANT_DENOM=uakt +LOG_LEVEL=debug +BILLING_ENABLED=true +ANONYMOUS_USER_TOKEN_SECRET=ANONYMOUS_USER_TOKEN_SECRET +STRIPE_SECRET_KEY=STRIPE_SECRET_KEY +STRIPE_PRICE_ID=STRIPE_PRICE_ID +STRIPE_WEBHOOK_SECRET=STRIPE_WEBHOOK_SECRET +ALLOWED_CHECKOUT_REFERRERS=["http://localhost:3000"] +STRIPE_CHECKOUT_REDIRECT_URL=http://localhost:3000 +STD_OUT_LOG_FORMAT=pretty +SQL_LOG_FORMAT=pretty \ No newline at end of file diff --git a/apps/api/jest.config.js b/apps/api/jest.config.js index 907461d16..ebaac8ad9 100644 --- a/apps/api/jest.config.js +++ b/apps/api/jest.config.js @@ -21,7 +21,8 @@ module.exports = { displayName: "unit", ...common, testMatch: ["/src/**/*.spec.ts"], - setupFilesAfterEnv: ["./test/setup-unit-tests.ts"] + setupFilesAfterEnv: ["./test/setup-unit-tests.ts"], + setupFiles: ["./test/setup-unit-env.ts"] }, { displayName: "functional", diff --git a/apps/api/package.json b/apps/api/package.json index 88a4f3fb9..17abe5e13 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -131,6 +131,7 @@ "supertest": "^6.1.5", "ts-jest": "^29.1.4", "ts-loader": "^9.2.5", + "type-fest": "^4.26.1", "typescript": "5.1.3", "webpack": "^5.91.0", "webpack-cli": "4.10.0", diff --git a/apps/api/src/billing/providers/index.ts b/apps/api/src/billing/providers/index.ts index 648403be1..74b216119 100644 --- a/apps/api/src/billing/providers/index.ts +++ b/apps/api/src/billing/providers/index.ts @@ -1,5 +1,4 @@ import "./config.provider"; -import "./http-sdk.provider"; import "./wallet.provider"; export * from "./config.provider"; diff --git a/apps/api/src/billing/providers/signing-client.provider.ts b/apps/api/src/billing/providers/signing-client.provider.ts index 4e7fe8f44..38e6110eb 100644 --- a/apps/api/src/billing/providers/signing-client.provider.ts +++ b/apps/api/src/billing/providers/signing-client.provider.ts @@ -3,7 +3,7 @@ import { container, inject } from "tsyringe"; import { config } from "@src/billing/config"; import { TYPE_REGISTRY } from "@src/billing/providers/type-registry.provider"; import { MANAGED_MASTER_WALLET, UAKT_TOP_UP_MASTER_WALLET, USDC_TOP_UP_MASTER_WALLET } from "@src/billing/providers/wallet.provider"; -import { MasterSigningClientService } from "@src/billing/services"; +import { MasterSigningClientService } from "@src/billing/services/master-signing-client/master-signing-client.service"; import { MasterWalletType } from "@src/billing/types/wallet.type"; export const MANAGED_MASTER_SIGNING_CLIENT = "MANAGED_MASTER_SIGNING_CLIENT"; diff --git a/apps/api/src/billing/providers/http-sdk.provider.ts b/apps/api/src/core/providers/http-sdk.provider.ts similarity index 52% rename from apps/api/src/billing/providers/http-sdk.provider.ts rename to apps/api/src/core/providers/http-sdk.provider.ts index 6b2067062..535714365 100644 --- a/apps/api/src/billing/providers/http-sdk.provider.ts +++ b/apps/api/src/core/providers/http-sdk.provider.ts @@ -1,8 +1,8 @@ -import { AllowanceHttpService, BalanceHttpService } from "@akashnetwork/http-sdk"; +import { AllowanceHttpService, BalanceHttpService, BlockHttpService } from "@akashnetwork/http-sdk"; import { container } from "tsyringe"; import { apiNodeUrl } from "@src/utils/constants"; -const SERVICES = [BalanceHttpService, AllowanceHttpService]; +const SERVICES = [BalanceHttpService, AllowanceHttpService, BlockHttpService]; SERVICES.forEach(Service => container.register(Service, { useValue: new Service({ baseURL: apiNodeUrl }) })); diff --git a/apps/api/src/core/providers/index.ts b/apps/api/src/core/providers/index.ts index bf289a425..ed5f1b9f1 100644 --- a/apps/api/src/core/providers/index.ts +++ b/apps/api/src/core/providers/index.ts @@ -1,2 +1,3 @@ export * from "./postgres.provider"; export * from "./config.provider"; +import "./http-sdk.provider"; diff --git a/apps/api/src/deployment/config/config.provider.ts b/apps/api/src/deployment/config/config.provider.ts new file mode 100644 index 000000000..96689da90 --- /dev/null +++ b/apps/api/src/deployment/config/config.provider.ts @@ -0,0 +1,11 @@ +import { container, inject } from "tsyringe"; + +import { config } from "@src/deployment/config"; + +export const DEPLOYMENT_CONFIG = "DEPLOYMENT_CONFIG"; + +container.register(DEPLOYMENT_CONFIG, { useValue: config }); + +export type DeploymentConfig = typeof config; + +export const InjectDeploymentConfig = () => inject(DEPLOYMENT_CONFIG); diff --git a/apps/api/src/deployment/config/env.config.ts b/apps/api/src/deployment/config/env.config.ts new file mode 100644 index 000000000..8e2272f21 --- /dev/null +++ b/apps/api/src/deployment/config/env.config.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +const envSchema = z.object({ + AUTO_TOP_UP_JOB_INTERVAL_IN_H: z.number({ coerce: true }).optional().default(1), + AUTO_TOP_UP_DEPLOYMENT_INTERVAL_IN_DAYS: z.number({ coerce: true }).optional().default(7) +}); + +export const envConfig = envSchema.parse(process.env); diff --git a/apps/api/src/deployment/config/index.ts b/apps/api/src/deployment/config/index.ts new file mode 100644 index 000000000..1870f680f --- /dev/null +++ b/apps/api/src/deployment/config/index.ts @@ -0,0 +1 @@ +export { envConfig as config } from "./env.config"; diff --git a/apps/api/src/deployment/controllers/deployment/deployment.controller.ts b/apps/api/src/deployment/controllers/deployment/deployment.controller.ts index 73ec8b161..0e73d0413 100644 --- a/apps/api/src/deployment/controllers/deployment/deployment.controller.ts +++ b/apps/api/src/deployment/controllers/deployment/deployment.controller.ts @@ -1,10 +1,10 @@ import { singleton } from "tsyringe"; -import { TopUpDeploymentsService } from "@src/deployment/services/top-up-deployments/top-up-deployments.service"; +import { TopUpCustodialDeploymentsService } from "@src/deployment/services/top-up-custodial-deployments/top-up-custodial-deployments.service"; @singleton() export class TopUpDeploymentsController { - constructor(private readonly topUpDeploymentsService: TopUpDeploymentsService) {} + constructor(private readonly topUpDeploymentsService: TopUpCustodialDeploymentsService) {} async topUpDeployments() { await this.topUpDeploymentsService.topUpDeployments(); diff --git a/apps/api/src/deployment/repositories/lease/lease.repository.ts b/apps/api/src/deployment/repositories/lease/lease.repository.ts index fd8c23086..884f0e468 100644 --- a/apps/api/src/deployment/repositories/lease/lease.repository.ts +++ b/apps/api/src/deployment/repositories/lease/lease.repository.ts @@ -2,31 +2,36 @@ import { Lease } from "@akashnetwork/database/dbSchemas/akash"; import { col, fn, Op } from "sequelize"; import { singleton } from "tsyringe"; -interface DrainingLeasesOptions { +export interface DrainingLeasesOptions { closureHeight: number; owner: string; + denom: string; } export interface DrainingDeploymentOutput { dseq: number; denom: string; blockRate: number; + predictedClosedHeight: number; } @singleton() export class LeaseRepository { async findDrainingLeases(options: DrainingLeasesOptions): Promise { - return (await Lease.findAll({ + const leases = await Lease.findAll({ where: { closedHeight: null, owner: options.owner, + denom: options.denom, predictedClosedHeight: { [Op.lte]: options.closureHeight } }, - attributes: ["dseq", "denom", [fn("sum", col("price")), "blockRate"]], + attributes: ["dseq", "denom", [fn("min", col("predictedClosedHeight")), "predictedClosedHeight"], [fn("sum", col("price")), "blockRate"]], group: ["dseq", "denom"], plain: true - })) as unknown as DrainingDeploymentOutput[]; + }); + + return leases ? (leases as unknown as DrainingDeploymentOutput[]) : []; } } diff --git a/apps/api/src/deployment/services/top-up-custodial-deployments/top-up-custodial-deployments.service.spec.ts b/apps/api/src/deployment/services/top-up-custodial-deployments/top-up-custodial-deployments.service.spec.ts new file mode 100644 index 000000000..32433c59e --- /dev/null +++ b/apps/api/src/deployment/services/top-up-custodial-deployments/top-up-custodial-deployments.service.spec.ts @@ -0,0 +1,172 @@ +import { AllowanceHttpService, BalanceHttpService, BlockHttpService, Denom } from "@akashnetwork/http-sdk"; +import { container } from "tsyringe"; + +import { MasterSigningClientService, MasterWalletService } from "@src/billing/services"; +import type { Sentry } from "@src/core/providers/sentry.provider"; +import { SentryEventService } from "@src/core/services/sentry-event/sentry-event.service"; +import { config } from "@src/deployment/config"; +import { DrainingLeasesOptions, LeaseRepository } from "@src/deployment/repositories/lease/lease.repository"; +import { TopUpCustodialDeploymentsService } from "./top-up-custodial-deployments.service"; + +import { AkashAddressSeeder } from "@test/seeders/akash-address.seeder"; +import { BalanceSeeder } from "@test/seeders/balance.seeder"; +import { DeploymentGrantSeeder } from "@test/seeders/deployment-grant.seeder"; +import { DrainingDeploymentSeeder } from "@test/seeders/draining-deployment.seeder"; +import { FeesAuthorizationSeeder } from "@test/seeders/fees-authorization.seeder"; + +describe(TopUpCustodialDeploymentsService.name, () => { + const CURRENT_BLOCK_HEIGHT = 7481457; + const UAKT_TOP_UP_MASTER_WALLET_ADDRESS = AkashAddressSeeder.create(); + const USDT_TOP_UP_MASTER_WALLET_ADDRESS = AkashAddressSeeder.create(); + const mockManagedWalletService = (address: string) => { + return { + getFirstAddress: async () => address + } as unknown as MasterWalletService; + }; + const mockMasterSigningClientService = () => { + return { + execTx: jest.fn() + } as unknown as MasterSigningClientService; + }; + + const allowanceHttpService = new AllowanceHttpService(); + const balanceHttpService = new BalanceHttpService(); + const blockHttpService = new BlockHttpService(); + const uaktMasterWalletService = mockManagedWalletService(UAKT_TOP_UP_MASTER_WALLET_ADDRESS); + const usdtMasterWalletService = mockManagedWalletService(USDT_TOP_UP_MASTER_WALLET_ADDRESS); + const uaktMasterSigningClientService = mockMasterSigningClientService(); + const usdtMasterSigningClientService = mockMasterSigningClientService(); + + jest.spyOn(blockHttpService, "getCurrentHeight").mockResolvedValue(CURRENT_BLOCK_HEIGHT); + + const leaseRepository = container.resolve(LeaseRepository); + jest.spyOn(leaseRepository, "findDrainingLeases").mockResolvedValue([]); + const sentryEventService = container.resolve(SentryEventService); + const sentry = { + captureEvent: jest.fn() + } as unknown as Sentry; + const topUpDeploymentsService = new TopUpCustodialDeploymentsService( + allowanceHttpService, + balanceHttpService, + blockHttpService, + uaktMasterWalletService, + usdtMasterWalletService, + uaktMasterSigningClientService, + usdtMasterSigningClientService, + leaseRepository, + config, + sentry, + sentryEventService + ); + + type SeedParams = { + denom: Denom; + balance?: string; + grantee: string; + expectedDeploymentsTopUpCount?: 0 | 1 | 2; + hasDeployments?: boolean; + client: MasterSigningClientService; + }; + + const seedFor = ({ denom, balance = "100000000", grantee, expectedDeploymentsTopUpCount = 2, hasDeployments = true, client }: SeedParams) => { + const owner = AkashAddressSeeder.create(); + + return { + balance: BalanceSeeder.create({ denom, amount: balance }), + grant: DeploymentGrantSeeder.create({ + granter: owner, + grantee: grantee, + authorization: { spend_limit: { denom, amount: "100000000" } } + }), + feeAllowance: FeesAuthorizationSeeder.create({ + granter: owner, + grantee: grantee, + allowance: { spend_limit: { denom } } + }), + drainingDeployments: hasDeployments + ? [ + { + deployment: DrainingDeploymentSeeder.create({ denom, blockRate: 50, predictedClosedHeight: CURRENT_BLOCK_HEIGHT + 1500 }), + expectedTopUpAmount: expectedDeploymentsTopUpCount ? 4897959 : undefined + }, + { + deployment: DrainingDeploymentSeeder.create({ denom, blockRate: 45, predictedClosedHeight: CURRENT_BLOCK_HEIGHT + 1700 }), + expectedTopUpAmount: expectedDeploymentsTopUpCount > 1 ? 4408163 : undefined + } + ] + : [], + client: client + }; + }; + + const data = [ + seedFor({ + denom: "uakt", + grantee: UAKT_TOP_UP_MASTER_WALLET_ADDRESS, + client: uaktMasterSigningClientService + }), + seedFor({ + denom: "ibc/170C677610AC31DF0904FFE09CD3B5C657492170E7E52372E48756B71E56F2F1", + grantee: USDT_TOP_UP_MASTER_WALLET_ADDRESS, + client: usdtMasterSigningClientService + }), + seedFor({ + denom: "uakt", + balance: "5500000", + grantee: UAKT_TOP_UP_MASTER_WALLET_ADDRESS, + expectedDeploymentsTopUpCount: 1, + client: uaktMasterSigningClientService + }), + seedFor({ + denom: "uakt", + balance: "5500000", + grantee: UAKT_TOP_UP_MASTER_WALLET_ADDRESS, + hasDeployments: false, + client: uaktMasterSigningClientService + }), + seedFor({ + denom: "uakt", + balance: "0", + grantee: UAKT_TOP_UP_MASTER_WALLET_ADDRESS, + expectedDeploymentsTopUpCount: 0, + client: uaktMasterSigningClientService + }) + ]; + + jest.spyOn(allowanceHttpService, "paginateDeploymentGrants").mockImplementation(async (params, cb) => { + return await cb(data.filter(({ grant }) => "grantee" in params && grant.grantee === params.grantee).map(({ grant }) => grant)); + }); + jest.spyOn(allowanceHttpService, "getFeeAllowanceForGranterAndGrantee").mockImplementation(async (granter: string, grantee: string) => { + return data.find(({ grant }) => grant.granter === granter && grant.grantee === grantee)?.feeAllowance; + }); + jest.spyOn(balanceHttpService, "getBalance").mockImplementation(async (address: string, denom: Denom) => { + return ( + data.find(({ grant }) => grant.granter === address)?.balance || { + amount: "0", + denom + } + ); + }); + jest.spyOn(leaseRepository, "findDrainingLeases").mockImplementation(async ({ owner, denom }: DrainingLeasesOptions) => { + return ( + data + .find(({ grant }) => grant.granter === owner && grant.authorization.spend_limit.denom === denom) + ?.drainingDeployments?.map(({ deployment }) => deployment) || [] + ); + }); + jest.spyOn(topUpDeploymentsService, "topUpDeployment"); + + it("should top up draining deployment given owners have sufficient grants and balances", async () => { + await topUpDeploymentsService.topUpDeployments(); + + expect(topUpDeploymentsService.topUpDeployment).toHaveBeenCalledTimes(5); + + data.forEach(({ drainingDeployments, client }) => { + drainingDeployments.forEach(({ expectedTopUpAmount, deployment }) => { + if (expectedTopUpAmount) { + expect(topUpDeploymentsService.topUpDeployment).toHaveBeenCalledWith(expectedTopUpAmount, deployment, client); + } + }); + }); + }); +}); diff --git a/apps/api/src/deployment/services/top-up-custodial-deployments/top-up-custodial-deployments.service.ts b/apps/api/src/deployment/services/top-up-custodial-deployments/top-up-custodial-deployments.service.ts new file mode 100644 index 000000000..16513de51 --- /dev/null +++ b/apps/api/src/deployment/services/top-up-custodial-deployments/top-up-custodial-deployments.service.ts @@ -0,0 +1,142 @@ +import { AllowanceHttpService, BalanceHttpService, BlockHttpService, DeploymentAllowance } from "@akashnetwork/http-sdk"; +import { singleton } from "tsyringe"; + +import { InjectSigningClient } from "@src/billing/providers/signing-client.provider"; +import { InjectWallet } from "@src/billing/providers/wallet.provider"; +import { MasterSigningClientService, MasterWalletService } from "@src/billing/services"; +import { Memoize } from "@src/caching/helpers"; +import { LoggerService } from "@src/core"; +import { InjectSentry, Sentry } from "@src/core/providers/sentry.provider"; +import { SentryEventService } from "@src/core/services/sentry-event/sentry-event.service"; +import { DeploymentConfig, InjectDeploymentConfig } from "@src/deployment/config/config.provider"; +import { DrainingDeploymentOutput, LeaseRepository } from "@src/deployment/repositories/lease/lease.repository"; +import { averageBlockCountInAnHour, averageBlockTime } from "@src/utils/constants"; + +interface Balances { + denom: string; + feesLimit: number; + deploymentLimit: number; + balance: number; +} + +@singleton() +export class TopUpCustodialDeploymentsService { + private readonly CONCURRENCY = 10; + + private readonly MIN_FEES_AVAILABLE = 5000; + + private readonly logger = new LoggerService({ context: TopUpCustodialDeploymentsService.name }); + + constructor( + private readonly allowanceHttpService: AllowanceHttpService, + private readonly balanceHttpService: BalanceHttpService, + private readonly blockHttpService: BlockHttpService, + @InjectWallet("UAKT_TOP_UP") private readonly uaktMasterWalletService: MasterWalletService, + @InjectWallet("USDC_TOP_UP") private readonly usdtMasterWalletService: MasterWalletService, + @InjectSigningClient("UAKT_TOP_UP") private readonly uaktMasterSigningClientService: MasterSigningClientService, + @InjectSigningClient("USDC_TOP_UP") private readonly usdtMasterSigningClientService: MasterSigningClientService, + private readonly leaseRepository: LeaseRepository, + @InjectDeploymentConfig() private readonly config: DeploymentConfig, + @InjectSentry() private readonly sentry: Sentry, + private readonly sentryEventService: SentryEventService + ) {} + + async topUpDeployments() { + const wallets = [ + { wallet: this.uaktMasterWalletService, client: this.uaktMasterSigningClientService }, + { wallet: this.usdtMasterWalletService, client: this.usdtMasterSigningClientService } + ]; + + const topUpAllCustodialDeployments = wallets.map(async ({ wallet, client }) => { + const address = await wallet.getFirstAddress(); + await this.allowanceHttpService.paginateDeploymentGrants({ grantee: address, limit: this.CONCURRENCY }, async grants => { + await Promise.all( + grants.map(async grant => { + await this.execWithErrorHandler(grant, () => this.topUpForGrant(grant, client)); + }) + ); + }); + }); + await Promise.all(topUpAllCustodialDeployments); + } + + private async topUpForGrant(grant: DeploymentAllowance, client: MasterSigningClientService) { + const owner = grant.granter; + + const balances = await this.collectWalletBalances(grant); + this.logger.debug({ event: "BALANCES_COLLECTED", granter: owner, grantee: grant.grantee, balances }); + + const drainingDeployments = await this.retrieveDrainingDeployments(owner, balances.denom); + let { deploymentLimit, feesLimit, balance } = balances; + + for (const deployment of drainingDeployments) { + const topUpAmount = await this.calculateTopUpAmount(deployment); + if (!this.canTopUp(topUpAmount, { deploymentLimit, feesLimit, balance })) { + this.logger.debug({ event: "INSUFFICIENT_BALANCE", granter: owner, grantee: grant.grantee, balances: { deploymentLimit, feesLimit, balance } }); + break; + } + deploymentLimit -= topUpAmount; + feesLimit -= this.MIN_FEES_AVAILABLE; + balance -= topUpAmount + this.MIN_FEES_AVAILABLE; + + await this.topUpDeployment(topUpAmount, deployment, client); + } + } + + private async collectWalletBalances(grant: DeploymentAllowance): Promise { + const denom = grant.authorization.spend_limit.denom; + const deploymentLimit = parseFloat(grant.authorization.spend_limit.amount); + + const feesLimit = await this.retrieveFeesLimit(grant.granter, grant.grantee, denom); + const { amount } = await this.balanceHttpService.getBalance(grant.granter, denom); + const balance = parseFloat(amount); + + return { + denom, + feesLimit, + deploymentLimit, + balance + }; + } + + private async retrieveFeesLimit(granter: string, grantee: string, denom: string) { + const feesAllowance = await this.allowanceHttpService.getFeeAllowanceForGranterAndGrantee(granter, grantee); + const feesSpendLimit = feesAllowance.allowance.spend_limit.find(limit => limit.denom === denom); + + return feesSpendLimit ? parseFloat(feesSpendLimit.amount) : 0; + } + + private async retrieveDrainingDeployments(owner: string, denom: string): Promise { + const currentHeight = await this.getCurrentHeight(); + const closureHeight = currentHeight + averageBlockCountInAnHour * this.config.AUTO_TOP_UP_JOB_INTERVAL_IN_H; + + return await this.leaseRepository.findDrainingLeases({ owner, closureHeight, denom }); + } + + @Memoize({ ttlInSeconds: averageBlockTime }) + private getCurrentHeight() { + return this.blockHttpService.getCurrentHeight(); + } + + private async calculateTopUpAmount(deployment: DrainingDeploymentOutput): Promise { + return Math.floor(deployment.blockRate * (averageBlockCountInAnHour * 24 * this.config.AUTO_TOP_UP_DEPLOYMENT_INTERVAL_IN_DAYS)); + } + + private canTopUp(amount: number, balances: Pick) { + return balances.deploymentLimit > amount && balances.feesLimit > this.MIN_FEES_AVAILABLE && balances.balance > amount + this.MIN_FEES_AVAILABLE; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async topUpDeployment(amount: number, deployment: DrainingDeploymentOutput, client: MasterSigningClientService) { + this.logger.debug({ event: "TOPPING_UP_CUSTODIAL_DEPLOYMENT", amount, deployment, warning: "Not implemented yet" }); + } + + private async execWithErrorHandler(grant: DeploymentAllowance, cb: () => Promise) { + try { + await cb(); + } catch (error) { + const sentryEventId = this.sentry.captureEvent(this.sentryEventService.toEvent(error)); + this.logger.error({ event: "TOP_UP_FAILED", error: error.stack, sentryEventId, grant }); + } + } +} diff --git a/apps/api/src/deployment/services/top-up-deployments/top-up-deployments.service.ts b/apps/api/src/deployment/services/top-up-deployments/top-up-deployments.service.ts index 802b23e53..e69de29bb 100644 --- a/apps/api/src/deployment/services/top-up-deployments/top-up-deployments.service.ts +++ b/apps/api/src/deployment/services/top-up-deployments/top-up-deployments.service.ts @@ -1,135 +0,0 @@ -import { AllowanceHttpService, BalanceHttpService, DeploymentAllowance } from "@akashnetwork/http-sdk"; -import { LoggerService } from "@akashnetwork/logging"; -import { PromisePool } from "@supercharge/promise-pool"; -import { singleton } from "tsyringe"; - -import { InjectWallet } from "@src/billing/providers/wallet.provider"; -import { UserWalletOutput, UserWalletRepository } from "@src/billing/repositories"; -import { MasterWalletService } from "@src/billing/services"; -import { DrainingDeploymentOutput, LeaseRepository } from "@src/deployment/repositories/lease/lease.repository"; - -interface Balances { - denom: string; - feesLimit: number; - deploymentLimit: number; - balance: number; - isManaged: boolean; -} - -@singleton() -export class TopUpDeploymentsService { - private readonly CONCURRENCY = 10; - - private readonly logger = new LoggerService({ context: TopUpDeploymentsService.name }); - - constructor( - private readonly userWalletRepository: UserWalletRepository, - private readonly allowanceHttpService: AllowanceHttpService, - private readonly balanceHttpService: BalanceHttpService, - @InjectWallet("MANAGED") private readonly managedMasterWalletService: MasterWalletService, - @InjectWallet("UAKT_TOP_UP") private readonly uaktMasterWalletService: MasterWalletService, - @InjectWallet("USDC_TOP_UP") private readonly usdtMasterWalletService: MasterWalletService, - private readonly leaseRepository: LeaseRepository - ) {} - - async topUpDeployments() { - const wallets = [this.uaktMasterWalletService, this.usdtMasterWalletService]; - - const topUpAllManagedDeployments = wallets.map(async wallet => { - const address = await wallet.getFirstAddress(); - await this.allowanceHttpService.paginateDeploymentGrantsForGrantee(address, async grants => { - await PromisePool.withConcurrency(this.CONCURRENCY) - .for(grants) - .process(async grant => this.topUpForGrant(grant)); - }); - }); - await Promise.all(topUpAllManagedDeployments); - - await this.paginateManagedWallets(async userWallets => { - await Promise.all(userWallets.map(async userWallet => this.topUpForManagedWallet(userWallet))); - }); - } - - private async topUpForGrant(grant: DeploymentAllowance) { - const balances = await this.collectCustodialWalletBalances(grant); - const owner = grant.granter; - this.logger.debug({ event: "BALANCES_COLLECTED", granter: owner, grantee: grant.grantee, balances }); - - const drainingDeployments = await this.retrieveDrainingDeployments(owner); - - drainingDeployments.map(async deployment => { - const topUpAmount = await this.calculateTopUpAmount(deployment); - this.validateTopUpAmount(topUpAmount, balances); - }); - } - - private async collectCustodialWalletBalances(grant: DeploymentAllowance): Promise { - const denom = grant.authorization.spend_limit.denom; - const deploymentLimit = parseFloat(grant.authorization.spend_limit.amount); - - const feesAllowance = await this.allowanceHttpService.getFeeAllowanceForGranterAndGrantee(grant.granter, grant.grantee); - const feesSpendLimit = feesAllowance.allowance.spend_limit.find(limit => limit.denom === denom); - const feesLimit = feesSpendLimit ? parseFloat(feesSpendLimit.amount) : 0; - - const { amount } = await this.balanceHttpService.getBalance(grant.granter, "uakt"); - const balance = parseFloat(amount); - - return { - denom, - feesLimit: feesLimit, - deploymentLimit, - balance, - isManaged: false - }; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - private async paginateManagedWallets(cb: (page: UserWalletOutput[]) => Promise) { - this.logger.debug({ event: "PAGINATING_MANAGED_WALLETS", warning: "Not implemented yet" }); - } - - private async topUpForManagedWallet(userWallet: UserWalletOutput) { - const balances = await this.collectManagedWalletBalances(userWallet); - this.logger.debug({ event: "BALANCES_COLLECTED", wallet: userWallet, balances }); - - const drainingDeployments = await this.retrieveDrainingDeployments(userWallet.address); - - drainingDeployments.map(async deployment => { - const topUpAmount = await this.calculateTopUpAmount(deployment); - this.validateTopUpAmount(topUpAmount, balances); - }); - } - - private async collectManagedWalletBalances(userWallet: UserWalletOutput): Promise { - this.logger.debug({ event: "CALCULATING_MANAGE_WALLET_BALANCES", userWallet, warning: "Not implemented yet" }); - return { - denom: "usdc", - feesLimit: 0, - deploymentLimit: 0, - balance: 0, - isManaged: true - }; - } - - private async retrieveDrainingDeployments(owner: string): Promise { - this.logger.debug({ event: "RETRIEVING_DRAINING_DEPLOYMENTS", owner, warning: "Not implemented yet" }); - return []; - } - - private async calculateTopUpAmount(deployment: DrainingDeploymentOutput): Promise { - this.logger.debug({ event: "CALCULATING_TOP_UP_AMOUNT", deployment, warning: "Not implemented yet" }); - return 0; - } - - private validateTopUpAmount(amount: number, balances: Balances) { - this.logger.debug({ event: "VALIDATING_TOP_UP_AMOUNT", amount, balances, warning: "Not implemented yet" }); - } - - private async topUpCustodialDeployment() { - this.logger.debug({ event: "TOPPING_UP_CUSTODIAL_DEPLOYMENT", warning: "Not implemented yet" }); - } - - private async topUpManagedDeployment() { - this.logger.debug({ event: "TOPPING_UP_MANAGED_DEPLOYMENT", warning: "Not implemented yet" }); - } -} diff --git a/apps/api/test/functional/anonymous-user.spec.ts b/apps/api/test/functional/anonymous-user.spec.ts index 6720d549d..d7acdc680 100644 --- a/apps/api/test/functional/anonymous-user.spec.ts +++ b/apps/api/test/functional/anonymous-user.spec.ts @@ -1,9 +1,10 @@ -import { DbTestingService } from "@test/services/db-testing.service"; import { container } from "tsyringe"; import { app } from "@src/app"; import { AnonymousUserResponseOutput } from "@src/user/schemas/user.schema"; +import { DbTestingService } from "@test/services/db-testing.service"; + describe("Users", () => { let user: AnonymousUserResponseOutput["data"]; let token: AnonymousUserResponseOutput["token"]; diff --git a/apps/api/test/functional/create-deployment.spec.ts b/apps/api/test/functional/create-deployment.spec.ts index 6f78d8e9c..0a45dcf51 100644 --- a/apps/api/test/functional/create-deployment.spec.ts +++ b/apps/api/test/functional/create-deployment.spec.ts @@ -1,8 +1,6 @@ import { certificateManager } from "@akashnetwork/akashjs/build/certificates/certificate-manager"; import { SDL } from "@akashnetwork/akashjs/build/sdl"; import type { Registry } from "@cosmjs/proto-signing"; -import { DbTestingService } from "@test/services/db-testing.service"; -import { WalletTestingService } from "@test/services/wallet-testing.service"; import axios from "axios"; import * as fs from "node:fs"; import * as path from "node:path"; @@ -14,6 +12,9 @@ import { TYPE_REGISTRY } from "@src/billing/providers/type-registry.provider"; import { MANAGED_MASTER_WALLET } from "@src/billing/providers/wallet.provider"; import { MasterWalletService } from "@src/billing/services"; +import { DbTestingService } from "@test/services/db-testing.service"; +import { WalletTestingService } from "@test/services/wallet-testing.service"; + jest.setTimeout(30000); const yml = fs.readFileSync(path.resolve(__dirname, "../mocks/hello-world-sdl.yml"), "utf8"); diff --git a/apps/api/test/functional/nodes-v1.spec.ts b/apps/api/test/functional/nodes-v1.spec.ts index 8f892d23f..711c29874 100644 --- a/apps/api/test/functional/nodes-v1.spec.ts +++ b/apps/api/test/functional/nodes-v1.spec.ts @@ -1,5 +1,4 @@ import { faker } from "@faker-js/faker"; -import { NodeSeeder } from "@test/seeders/node-seeder"; import mcache from "memory-cache"; import nock from "nock"; @@ -7,6 +6,8 @@ import { app, initDb } from "@src/app"; import { closeConnections } from "@src/db/dbConnection"; import { env } from "@src/utils/env"; +import { NodeSeeder } from "@test/seeders/node.seeder"; + describe("Nodes API", () => { const interceptor = nock(env.NODE_API_BASE_PATH); diff --git a/apps/api/test/functional/sign-and-broadcast-tx.spec.ts b/apps/api/test/functional/sign-and-broadcast-tx.spec.ts index 56deefe74..4ee68d78f 100644 --- a/apps/api/test/functional/sign-and-broadcast-tx.spec.ts +++ b/apps/api/test/functional/sign-and-broadcast-tx.spec.ts @@ -1,12 +1,13 @@ import { certificateManager } from "@akashnetwork/akashjs/build/certificates/certificate-manager"; import type { Registry } from "@cosmjs/proto-signing"; -import { DbTestingService } from "@test/services/db-testing.service"; -import { WalletTestingService } from "@test/services/wallet-testing.service"; import { container } from "tsyringe"; import { app } from "@src/app"; import { TYPE_REGISTRY } from "@src/billing/providers/type-registry.provider"; +import { DbTestingService } from "@test/services/db-testing.service"; +import { WalletTestingService } from "@test/services/wallet-testing.service"; + jest.setTimeout(30000); describe("Tx Sign", () => { diff --git a/apps/api/test/functional/start-trial.spec.ts b/apps/api/test/functional/start-trial.spec.ts index 468eb4c8a..2eae5a324 100644 --- a/apps/api/test/functional/start-trial.spec.ts +++ b/apps/api/test/functional/start-trial.spec.ts @@ -1,6 +1,5 @@ import { AllowanceHttpService } from "@akashnetwork/http-sdk"; import { faker } from "@faker-js/faker"; -import { DbTestingService } from "@test/services/db-testing.service"; import { eq } from "drizzle-orm"; import { container } from "tsyringe"; @@ -8,6 +7,8 @@ import { app } from "@src/app"; import { BILLING_CONFIG, BillingConfig } from "@src/billing/providers"; import { ApiPgDatabase, POSTGRES_DB, resolveTable } from "@src/core"; +import { DbTestingService } from "@test/services/db-testing.service"; + jest.setTimeout(20000); describe("start trial", () => { diff --git a/apps/api/test/functional/user-init.spec.ts b/apps/api/test/functional/user-init.spec.ts index 78b5cbdca..42d3ddc3b 100644 --- a/apps/api/test/functional/user-init.spec.ts +++ b/apps/api/test/functional/user-init.spec.ts @@ -1,6 +1,4 @@ import { faker } from "@faker-js/faker"; -import { DbTestingService } from "@test/services/db-testing.service"; -import { WalletTestingService } from "@test/services/wallet-testing.service"; import type { Context, Next } from "hono"; import first from "lodash/first"; import omit from "lodash/omit"; @@ -11,6 +9,9 @@ import { UserWalletRepository } from "@src/billing/repositories"; import { ApiPgDatabase, POSTGRES_DB, resolveTable } from "@src/core"; import { getCurrentUserId } from "@src/middlewares/userMiddleware"; +import { DbTestingService } from "@test/services/db-testing.service"; +import { WalletTestingService } from "@test/services/wallet-testing.service"; + jest.mock("../../src/middlewares/userMiddleware.ts", () => ({ getCurrentUserId: jest.fn(), requiredUserMiddleware: async (c: Context, next: Next) => next(), diff --git a/apps/api/test/functional/wallets-refill.spec.ts b/apps/api/test/functional/wallets-refill.spec.ts index 26d21752e..20497b43a 100644 --- a/apps/api/test/functional/wallets-refill.spec.ts +++ b/apps/api/test/functional/wallets-refill.spec.ts @@ -1,5 +1,3 @@ -import { DbTestingService } from "@test/services/db-testing.service"; -import { WalletTestingService } from "@test/services/wallet-testing.service"; import { container } from "tsyringe"; import { app } from "@src/app"; @@ -8,6 +6,9 @@ import { BILLING_CONFIG, BillingConfig } from "@src/billing/providers"; import { UserWalletRepository } from "@src/billing/repositories"; import { ManagedUserWalletService } from "@src/billing/services"; +import { DbTestingService } from "@test/services/db-testing.service"; +import { WalletTestingService } from "@test/services/wallet-testing.service"; + jest.setTimeout(240000); describe("Wallets Refill", () => { diff --git a/apps/api/test/seeders/akash-address.seeder.ts b/apps/api/test/seeders/akash-address.seeder.ts new file mode 100644 index 000000000..b8b687da3 --- /dev/null +++ b/apps/api/test/seeders/akash-address.seeder.ts @@ -0,0 +1,7 @@ +import { faker } from "@faker-js/faker"; + +export class AkashAddressSeeder { + static create(): string { + return `akash${faker.string.alphanumeric({ length: 39 })}`; + } +} diff --git a/apps/api/test/seeders/balance.seeder.ts b/apps/api/test/seeders/balance.seeder.ts new file mode 100644 index 000000000..ac308f23d --- /dev/null +++ b/apps/api/test/seeders/balance.seeder.ts @@ -0,0 +1,17 @@ +import type { Balance } from "@akashnetwork/http-sdk"; +import { faker } from "@faker-js/faker"; +import { merge } from "lodash"; + +import { DenomSeeder } from "@test/seeders/denom.seeder"; + +export class BalanceSeeder { + static create(input: Partial = {}): Balance { + return merge( + { + denom: DenomSeeder.create(), + amount: faker.number.int({ min: 0, max: 10000000 }).toString() + }, + input + ); + } +} diff --git a/apps/api/test/seeders/denom.seeder.ts b/apps/api/test/seeders/denom.seeder.ts new file mode 100644 index 000000000..a87f36a77 --- /dev/null +++ b/apps/api/test/seeders/denom.seeder.ts @@ -0,0 +1,8 @@ +import type { Denom } from "@akashnetwork/http-sdk"; +import { faker } from "@faker-js/faker"; + +export class DenomSeeder { + static create(): Denom { + return faker.helpers.arrayElement(["uakt", "ibc/170C677610AC31DF0904FFE09CD3B5C657492170E7E52372E48756B71E56F2F1"]); + } +} diff --git a/apps/api/test/seeders/deployment-grant.seeder.ts b/apps/api/test/seeders/deployment-grant.seeder.ts new file mode 100644 index 000000000..5ef4914c1 --- /dev/null +++ b/apps/api/test/seeders/deployment-grant.seeder.ts @@ -0,0 +1,28 @@ +import type { DeploymentAllowance } from "@akashnetwork/http-sdk"; +import { faker } from "@faker-js/faker"; +import { merge } from "lodash"; +import type { PartialDeep } from "type-fest"; + +import { AkashAddressSeeder } from "./akash-address.seeder"; + +import { DenomSeeder } from "@test/seeders/denom.seeder"; + +export class DeploymentGrantSeeder { + static create(input: PartialDeep = {}): DeploymentAllowance { + return merge( + { + granter: AkashAddressSeeder.create(), + grantee: AkashAddressSeeder.create(), + authorization: { + "@type": "/akash.deployment.v1beta3.DepositDeploymentAuthorization", + spend_limit: { + denom: DenomSeeder.create(), + amount: faker.number.int({ min: 0, max: 10000000 }).toString() + }, + expiration: faker.date.future().toISOString() + } + }, + input + ); + } +} diff --git a/apps/api/test/seeders/draining-deployment.seeder.ts b/apps/api/test/seeders/draining-deployment.seeder.ts new file mode 100644 index 000000000..251b75e2e --- /dev/null +++ b/apps/api/test/seeders/draining-deployment.seeder.ts @@ -0,0 +1,21 @@ +import { faker } from "@faker-js/faker"; + +import type { DrainingDeploymentOutput } from "@src/deployment/repositories/lease/lease.repository"; + +import { DenomSeeder } from "@test/seeders/denom.seeder"; + +export class DrainingDeploymentSeeder { + static create({ + dseq = faker.number.int({ min: 1, max: 99999999 }), + denom = DenomSeeder.create(), + blockRate = faker.number.int({ min: 1, max: 100 }), + predictedClosedHeight = faker.number.int({ min: 1, max: 99999999 }) + }: Partial = {}): DrainingDeploymentOutput { + return { + dseq, + denom, + blockRate, + predictedClosedHeight + }; + } +} diff --git a/apps/api/test/seeders/fees-authorization.seeder.ts b/apps/api/test/seeders/fees-authorization.seeder.ts new file mode 100644 index 000000000..a9ebbe9b0 --- /dev/null +++ b/apps/api/test/seeders/fees-authorization.seeder.ts @@ -0,0 +1,36 @@ +import type { FeeAllowance, SpendLimit } from "@akashnetwork/http-sdk"; +import { faker } from "@faker-js/faker"; +import { merge } from "lodash"; +import type { PartialDeep } from "type-fest"; + +import { AkashAddressSeeder } from "./akash-address.seeder"; + +import { DenomSeeder } from "@test/seeders/denom.seeder"; + +export interface FeeAllowanceSeederInput { + granter: string; + grantee: string; + allowance: { + spend_limit: SpendLimit; + expiration: string; + }; +} + +export class FeesAuthorizationSeeder { + static create(input: PartialDeep = {}): FeeAllowance { + return merge({ + granter: input.granter || AkashAddressSeeder.create(), + grantee: input.grantee || AkashAddressSeeder.create(), + allowance: { + "@type": "/akash.deployment.v1beta3.DepositDeploymentAuthorization", + spend_limit: [ + { + denom: input.allowance.spend_limit.denom || DenomSeeder.create(), + amount: input.allowance.spend_limit.amount || faker.number.int({ min: 0, max: 10000000 }).toString() + } + ], + expiration: faker.date.future().toISOString() + } + }); + } +} diff --git a/apps/api/test/seeders/node-seeder.ts b/apps/api/test/seeders/node.seeder.ts similarity index 100% rename from apps/api/test/seeders/node-seeder.ts rename to apps/api/test/seeders/node.seeder.ts diff --git a/apps/api/test/setup-unit-env.ts b/apps/api/test/setup-unit-env.ts new file mode 100644 index 000000000..586ee2f63 --- /dev/null +++ b/apps/api/test/setup-unit-env.ts @@ -0,0 +1,6 @@ +import "reflect-metadata"; + +import dotenv from "dotenv"; +import dotenvExpand from "dotenv-expand"; + +dotenvExpand.expand(dotenv.config({ path: "env/.env.unit.test" })); diff --git a/apps/api/test/setup-unit-tests.ts b/apps/api/test/setup-unit-tests.ts index e69de29bb..fded23ac1 100644 --- a/apps/api/test/setup-unit-tests.ts +++ b/apps/api/test/setup-unit-tests.ts @@ -0,0 +1 @@ +import "reflect-metadata"; diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index 579446f28..7670b2e50 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -1,5 +1,5 @@ { - "exclude": ["node_modules", "dist", "**/*spec.ts"], + "exclude": ["node_modules", "dist", "test"], "extends": "./tsconfig.build.json", "include": ["src/**/*"] } diff --git a/package-lock.json b/package-lock.json index 7d0d9dd2f..b5ebd1c27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ }, "apps/api": { "name": "@akashnetwork/console-api", - "version": "2.28.0-beta.1", + "version": "2.28.0-beta.3", "license": "Apache-2.0", "dependencies": { "@akashnetwork/akash-api": "^1.3.0", @@ -131,6 +131,7 @@ "supertest": "^6.1.5", "ts-jest": "^29.1.4", "ts-loader": "^9.2.5", + "type-fest": "^4.26.1", "typescript": "5.1.3", "webpack": "^5.91.0", "webpack-cli": "4.10.0", @@ -269,9 +270,21 @@ "real-require": "^0.2.0" } }, + "apps/api/node_modules/type-fest": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "apps/deploy-web": { "name": "@akashnetwork/console-web", - "version": "2.20.3", + "version": "2.21.0-beta.0", "license": "Apache-2.0", "dependencies": { "@akashnetwork/akash-api": "^1.3.0", @@ -39461,7 +39474,7 @@ }, "packages/http-sdk": { "name": "@akashnetwork/http-sdk", - "version": "1.0.7", + "version": "1.0.8", "license": "Apache-2.0", "dependencies": { "axios": "^1.7.2", diff --git a/packages/dev-config/.eslintrc.base.js b/packages/dev-config/.eslintrc.base.js index 0b6ca3f6f..52b13ebd6 100644 --- a/packages/dev-config/.eslintrc.base.js +++ b/packages/dev-config/.eslintrc.base.js @@ -11,7 +11,7 @@ module.exports = { "simple-import-sort/imports": [ "error", { - groups: [["^\\u0000"], ["^react", "^(?!@src)@?\\w"], ["^@src", "^\\.\\.(?!/?$)", "^\\.\\./?$", "^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"]] + groups: [["^\\u0000"], ["^react", "^(?!(@src|@test))@?\\w"], ["^@src", "^\\.\\.(?!/?$)", "^\\.\\./?$", "^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"]] } ], "space-infix-ops": ["error", { int32Hint: false }], diff --git a/packages/http-sdk/src/allowance/allowance-http.service.ts b/packages/http-sdk/src/allowance/allowance-http.service.ts index a915fcc5d..b1a3a2deb 100644 --- a/packages/http-sdk/src/allowance/allowance-http.service.ts +++ b/packages/http-sdk/src/allowance/allowance-http.service.ts @@ -3,12 +3,12 @@ import type { AxiosRequestConfig } from "axios"; import { HttpService } from "../http/http.service"; import type { Denom } from "../types/denom.type"; -type SpendLimit = { +export interface SpendLimit { denom: Denom; amount: string; -}; +} -interface FeeAllowance { +export interface FeeAllowance { granter: string; grantee: string; allowance: { @@ -63,16 +63,21 @@ export class AllowanceHttpService extends HttpService { return allowances.grants; } - async paginateDeploymentGrantsForGrantee(address: string, cb: (page: DeploymentAllowanceResponse["grants"]) => Promise) { + async paginateDeploymentGrants( + options: ({ granter: string } | { grantee: string }) & { limit: number }, + cb: (page: DeploymentAllowanceResponse["grants"]) => Promise + ) { let nextPageKey: string | null = null; + const side = "granter" in options ? "granter" : "grantee"; + const address = "granter" in options ? options.granter : options.grantee; do { const response = this.extractData( await this.get( - `cosmos/authz/v1beta1/grants/grantee/${address}`, + `cosmos/authz/v1beta1/grants/${side}/${address}`, nextPageKey ? { - params: { "pagination.key": nextPageKey } + params: { "pagination.key": nextPageKey, "pagination.limit": options.limit } } : undefined ) diff --git a/packages/http-sdk/src/balance/balance-http.service.ts b/packages/http-sdk/src/balance/balance-http.service.ts index 558cfa896..a9c3477ed 100644 --- a/packages/http-sdk/src/balance/balance-http.service.ts +++ b/packages/http-sdk/src/balance/balance-http.service.ts @@ -3,7 +3,7 @@ import type { AxiosRequestConfig } from "axios"; import { HttpService } from "../http/http.service"; import type { Denom } from "../types/denom.type"; -interface Balance { +export interface Balance { amount: string; denom: Denom; } diff --git a/packages/http-sdk/src/block/block-http.service.ts b/packages/http-sdk/src/block/block-http.service.ts new file mode 100644 index 000000000..807f05085 --- /dev/null +++ b/packages/http-sdk/src/block/block-http.service.ts @@ -0,0 +1,22 @@ +import { AxiosRequestConfig } from "axios"; + +import { HttpService } from "../http/http.service"; + +interface BlockResponse { + block: { + header: { + height: string; + }; + }; +} + +export class BlockHttpService extends HttpService { + constructor(config?: Pick) { + super(config); + } + + async getCurrentHeight() { + const response = this.extractData(await this.get("blocks/latest")); + return parseInt(response.block.header.height); + } +} diff --git a/packages/http-sdk/src/index.ts b/packages/http-sdk/src/index.ts index 4d1a8795e..56288ad70 100644 --- a/packages/http-sdk/src/index.ts +++ b/packages/http-sdk/src/index.ts @@ -5,4 +5,5 @@ export * from "./tx-http/tx-http.service"; export * from "./managed-wallet-http/managed-wallet-http.service"; export * from "./user-http/user-http.service"; export * from "./balance/balance-http.service"; +export * from "./block/block-http.service"; export * from "./types/denom.type";