From 094beaa7b7699dfc2754a314f43dfb5ba5ce8f47 Mon Sep 17 00:00:00 2001 From: forbesus Date: Fri, 29 Nov 2024 07:46:15 -0800 Subject: [PATCH] feat(observability): utilise new logger in stats-web ref #436 --- apps/api/CHANGELOG.md | 41 ++++ apps/api/mvm.lock | 2 +- apps/api/package.json | 8 +- .../provider-cleanup.service.ts | 90 ++++++++ apps/api/src/console.ts | 15 +- .../provider/provider.controller.ts | 10 +- .../provider-cleanup-summarizer.ts | 25 ++ .../deployment/deployment.repository.ts | 28 +++ .../test/seeders/deployment-grant.seeder.ts | 6 +- apps/deploy-web/CHANGELOG.md | 21 ++ apps/deploy-web/mvm.lock | 1 + apps/deploy-web/next-env.d.ts | 2 +- apps/deploy-web/package.json | 2 +- .../authorizations/Authorizations.tsx | 15 +- .../get-started/GetStartedStepper.tsx | 2 +- .../src/components/home/YourAccount.tsx | 2 +- .../AutoTopUpSetting/AutoTopUpSetting.tsx | 218 ++++++++++++++++++ .../AutoTopUpSettingContainer.tsx | 52 +++++ .../components/settings/SettingsContainer.tsx | 6 + .../src/components/settings/SettingsForm.tsx | 8 +- .../TopUpAmountPicker.tsx | 80 ++++--- .../components/wallet/ManagedWalletPopup.tsx | 2 +- .../src/hooks/useAllowanceService.tsx | 9 + .../src/hooks/useAutoTopUpLimits.tsx | 91 ++++++++ .../src/hooks/useAutoTopUpService.ts | 12 + apps/deploy-web/src/queries/queryKeys.ts | 3 + .../queries/useExactDeploymentGrantsQuery.ts | 11 + .../src/queries/useExactFeeAllowanceQuery.ts | 11 + apps/deploy-web/src/queries/useGrantsQuery.ts | 1 - .../src/queries/useProvidersQuery.ts | 16 -- .../auto-top-up-message.service.ts | 134 +++++++++++ apps/provider-console/package.json | 1 - apps/provider-console/public/favicon.ico | Bin 25931 -> 15086 bytes .../components/become-provider/ServerForm.tsx | 56 +++-- .../become-provider/WalletImport.tsx | 78 ++++--- .../src/components/home/HomeContainer.tsx | 8 +- .../src/components/layout/Sidebar.tsx | 38 ++- .../ControlMachineProvider.tsx | 104 +++++++++ .../context/ControlMachineProvider/index.ts | 1 + apps/provider-console/src/pages/_app.tsx | 5 +- .../src/store/controlMachineStore.ts | 12 + .../src/types/controlMachine.ts | 5 + .../src/types/machineAccess.ts | 8 +- apps/provider-console/src/utils/dateUtils.ts | 2 +- apps/stats-web/mvm.lock | 1 + apps/stats-web/package.json | 1 + .../src/app/blocks/[height]/errors.tsx | 5 +- apps/stats-web/src/app/error.tsx | 4 +- .../src/app/transactions/[hash]/error.tsx | 4 +- .../src/app/transactions/[hash]/page.tsx | 13 +- .../src/config/browser-env.config.ts | 3 +- .../stats-web/src/config/env-config.schema.ts | 3 +- apps/stats-web/src/config/logger.config.ts | 7 + apps/stats-web/src/hooks/useLogger.ts | 8 + apps/stats-web/src/lib/copyClipboard.ts | 79 ++++--- package-lock.json | 77 ++----- .../src/allowance/allowance-http.service.ts | 39 +++- packages/logging/package.json | 14 +- packages/logging/src/config/env.config.ts | 7 +- packages/logging/src/config/index.ts | 6 +- .../servicies/logger/logger.service.spec.ts | 26 ++- .../src/servicies/logger/logger.service.ts | 53 +++-- packages/ui/components/input.tsx | 12 +- packages/ui/components/switch.tsx | 2 +- 64 files changed, 1314 insertions(+), 292 deletions(-) create mode 100644 apps/api/src/billing/services/provider-cleanup/provider-cleanup.service.ts create mode 100644 apps/api/src/deployment/lib/provider-cleanup-summarizer/provider-cleanup-summarizer.ts create mode 100644 apps/deploy-web/src/components/settings/AutoTopUpSetting/AutoTopUpSetting.tsx create mode 100644 apps/deploy-web/src/components/settings/AutoTopUpSetting/AutoTopUpSettingContainer.tsx create mode 100644 apps/deploy-web/src/hooks/useAllowanceService.tsx create mode 100644 apps/deploy-web/src/hooks/useAutoTopUpLimits.tsx create mode 100644 apps/deploy-web/src/hooks/useAutoTopUpService.ts create mode 100644 apps/deploy-web/src/queries/useExactDeploymentGrantsQuery.ts create mode 100644 apps/deploy-web/src/queries/useExactFeeAllowanceQuery.ts create mode 100644 apps/deploy-web/src/services/auto-top-up-message/auto-top-up-message.service.ts create mode 100644 apps/provider-console/src/context/ControlMachineProvider/ControlMachineProvider.tsx create mode 100644 apps/provider-console/src/context/ControlMachineProvider/index.ts create mode 100644 apps/provider-console/src/store/controlMachineStore.ts create mode 100644 apps/provider-console/src/types/controlMachine.ts create mode 100644 apps/stats-web/src/config/logger.config.ts create mode 100644 apps/stats-web/src/hooks/useLogger.ts diff --git a/apps/api/CHANGELOG.md b/apps/api/CHANGELOG.md index 3b175adff..fd77d3251 100644 --- a/apps/api/CHANGELOG.md +++ b/apps/api/CHANGELOG.md @@ -1,5 +1,46 @@ +## [2.35.2-beta.0](https://github.com/akash-network/console/compare/console-api/v2.35.1...console-api/v2.35.2-beta.0) (2024-11-28) + + +### Bug Fixes + +* **observability:** ensure pino-pretty works in built app ([7f6f9ca](https://github.com/akash-network/console/commit/7f6f9ca7ca4e1ff4bc3b85735270f61cc8120242)), closes [#474](https://github.com/akash-network/console/issues/474) + +## [2.35.1](https://github.com/akash-network/console/compare/console-api/v2.35.1-beta.1...console-api/v2.35.1) (2024-11-28) + +## [2.35.1-beta.1](https://github.com/akash-network/console/compare/console-api/v2.35.1-beta.0...console-api/v2.35.1-beta.1) (2024-11-28) + + +### Bug Fixes + +* **deployment:** provider deployments query fix ([4278bbd](https://github.com/akash-network/console/commit/4278bbd718d56a71d49baefd73d1b2d35e427aff)), closes [#504](https://github.com/akash-network/console/issues/504) + +## [2.35.1-beta.0](https://github.com/akash-network/console/compare/console-api/v2.35.0...console-api/v2.35.1-beta.0) (2024-11-28) + + +### Bug Fixes + +* **deployment:** fix console arg to object mapping ([6126106](https://github.com/akash-network/console/commit/6126106a800d7006b726ff98190e09368cc0c130)), closes [#503](https://github.com/akash-network/console/issues/503) + +## [2.35.0](https://github.com/akash-network/console/compare/console-api/v2.35.0-beta.0...console-api/v2.35.0) (2024-11-27) + +## [2.35.0-beta.0](https://github.com/akash-network/console/compare/console-api/v2.34.0...console-api/v2.35.0-beta.0) (2024-11-27) + + +### Features + +* **deployment:** clean up trial deployments for a provider ([41018af](https://github.com/akash-network/console/commit/41018afc0593621c4627369b9f114f849e249e44)), closes [#502](https://github.com/akash-network/console/issues/502) + +## [2.34.0](https://github.com/akash-network/console/compare/console-api/v2.34.0-beta.1...console-api/v2.34.0) (2024-11-26) + +## [2.34.0-beta.1](https://github.com/akash-network/console/compare/console-api/v2.34.0-beta.0...console-api/v2.34.0-beta.1) (2024-11-26) + + +### Features + +* **deployment:** implement ato top up setting ([1301314](https://github.com/akash-network/console/commit/130131485a68f699587415f96283e0dc83072502)), closes [#412](https://github.com/akash-network/console/issues/412) + ## [2.34.0-beta.0](https://github.com/akash-network/console/compare/console-api/v2.33.1...console-api/v2.34.0-beta.0) (2024-11-23) diff --git a/apps/api/mvm.lock b/apps/api/mvm.lock index e3c3f96a8..eeba268dd 100644 --- a/apps/api/mvm.lock +++ b/apps/api/mvm.lock @@ -3,6 +3,6 @@ "@akashnetwork/database": "1.0.0", "@akashnetwork/env-loader": "1.0.1", "@akashnetwork/http-sdk": "1.0.8", - "@akashnetwork/logging": "2.0.1" + "@akashnetwork/logging": "2.0.2" } } diff --git a/apps/api/package.json b/apps/api/package.json index e198ba80a..923728788 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "@akashnetwork/console-api", - "version": "2.34.0-beta.0", + "version": "2.35.2-beta.0", "description": "Api providing data to the deploy tool", "repository": { "type": "git", @@ -54,9 +54,9 @@ "@hono/swagger-ui": "0.2.1", "@hono/zod-openapi": "0.9.5", "@octokit/rest": "^18.12.0", - "@opentelemetry/instrumentation": "^0.54.0", - "@opentelemetry/instrumentation-http": "^0.54.0", - "@opentelemetry/sdk-node": "^0.54.0", + "@opentelemetry/instrumentation": "^0.54.2", + "@opentelemetry/instrumentation-http": "^0.54.2", + "@opentelemetry/sdk-node": "^0.54.2", "@sentry/node": "^7.55.2", "@supercharge/promise-pool": "^3.2.0", "@ucast/core": "^1.10.2", 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 new file mode 100644 index 000000000..111697400 --- /dev/null +++ b/apps/api/src/billing/services/provider-cleanup/provider-cleanup.service.ts @@ -0,0 +1,90 @@ +import { LoggerService } from "@akashnetwork/logging"; +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 { 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); + + constructor( + @InjectBillingConfig() private readonly config: BillingConfig, + private readonly userWalletRepository: UserWalletRepository, + private readonly managedUserWalletService: ManagedUserWalletService, + private readonly txSignerService: TxSignerService, + private readonly deploymentRepository: DeploymentRepository, + private readonly rpcMessageService: RpcMessageService, + private readonly errorService: ErrorService + ) {} + + async cleanup(options: ProviderCleanupParams) { + const summary = new ProviderCleanupSummarizer(); + await this.userWalletRepository.paginate({ query: { isTrialing: true }, limit: options.concurrency || 10 }, async wallets => { + const cleanUpAllWallets = wallets.map(async wallet => { + await this.errorService.execWithErrorHandler( + { + wallet, + event: "PROVIDER_CLEAN_UP_ERROR", + context: ProviderCleanupService.name + }, + () => this.cleanUpForWallet(wallet, options, summary) + ); + }); + + await Promise.all(cleanUpAllWallets); + }); + + this.logger.info({ event: "PROVIDER_CLEAN_UP_SUMMARY", summary: summary.summarize(), dryRun: options.dryRun }); + } + + private async cleanUpForWallet(wallet: UserWalletOutput, options: ProviderCleanupParams, summary: ProviderCleanupSummarizer) { + const client = await this.txSignerService.getClientForAddressIndex(wallet.id); + const deployments = await this.deploymentRepository.findDeploymentsForProvider({ + owner: wallet.address, + provider: options.provider + }); + + const closeAllWalletStaleDeployments = deployments.map(async deployment => { + const message = this.rpcMessageService.getCloseDeploymentMsg(wallet.address, deployment.dseq); + this.logger.info({ event: "PROVIDER_CLEAN_UP", params: { owner: wallet.address, dseq: deployment.dseq } }); + + try { + if (!options.dryRun) { + await client.signAndBroadcast([message]); + this.logger.info({ event: "PROVIDER_CLEAN_UP_SUCCESS" }); + } + } catch (error) { + if (error.message.includes("not allowed to pay fees")) { + if (!options.dryRun) { + await this.managedUserWalletService.authorizeSpending({ + address: wallet.address, + limits: { + fees: this.config.FEE_ALLOWANCE_REFILL_AMOUNT + } + }); + await client.signAndBroadcast([message]); + this.logger.info({ event: "PROVIDER_CLEAN_UP_SUCCESS" }); + } + } else { + throw error; + } + } finally { + summary.inc("deploymentCount"); + } + }); + + await Promise.all(closeAllWalletStaleDeployments); + } +} diff --git a/apps/api/src/console.ts b/apps/api/src/console.ts index e2ec98ff6..115bc32b1 100644 --- a/apps/api/src/console.ts +++ b/apps/api/src/console.ts @@ -14,6 +14,7 @@ import { chainDb } from "@src/db/dbConnection"; import { TopUpDeploymentsController } from "@src/deployment/controllers/deployment/deployment.controller"; import { UserController } from "@src/user/controllers/user/user.controller"; import { UserConfigService } from "@src/user/services/user-config/user-config.service"; +import { ProviderController } from "./deployment/controllers/provider/provider.controller"; const program = new Command(); @@ -42,13 +43,25 @@ program program .command("cleanup-stale-deployments") .description("Close deployments without leases created at least 10min ago") - .option("-c, --concurrency ", "How much wallets is processed concurrently", value => z.number({ coerce: true }).optional().default(10).parse(value)) + .option("-c, --concurrency ", "How many wallets is processed concurrently", value => z.number({ coerce: true }).optional().default(10).parse(value)) .action(async (options, command) => { await executeCliHandler(command.name(), async () => { await container.resolve(TopUpDeploymentsController).cleanUpStaleDeployment(options); }); }); +program + .command("cleanup-provider-deployments") + .description("Close trial deployments for a provider") + .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 trial provider cleanup", false) + .option("-p, --provider ", "Provider address", value => z.string().parse(value)) + .action(async (options, command) => { + await executeCliHandler(command.name(), async () => { + await container.resolve(ProviderController).cleanupProviderDeployments(options); + }); + }); + const userConfig = container.resolve(UserConfigService); program .command("cleanup-stale-anonymous-users") diff --git a/apps/api/src/deployment/controllers/provider/provider.controller.ts b/apps/api/src/deployment/controllers/provider/provider.controller.ts index d2c6261d8..de90d8154 100644 --- a/apps/api/src/deployment/controllers/provider/provider.controller.ts +++ b/apps/api/src/deployment/controllers/provider/provider.controller.ts @@ -1,12 +1,20 @@ import { singleton } from "tsyringe"; +import { ProviderCleanupParams, ProviderCleanupService } from "@src/billing/services/provider-cleanup/provider-cleanup.service"; import { TrialProvidersService } from "@src/deployment/services/trial-providers/trial-providers.service"; @singleton() export class ProviderController { - constructor(private readonly trialProvidersService: TrialProvidersService) {} + constructor( + private readonly trialProvidersService: TrialProvidersService, + private readonly providerCleanupService: ProviderCleanupService + ) {} async getTrialProviders(): Promise { return await this.trialProvidersService.getTrialProviders(); } + + async cleanupProviderDeployments(options: ProviderCleanupParams) { + return await this.providerCleanupService.cleanup(options); + } } diff --git a/apps/api/src/deployment/lib/provider-cleanup-summarizer/provider-cleanup-summarizer.ts b/apps/api/src/deployment/lib/provider-cleanup-summarizer/provider-cleanup-summarizer.ts new file mode 100644 index 000000000..a2e82a84e --- /dev/null +++ b/apps/api/src/deployment/lib/provider-cleanup-summarizer/provider-cleanup-summarizer.ts @@ -0,0 +1,25 @@ +interface ProviderCleanupSummary { + deploymentCount: number; +} + +export class ProviderCleanupSummarizer { + private deploymentCount = 0; + + inc(param: keyof ProviderCleanupSummary, value = 1) { + this[param] += value; + } + + set(param: keyof ProviderCleanupSummary, value: number) { + this[param] = value; + } + + get(param: keyof ProviderCleanupSummary) { + return this[param]; + } + + summarize(): ProviderCleanupSummary { + return { + deploymentCount: this.deploymentCount + }; + } +} diff --git a/apps/api/src/deployment/repositories/deployment/deployment.repository.ts b/apps/api/src/deployment/repositories/deployment/deployment.repository.ts index db10f5c13..373eb1fc2 100644 --- a/apps/api/src/deployment/repositories/deployment/deployment.repository.ts +++ b/apps/api/src/deployment/repositories/deployment/deployment.repository.ts @@ -7,6 +7,11 @@ export interface StaleDeploymentsOptions { owner: string; } +export interface ProviderCleanupOptions { + owner: string; + provider: string; +} + export interface StaleDeploymentsOutput { dseq: number; } @@ -37,4 +42,27 @@ export class DeploymentRepository { return deployments ? (deployments as unknown as StaleDeploymentsOutput[]) : []; } + + async findDeploymentsForProvider(options: ProviderCleanupOptions): Promise { + const deployments = await Deployment.findAll({ + attributes: ["dseq"], + where: { + owner: options.owner, + closedHeight: null + }, + include: [ + { + model: Lease, + attributes: [], + required: true, + where: { + providerAddress: options.provider + } + } + ], + raw: true + }); + + return deployments ? (deployments as unknown as StaleDeploymentsOutput[]) : []; + } } diff --git a/apps/api/test/seeders/deployment-grant.seeder.ts b/apps/api/test/seeders/deployment-grant.seeder.ts index 5ef4914c1..f99dd2f15 100644 --- a/apps/api/test/seeders/deployment-grant.seeder.ts +++ b/apps/api/test/seeders/deployment-grant.seeder.ts @@ -18,9 +18,9 @@ export class DeploymentGrantSeeder { spend_limit: { denom: DenomSeeder.create(), amount: faker.number.int({ min: 0, max: 10000000 }).toString() - }, - expiration: faker.date.future().toISOString() - } + } + }, + expiration: faker.date.future().toISOString() }, input ); diff --git a/apps/deploy-web/CHANGELOG.md b/apps/deploy-web/CHANGELOG.md index 6a13ac7e5..8889157a1 100644 --- a/apps/deploy-web/CHANGELOG.md +++ b/apps/deploy-web/CHANGELOG.md @@ -1,5 +1,26 @@ +## [2.25.1-beta.0](https://github.com/akash-network/console/compare/console-web/v2.25.0...console-web/v2.25.1-beta.0) (2024-11-28) + + +### Bug Fixes + +* **observability:** ensure pino-pretty works in built app ([7f6f9ca](https://github.com/akash-network/console/commit/7f6f9ca7ca4e1ff4bc3b85735270f61cc8120242)), closes [#474](https://github.com/akash-network/console/issues/474) + +## [2.25.0](https://github.com/akash-network/console/compare/console-web/v2.25.0-beta.1...console-web/v2.25.0) (2024-11-26) + +## [2.25.0-beta.1](https://github.com/akash-network/console/compare/console-web/v2.25.0-beta.0...console-web/v2.25.0-beta.1) (2024-11-26) + + +### Features + +* **deployment:** implement ato top up setting ([1301314](https://github.com/akash-network/console/commit/130131485a68f699587415f96283e0dc83072502)), closes [#412](https://github.com/akash-network/console/issues/412) + + +### Bug Fixes + +* **billing:** ensure checkout pricing is displayed correctly ([3bcb4a8](https://github.com/akash-network/console/commit/3bcb4a881e3bb58e741de8bb8a0a661dede0d8ae)) + ## [2.25.0-beta.0](https://github.com/akash-network/console/compare/console-web/v2.24.1...console-web/v2.25.0-beta.0) (2024-11-23) diff --git a/apps/deploy-web/mvm.lock b/apps/deploy-web/mvm.lock index 6ed210e74..8b020adc1 100644 --- a/apps/deploy-web/mvm.lock +++ b/apps/deploy-web/mvm.lock @@ -2,6 +2,7 @@ "dependencies": { "@akashnetwork/env-loader": "1.0.1", "@akashnetwork/http-sdk": "1.0.8", + "@akashnetwork/logging": "2.0.2", "@akashnetwork/network-store": "1.0.1", "@akashnetwork/ui": "1.0.0" }, diff --git a/apps/deploy-web/next-env.d.ts b/apps/deploy-web/next-env.d.ts index 4f11a03dc..a4a7b3f5c 100644 --- a/apps/deploy-web/next-env.d.ts +++ b/apps/deploy-web/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/apps/deploy-web/package.json b/apps/deploy-web/package.json index b3d4b7da1..4273c2507 100644 --- a/apps/deploy-web/package.json +++ b/apps/deploy-web/package.json @@ -1,6 +1,6 @@ { "name": "@akashnetwork/console-web", - "version": "2.25.0-beta.0", + "version": "2.25.1-beta.0", "private": true, "description": "Web UI to deploy on the Akash Network and view statistic about network usage.", "license": "Apache-2.0", diff --git a/apps/deploy-web/src/components/authorizations/Authorizations.tsx b/apps/deploy-web/src/components/authorizations/Authorizations.tsx index 3fcaf1f35..c4f9a161e 100644 --- a/apps/deploy-web/src/components/authorizations/Authorizations.tsx +++ b/apps/deploy-web/src/components/authorizations/Authorizations.tsx @@ -5,6 +5,7 @@ import { Bank } from "iconoir-react"; import { NextSeo } from "next-seo"; import { Fieldset } from "@src/components/shared/Fieldset"; +import { browserEnvConfig } from "@src/config/browser-env.config"; import { useWallet } from "@src/context/WalletProvider"; import { useAllowance } from "@src/hooks/useAllowance"; import { useAllowancesIssued, useGranteeGrants, useGranterGrants } from "@src/queries/useGrantsQuery"; @@ -26,6 +27,14 @@ type RefreshingType = "granterGrants" | "granteeGrants" | "allowancesIssued" | " const defaultRefetchInterval = 30 * 1000; const refreshingInterval = 1000; +const MASTER_WALLETS = new Set([ + browserEnvConfig.NEXT_PUBLIC_USDC_TOP_UP_MASTER_WALLET_ADDRESS, + browserEnvConfig.NEXT_PUBLIC_UAKT_TOP_UP_MASTER_WALLET_ADDRESS +]); + +const selectNonMaster = (records: Pick[] | Pick[]) => + records.filter(({ grantee }) => !MASTER_WALLETS.has(grantee)); + export const Authorizations: React.FunctionComponent = () => { const { address, signAndBroadcastTx, isManaged } = useWallet(); const { @@ -41,13 +50,15 @@ export const Authorizations: React.FunctionComponent = () => { const [selectedGrants, setSelectedGrants] = useState([]); const [selectedAllowances, setSelectedAllowances] = useState([]); const { data: granterGrants, isLoading: isLoadingGranterGrants } = useGranterGrants(address, { - refetchInterval: isRefreshing === "granterGrants" ? refreshingInterval : defaultRefetchInterval + refetchInterval: isRefreshing === "granterGrants" ? refreshingInterval : defaultRefetchInterval, + select: selectNonMaster }); const { data: granteeGrants, isLoading: isLoadingGranteeGrants } = useGranteeGrants(address, { refetchInterval: isRefreshing === "granteeGrants" ? refreshingInterval : defaultRefetchInterval }); const { data: allowancesIssued, isLoading: isLoadingAllowancesIssued } = useAllowancesIssued(address, { - refetchInterval: isRefreshing === "allowancesIssued" ? refreshingInterval : defaultRefetchInterval + refetchInterval: isRefreshing === "allowancesIssued" ? refreshingInterval : defaultRefetchInterval, + select: selectNonMaster }); useEffect(() => { diff --git a/apps/deploy-web/src/components/get-started/GetStartedStepper.tsx b/apps/deploy-web/src/components/get-started/GetStartedStepper.tsx index e264fc36a..97a8f9899 100644 --- a/apps/deploy-web/src/components/get-started/GetStartedStepper.tsx +++ b/apps/deploy-web/src/components/get-started/GetStartedStepper.tsx @@ -104,7 +104,7 @@ export const GetStartedStepper: React.FunctionComponent = () => {
{isManagedWallet && ( - + = ({ isLoadingBalances, )} {isManagedWallet && ( - + { + if (data.usdcDeploymentLimit > 0) { + return data.usdcFeeLimit > 0; + } + return true; + }, + { + message: "Must be greater than 0 if `USDC Deployments Limit` is greater than 0", + path: ["usdcFeeLimit"] + } + ) + .refine( + data => { + if (data.usdcFeeLimit > 0) { + return data.usdcDeploymentLimit > 0; + } + return true; + }, + { + message: "Must be greater than 0 if `USDC Fees Limit` is greater than 0", + path: ["usdcDeploymentLimit"] + } + ) + .refine( + data => { + if (data.uaktDeploymentLimit > 0) { + return data.uaktFeeLimit > 0; + } + return true; + }, + { + message: "Must be greater than 0 if `AKT Deployments Limit` is greater than 0", + path: ["uaktFeeLimit"] + } + ) + .refine( + data => { + if (data.uaktFeeLimit > 0) { + return data.uaktDeploymentLimit > 0; + } + return true; + }, + { + message: "Must be greater than 0 if `AKT Fees Limit` is greater than 0", + path: ["uaktDeploymentLimit"] + } + ); + +type FormValues = z.infer; + +type LimitFields = keyof Omit; + +type AutoTopUpSubmitHandler = (action: "revoke-all" | "update", next: FormValues) => Promise; + +export interface AutoTopUpSettingProps extends Partial> { + onSubmit: AutoTopUpSubmitHandler; + expiration?: Date; +} + +const fields: LimitFields[] = ["uaktFeeLimit", "usdcFeeLimit", "uaktDeploymentLimit", "usdcDeploymentLimit"]; + +export const AutoTopUpSetting: FC = ({ onSubmit, expiration, ...props }) => { + const hasAny = useMemo(() => fields.some(field => props[field]), [props]); + + const defaultLimitValues = useMemo(() => { + return fields.reduce( + (acc, field) => { + acc[field] = uaktToAKT(props[field] || 0); + return acc; + }, + {} as Record + ); + }, [props]); + + const form = useForm>({ + defaultValues: { + ...defaultLimitValues, + expiration: format(expiration || addYears(new Date(), 1), "yyyy-MM-dd'T'HH:mm") + }, + resolver: zodResolver(formSchema) + }); + const { handleSubmit, control, setValue, reset } = form; + + useEffect(() => { + setValue("uaktFeeLimit", uaktToAKT(props.uaktFeeLimit || 0)); + }, [props.uaktFeeLimit]); + + useEffect(() => { + setValue("usdcFeeLimit", uaktToAKT(props.usdcFeeLimit || 0)); + }, [props.usdcFeeLimit]); + + useEffect(() => { + setValue("uaktDeploymentLimit", uaktToAKT(props.uaktDeploymentLimit || 0)); + }, [props.uaktDeploymentLimit]); + + useEffect(() => { + setValue("usdcDeploymentLimit", uaktToAKT(props.usdcDeploymentLimit || 0)); + }, [props.usdcDeploymentLimit]); + + useEffect(() => { + if (expiration) { + setValue("expiration", format(expiration || addYears(new Date(), 1), "yyyy-MM-dd'T'HH:mm")); + } + }, [expiration]); + + const execSubmitterRoleAction: SubmitHandler = useCallback( + async (next: FormValues, event: React.BaseSyntheticEvent) => { + const role = event.nativeEvent.submitter?.getAttribute("data-role"); + await onSubmit(role as "revoke-all" | "update", convertToUakt(next)); + reset(next); + }, + [onSubmit, reset] + ); + + return ( +
+
+ +
Deployments billed in AKT
+
+
+ { + return ; + }} + /> +
+ +
+ { + return ; + }} + /> +
+
+ +
Deployments billed in USDC
+
+
+ { + return ; + }} + /> +
+ +
+ { + return ; + }} + /> +
+
+ +
+ { + return ; + }} + /> +
+ + + + {hasAny && ( + + )} +
+ +
+ ); +}; + +function convertToUakt({ ...values }: FormValues) { + return fields.reduce((acc, field) => { + acc[field] = aktToUakt(values[field]); + return acc; + }, values); +} diff --git a/apps/deploy-web/src/components/settings/AutoTopUpSetting/AutoTopUpSettingContainer.tsx b/apps/deploy-web/src/components/settings/AutoTopUpSetting/AutoTopUpSettingContainer.tsx new file mode 100644 index 000000000..5eb6e0e2b --- /dev/null +++ b/apps/deploy-web/src/components/settings/AutoTopUpSetting/AutoTopUpSettingContainer.tsx @@ -0,0 +1,52 @@ +import React, { FC, useCallback, useEffect } from "react"; + +import { AutoTopUpSetting, AutoTopUpSettingProps } from "@src/components/settings/AutoTopUpSetting/AutoTopUpSetting"; +import { useWallet } from "@src/context/WalletProvider"; +import { useAutoTopUpLimits } from "@src/hooks/useAutoTopUpLimits"; +import { useAutoTopUpService } from "@src/hooks/useAutoTopUpService"; + +export const AutoTopUpSettingContainer: FC = () => { + const { address, signAndBroadcastTx } = useWallet(); + const { fetch, uaktFeeLimit, usdcFeeLimit, uaktDeploymentLimit, usdcDeploymentLimit, expiration } = useAutoTopUpLimits(); + const autoTopUpMessageService = useAutoTopUpService(); + + useEffect(() => { + fetch(); + }, []); + + const updateAllowancesAndGrants: AutoTopUpSettingProps["onSubmit"] = useCallback( + async (action, next) => { + const prev = { + uaktFeeLimit, + usdcFeeLimit, + uaktDeploymentLimit, + usdcDeploymentLimit, + expiration + }; + + const messages = autoTopUpMessageService.collectMessages({ + granter: address, + prev, + next: action === "revoke-all" ? undefined : { ...next, expiration: new Date(next.expiration) } + }); + + if (messages.length) { + await signAndBroadcastTx(messages); + } + + await fetch(); + }, + [address, autoTopUpMessageService, expiration, fetch, signAndBroadcastTx, uaktDeploymentLimit, uaktFeeLimit, usdcDeploymentLimit, usdcFeeLimit] + ); + + return ( + + ); +}; diff --git a/apps/deploy-web/src/components/settings/SettingsContainer.tsx b/apps/deploy-web/src/components/settings/SettingsContainer.tsx index 560cdc990..d6303e513 100644 --- a/apps/deploy-web/src/components/settings/SettingsContainer.tsx +++ b/apps/deploy-web/src/components/settings/SettingsContainer.tsx @@ -5,6 +5,8 @@ import { Edit } from "iconoir-react"; import { useRouter } from "next/navigation"; import { NextSeo } from "next-seo"; +import { AutoTopUpSetting } from "@src/components/settings/AutoTopUpSetting/AutoTopUpSetting"; +import { AutoTopUpSettingContainer } from "@src/components/settings/AutoTopUpSetting/AutoTopUpSettingContainer"; import { LocalDataManager } from "@src/components/settings/LocalDataManager"; import { Fieldset } from "@src/components/shared/Fieldset"; import { LabelValue } from "@src/components/shared/LabelValue"; @@ -58,6 +60,10 @@ export const SettingsContainer: React.FunctionComponent = () => { + +
+ +
diff --git a/apps/deploy-web/src/components/settings/SettingsForm.tsx b/apps/deploy-web/src/components/settings/SettingsForm.tsx index b439e472d..c40fcb96a 100644 --- a/apps/deploy-web/src/components/settings/SettingsForm.tsx +++ b/apps/deploy-web/src/components/settings/SettingsForm.tsx @@ -94,9 +94,7 @@ export const SettingsForm: React.FunctionComponent = () => { control={control} name="apiEndpoint" defaultValue={settings.apiEndpoint} - render={({ field }) => { - return ; - }} + render={({ field }) => } /> ) : (

{settings.apiEndpoint}

@@ -111,9 +109,7 @@ export const SettingsForm: React.FunctionComponent = () => { control={control} name="rpcEndpoint" defaultValue={settings.rpcEndpoint} - render={({ field }) => { - return ; - }} + render={({ field }) => } /> ) : (

{settings.rpcEndpoint}

diff --git a/apps/deploy-web/src/components/top-up-amount-picker/TopUpAmountPicker.tsx b/apps/deploy-web/src/components/top-up-amount-picker/TopUpAmountPicker.tsx index 66abd64ff..8932fe92a 100644 --- a/apps/deploy-web/src/components/top-up-amount-picker/TopUpAmountPicker.tsx +++ b/apps/deploy-web/src/components/top-up-amount-picker/TopUpAmountPicker.tsx @@ -1,6 +1,8 @@ import React from "react"; import { Button, buttonVariants } from "@akashnetwork/ui/components"; import { cn } from "@akashnetwork/ui/utils"; +import { useMediaQuery } from "@mui/material"; +import { useTheme as useMuiTheme } from "@mui/material/styles"; import { MoreHoriz } from "iconoir-react"; import { LoginRequiredLink } from "@src/components/user/LoginRequiredLink"; @@ -10,43 +12,55 @@ import { FCWithChildren } from "@src/types/component"; interface TopUpAmountPickerProps { popoverClassName?: string; - fullWidth?: boolean; + className?: string; + mdMode: "hover" | "click"; } -export const TopUpAmountPicker: FCWithChildren = ({ children, popoverClassName, fullWidth }) => { +export const TopUpAmountPicker: FCWithChildren = ({ children, popoverClassName, className, mdMode }) => { const user = useUser(); + const [isOpened, setIsOpened] = React.useState(false); const { data = [] } = useStripePricesQuery({ enabled: !!user?.userId }); + const muiTheme = useMuiTheme(); + const isSmallScreen = useMediaQuery(muiTheme.breakpoints.down("md")); + const isOnClick = mdMode === "click" || isSmallScreen; - return data.length > 1 ? ( - - - {children} - - -
- {data.map(price => { - return ( - - {price.isCustom ? "Custom" : "$"} - {price.unitAmount} - - ); - })} -
-
- ) : ( - <>{children} + return ( +
+ {data.length > 1 ? ( + <> +
+ {children} + {isOnClick && ( + + )} +
+
+ {data.map(price => { + return ( + + {price.isCustom ? "Custom" : "$"} + {price.unitAmount} + + ); + })} +
+ + ) : ( + <>{children} + )} +
); }; diff --git a/apps/deploy-web/src/components/wallet/ManagedWalletPopup.tsx b/apps/deploy-web/src/components/wallet/ManagedWalletPopup.tsx index 8cd24fcac..71fc6d259 100644 --- a/apps/deploy-web/src/components/wallet/ManagedWalletPopup.tsx +++ b/apps/deploy-web/src/components/wallet/ManagedWalletPopup.tsx @@ -71,7 +71,7 @@ export const ManagedWalletPopup: React.FC = ({ walletBa )}
- +
- {currentServerNumber === 0 && ( + {currentServerNumber === 0 && !editMode && (

Heads up!

diff --git a/apps/provider-console/src/components/become-provider/WalletImport.tsx b/apps/provider-console/src/components/become-provider/WalletImport.tsx index c6d78f2ef..c31905623 100644 --- a/apps/provider-console/src/components/become-provider/WalletImport.tsx +++ b/apps/provider-console/src/components/become-provider/WalletImport.tsx @@ -21,7 +21,10 @@ import { useAtom } from "jotai"; import { useRouter } from "next/router"; import { z } from "zod"; +import { useControlMachine } from "@src/context/ControlMachineProvider"; +import { useWallet } from "@src/context/WalletProvider"; import providerProcessStore from "@src/store/providerProcessStore"; +import { ControlMachineWithAddress } from "@src/types/controlMachine"; import restClient from "@src/utils/restClient"; import { ResetProviderForm } from "./ResetProviderProcess"; @@ -77,6 +80,8 @@ export const WalletImport: React.FC = ({ onComplete }) => { const [providerProcess] = useAtom(providerProcessStore.providerProcessAtom); const [, resetProviderProcess] = useAtom(providerProcessStore.resetProviderProcess); + const { setControlMachine } = useControlMachine(); + const { address } = useWallet(); const defaultValues: Partial = { walletMode: "seed" @@ -95,51 +100,58 @@ export const WalletImport: React.FC = ({ onComplete }) => { }; const submitForm = async (data: SeedFormValues) => { - if (!providerProcess.machines || providerProcess.machines.length === 0) { - setError("No machine information available"); - } setIsLoading(true); setError(null); try { - const publicKey = providerProcess.machines[0].systemInfo.public_key; - const keyId = providerProcess.machines[0].systemInfo.key_id; - const encryptedSeedPhrase = await encrypt(data.seedPhrase, publicKey); + if (providerProcess.machines && providerProcess.machines.length > 0) { + const publicKey = providerProcess.machines[0].systemInfo.public_key; + const keyId = providerProcess.machines[0].systemInfo.key_id; + const encryptedSeedPhrase = await encrypt(data.seedPhrase, publicKey); - const finalRequest = { - wallet: { - key_id: keyId, - wallet_phrase: encryptedSeedPhrase - }, - nodes: providerProcess.machines.map(machine => ({ - hostname: machine.access.hostname, - port: machine.access.port, - username: machine.access.username, - keyfile: machine.access.file, - password: machine.access.password, - install_gpu_drivers: machine.systemInfo.gpu.count > 0 ? true : false - })), - provider: { - attributes: providerProcess.attributes, - pricing: providerProcess.pricing, - config: providerProcess.config - } - }; + const finalRequest = { + wallet: { + key_id: keyId, + wallet_phrase: encryptedSeedPhrase + }, + nodes: providerProcess.machines.map(machine => ({ + hostname: machine.access.hostname, + port: machine.access.port, + username: machine.access.username, + keyfile: machine.access.file, + password: machine.access.password, + install_gpu_drivers: machine.systemInfo.gpu.count > 0 ? true : false + })), + provider: { + attributes: providerProcess.attributes, + pricing: providerProcess.pricing, + config: providerProcess.config + } + }; - const response: any = await restClient.post("/build-provider", finalRequest, { - headers: { "Content-Type": "application/json" } - }); + const response: any = await restClient.post("/build-provider", finalRequest, { + headers: { "Content-Type": "application/json" } + }); - if (response.action_id) { - resetProviderProcess(); - router.push(`/action?id=${response.action_id}`); + if (response.action_id) { + const machineWithAddress: ControlMachineWithAddress = { + address: address, + ...providerProcess.machines[0] + }; + await setControlMachine(machineWithAddress); + resetProviderProcess(); + router.push(`/action?id=${response.action_id}`); + } else { + throw new Error("Invalid response from server"); + } } else { - throw new Error("Invalid response from server"); + throw new Error("No machine information available"); } } catch (error) { + console.error("Error during wallet verification:", error); setError("An error occurred while processing your request. Please try again."); } finally { - onComplete(); setIsLoading(false); + onComplete(); } }; diff --git a/apps/provider-console/src/components/home/HomeContainer.tsx b/apps/provider-console/src/components/home/HomeContainer.tsx index a0aca8862..8f9ba9105 100644 --- a/apps/provider-console/src/components/home/HomeContainer.tsx +++ b/apps/provider-console/src/components/home/HomeContainer.tsx @@ -12,14 +12,10 @@ import { WalletNotConnected } from "./WalletNotConnected"; export const HomeContainer: React.FC = () => { const router = useRouter(); - const { isWalletConnected, isWalletArbitrarySigned, isProvider, isOnline, isProviderStatusFetched } = useWallet(); - const [isLoading, setIsLoading] = useState(false); + const { isWalletConnected, isProvider, isOnline, isProviderStatusFetched } = useWallet(); + const [isLoading] = useState(false); const { data: providerActions } = useProviderActions(); - useEffect(() => { - setIsLoading(true); - }, [isProvider, isOnline, isWalletArbitrarySigned]); - useEffect(() => { if (isWalletConnected && isProvider) { router.push("/dashboard"); diff --git a/apps/provider-console/src/components/layout/Sidebar.tsx b/apps/provider-console/src/components/layout/Sidebar.tsx index 28b1bad51..c476aae7f 100644 --- a/apps/provider-console/src/components/layout/Sidebar.tsx +++ b/apps/provider-console/src/components/layout/Sidebar.tsx @@ -1,6 +1,6 @@ "use client"; import React, { ReactNode, useState } from "react"; -import { Button, buttonVariants } from "@akashnetwork/ui/components"; +import { Button, buttonVariants, Spinner } from "@akashnetwork/ui/components"; import Drawer from "@mui/material/Drawer"; import { useTheme as useMuiTheme } from "@mui/material/styles"; import useMediaQuery from "@mui/material/useMediaQuery"; @@ -10,6 +10,7 @@ import getConfig from "next/config"; import Image from "next/image"; import Link from "next/link"; +import { useControlMachine } from "@src/context/ControlMachineProvider"; import { useWallet } from "@src/context/WalletProvider"; import { ISidebarGroupMenu } from "@src/types"; import { closedDrawerWidth, drawerWidth } from "@src/utils/constants"; @@ -35,6 +36,8 @@ export const Sidebar: React.FC = ({ isMobileOpen, handleDrawerToggle, isN const muiTheme = useMuiTheme(); const smallScreen = useMediaQuery(muiTheme.breakpoints.down("md")); + const { activeControlMachine, openControlMachineDrawer, controlMachineLoading } = useControlMachine(); + const routeGroups: ISidebarGroupMenu[] = [ { hasDivider: false, @@ -162,6 +165,37 @@ export const Sidebar: React.FC = ({ isMobileOpen, handleDrawerToggle, isN {_isNavOpen && (
{/* */} + {controlMachineLoading ? ( +
+
+ Machine: +
+ +
Connecting...
+
+
+
+ ) : activeControlMachine ? ( +
+
+ Machine: +
+
+ {activeControlMachine.access.hostname} +
+
+
+ ) : ( +
+
+ Machine: +
+
+
Not Connected
+
+
+
+ )}
= ({ isMobileOpen, handleDrawerToggle, isN return (