diff --git a/apps/api/CHANGELOG.md b/apps/api/CHANGELOG.md index 733c5facf..0a5b5f6f2 100644 --- a/apps/api/CHANGELOG.md +++ b/apps/api/CHANGELOG.md @@ -1,5 +1,37 @@ +## [2.38.0-beta.1](https://github.com/akash-network/console/compare/console-api/v2.38.0-beta.0...console-api/v2.38.0-beta.1) (2024-12-10) + + +### Features + +* **deployment:** auto top up custodial deployment with available amount ([0792a36](https://github.com/akash-network/console/commit/0792a367f64d83ed040043c021b98e5be2d82c80)), closes [#524](https://github.com/akash-network/console/issues/524) + +## [2.38.0-beta.0](https://github.com/akash-network/console/compare/console-api/v2.37.0...console-api/v2.38.0-beta.0) (2024-12-09) + + +### Features + +* **deployment:** add context to top up error logging ([d8ab845](https://github.com/akash-network/console/commit/d8ab845a3ab6f337f2eefeacec5534a7d90c5837)) + +## [2.37.0](https://github.com/akash-network/console/compare/console-api/v2.37.0-beta.0...console-api/v2.37.0) (2024-12-06) + +## [2.37.0-beta.0](https://github.com/akash-network/console/compare/console-api/v2.36.0...console-api/v2.37.0-beta.0) (2024-12-05) + + +### Features + +* **deployment:** auto top up managed deployment with available amount ([644332f](https://github.com/akash-network/console/commit/644332ffb6b7dffab2150f4ec6dfb70601df6816)) + +## [2.36.0](https://github.com/akash-network/console/compare/console-api/v2.36.0-beta.0...console-api/v2.36.0) (2024-12-04) + +## [2.36.0-beta.0](https://github.com/akash-network/console/compare/console-api/v2.35.4-beta.0...console-api/v2.36.0-beta.0) (2024-12-04) + + +### Features + +* **deployment:** add concurrency for top up deployment ([0b4abf5](https://github.com/akash-network/console/commit/0b4abf5510eca6986257081ba5481936b6cb54f6)), closes [#519](https://github.com/akash-network/console/issues/519) + ## [2.35.4-beta.0](https://github.com/akash-network/console/compare/console-api/v2.35.3...console-api/v2.35.4-beta.0) (2024-12-03) diff --git a/apps/api/package.json b/apps/api/package.json index a8f36800b..1d2edce80 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "@akashnetwork/console-api", - "version": "2.35.4-beta.0", + "version": "2.38.0-beta.1", "description": "Api providing data to the deploy tool", "repository": { "type": "git", diff --git a/apps/api/src/billing/services/provider-cleanup/provider-cleanup.service.ts b/apps/api/src/billing/services/provider-cleanup/provider-cleanup.service.ts index 111697400..88794548f 100644 --- a/apps/api/src/billing/services/provider-cleanup/provider-cleanup.service.ts +++ b/apps/api/src/billing/services/provider-cleanup/provider-cleanup.service.ts @@ -4,17 +4,12 @@ import { singleton } from "tsyringe"; import { BillingConfig, InjectBillingConfig } from "@src/billing/providers"; import { UserWalletOutput, UserWalletRepository } from "@src/billing/repositories"; import { ManagedUserWalletService, RpcMessageService } from "@src/billing/services"; +import { ProviderCleanupParams } from "@src/billing/types/provider-cleanup"; import { ErrorService } from "@src/core/services/error/error.service"; import { ProviderCleanupSummarizer } from "@src/deployment/lib/provider-cleanup-summarizer/provider-cleanup-summarizer"; import { DeploymentRepository } from "@src/deployment/repositories/deployment/deployment.repository"; import { TxSignerService } from "../tx-signer/tx-signer.service"; -export interface ProviderCleanupParams { - concurrency: number; - provider: string; - dryRun: boolean; -} - @singleton() export class ProviderCleanupService { private readonly logger = LoggerService.forContext(ProviderCleanupService.name); diff --git a/apps/api/src/billing/types/provider-cleanup.ts b/apps/api/src/billing/types/provider-cleanup.ts new file mode 100644 index 000000000..90c72fff3 --- /dev/null +++ b/apps/api/src/billing/types/provider-cleanup.ts @@ -0,0 +1,5 @@ +import { ConcurrencyOptions, DryRunOptions } from "@src/core/types/console"; + +export interface ProviderCleanupParams extends DryRunOptions, ConcurrencyOptions { + provider: string; +} diff --git a/apps/api/src/console.ts b/apps/api/src/console.ts index 115bc32b1..98437f963 100644 --- a/apps/api/src/console.ts +++ b/apps/api/src/console.ts @@ -33,10 +33,11 @@ program program .command("top-up-deployments") .description("Refill deployments with auto top up enabled") + .option("-c, --concurrency ", "How many wallets is processed concurrently", value => z.number({ coerce: true }).optional().default(10).parse(value)) .option("-d, --dry-run", "Dry run the top up deployments", false) .action(async (options, command) => { await executeCliHandler(command.name(), async () => { - await container.resolve(TopUpDeploymentsController).topUpDeployments({ dryRun: options.dryRun }); + await container.resolve(TopUpDeploymentsController).topUpDeployments(options); }); }); diff --git a/apps/api/src/core/types/console.ts b/apps/api/src/core/types/console.ts index a53d4170a..948768c0b 100644 --- a/apps/api/src/core/types/console.ts +++ b/apps/api/src/core/types/console.ts @@ -1,3 +1,8 @@ export interface DryRunOptions { dryRun: boolean; } + +export interface ConcurrencyOptions { + concurrency?: number; +} + diff --git a/apps/api/src/deployment/controllers/deployment/deployment.controller.ts b/apps/api/src/deployment/controllers/deployment/deployment.controller.ts index b6d8157cc..37dc0497b 100644 --- a/apps/api/src/deployment/controllers/deployment/deployment.controller.ts +++ b/apps/api/src/deployment/controllers/deployment/deployment.controller.ts @@ -1,12 +1,10 @@ import { singleton } from "tsyringe"; -import { - CleanUpStaleDeploymentsParams, - StaleManagedDeploymentsCleanerService -} from "@src/deployment/services/stale-managed-deployments-cleaner/stale-managed-deployments-cleaner.service"; +import { StaleManagedDeploymentsCleanerService } from "@src/deployment/services/stale-managed-deployments-cleaner/stale-managed-deployments-cleaner.service"; import { TopUpCustodialDeploymentsService } from "@src/deployment/services/top-up-custodial-deployments/top-up-custodial-deployments.service"; import { TopUpManagedDeploymentsService } from "@src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service"; import { TopUpDeploymentsOptions } from "@src/deployment/types/deployments-refiller"; +import { CleanUpStaleDeploymentsParams } from "@src/deployment/types/state-deployments"; @singleton() export class TopUpDeploymentsController { diff --git a/apps/api/src/deployment/controllers/provider/provider.controller.ts b/apps/api/src/deployment/controllers/provider/provider.controller.ts index de90d8154..2798097c7 100644 --- a/apps/api/src/deployment/controllers/provider/provider.controller.ts +++ b/apps/api/src/deployment/controllers/provider/provider.controller.ts @@ -1,6 +1,7 @@ import { singleton } from "tsyringe"; -import { ProviderCleanupParams, ProviderCleanupService } from "@src/billing/services/provider-cleanup/provider-cleanup.service"; +import { ProviderCleanupService } from "@src/billing/services/provider-cleanup/provider-cleanup.service"; +import { ProviderCleanupParams } from "@src/billing/types/provider-cleanup"; import { TrialProvidersService } from "@src/deployment/services/trial-providers/trial-providers.service"; @singleton() diff --git a/apps/api/src/deployment/services/stale-managed-deployments-cleaner/stale-managed-deployments-cleaner.service.ts b/apps/api/src/deployment/services/stale-managed-deployments-cleaner/stale-managed-deployments-cleaner.service.ts index 73922e992..ded9b8eab 100644 --- a/apps/api/src/deployment/services/stale-managed-deployments-cleaner/stale-managed-deployments-cleaner.service.ts +++ b/apps/api/src/deployment/services/stale-managed-deployments-cleaner/stale-managed-deployments-cleaner.service.ts @@ -9,12 +9,9 @@ import { TxSignerService } from "@src/billing/services/tx-signer/tx-signer.servi import { BlockRepository } from "@src/chain/repositories/block.repository"; import { ErrorService } from "@src/core/services/error/error.service"; import { DeploymentRepository } from "@src/deployment/repositories/deployment/deployment.repository"; +import { CleanUpStaleDeploymentsParams } from "@src/deployment/types/state-deployments"; import { averageBlockTime } from "@src/utils/constants"; -export interface CleanUpStaleDeploymentsParams { - concurrency: number; -} - @singleton() export class StaleManagedDeploymentsCleanerService { private readonly logger = LoggerService.forContext(StaleManagedDeploymentsCleanerService.name); diff --git a/apps/api/src/deployment/services/top-up-custodial-balance/top-up-custodial-balance.service.ts b/apps/api/src/deployment/services/top-up-custodial-balance/top-up-custodial-balance.service.ts new file mode 100644 index 000000000..b2623e2d3 --- /dev/null +++ b/apps/api/src/deployment/services/top-up-custodial-balance/top-up-custodial-balance.service.ts @@ -0,0 +1,23 @@ +interface Balances { + denom: string; + feesLimit: number; + deploymentLimit: number; + balance: number; + feesBalance?: number; +} + +export class TopUpCustodialBalanceService { + constructor(readonly balances: Balances) {} + + recordTx(amount: number, fees: number) { + this.balances.deploymentLimit -= amount; + this.balances.balance -= amount; + this.balances.feesLimit -= fees; + + if (this.balances.denom === "uakt") { + this.balances.balance -= fees; + } else { + this.balances.feesBalance -= fees; + } + } +} 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 index 2bc4354ce..7d67ee030 100644 --- 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 @@ -1,8 +1,8 @@ import "@test/mocks/logger-service.mock"; import { AllowanceHttpService, BalanceHttpService, Denom } from "@akashnetwork/http-sdk"; -import { faker } from "@faker-js/faker"; import { MsgExec } from "cosmjs-types/cosmos/authz/v1beta1/tx"; +import { secondsInWeek } from "date-fns/constants"; import { describe } from "node:test"; import { container } from "tsyringe"; @@ -25,6 +25,8 @@ import { DrainingDeploymentSeeder } from "@test/seeders/draining-deployment.seed import { FeesAuthorizationSeeder } from "@test/seeders/fees-authorization.seeder"; import { stub } from "@test/services/stub"; +const USDC_IBC_DENOM = "ibc/170C677610AC31DF0904FFE09CD3B5C657492170E7E52372E48756B71E56F2F1"; + describe(TopUpCustodialDeploymentsService.name, () => { const CURRENT_BLOCK_HEIGHT = 7481457; const UAKT_TOP_UP_MASTER_WALLET_ADDRESS = AkashAddressSeeder.create(); @@ -71,13 +73,14 @@ describe(TopUpCustodialDeploymentsService.name, () => { type SeedParams = { denom: Denom; - balance?: string; + balance?: number; + feesBalance?: number; grantee: string; expectedDeploymentsTopUpCount?: 0 | 1 | 2; hasDeployments?: boolean; }; - const seedFor = ({ denom, balance = "100000000", grantee, expectedDeploymentsTopUpCount = 2, hasDeployments = true }: SeedParams) => { + const seedFor = ({ denom, balance = 100000000, feesBalance = 1000000, grantee, expectedDeploymentsTopUpCount = 2, hasDeployments = true }: SeedParams) => { const owner = AkashAddressSeeder.create(); return { @@ -90,8 +93,9 @@ describe(TopUpCustodialDeploymentsService.name, () => { feeAllowance: FeesAuthorizationSeeder.create({ granter: owner, grantee: grantee, - allowance: { spend_limit: { denom } } + allowance: { spend_limit: { denom: "uakt" } } }), + feesBalance: denom === "uakt" ? undefined : BalanceSeeder.create({ denom: "uakt", amount: feesBalance }), drainingDeployments: hasDeployments ? [ { @@ -113,24 +117,66 @@ describe(TopUpCustodialDeploymentsService.name, () => { grantee: UAKT_TOP_UP_MASTER_WALLET_ADDRESS }), seedFor({ - denom: "ibc/170C677610AC31DF0904FFE09CD3B5C657492170E7E52372E48756B71E56F2F1", + denom: USDC_IBC_DENOM, grantee: USDT_TOP_UP_MASTER_WALLET_ADDRESS }), + seedFor({ + denom: USDC_IBC_DENOM, + grantee: USDT_TOP_UP_MASTER_WALLET_ADDRESS, + balance: 5500000, + expectedDeploymentsTopUpCount: 2 + }), + seedFor({ + denom: USDC_IBC_DENOM, + grantee: USDT_TOP_UP_MASTER_WALLET_ADDRESS, + balance: 5040000, + expectedDeploymentsTopUpCount: 1 + }), + seedFor({ + denom: USDC_IBC_DENOM, + grantee: USDT_TOP_UP_MASTER_WALLET_ADDRESS, + balance: 5500000, + expectedDeploymentsTopUpCount: 2 + }), + seedFor({ + denom: USDC_IBC_DENOM, + grantee: USDT_TOP_UP_MASTER_WALLET_ADDRESS, + feesBalance: 0, + expectedDeploymentsTopUpCount: 0 + }), + seedFor({ + denom: USDC_IBC_DENOM, + grantee: USDT_TOP_UP_MASTER_WALLET_ADDRESS, + feesBalance: 5000, + expectedDeploymentsTopUpCount: 1 + }), + seedFor({ + denom: "uakt", + balance: 5045000, + grantee: UAKT_TOP_UP_MASTER_WALLET_ADDRESS, + expectedDeploymentsTopUpCount: 1 + }), seedFor({ denom: "uakt", - balance: "5500000", + balance: 5000, + grantee: UAKT_TOP_UP_MASTER_WALLET_ADDRESS, + expectedDeploymentsTopUpCount: 0 + }), + seedFor({ + denom: "uakt", + balance: 10000, grantee: UAKT_TOP_UP_MASTER_WALLET_ADDRESS, expectedDeploymentsTopUpCount: 1 }), seedFor({ denom: "uakt", - balance: "5500000", + balance: 5500000, grantee: UAKT_TOP_UP_MASTER_WALLET_ADDRESS, hasDeployments: false }), seedFor({ denom: "uakt", - balance: "0", + balance: 0, grantee: UAKT_TOP_UP_MASTER_WALLET_ADDRESS, expectedDeploymentsTopUpCount: 0 }) @@ -143,12 +189,20 @@ describe(TopUpCustodialDeploymentsService.name, () => { 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 - } - ); + const record = data.find(({ grant }) => grant.granter === address); + + if (record?.balance.denom === denom) { + return record.balance; + } + + if (record?.feesBalance.denom === denom) { + return record.feesBalance; + } + + return { + amount: 0, + denom + }; }); jest.spyOn(drainingDeploymentService, "findDeployments").mockImplementation(async (owner, denom) => { return ( @@ -157,18 +211,22 @@ describe(TopUpCustodialDeploymentsService.name, () => { ?.drainingDeployments?.map(({ deployment }) => deployment) || [] ); }); - jest.spyOn(drainingDeploymentService, "calculateTopUpAmount").mockImplementation(async () => faker.number.int({ min: 3500000, max: 4000000 })); + jest.spyOn(drainingDeploymentService, "calculateTopUpAmount").mockImplementation(async ({ blockRate }) => (blockRate * secondsInWeek) / 6); it("should top up draining deployment given owners have sufficient grants and balances", async () => { await topUpDeploymentsService.topUpDeployments({ dryRun: false }); - expect(uaktMasterSigningClientService.executeTx).toHaveBeenCalledTimes(3); - expect(usdtMasterSigningClientService.executeTx).toHaveBeenCalledTimes(2); + let uaktCount = 0; + let usdtCount = 0; data.forEach(({ drainingDeployments, grant }) => { drainingDeployments.forEach(({ isExpectedToTopUp, deployment }) => { if (isExpectedToTopUp) { - const client = deployment.denom === "uakt" ? uaktMasterSigningClientService : usdtMasterSigningClientService; + const isAkt = deployment.denom === "uakt"; + const client = isAkt ? uaktMasterSigningClientService : usdtMasterSigningClientService; + uaktCount += isAkt ? 1 : 0; + usdtCount += isAkt ? 0 : 1; + expect(client.executeTx).toHaveBeenCalledWith( [ { @@ -189,6 +247,9 @@ describe(TopUpCustodialDeploymentsService.name, () => { } }); }); + + expect(uaktMasterSigningClientService.executeTx).toHaveBeenCalledTimes(uaktCount); + expect(usdtMasterSigningClientService.executeTx).toHaveBeenCalledTimes(usdtCount); }); xdescribe("actual top up deployment tx on demand", () => { 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 index 53f021bbe..8e1586b3a 100644 --- 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 @@ -7,6 +7,7 @@ import { BlockHttpService } from "@src/chain/services/block-http/block-http.serv import { ErrorService } from "@src/core/services/error/error.service"; import { TopUpSummarizer } from "@src/deployment/lib/top-up-summarizer/top-up-summarizer"; import { DrainingDeploymentService } from "@src/deployment/services/draining-deployment/draining-deployment.service"; +import { TopUpCustodialBalanceService } from "@src/deployment/services/top-up-custodial-balance/top-up-custodial-balance.service"; import { TopUpToolsService } from "@src/deployment/services/top-up-tools/top-up-tools.service"; import { DeploymentsRefiller, TopUpDeploymentsOptions } from "@src/deployment/types/deployments-refiller"; @@ -15,6 +16,7 @@ interface Balances { feesLimit: number; deploymentLimit: number; balance: number; + feesBalance?: number; } interface TopUpSummary { @@ -52,7 +54,7 @@ export class TopUpCustodialDeploymentsService implements DeploymentsRefiller { await Promise.all( grants.map(async grant => { await this.errorService.execWithErrorHandler( - { grant, event: "TOP_UP_ERROR" }, + { context: TopUpCustodialDeploymentsService.name, grant, event: "TOP_UP_ERROR" }, () => this.topUpForGrant(grant, client, options, summary), () => summary.inc("walletsTopUpErrorCount") ); @@ -82,21 +84,19 @@ export class TopUpCustodialDeploymentsService implements DeploymentsRefiller { return; } - const balances = await this.collectWalletBalances(grant); - - let { deploymentLimit, feesLimit, balance } = balances; + const balancesService = new TopUpCustodialBalanceService(await this.collectWalletBalances(grant)); let hasTopUp = false; for (const { dseq, denom, blockRate, predictedClosedHeight } of drainingDeployments) { - const amount = await this.drainingDeploymentService.calculateTopUpAmount({ blockRate }); - if (!this.canTopUp(amount, { deploymentLimit, feesLimit, balance })) { - this.logger.info({ event: "INSUFFICIENT_BALANCE", granter: owner, grantee, balances: { deploymentLimit, feesLimit, balance } }); + const amount = await this.calculateTopUpAmount(blockRate, balancesService.balances); + + if (!this.canTopUp(amount, balancesService.balances)) { + this.logger.info({ event: "INSUFFICIENT_BALANCE", granter: owner, grantee, balances: balancesService.balances }); summary.inc("insufficientBalanceCount"); break; } - deploymentLimit -= amount; - feesLimit -= this.MIN_FEES_AVAILABLE; - balance -= amount + this.MIN_FEES_AVAILABLE; + + balancesService.recordTx(amount, this.MIN_FEES_AVAILABLE); await this.topUpDeployment( { @@ -124,27 +124,50 @@ export class TopUpCustodialDeploymentsService implements DeploymentsRefiller { 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); + const feesLimit = await this.retrieveFeesLimit(grant.granter, grant.grantee); + const [{ amount: balance }, feesBalance] = await Promise.all([ + this.balanceHttpService.getBalance(grant.granter, denom), + denom !== "uakt" && this.balanceHttpService.getBalance(grant.granter, "uakt") + ]); return { denom, feesLimit, deploymentLimit, - balance + balance, + feesBalance: feesBalance?.amount }; } - private async retrieveFeesLimit(granter: string, grantee: string, denom: string) { + private async retrieveFeesLimit(granter: string, grantee: string) { const feesAllowance = await this.allowanceHttpService.getFeeAllowanceForGranterAndGrantee(granter, grantee); - const feesSpendLimit = feesAllowance.allowance.spend_limit.find(limit => limit.denom === denom); + const feesSpendLimit = feesAllowance.allowance.spend_limit.find(limit => limit.denom === "uakt"); return feesSpendLimit ? parseFloat(feesSpendLimit.amount) : 0; } - private canTopUp(amount: number, balances: Pick) { - return balances.deploymentLimit > amount && balances.feesLimit > this.MIN_FEES_AVAILABLE && balances.balance > amount + this.MIN_FEES_AVAILABLE; + private async calculateTopUpAmount(blockRate: number, balances: Balances) { + const amount = await this.drainingDeploymentService.calculateTopUpAmount({ blockRate }); + + if (balances.denom === "uakt") { + const smallestAmount = Math.min(amount, balances.deploymentLimit - this.MIN_FEES_AVAILABLE, balances.balance - this.MIN_FEES_AVAILABLE); + return Math.max(smallestAmount, 0); + } + + return Math.min(amount, balances.deploymentLimit, balances.balance); + } + + private canTopUp(amount: number, balances: Balances) { + if (!amount) { + return false; + } + + const hasSufficientDeploymentLimit = amount <= balances.deploymentLimit; + const hasSufficientFeesLimit = balances.feesLimit >= this.MIN_FEES_AVAILABLE; + const hasSufficientFeesBalance = typeof balances.feesBalance === "undefined" || balances.feesBalance >= this.MIN_FEES_AVAILABLE; + const hasSufficientBalance = balances.balance >= (balances.denom === "uakt" ? amount + this.MIN_FEES_AVAILABLE : amount); + + return hasSufficientDeploymentLimit && hasSufficientFeesLimit && hasSufficientFeesBalance && hasSufficientBalance; } async topUpDeployment({ grantee, ...messageInput }: ExecDepositDeploymentMsgOptions, client: MasterSigningClientService, options: TopUpDeploymentsOptions) { diff --git a/apps/api/src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service.spec.ts b/apps/api/src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service.spec.ts index 700b3232d..57961e98b 100644 --- a/apps/api/src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service.spec.ts +++ b/apps/api/src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service.spec.ts @@ -83,7 +83,8 @@ describe(TopUpManagedDeploymentsService.name, () => { const data = [ seedFor({}), - seedFor({ balance: "5000000", expectedDeploymentsTopUpCount: 1 }), + seedFor({ balance: "50000000", expectedDeploymentsTopUpCount: 2 }), + seedFor({ balance: "1408022", expectedDeploymentsTopUpCount: 1 }), seedFor({ hasDeployments: false }), seedFor({ balance: "0", expectedDeploymentsTopUpCount: 0 }) ]; diff --git a/apps/api/src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service.ts b/apps/api/src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service.ts index bd8572510..68c544f80 100644 --- a/apps/api/src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service.ts +++ b/apps/api/src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service.ts @@ -15,8 +15,6 @@ import { DeploymentsRefiller, TopUpDeploymentsOptions } from "@src/deployment/ty @singleton() export class TopUpManagedDeploymentsService implements DeploymentsRefiller { - private readonly CONCURRENCY = 10; - private readonly logger = LoggerService.forContext(TopUpManagedDeploymentsService.name); constructor( @@ -35,7 +33,7 @@ export class TopUpManagedDeploymentsService implements DeploymentsRefiller { const summary = new TopUpSummarizer(); summary.set("startBlockHeight", await this.blockHttpService.getCurrentHeight()); - await this.userWalletRepository.paginate({ limit: this.CONCURRENCY }, async wallets => { + await this.userWalletRepository.paginate({ limit: options.concurrency || 10 }, async wallets => { await Promise.all( wallets.map(async wallet => { await this.errorService.execWithErrorHandler( @@ -48,10 +46,11 @@ export class TopUpManagedDeploymentsService implements DeploymentsRefiller { }); summary.set("endBlockHeight", await this.blockHttpService.getCurrentHeight()); - this.logger.info({ event: "TOP_UP_SUMMARY", summary: summary.summarize(), dryRun: options.dryRun }); + this.logger.info({ context: TopUpManagedDeploymentsService.name, event: "TOP_UP_SUMMARY", summary: summary.summarize(), dryRun: options.dryRun }); } private async topUpForWallet(wallet: UserWalletOutput, options: TopUpDeploymentsOptions, summary: TopUpSummarizer) { + const depositor = await this.managedMasterWalletService.getFirstAddress(); summary.inc("walletsCount"); const owner = wallet.address; const denom = this.billingConfig.DEPLOYMENT_GRANT_DENOM; @@ -63,14 +62,15 @@ export class TopUpManagedDeploymentsService implements DeploymentsRefiller { } const signer = await this.txSignerService.getClientForAddressIndex(wallet.id); - const depositor = await this.managedMasterWalletService.getFirstAddress(); let balance = await this.balancesService.retrieveAndCalcDeploymentLimit(wallet); let hasTopUp = false; for (const deployment of drainingDeployments) { - const amount = await this.drainingDeploymentService.calculateTopUpAmount(deployment); - if (amount > balance) { + let amount = await this.drainingDeploymentService.calculateTopUpAmount(deployment); + amount = Math.min(amount, balance); + + if (amount <= 0) { this.logger.info({ event: "INSUFFICIENT_BALANCE", owner, balance, amount }); summary.inc("insufficientBalanceCount"); break; diff --git a/apps/api/src/deployment/types/deployments-refiller.ts b/apps/api/src/deployment/types/deployments-refiller.ts index c49df7631..f51f79467 100644 --- a/apps/api/src/deployment/types/deployments-refiller.ts +++ b/apps/api/src/deployment/types/deployments-refiller.ts @@ -1,6 +1,6 @@ -import { DryRunOptions } from "@src/core/types/console"; +import { ConcurrencyOptions, DryRunOptions } from "@src/core/types/console"; -export interface TopUpDeploymentsOptions extends DryRunOptions {} +export interface TopUpDeploymentsOptions extends DryRunOptions, ConcurrencyOptions {} export interface DeploymentsRefiller { topUpDeployments(options: TopUpDeploymentsOptions): Promise; diff --git a/apps/api/src/deployment/types/state-deployments.ts b/apps/api/src/deployment/types/state-deployments.ts new file mode 100644 index 000000000..3a74479f2 --- /dev/null +++ b/apps/api/src/deployment/types/state-deployments.ts @@ -0,0 +1,3 @@ +import { ConcurrencyOptions } from "@src/core/types/console"; + +export interface CleanUpStaleDeploymentsParams extends ConcurrencyOptions {} diff --git a/apps/api/src/routes/internal/gpuPrices.ts b/apps/api/src/routes/internal/gpuPrices.ts index 50d972938..1a915674c 100644 --- a/apps/api/src/routes/internal/gpuPrices.ts +++ b/apps/api/src/routes/internal/gpuPrices.ts @@ -123,15 +123,25 @@ async function getGpuPrices(debug: boolean) { where: { createdHeight: { [Op.gte]: minHeight } }, include: [ { + attributes: [], model: DeploymentGroup, required: true, - include: [{ model: DeploymentGroupResource, required: true, where: { gpuUnits: 1 } }] + include: [ + { + attributes: [], + model: DeploymentGroupResource, + required: true, + where: { gpuUnits: 1 } + } + ] }, { + attributes: ["height", "data"], model: AkashMessage, as: "relatedMessages", where: { - type: "/akash.market.v1beta4.MsgCreateBid" + type: "/akash.market.v1beta4.MsgCreateBid", + height: { [Op.gte]: minHeight } }, include: [ { model: Block, attributes: ["height", "dayId", "datetime"], required: true }, @@ -148,7 +158,7 @@ async function getGpuPrices(debug: boolean) { const gpuBids: GpuBidType[] = deployments .flatMap(d => d.relatedMessages.map(x => { - const day = days.find(d => d.id === x.block.dayId); + const day = days.find(day => day.id === x.block.dayId); const decodedBid = decodeMsg("/akash.market.v1beta4.MsgCreateBid", x.data) as MsgCreateBid; if (!day || !day.aktPrice) return null; // Ignore bids for days where we don't have the AKT price diff --git a/apps/deploy-web/CHANGELOG.md b/apps/deploy-web/CHANGELOG.md index 2b1a77a77..ac08603a4 100644 --- a/apps/deploy-web/CHANGELOG.md +++ b/apps/deploy-web/CHANGELOG.md @@ -1,5 +1,34 @@ +## [2.25.3](https://github.com/akash-network/console/compare/console-web/v2.25.3-beta.1...console-web/v2.25.3) (2024-12-05) + +## [2.25.3-beta.1](https://github.com/akash-network/console/compare/console-web/v2.25.3-beta.0...console-web/v2.25.3-beta.1) (2024-12-05) + + +### Bug Fixes + +* **config:** increase nginx large header buffer limit ([#526](https://github.com/akash-network/console/issues/526)) ([192b9a2](https://github.com/akash-network/console/commit/192b9a27d1f43f88762cca625ff19460281a9995)) + +## [2.25.3-beta.0](https://github.com/akash-network/console/compare/console-web/v2.25.2...console-web/v2.25.3-beta.0) (2024-12-05) + + +### Bug Fixes + +* **billing:** mobile banner styling ([566d941](https://github.com/akash-network/console/commit/566d9416149698d195e2463f32903496ff497a22)), closes [#525](https://github.com/akash-network/console/issues/525) + +## [2.25.2](https://github.com/akash-network/console/compare/console-web/v2.25.2-beta.0...console-web/v2.25.2) (2024-12-04) + +## [2.25.2-beta.0](https://github.com/akash-network/console/compare/console-web/v2.25.1...console-web/v2.25.2-beta.0) (2024-12-04) + + +### Bug Fixes + +* **billing:** banner light mode styling ([a5aa61b](https://github.com/akash-network/console/commit/a5aa61bff15bfbb4f2e4bdf7664f983f79b10f3a)), closes [#520](https://github.com/akash-network/console/issues/520) + +## [2.25.1](https://github.com/akash-network/console/compare/console-web/v2.25.1-beta.2...console-web/v2.25.1) (2024-12-04) + +## [2.25.1-beta.2](https://github.com/akash-network/console/compare/console-web/v2.25.1-beta.1...console-web/v2.25.1-beta.2) (2024-12-04) + ## [2.25.1-beta.1](https://github.com/akash-network/console/compare/console-web/v2.25.1-beta.0...console-web/v2.25.1-beta.1) (2024-12-03) diff --git a/apps/deploy-web/nginx.conf b/apps/deploy-web/nginx.conf index 12dde60e7..6b2ad1ef1 100644 --- a/apps/deploy-web/nginx.conf +++ b/apps/deploy-web/nginx.conf @@ -21,6 +21,8 @@ http { ssl_certificate /etc/nginx/ssl/my_ssl_cert.crt; ssl_certificate_key /etc/nginx/ssl/my_ssl_key.key; + large_client_header_buffers 4 16k; + location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; diff --git a/apps/deploy-web/package.json b/apps/deploy-web/package.json index 3784e04d9..108fe7364 100644 --- a/apps/deploy-web/package.json +++ b/apps/deploy-web/package.json @@ -1,6 +1,6 @@ { "name": "@akashnetwork/console-web", - "version": "2.25.1-beta.1", + "version": "2.25.3", "private": true, "description": "Web UI to deploy on the Akash Network and view statistic about network usage.", "license": "Apache-2.0", @@ -21,6 +21,7 @@ "@akashnetwork/akashjs": "^0.10.0", "@akashnetwork/env-loader": "*", "@akashnetwork/http-sdk": "*", + "@akashnetwork/logging": "*", "@akashnetwork/network-store": "*", "@akashnetwork/ui": "*", "@auth0/nextjs-auth0": "^3.5.0", diff --git a/apps/deploy-web/src/components/layout/CreditCardBanner.tsx b/apps/deploy-web/src/components/layout/CreditCardBanner.tsx new file mode 100644 index 000000000..f6d806340 --- /dev/null +++ b/apps/deploy-web/src/components/layout/CreditCardBanner.tsx @@ -0,0 +1,14 @@ +import { useWallet } from "@src/context/WalletProvider/WalletProvider"; +import { ConnectManagedWalletButton } from "../wallet/ConnectManagedWalletButton"; + +export function CreditCardBanner() { + const { hasManagedWallet } = useWallet(); + + return ( +
+ Credit Card payments are now available! + + {!hasManagedWallet && } +
+ ); +} diff --git a/apps/deploy-web/src/components/layout/Layout.tsx b/apps/deploy-web/src/components/layout/Layout.tsx index 97afb5d2f..bc2e4faf8 100644 --- a/apps/deploy-web/src/components/layout/Layout.tsx +++ b/apps/deploy-web/src/components/layout/Layout.tsx @@ -9,10 +9,11 @@ import { useMediaQuery, useTheme as useMuiTheme } from "@mui/material"; import { ACCOUNT_BAR_HEIGHT } from "@src/config/ui.config"; import { useSettings } from "@src/context/SettingsProvider"; import { useWallet } from "@src/context/WalletProvider"; +import { useHasCreditCardBanner } from "@src/hooks/useHasCreditCardBanner"; import { LinearLoadingSkeleton } from "../shared/LinearLoadingSkeleton"; +import { CreditCardBanner } from "./CreditCardBanner"; import { Nav } from "./Nav"; import { Sidebar } from "./Sidebar"; -import { WelcomeModal } from "./WelcomeModal"; type Props = { isLoading?: boolean; @@ -49,12 +50,12 @@ const Layout: React.FunctionComponent = ({ children, isLoading, isUsingSe const LayoutApp: React.FunctionComponent = ({ children, isLoading, isUsingSettings, isUsingWallet, disableContainer, containerClassName = "" }) => { const muiTheme = useMuiTheme(); - const [isShowingWelcome, setIsShowingWelcome] = useState(false); const [isNavOpen, setIsNavOpen] = useState(true); const [isMobileOpen, setIsMobileOpen] = useState(false); const { refreshNodeStatuses, isSettingsInit } = useSettings(); const { isWalletLoaded } = useWallet(); const smallScreen = useMediaQuery(muiTheme.breakpoints.down("md")); + const hasCreditCardBanner = useHasCreditCardBanner(); useEffect(() => { const _isNavOpen = localStorage.getItem("isNavOpen"); @@ -71,22 +72,6 @@ const LayoutApp: React.FunctionComponent = ({ children, isLoading, isUsin clearInterval(refreshNodeIntervalId); }; }, [refreshNodeStatuses]); - - useEffect(() => { - const agreedToTerms = localStorage.getItem("agreedToTerms") === "true"; - - if (!agreedToTerms) { - setIsShowingWelcome(true); - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const onWelcomeClose = () => { - localStorage.setItem("agreedToTerms", "true"); - setIsShowingWelcome(false); - }; - const onOpenMenuClick = () => { setIsNavOpen(prev => { const newValue = !prev; @@ -102,42 +87,46 @@ const LayoutApp: React.FunctionComponent = ({ children, isLoading, isUsin }; return ( - <> - - -
-
-
-