From c7d16d6a0d942cef8e64c6978d9ff565a0336c0d Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp <15185355+baktun14@users.noreply.github.com> Date: Fri, 30 Aug 2024 11:16:46 -0400 Subject: [PATCH] feat(console): managed wallets popup confirmation (#342) --- .../user-wallet/user-wallet.repository.ts | 4 +- apps/api/src/index.ts | 3 + .../deployments/DeploymentDetailTopBar.tsx | 10 ++- .../components/deployments/DeploymentList.tsx | 8 +++ .../deployments/DeploymentListRow.tsx | 8 +++ .../src/components/deployments/LeaseRow.tsx | 6 +- .../components/new-deployment/CreateLease.tsx | 8 +++ .../new-deployment/ManifestEdit.tsx | 16 ++++- .../new-deployment/TemplateList.tsx | 2 +- .../src/components/sdl/RentGpusForm.tsx | 48 +++++++++++-- .../src/hooks/useManagedDeploymentConfirm.tsx | 67 +++++++++++++++++++ apps/deploy-web/src/types/sdlBuilder.ts | 13 ---- apps/deploy-web/src/utils/sdl/sdlImport.ts | 10 +-- .../providers/statusEndpointHandlers/grpc.ts | 2 +- packages/ui/components/custom/popup.tsx | 2 +- 15 files changed, 170 insertions(+), 37 deletions(-) create mode 100644 apps/deploy-web/src/hooks/useManagedDeploymentConfirm.tsx diff --git a/apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts b/apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts index 697cd35b0..24af4efcc 100644 --- a/apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts +++ b/apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts @@ -84,11 +84,11 @@ export class UserWalletRepository extends BaseRepository = ({ address, loadDeploymentDetail, removeLeases, setActiveTab, deployment }) => { const { changeDeploymentName, getDeploymentData, getDeploymentName } = useLocalNotes(); const router = useRouter(); - const { signAndBroadcastTx } = useWallet(); + const { signAndBroadcastTx, isManaged } = useWallet(); const [isDepositingDeployment, setIsDepositingDeployment] = useState(false); const storageDeploymentData = getDeploymentData(deployment?.dseq); const deploymentName = getDeploymentName(deployment?.dseq); const previousRoute = usePreviousRoute(); const wallet = useWallet(); + const { closeDeploymentConfirm } = useManagedDeploymentConfirm(); function handleBackClick() { if (previousRoute) { @@ -44,6 +46,12 @@ export const DeploymentDetailTopBar: React.FunctionComponent = ({ address } const onCloseDeployment = async () => { + const isConfirmed = await closeDeploymentConfirm([deployment.dseq]); + + if (!isConfirmed) { + return; + } + const message = TransactionMessageData.getCloseDeploymentMsg(address, deployment.dseq); const response = await signAndBroadcastTx([message]); if (response) { diff --git a/apps/deploy-web/src/components/deployments/DeploymentList.tsx b/apps/deploy-web/src/components/deployments/DeploymentList.tsx index cb0eb4396..9427fb369 100644 --- a/apps/deploy-web/src/components/deployments/DeploymentList.tsx +++ b/apps/deploy-web/src/components/deployments/DeploymentList.tsx @@ -22,6 +22,7 @@ import { LinkTo } from "@src/components/shared/LinkTo"; import { useLocalNotes } from "@src/context/LocalNoteProvider"; import { useSettings } from "@src/context/SettingsProvider"; import { useWallet } from "@src/context/WalletProvider"; +import { useManagedDeploymentConfirm } from "@src/hooks/useManagedDeploymentConfirm"; import { useDeploymentList } from "@src/queries/useDeploymentQuery"; import { useProviderList } from "@src/queries/useProvidersQuery"; import sdlStore from "@src/store/sdlStore"; @@ -54,6 +55,7 @@ export const DeploymentList: React.FunctionComponent = () => { const currentPageDeployments = orderedDeployments.slice(start, end); const pageCount = Math.ceil(orderedDeployments.length / pageSize); const [, setDeploySdl] = useAtom(sdlStore.deploySdl); + const { closeDeploymentConfirm } = useManagedDeploymentConfirm(); useEffect(() => { if (isWalletLoaded && isSettingsInit) { @@ -109,6 +111,12 @@ export const DeploymentList: React.FunctionComponent = () => { const onCloseSelectedDeployments = async () => { try { + const isConfirmed = await closeDeploymentConfirm(selectedDeploymentDseqs); + + if (!isConfirmed) { + return; + } + const messages = selectedDeploymentDseqs.map(dseq => TransactionMessageData.getCloseDeploymentMsg(address, `${dseq}`)); const response = await signAndBroadcastTx(messages); if (response) { diff --git a/apps/deploy-web/src/components/deployments/DeploymentListRow.tsx b/apps/deploy-web/src/components/deployments/DeploymentListRow.tsx index 8ff42c69b..f809a0cfe 100644 --- a/apps/deploy-web/src/components/deployments/DeploymentListRow.tsx +++ b/apps/deploy-web/src/components/deployments/DeploymentListRow.tsx @@ -20,6 +20,7 @@ import { useRouter } from "next/navigation"; import { event } from "nextjs-google-analytics"; import { useWallet } from "@src/context/WalletProvider"; +import { useManagedDeploymentConfirm } from "@src/hooks/useManagedDeploymentConfirm"; import { getShortText } from "@src/hooks/useShortText"; import { useDenomData } from "@src/hooks/useWalletBalance"; import { useAllLeases } from "@src/queries/useLeaseQuery"; @@ -110,6 +111,7 @@ export const DeploymentListRow: React.FunctionComponent = ({ deployment, const avgCost = udenomToDenom(getAvgCostPerMonth(deploymentCost || 0)); const storageDeploymentData = getDeploymentData(deployment?.dseq); const denomData = useDenomData(deployment.escrowAccount.balance.denom); + const { closeDeploymentConfirm } = useManagedDeploymentConfirm(); function viewDeployment() { router.push(UrlService.deploymentDetails(deployment.dseq)); @@ -143,6 +145,12 @@ export const DeploymentListRow: React.FunctionComponent = ({ deployment, const onCloseDeployment = async () => { handleMenuClose(); + const isConfirmed = await closeDeploymentConfirm([deployment.dseq]); + + if (!isConfirmed) { + return; + } + const message = TransactionMessageData.getCloseDeploymentMsg(address, deployment.dseq); const response = await signAndBroadcastTx([message]); if (response) { diff --git a/apps/deploy-web/src/components/deployments/LeaseRow.tsx b/apps/deploy-web/src/components/deployments/LeaseRow.tsx index 858e94aee..b584ae893 100644 --- a/apps/deploy-web/src/components/deployments/LeaseRow.tsx +++ b/apps/deploy-web/src/components/deployments/LeaseRow.tsx @@ -68,11 +68,7 @@ export const LeaseRow = React.forwardRef(({ lease, setActi } } }); - const { - data: providerStatus, - isLoading: isLoadingProviderStatus, - refetch: getProviderStatus - } = useProviderStatus(provider?.hostUri || "", { + const { isLoading: isLoadingProviderStatus, refetch: getProviderStatus } = useProviderStatus(provider?.hostUri || "", { enabled: false, retry: false }); diff --git a/apps/deploy-web/src/components/new-deployment/CreateLease.tsx b/apps/deploy-web/src/components/new-deployment/CreateLease.tsx index 3cc7b4213..9a3166191 100644 --- a/apps/deploy-web/src/components/new-deployment/CreateLease.tsx +++ b/apps/deploy-web/src/components/new-deployment/CreateLease.tsx @@ -20,6 +20,7 @@ import { useSnackbar } from "notistack"; import { LocalCert } from "@src/context/CertificateProvider/CertificateProviderContext"; import { useWallet } from "@src/context/WalletProvider"; +import { useManagedDeploymentConfirm } from "@src/hooks/useManagedDeploymentConfirm"; import { useBidList } from "@src/queries/useBidQuery"; import { useDeploymentDetail } from "@src/queries/useDeploymentQuery"; import { useProviderList } from "@src/queries/useProvidersQuery"; @@ -88,6 +89,7 @@ export const CreateLease: React.FunctionComponent = ({ dseq }) => { const allClosed = (bids?.length || 0) > 0 && bids?.every(bid => bid.state === "closed"); const { enqueueSnackbar, closeSnackbar } = useSnackbar(); const wallet = useWallet(); + const { closeDeploymentConfirm } = useManagedDeploymentConfirm(); useEffect(() => { getDeploymentDetail(); @@ -202,6 +204,12 @@ export const CreateLease: React.FunctionComponent = ({ dseq }) => { } async function handleCloseDeployment() { + const isConfirmed = await closeDeploymentConfirm([dseq]); + + if (!isConfirmed) { + return; + } + const message = TransactionMessageData.getCloseDeploymentMsg(address, dseq); const response = await signAndBroadcastTx([message]); diff --git a/apps/deploy-web/src/components/new-deployment/ManifestEdit.tsx b/apps/deploy-web/src/components/new-deployment/ManifestEdit.tsx index 63ec61847..0c46b5491 100644 --- a/apps/deploy-web/src/components/new-deployment/ManifestEdit.tsx +++ b/apps/deploy-web/src/components/new-deployment/ManifestEdit.tsx @@ -15,6 +15,7 @@ import { useCertificate } from "@src/context/CertificateProvider"; import { useChainParam } from "@src/context/ChainParamProvider"; import { useSdlBuilder } from "@src/context/SdlBuilderProvider/SdlBuilderProvider"; import { useWallet } from "@src/context/WalletProvider"; +import { useManagedDeploymentConfirm } from "@src/hooks/useManagedDeploymentConfirm"; import { useManagedWalletDenom } from "@src/hooks/useManagedWalletDenom"; import { useWhen } from "@src/hooks/useWhen"; import { useDepositParams } from "@src/queries/useSettings"; @@ -26,6 +27,7 @@ import { defaultInitialDeposit, RouteStepKeys } from "@src/utils/constants"; import { deploymentData } from "@src/utils/deploymentData"; import { saveDeploymentManifestAndName } from "@src/utils/deploymentLocalDataUtils"; import { validateDeploymentData } from "@src/utils/deploymentUtils"; +import { importSimpleSdl } from "@src/utils/sdl/sdlImport"; import { cn } from "@src/utils/styleUtils"; import { Timer } from "@src/utils/timer"; import { TransactionMessageData } from "@src/utils/TransactionMessageData"; @@ -72,6 +74,7 @@ export const ManifestEdit: React.FunctionComponent = ({ editedManifest, s const fileUploadRef = useRef(null); const wallet = useWallet(); const managedDenom = useManagedWalletDenom(); + const { createDeploymentConfirm } = useManagedDeploymentConfirm(); useWhen(wallet.isManaged && sdlDenom === "uakt", () => { setSdlDenom(managedDenom); @@ -182,7 +185,18 @@ export const ManifestEdit: React.FunctionComponent = ({ editedManifest, s } if (isManaged) { - await handleCreateClick(defaultDeposit, envConfig.NEXT_PUBLIC_MASTER_WALLET_ADDRESS); + const services = importSimpleSdl(editedManifest as string); + + if (!services) { + setParsingError("Error while parsing SDL file"); + return; + } + + const isConfirmed = await createDeploymentConfirm(services); + + if (isConfirmed) { + await handleCreateClick(defaultDeposit, envConfig.NEXT_PUBLIC_MASTER_WALLET_ADDRESS); + } } else { setIsCheckingPrerequisites(true); } diff --git a/apps/deploy-web/src/components/new-deployment/TemplateList.tsx b/apps/deploy-web/src/components/new-deployment/TemplateList.tsx index 5e19692bd..2bc99ad24 100644 --- a/apps/deploy-web/src/components/new-deployment/TemplateList.tsx +++ b/apps/deploy-web/src/components/new-deployment/TemplateList.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useEffect, useState } from "react"; import { Button, buttonVariants } from "@akashnetwork/ui/components"; -import { ArrowRight, Cpu, Linux,Page, Rocket, Wrench } from "iconoir-react"; +import { ArrowRight, Cpu, Linux, Rocket, Wrench } from "iconoir-react"; import { NavArrowLeft } from "iconoir-react"; import { useAtom } from "jotai"; import Link from "next/link"; diff --git a/apps/deploy-web/src/components/sdl/RentGpusForm.tsx b/apps/deploy-web/src/components/sdl/RentGpusForm.tsx index 8801f9af7..e97f04ad3 100644 --- a/apps/deploy-web/src/components/sdl/RentGpusForm.tsx +++ b/apps/deploy-web/src/components/sdl/RentGpusForm.tsx @@ -10,13 +10,19 @@ import { useAtom } from "jotai"; import { useRouter, useSearchParams } from "next/navigation"; import { event } from "nextjs-google-analytics"; +import { envConfig } from "@src/config/env.config"; import { useCertificate } from "@src/context/CertificateProvider"; import { useChainParam } from "@src/context/ChainParamProvider"; import { useSettings } from "@src/context/SettingsProvider"; import { useWallet } from "@src/context/WalletProvider"; +import { useManagedDeploymentConfirm } from "@src/hooks/useManagedDeploymentConfirm"; +import { useManagedWalletDenom } from "@src/hooks/useManagedWalletDenom"; +import { useWhen } from "@src/hooks/useWhen"; import { useGpuModels } from "@src/queries/useGpuQuery"; +import { useDepositParams } from "@src/queries/useSettings"; import sdlStore from "@src/store/sdlStore"; import { ApiTemplate, ProfileGpuModelType, RentGpusFormValuesSchema, RentGpusFormValuesType, ServiceType } from "@src/types"; +import { DepositParams } from "@src/types/deployment"; import { ProviderAttributeSchemaDetailValue } from "@src/types/providerAttributes"; import { AnalyticsEvents } from "@src/utils/analytics"; import { defaultInitialDeposit, RouteStepKeys } from "@src/utils/constants"; @@ -65,11 +71,20 @@ export const RentGpusForm: React.FunctionComponent = () => { const searchParams = useSearchParams(); const currentService: ServiceType = (_services && _services[0]) || ({} as any); const { settings } = useSettings(); - const { address, signAndBroadcastTx } = useWallet(); + const { address, signAndBroadcastTx, isManaged } = useWallet(); const { loadValidCertificates, localCert, isLocalCertMatching, loadLocalCert, setSelectedCertificate } = useCertificate(); const [sdlDenom, setSdlDenom] = useState("uakt"); const { minDeposit } = useChainParam(); const router = useRouter(); + const { createDeploymentConfirm } = useManagedDeploymentConfirm(); + const managedDenom = useManagedWalletDenom(); + const { data: depositParams } = useDepositParams(); + const defaultDeposit = depositParams || defaultInitialDeposit; + + useWhen(isManaged && sdlDenom === "uakt", () => { + setSdlDenom(managedDenom); + setValue("services.0.placement.pricing.denom", managedDenom); + }); useEffect(() => { if (rentGpuSdl && rentGpuSdl.services) { @@ -113,7 +128,12 @@ export const RentGpusForm: React.FunctionComponent = () => { } }, [searchParams, gpuModels, isQueryInit]); - async function createAndValidateDeploymentData(yamlStr: string, dseq = null, deposit = defaultInitialDeposit, depositorAddress: string | null = null) { + async function createAndValidateDeploymentData( + yamlStr: string, + dseq: string | null = null, + deposit = defaultDeposit, + depositorAddress: string | null = null + ) { try { if (!yamlStr) return null; @@ -168,6 +188,7 @@ export const RentGpusForm: React.FunctionComponent = () => { setValue("services", result as ServiceType[]); setValue("services.0.profile.gpuModels", _gpuModels); + setValue("services.0.placement.pricing.denom", managedDenom); trigger(); }; @@ -183,10 +204,21 @@ export const RentGpusForm: React.FunctionComponent = () => { const onSubmit = async (data: RentGpusFormValuesType) => { setRentGpuSdl(data); - setIsCheckingPrerequisites(true); + + if (isManaged) { + const isConfirmed = await createDeploymentConfirm(rentGpuSdl?.services as ServiceType[]); + + if (!isConfirmed) { + return; + } + + await handleCreateClick(defaultDeposit, envConfig.NEXT_PUBLIC_MASTER_WALLET_ADDRESS); + } else { + setIsCheckingPrerequisites(true); + } }; - async function handleCreateClick(deposit: number, depositorAddress: string) { + async function handleCreateClick(deposit: number | DepositParams[], depositorAddress: string) { setError(null); try { @@ -309,9 +341,11 @@ export const RentGpusForm: React.FunctionComponent = () => {
-
- -
+ {!isManaged && ( +
+ +
+ )} diff --git a/apps/deploy-web/src/hooks/useManagedDeploymentConfirm.tsx b/apps/deploy-web/src/hooks/useManagedDeploymentConfirm.tsx new file mode 100644 index 000000000..7c09908f8 --- /dev/null +++ b/apps/deploy-web/src/hooks/useManagedDeploymentConfirm.tsx @@ -0,0 +1,67 @@ +import { usePopup } from "@akashnetwork/ui/context"; + +import { LeaseSpecDetail } from "@src/components/shared/LeaseSpecDetail"; +import { useWallet } from "@src/context/WalletProvider"; +import { ServiceType } from "@src/types"; + +export const useManagedDeploymentConfirm = () => { + const { isManaged } = useWallet(); + const { confirm } = usePopup(); + + const closeDeploymentConfirm = async (dseq: string[]) => { + if (isManaged) { + const isConfirmed = await confirm({ + title: `Are you sure you want to close ${dseq.length > 1 ? "these deployments" : "this deployment"}?`, + message: ( +
+

+ DSEQ ({dseq.join(",")}) +

+

Closing a deployment will stop all services and release any unused escrowed funds.

+
+ ) + }); + + if (!isConfirmed) { + return false; + } + } + + return true; + }; + + const createDeploymentConfirm = async (services: ServiceType[]) => { + if (isManaged) { + const isConfirmed = await confirm({ + title: "Confirm deployment creation?", + message: ( +
+ {services.map(service => { + return ( +
+
+ {service.title}:{service.image} +
+
+ + {service.profile?.hasGpu && } + + +
+
+ ); + })} +
+ ) + }); + + if (!isConfirmed) { + return false; + } + } + + return true; + }; + + return { closeDeploymentConfirm, createDeploymentConfirm }; +}; diff --git a/apps/deploy-web/src/types/sdlBuilder.ts b/apps/deploy-web/src/types/sdlBuilder.ts index 96b9dcb24..73b769121 100644 --- a/apps/deploy-web/src/types/sdlBuilder.ts +++ b/apps/deploy-web/src/types/sdlBuilder.ts @@ -374,18 +374,6 @@ export const RentGpusFormValuesSchema = z.object({ region: ProviderRegionValueSchema.optional() }); -export const ImportServiceSchema = z.object({ - id: z.string().optional(), - title: z.string().optional(), - image: z.string().optional(), - profile: ProfileSchema.optional(), - expose: z.array(ExposeSchema).optional(), - command: CommandSchema.optional(), - env: z.array(EnvironmentVariableSchema).optional(), - placement: PlacementSchema.optional(), - count: z.number().optional() -}); - export type ServiceType = z.infer; export type SdlBuilderFormValuesType = z.infer; export type ProfileGpuModelType = z.infer; @@ -402,4 +390,3 @@ export type ExposeType = z.infer; export type PlacementType = z.infer; export type ProviderRegionValueType = z.infer; export type RentGpusFormValuesType = z.infer; -export type ImportServiceType = z.infer; \ No newline at end of file diff --git a/apps/deploy-web/src/utils/sdl/sdlImport.ts b/apps/deploy-web/src/utils/sdl/sdlImport.ts index dc803cc7b..490bbe060 100644 --- a/apps/deploy-web/src/utils/sdl/sdlImport.ts +++ b/apps/deploy-web/src/utils/sdl/sdlImport.ts @@ -1,7 +1,7 @@ import yaml from "js-yaml"; import { nanoid } from "nanoid"; -import { ExposeType, ImportServiceType, ProfileGpuModelType } from "@src/types"; +import { ExposeType, ProfileGpuModelType,ServiceType } from "@src/types"; import { CustomValidationError } from "../deploymentData"; import { capitalizeFirstLetter } from "../stringUtils"; import { defaultHttpOptions } from "./data"; @@ -9,13 +9,13 @@ import { defaultHttpOptions } from "./data"; export const importSimpleSdl = (yamlStr: string) => { try { const yamlJson = yaml.load(yamlStr) as any; - const services: ImportServiceType[] = []; + const services: ServiceType[] = []; const sortedServicesNames = Object.keys(yamlJson.services).sort(); sortedServicesNames.forEach(svcName => { const svc = yamlJson.services[svcName]; - const service: ImportServiceType = { + const service: Partial = { id: nanoid(), title: svcName, image: svc.image @@ -32,7 +32,7 @@ export const importSimpleSdl = (yamlStr: string) => { // Service compute profile service.profile = { cpu: compute.resources.cpu.units, - gpu: compute.resources.gpu ? compute.resources.gpu.units : 1, + gpu: compute.resources.gpu ? compute.resources.gpu.units : 0, gpuModels: compute.resources.gpu ? getGpuModels(compute.resources.gpu.attributes.vendor) : [], hasGpu: !!compute.resources.gpu, ram: getResourceDigit(compute.resources.memory.size), @@ -126,7 +126,7 @@ export const importSimpleSdl = (yamlStr: string) => { service.count = deployment.count; - services.push(service); + services.push(service as ServiceType); }); return services; diff --git a/apps/indexer/src/providers/statusEndpointHandlers/grpc.ts b/apps/indexer/src/providers/statusEndpointHandlers/grpc.ts index 866d2288c..f14a09b15 100644 --- a/apps/indexer/src/providers/statusEndpointHandlers/grpc.ts +++ b/apps/indexer/src/providers/statusEndpointHandlers/grpc.ts @@ -3,13 +3,13 @@ import { ResourcesMetric, Status } from "@akashnetwork/akash-api/akash/provider/ import { ProviderRPCClient } from "@akashnetwork/akash-api/akash/provider/v1/grpc-js"; import { Empty } from "@akashnetwork/akash-api/google/protobuf"; import { Provider } from "@akashnetwork/database/dbSchemas/akash"; +import minutesToMilliseconds from "date-fns/minutesToMilliseconds"; import memoize from "lodash/memoize"; import { promisify } from "util"; import { parseDecimalKubernetesString, parseSizeStr } from "@src/shared/utils/files"; import { FakeInsecureCredentials } from "./fake-insecure-credentials"; import { ProviderStatusInfo } from "./types"; -import minutesToMilliseconds from "date-fns/minutesToMilliseconds"; export async function fetchProviderStatusFromGRPC(provider: Provider, timeout: number): Promise { const data = await queryStatus(provider.hostUri, timeout); diff --git a/packages/ui/components/custom/popup.tsx b/packages/ui/components/custom/popup.tsx index 5f7c0fab7..2ccf4eff3 100644 --- a/packages/ui/components/custom/popup.tsx +++ b/packages/ui/components/custom/popup.tsx @@ -70,7 +70,7 @@ export type TOnCloseHandler = { export type CommonProps = { title?: string | React.ReactNode; - message?: string; + message?: string | React.ReactNode; open?: boolean; onClose?: TOnCloseHandler; fullWidth?: boolean;