diff --git a/apps/deploy-web/src/components/deployments/ManifestUpdate.tsx b/apps/deploy-web/src/components/deployments/ManifestUpdate.tsx index b498da589..dc3bf0075 100644 --- a/apps/deploy-web/src/components/deployments/ManifestUpdate.tsx +++ b/apps/deploy-web/src/components/deployments/ManifestUpdate.tsx @@ -11,10 +11,8 @@ import { LinkTo } from "@src/components/shared/LinkTo"; import ViewPanel from "@src/components/shared/ViewPanel"; import { useCertificate } from "@src/context/CertificateProvider"; import { useServices } from "@src/context/ServicesProvider"; -import { useSettings } from "@src/context/SettingsProvider"; import { useWallet } from "@src/context/WalletProvider"; import { useProviderList } from "@src/queries/useProvidersQuery"; -import { analyticsService } from "@src/services/analytics/analytics.service"; import networkStore from "@src/store/networkStore"; import type { DeploymentDto, LeaseDto } from "@src/types/deployment"; import type { ApiProviderList } from "@src/types/provider"; @@ -43,12 +41,11 @@ export const ManifestUpdate: React.FunctionComponent = ({ editedManifest, onManifestChange }) => { - const { providerProxy } = useServices(); + const { providerProxy, analyticsService, chainApiHttpClient } = useServices(); const [parsingError, setParsingError] = useState(null); const [deploymentVersion, setDeploymentVersion] = useState(null); const [showOutsideDeploymentMessage, setShowOutsideDeploymentMessage] = useState(false); const [isSendingManifest, setIsSendingManifest] = useState(false); - const { settings } = useSettings(); const { address, signAndBroadcastTx, isManaged: isManagedWallet } = useWallet(); const { data: providers } = useProviderList(); const { localCert, isLocalCertMatching } = useCertificate(); @@ -85,7 +82,7 @@ export const ManifestUpdate: React.FunctionComponent = ({ try { if (!editedManifest) return null; - await deploymentData.NewDeploymentData(settings.apiEndpoint, yamlStr, dseq, address); + await deploymentData.NewDeploymentData(chainApiHttpClient, yamlStr, dseq, address); setParsingError(null); } catch (err: any) { @@ -107,7 +104,7 @@ export const ManifestUpdate: React.FunctionComponent = ({ clearTimeout(timeoutId); } }; - }, [editedManifest, deployment.dseq, settings.apiEndpoint, address]); + }, [editedManifest, deployment.dseq, chainApiHttpClient, address]); function handleTextChange(value: string | undefined) { onManifestChange(value || ""); @@ -143,7 +140,7 @@ export const ManifestUpdate: React.FunctionComponent = ({ try { const doc = yaml.load(editedManifest); - const dd = await deploymentData.NewDeploymentData(settings.apiEndpoint, editedManifest, deployment.dseq, address); // TODO Flags + const dd = await deploymentData.NewDeploymentData(chainApiHttpClient, editedManifest, deployment.dseq, address); // TODO Flags const mani = deploymentData.getManifest(doc, true); // If it's actual update, send a transaction, else just send the manifest diff --git a/apps/deploy-web/src/components/home/YourAccount.tsx b/apps/deploy-web/src/components/home/YourAccount.tsx index 45efe63de..590117488 100644 --- a/apps/deploy-web/src/components/home/YourAccount.tsx +++ b/apps/deploy-web/src/components/home/YourAccount.tsx @@ -221,7 +221,7 @@ export const YourAccount: React.FunctionComponent = ({ isLoadingBalances,
{userProviders?.map(p => ( - + {p.name} ))} diff --git a/apps/deploy-web/src/components/layout/TopBanner.tsx b/apps/deploy-web/src/components/layout/TopBanner.tsx index 026403fba..07214850c 100644 --- a/apps/deploy-web/src/components/layout/TopBanner.tsx +++ b/apps/deploy-web/src/components/layout/TopBanner.tsx @@ -22,7 +22,7 @@ function CreditCardBanner() { function NetworkDownBanner() { return (
- The network is down. Unable to change deployments at the moment. + The blockchain is down. Console is in read-only mode.
); } @@ -49,14 +49,14 @@ function MaintenanceBanner({ onClose }: { onClose: () => void }) { export function TopBanner() { const { isMaintenanceBannerOpen, setIsMaintenanceBannerOpen, isBlockchainDown, hasCreditCardBanner } = useTopBanner(); - if (isMaintenanceBannerOpen) { - return setIsMaintenanceBannerOpen(false)} />; - } - if (isBlockchainDown) { return ; } + if (isMaintenanceBannerOpen) { + return setIsMaintenanceBannerOpen(false)} />; + } + if (hasCreditCardBanner) { return ; } diff --git a/apps/deploy-web/src/components/layout/WalletStatus.tsx b/apps/deploy-web/src/components/layout/WalletStatus.tsx index 6cccba595..ba752f22e 100644 --- a/apps/deploy-web/src/components/layout/WalletStatus.tsx +++ b/apps/deploy-web/src/components/layout/WalletStatus.tsx @@ -35,44 +35,44 @@ export function WalletStatus() {
- {!!walletBalance && ( -
setOpen(true)} - > -
- {isManaged && isTrialing && Trial} - {!isManaged && ( - <> - - {walletName?.length > 20 ? ( - {getSplitText(walletName, 4, 4)} - ) : ( - {walletName} - )} - - )} -
+
setOpen(true)} + > +
+ {isManaged && isTrialing && Trial} + {!isManaged && ( + <> + + {walletName?.length > 20 ? ( + {getSplitText(walletName, 4, 4)} + ) : ( + {walletName} + )} + + )} +
- {((isManaged && isTrialing) || !isManaged) &&
|
} + {walletBalance && ((isManaged && isTrialing) || !isManaged) &&
|
} -
+
+ {walletBalance && ( -
+ )} +
-
- -
+
+
- )} +
- {!isManaged && walletBalance && } - {withBilling && isManaged && walletBalance && } + {!isManaged && } + {withBilling && isManaged && }
diff --git a/apps/deploy-web/src/components/new-deployment/CreateLease/CreateLease.spec.tsx b/apps/deploy-web/src/components/new-deployment/CreateLease/CreateLease.spec.tsx index d22643a8d..b13baeae7 100644 --- a/apps/deploy-web/src/components/new-deployment/CreateLease/CreateLease.spec.tsx +++ b/apps/deploy-web/src/components/new-deployment/CreateLease/CreateLease.spec.tsx @@ -176,14 +176,7 @@ describe(CreateLease.name, () => { }); await waitFor(() => { - expect((BidGroup as jest.Mock).mock.calls.length).toBeGreaterThan(0); - }); - const bidGroupProps = (BidGroup as jest.Mock).mock.calls[0][0]; - act(() => { - bidGroupProps.handleBidSelected(mapToBidDto(bids[0])); - }); - await waitFor(() => { - expect(screen.queryByRole("button", { name: /Accept Bid/i })).toBeDisabled(); + expect(screen.getByText(/Blockchain is down/i)).toBeInTheDocument(); }); }); @@ -435,6 +428,7 @@ describe(CreateLease.name, () => { errorHandler: () => mock(), chainApiHttpClient: () => mock({ + isFallbackEnabled: !!input?.isBlockchainDown, get: async url => { if (url.includes("bids/list")) { return { diff --git a/apps/deploy-web/src/components/new-deployment/CreateLease/CreateLease.tsx b/apps/deploy-web/src/components/new-deployment/CreateLease/CreateLease.tsx index 1bed8c3d8..31fbf602c 100644 --- a/apps/deploy-web/src/components/new-deployment/CreateLease/CreateLease.tsx +++ b/apps/deploy-web/src/components/new-deployment/CreateLease/CreateLease.tsx @@ -451,13 +451,19 @@ export const CreateLease: React.FunctionComponent = ({ dseq, dependencies
)} - {!isLoadingBids && allClosed && ( + {settings.isBlockchainDown && ( +
+ Blockchain is down. Please try to refresh the page or try again later. +
+ )} + + {!settings.isBlockchainDown && !isLoadingBids && allClosed && ( Close Deployment )} - {!zeroBidsForTrialWarningDisplayed && warningRequestsReached && !maxRequestsReached && (bids?.length || 0) === 0 && ( + {!settings.isBlockchainDown && !zeroBidsForTrialWarningDisplayed && warningRequestsReached && !maxRequestsReached && (bids?.length || 0) === 0 && (
There should be bids by now... You can wait longer in case a bid shows up or close the deployment and try again with a different configuration. @@ -465,14 +471,18 @@ export const CreateLease: React.FunctionComponent = ({ dseq, dependencies
)} - {(isLoadingBids || (bids?.length || 0) === 0) && !maxRequestsReached && !isSendingManifest && !zeroBidsForTrialWarningDisplayed && ( -
- -
Waiting for bids...
-
- )} + {!settings.isBlockchainDown && + (isLoadingBids || (bids?.length || 0) === 0) && + !maxRequestsReached && + !isSendingManifest && + !zeroBidsForTrialWarningDisplayed && ( +
+ +
Waiting for bids...
+
+ )} - {!zeroBidsForTrialWarningDisplayed && maxRequestsReached && (bids?.length || 0) === 0 && ( + {!settings.isBlockchainDown && !zeroBidsForTrialWarningDisplayed && maxRequestsReached && (bids?.length || 0) === 0 && (
There's no bid for the current deployment. You can close the deployment and try again with a different configuration. diff --git a/apps/deploy-web/src/components/new-deployment/ManifestEdit.tsx b/apps/deploy-web/src/components/new-deployment/ManifestEdit.tsx index beefd2978..e4d39e202 100644 --- a/apps/deploy-web/src/components/new-deployment/ManifestEdit.tsx +++ b/apps/deploy-web/src/components/new-deployment/ManifestEdit.tsx @@ -12,7 +12,6 @@ import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import { useSnackbar } from "notistack"; -import { browserEnvConfig } from "@src/config/browser-env.config"; import { useCertificate } from "@src/context/CertificateProvider"; import { useSdlBuilder } from "@src/context/SdlBuilderProvider/SdlBuilderProvider"; import { useServices } from "@src/context/ServicesProvider"; @@ -74,7 +73,7 @@ export const ManifestEdit: React.FunctionComponent = ({ const [sdlDenom, setSdlDenom] = useState("uakt"); const isAnonymousFreeTrialEnabled = useFlag("anonymous_free_trial"); - const { analyticsService } = useServices(); + const { analyticsService, chainApiHttpClient, appConfig } = useServices(); const { settings } = useSettings(); const { address, signAndBroadcastTx, isManaged, isTrialing, isOnboarding } = useWallet(); const router = useRouter(); @@ -87,7 +86,7 @@ export const ManifestEdit: React.FunctionComponent = ({ const searchParams = useSearchParams(); const templateId = searchParams.get("templateId"); const { data: depositParams } = useDepositParams(); - const defaultDeposit = depositParams || browserEnvConfig.NEXT_PUBLIC_DEFAULT_INITIAL_DEPOSIT; + const defaultDeposit = depositParams || appConfig.NEXT_PUBLIC_DEFAULT_INITIAL_DEPOSIT; const wallet = useWallet(); const managedDenom = useManagedWalletDenom(); const { enqueueSnackbar } = useSnackbar(); @@ -166,7 +165,7 @@ export const ManifestEdit: React.FunctionComponent = ({ try { if (!yamlStr) return null; - const dd = await deploymentData.NewDeploymentData(settings.apiEndpoint, yamlStr, dseq, address, deposit, depositorAddress); + const dd = await deploymentData.NewDeploymentData(chainApiHttpClient, yamlStr, dseq, address, deposit, depositorAddress); validateDeploymentData(dd, selectedTemplate); setSdlDenom(dd.deposit.denom); @@ -217,7 +216,7 @@ export const ManifestEdit: React.FunctionComponent = ({ setIsCheckingPrerequisites(false); if (isManaged) { - handleCreateClick(defaultDeposit, browserEnvConfig.NEXT_PUBLIC_MASTER_WALLET_ADDRESS); + handleCreateClick(defaultDeposit, appConfig.NEXT_PUBLIC_MASTER_WALLET_ADDRESS); } else { setIsDepositingDeployment(true); } diff --git a/apps/deploy-web/src/components/sdl/RentGpusForm.tsx b/apps/deploy-web/src/components/sdl/RentGpusForm.tsx index 1f2e0fcde..789988a4c 100644 --- a/apps/deploy-web/src/components/sdl/RentGpusForm.tsx +++ b/apps/deploy-web/src/components/sdl/RentGpusForm.tsx @@ -9,16 +9,14 @@ import { Rocket } from "iconoir-react"; import { useAtom } from "jotai"; import { useRouter, useSearchParams } from "next/navigation"; -import { browserEnvConfig } from "@src/config/browser-env.config"; import { useCertificate } from "@src/context/CertificateProvider"; -import { useSettings } from "@src/context/SettingsProvider"; +import { useServices } from "@src/context/ServicesProvider"; import { useWallet } from "@src/context/WalletProvider"; import { useManagedWalletDenom } from "@src/hooks/useManagedWalletDenom"; import { useWhen } from "@src/hooks/useWhen"; import { useGpuModels } from "@src/queries/useGpuQuery"; import { useDepositParams } from "@src/queries/useSaveSettings"; import type { TemplateOutputSummaryWithCategory } from "@src/queries/useTemplateQuery"; -import { analyticsService } from "@src/services/analytics/analytics.service"; import sdlStore from "@src/store/sdlStore"; import type { ProfileGpuModelType, RentGpusFormValuesType, ServiceType } from "@src/types"; import { RentGpusFormValuesSchema } from "@src/types"; @@ -49,6 +47,7 @@ import { RegionSelect } from "./RegionSelect"; import { TokenFormControl } from "./TokenFormControl"; export const RentGpusForm: React.FunctionComponent = () => { + const { chainApiHttpClient, analyticsService, appConfig } = useServices(); const [error, setError] = useState(null); // const [templateMetadata, setTemplateMetadata] = useState(null); const [isQueryInit, setIsQuertInit] = useState(false); @@ -70,14 +69,13 @@ export const RentGpusForm: React.FunctionComponent = () => { const { services: _services } = watch(); const searchParams = useSearchParams(); const currentService: ServiceType = (_services && _services[0]) || ({} as any); - const { settings } = useSettings(); const { address, signAndBroadcastTx, isManaged } = useWallet(); const { loadValidCertificates, localCert, isLocalCertMatching, loadLocalCert, setSelectedCertificate } = useCertificate(); const [sdlDenom, setSdlDenom] = useState("uakt"); const router = useRouter(); const managedDenom = useManagedWalletDenom(); const { data: depositParams } = useDepositParams(); - const defaultDeposit = depositParams || browserEnvConfig.NEXT_PUBLIC_DEFAULT_INITIAL_DEPOSIT; + const defaultDeposit = depositParams || appConfig.NEXT_PUBLIC_DEFAULT_INITIAL_DEPOSIT; useWhen(isManaged && sdlDenom === "uakt", () => { setSdlDenom(managedDenom); @@ -135,7 +133,7 @@ export const RentGpusForm: React.FunctionComponent = () => { try { if (!yamlStr) return null; - const dd = await deploymentData.NewDeploymentData(settings.apiEndpoint, yamlStr, dseq, address, deposit, depositorAddress); + const dd = await deploymentData.NewDeploymentData(chainApiHttpClient, yamlStr, dseq, address, deposit, depositorAddress); validateDeploymentData(dd); setSdlDenom(dd.deposit.denom); diff --git a/apps/deploy-web/src/components/wallet/CustodialWalletPopup.tsx b/apps/deploy-web/src/components/wallet/CustodialWalletPopup.tsx index c79843836..fa876cd35 100644 --- a/apps/deploy-web/src/components/wallet/CustodialWalletPopup.tsx +++ b/apps/deploy-web/src/components/wallet/CustodialWalletPopup.tsx @@ -20,7 +20,7 @@ import { PriceValue } from "../shared/PriceValue"; import { ConnectManagedWalletButton } from "./ConnectManagedWalletButton"; interface CustodialWalletPopupProps extends React.PropsWithChildren { - walletBalance: WalletBalance; + walletBalance?: WalletBalance | null; } const withBilling = browserEnvConfig.NEXT_PUBLIC_BILLING_ENABLED; @@ -43,22 +43,26 @@ export const CustodialWalletPopup: React.FC = ({ wall
Wallet Balance
-
- AKT - - - ({uaktToAKT(walletBalance.totalUAKT, 2)} AKT) - -
+ {(walletBalance && ( + <> +
+ AKT + + + ({uaktToAKT(walletBalance.totalUAKT, 2)} AKT) + +
- + -
- USDC - - - -
+
+ USDC + + + +
+ + )) ||
Wallet Balance is unknown because the blockchain is down
}
Wallet Actions
diff --git a/apps/deploy-web/src/components/wallet/ManagedWalletPopup.tsx b/apps/deploy-web/src/components/wallet/ManagedWalletPopup.tsx index bc4bce32f..65841eebf 100644 --- a/apps/deploy-web/src/components/wallet/ManagedWalletPopup.tsx +++ b/apps/deploy-web/src/components/wallet/ManagedWalletPopup.tsx @@ -13,7 +13,7 @@ import { LinkTo } from "../shared/LinkTo"; import { AddFundsLink } from "../user/AddFundsLink"; interface ManagedWalletPopupProps extends React.PropsWithChildren { - walletBalance: WalletBalance; + walletBalance?: WalletBalance | null; } export const ManagedWalletPopup: React.FC = ({ walletBalance }) => { @@ -29,31 +29,35 @@ export const ManagedWalletPopup: React.FC = ({ walletBa
)}
-
- Credits Remaining: - - - -
+ {(walletBalance && ( + <> +
+ Credits Remaining: + + + +
- + -
- Deposits: - - - -
+
+ Deposits: + + + +
+ + )) ||
Wallet Balance is unknown because the blockchain is down
}
showManagedEscrowFaqModal()}> diff --git a/apps/deploy-web/src/context/CertificateProvider/CertificateProviderContext.tsx b/apps/deploy-web/src/context/CertificateProvider/CertificateProviderContext.tsx index 1ae016127..ccb4e9f71 100644 --- a/apps/deploy-web/src/context/CertificateProvider/CertificateProviderContext.tsx +++ b/apps/deploy-web/src/context/CertificateProvider/CertificateProviderContext.tsx @@ -73,7 +73,7 @@ type Props = { }; export const CertificateProvider: React.FC = ({ children, dependencies: d = DEPENDENCIES }) => { - const { certificateManager, analyticsService, certificatesService, errorHandler } = d.useServices(); + const { certificateManager, analyticsService, certificatesService, errorHandler, chainApiHttpClient } = d.useServices(); const [isCreatingCert, setIsCreatingCert] = useState(false); const [validCertificates, setValidCertificates] = useState>([]); @@ -91,11 +91,13 @@ export const CertificateProvider: React.FC = ({ children, dependencies: d setIsLoadingCertificates(true); try { - const certificates = await certificatesService.getAllCertificates({ address, state: "valid" }); + const certificates = await certificatesService + .getAllCertificates({ address, state: "valid" }) + .catch(error => (chainApiHttpClient.isFallbackEnabled ? [] : Promise.reject(error))); + const certs = (certificates || []).map(cert => { const parsed = atob(cert.certificate.cert); const pem = certificateManager.parsePem(parsed); - return { ...cert, parsed, @@ -136,7 +138,7 @@ export const CertificateProvider: React.FC = ({ children, dependencies: d * When changing wallet, reset certs and load for new wallet */ useEffect(() => { - if (!isSettingsInit) return; + if (!isSettingsInit || chainApiHttpClient.isFallbackEnabled) return; setValidCertificates([]); setSelectedCertificate(null); @@ -147,7 +149,7 @@ export const CertificateProvider: React.FC = ({ children, dependencies: d loadLocalCert(); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [address, isSettingsInit]); + }, [address, isSettingsInit, chainApiHttpClient.isFallbackEnabled]); useEffect(() => { let isMatching = false; diff --git a/apps/deploy-web/src/context/ChainParamProvider/ChainParamProvider.tsx b/apps/deploy-web/src/context/ChainParamProvider/ChainParamProvider.tsx index cefce5702..79002d3d2 100644 --- a/apps/deploy-web/src/context/ChainParamProvider/ChainParamProvider.tsx +++ b/apps/deploy-web/src/context/ChainParamProvider/ChainParamProvider.tsx @@ -21,7 +21,7 @@ type ContextType = { const ChainParamContext = React.createContext({} as ContextType); export const ChainParamProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const { isSettingsInit } = useSettings(); + const { isSettingsInit, settings } = useSettings(); const { data: depositParams, refetch: getDepositParams } = useDepositParams({ enabled: false }); const usdcDenom = useUsdcDenom(); const aktMinDeposit = depositParams ? uaktToAKT(parseFloat(depositParams.find(x => x.denom === UAKT_DENOM)?.amount || "") || 0) : 0; @@ -29,10 +29,10 @@ export const ChainParamProvider: React.FC<{ children: React.ReactNode }> = ({ ch const minDeposit = { akt: aktMinDeposit, usdc: usdcMinDeposit }; useEffect(() => { - if (isSettingsInit && !depositParams) { + if (isSettingsInit && !depositParams && !settings.isBlockchainDown) { getDepositParams(); } - }, [isSettingsInit, depositParams]); + }, [isSettingsInit, depositParams, settings.isBlockchainDown]); return {children}; }; diff --git a/apps/deploy-web/src/context/ServicesProvider/ServicesProvider.tsx b/apps/deploy-web/src/context/ServicesProvider/ServicesProvider.tsx index bf03007c2..b2362f119 100644 --- a/apps/deploy-web/src/context/ServicesProvider/ServicesProvider.tsx +++ b/apps/deploy-web/src/context/ServicesProvider/ServicesProvider.tsx @@ -1,6 +1,7 @@ import React, { useContext } from "react"; import { AuthzHttpService, CertificatesService } from "@akashnetwork/http-sdk"; +import { withInterceptors } from "@src/services/app-di-container/app-di-container"; import { services as rootContainer } from "@src/services/app-di-container/browser-di-container"; import type { DIContainer, Factories } from "@src/services/container/createContainer"; import { createChildContainer } from "@src/services/container/createContainer"; @@ -29,25 +30,27 @@ export function useServices() { return useContext(ServicesContext) as AppDIContainer; } +const neverResolvedPromise = new Promise(() => {}); function createAppContainer(settingsState: SettingsContextType, services: T) { const di = createChildContainer(rootContainer, { authzHttpService: () => new AuthzHttpService(di.chainApiHttpClient), walletBalancesService: () => new WalletBalancesService(di.authzHttpService, di.chainApiHttpClient, di.appConfig.NEXT_PUBLIC_MASTER_WALLET_ADDRESS), certificatesService: () => new CertificatesService(di.chainApiHttpClient), chainApiHttpClient: () => - createFallbackableHttpClient( - rootContainer.createAxios, - rootContainer.externalApiHttpClient, // TODO: replace with indexer HttpClient - { + withInterceptors( + createFallbackableHttpClient(rootContainer.createAxios, rootContainer.fallbackChainApiHttpClient, { baseURL: settingsState.settings?.apiEndpoint, shouldFallback: () => settingsState.settings?.isBlockchainDown, - onFailure: () => { + onUnavailableError: () => { if (settingsState.settings?.isBlockchainDown) return; settingsState.setSettings(prev => ({ ...prev, isBlockchainDown: true })); }, onSuccess: () => { settingsState.refreshNodeStatuses(); } + }), + { + request: [config => (config.baseURL ? config : neverResolvedPromise)] } ), ...services diff --git a/apps/deploy-web/src/context/TopBannerProvider/TopBannerProvider.tsx b/apps/deploy-web/src/context/TopBannerProvider/TopBannerProvider.tsx index 6c847eef5..fb522832b 100644 --- a/apps/deploy-web/src/context/TopBannerProvider/TopBannerProvider.tsx +++ b/apps/deploy-web/src/context/TopBannerProvider/TopBannerProvider.tsx @@ -21,7 +21,7 @@ export const TopBannerProvider: FCWithChildren = ({ children }) => { const { settings } = useSettings(); const hasCreditCardBanner = useHasCreditCardBanner(); - const [isMaintenanceBannerOpen, setIsMaintenanceBannerOpen] = useState(!!maintenanceBannerFlag.enabled); + const [isMaintenanceBannerOpen, setIsMaintenanceBannerOpen] = useState(false); useWhen(maintenanceBannerFlag.enabled, () => setIsMaintenanceBannerOpen(true)); const hasBanner = useMemo( diff --git a/apps/deploy-web/src/hooks/useWalletBalance.ts b/apps/deploy-web/src/hooks/useWalletBalance.ts index d4db12361..47a631b27 100644 --- a/apps/deploy-web/src/hooks/useWalletBalance.ts +++ b/apps/deploy-web/src/hooks/useWalletBalance.ts @@ -1,10 +1,12 @@ import { useEffect, useState } from "react"; +import { useAtom } from "jotai"; import { UAKT_DENOM } from "@src/config/denom.config"; import { useChainParam } from "@src/context/ChainParamProvider"; import { usePricing } from "@src/context/PricingProvider"; import { useWallet } from "@src/context/WalletProvider"; import { useBalances } from "@src/queries/useBalancesQuery"; +import walletStore from "@src/store/walletStore"; import { udenomToDenom } from "@src/utils/mathHelpers"; import { uaktToAKT } from "@src/utils/priceUtils"; import { useUsdcDenom } from "./useDenom"; @@ -35,7 +37,7 @@ export const useWalletBalance = (): WalletBalanceReturnType => { const { isLoaded, price, udenomToUsd } = usePricing(); const { address, isManaged } = useWallet(); const { data: balances, isFetching: isLoadingBalances, refetch } = useBalances(address); - const [walletBalance, setWalletBalance] = useState(null); + const [walletBalance, setWalletBalance] = useAtom(walletStore.balance); useEffect(() => { if (isLoaded && balances && price) { diff --git a/apps/deploy-web/src/pages/payment.tsx b/apps/deploy-web/src/pages/payment.tsx index 476b7a061..d18c525e1 100644 --- a/apps/deploy-web/src/pages/payment.tsx +++ b/apps/deploy-web/src/pages/payment.tsx @@ -11,6 +11,7 @@ import { Title } from "@src/components/shared/Title"; import { AddPaymentMethodPopup, DeletePaymentMethodPopup, PaymentForm } from "@src/components/user/payment"; import { PaymentSuccessAnimation } from "@src/components/user/payment/PaymentSuccessAnimation"; import { usePaymentPolling } from "@src/context/PaymentPollingProvider"; +import { useSettings } from "@src/context/SettingsProvider"; import { useWallet } from "@src/context/WalletProvider"; import { use3DSecure } from "@src/hooks/use3DSecure"; import { useUser } from "@src/hooks/useUser"; @@ -54,6 +55,7 @@ const PayPage: React.FunctionComponent = () => { }, showSuccessMessage: false }); + const { settings } = useSettings(); const isLoading = isLoadingPaymentMethods; const { isTrialing } = useWallet(); @@ -258,6 +260,16 @@ const PayPage: React.FunctionComponent = () => { {paymentMethods.length > 0 && (

Add credits

+ {settings.isBlockchainDown && ( + +

+ We are currently experiencing a temporary blockchain outage, which may cause delays in processing your payments. Once the blockchain is back + online, all pending transactions will be processed automatically. +
+ If you encounter any issues or have urgent concerns, please don’t hesitate to reach out to us — we’re here to help. +

+
+ )} (address ? di.walletBalancesService.getBalances(address) : null), - enabled: !!address && di.authzHttpService.isReady, - ...options + ...options, + enabled: options?.enabled !== false && !!address && di.authzHttpService.isReady && !di.chainApiHttpClient.isFallbackEnabled }); } diff --git a/apps/deploy-web/src/queries/useBidQuery.ts b/apps/deploy-web/src/queries/useBidQuery.ts index 8092adec8..090a753d7 100644 --- a/apps/deploy-web/src/queries/useBidQuery.ts +++ b/apps/deploy-web/src/queries/useBidQuery.ts @@ -36,7 +36,8 @@ export function useBidList(address: string, dseq: string, options?: Omit getBidList(chainApiHttpClient, address, dseq), - ...options + ...options, + enabled: options?.enabled !== false && !chainApiHttpClient.isFallbackEnabled }); } @@ -48,11 +49,19 @@ async function getBidInfo(apiClient: AxiosInstance, address: string, dseq: strin return response.data; } -export function useBidInfo(address: string, dseq: string, gseq: number, oseq: number, provider: string, options = {}) { +export function useBidInfo( + address: string, + dseq: string, + gseq: number, + oseq: number, + provider: string, + options?: Omit, "queryKey" | "queryFn"> +) { const { chainApiHttpClient } = useServices(); return useQuery({ queryKey: QueryKeys.getBidInfoKey(address, dseq, gseq, oseq, provider), queryFn: () => getBidInfo(chainApiHttpClient, address, dseq, gseq, oseq, provider), - ...options + ...options, + enabled: options?.enabled !== false && !chainApiHttpClient.isFallbackEnabled }); } diff --git a/apps/deploy-web/src/queries/useBlocksQuery.ts b/apps/deploy-web/src/queries/useBlocksQuery.ts index 8631140fe..11c571b38 100644 --- a/apps/deploy-web/src/queries/useBlocksQuery.ts +++ b/apps/deploy-web/src/queries/useBlocksQuery.ts @@ -15,7 +15,8 @@ export function useBlock(id: string, options: Omit { describe(useAllowancesGranted.name, () => { it("fetches allowances granted when address is provided", async () => { const mockData = [{ id: faker.string.uuid() }]; - const chainApiHttpClient = mock({ - defaults: { baseURL: "https://api.akash.network" }, + const chainApiHttpClient = mock({ + isFallbackEnabled: false, get: jest.fn().mockResolvedValue({ data: { allowances: mockData, @@ -202,8 +202,8 @@ describe("useGrantsQuery", () => { }); it("does not fetch when address is not provided", () => { - const chainApiHttpClient = mock({ - defaults: { baseURL: "https://api.akash.network" }, + const chainApiHttpClient = mock({ + isFallbackEnabled: false, get: jest.fn().mockResolvedValue({ data: { allowances: [], diff --git a/apps/deploy-web/src/queries/useGrantsQuery.ts b/apps/deploy-web/src/queries/useGrantsQuery.ts index 4cc27686a..cf08858e0 100644 --- a/apps/deploy-web/src/queries/useGrantsQuery.ts +++ b/apps/deploy-web/src/queries/useGrantsQuery.ts @@ -4,7 +4,6 @@ import { useQuery } from "@tanstack/react-query"; import type { AxiosInstance } from "axios"; import { useServices } from "@src/context/ServicesProvider"; -import { useSettings } from "@src/context/SettingsProvider/SettingsProviderContext"; import type { AllowanceType, PaginatedAllowanceType, PaginatedGrantType } from "@src/types/grant"; import { ApiUrlService, loadWithPagination } from "@src/utils/apiUtils"; import { QueryKeys } from "./queryKeys"; @@ -15,29 +14,25 @@ export function useGranterGrants( limit: number, options: Omit, "queryKey" | "queryFn"> = {} ) { - const { settings } = useSettings(); - const { authzHttpService } = useServices(); + const { authzHttpService, chainApiHttpClient } = useServices(); const offset = page * limit; - options.enabled = options.enabled !== false && !!address && authzHttpService.isReady && !settings.isBlockchainDown; - return useQuery({ queryKey: QueryKeys.getGranterGrants(address, page, offset), queryFn: () => authzHttpService.getPaginatedDepositDeploymentGrants({ granter: address, limit, offset }), - ...options + ...options, + enabled: options.enabled !== false && !!address && !chainApiHttpClient.isFallbackEnabled }); } export function useGranteeGrants(address: string, options: Omit, "queryKey" | "queryFn"> = {}) { - const { settings } = useSettings(); - const { authzHttpService } = useServices(); - - options.enabled = options.enabled !== false && !!address && authzHttpService.isReady && !settings.isBlockchainDown; + const { authzHttpService, chainApiHttpClient } = useServices(); return useQuery({ queryKey: QueryKeys.getGranteeGrants(address || "UNDEFINED"), queryFn: () => authzHttpService.getAllDepositDeploymentGrants({ grantee: address, limit: 1000 }), - ...options + ...options, + enabled: options.enabled !== false && !!address && !chainApiHttpClient.isFallbackEnabled }); } @@ -47,16 +42,14 @@ export function useAllowancesIssued( limit: number, options: Omit, "queryKey" | "queryFn"> = {} ) { - const { settings } = useSettings(); - const { authzHttpService } = useServices(); + const { authzHttpService, chainApiHttpClient } = useServices(); const offset = page * limit; - options.enabled = options.enabled !== false && !!address && authzHttpService.isReady && !settings.isBlockchainDown; - return useQuery({ queryKey: QueryKeys.getAllowancesIssued(address, page, offset), queryFn: () => authzHttpService.getPaginatedFeeAllowancesForGranter(address, limit, offset), - ...options + ...options, + enabled: options.enabled !== false && !!address && !chainApiHttpClient.isFallbackEnabled }); } @@ -65,14 +58,12 @@ async function getAllowancesGranted(chainApiHttpClient: AxiosInstance, address: } export function useAllowancesGranted(address: string, options: Omit, "queryKey" | "queryFn"> = {}) { - const { settings } = useSettings(); const { chainApiHttpClient } = useServices(); - options.enabled = options.enabled !== false && !!address && !!chainApiHttpClient.defaults.baseURL && !settings.isBlockchainDown; - return useQuery({ queryKey: address ? QueryKeys.getAllowancesGranted(address) : [], queryFn: () => getAllowancesGranted(chainApiHttpClient, address), - ...options + ...options, + enabled: options.enabled !== false && !!address && !chainApiHttpClient.isFallbackEnabled }); } diff --git a/apps/deploy-web/src/queries/useSaveSettings.spec.tsx b/apps/deploy-web/src/queries/useSaveSettings.spec.tsx index ed0ba5124..e19e3627c 100644 --- a/apps/deploy-web/src/queries/useSaveSettings.spec.tsx +++ b/apps/deploy-web/src/queries/useSaveSettings.spec.tsx @@ -4,6 +4,7 @@ import type { AxiosInstance } from "axios"; import { mock } from "jest-mock-extended"; import type { Props as ServicesProviderProps } from "@src/context/ServicesProvider"; +import type { FallbackableHttpClient } from "@src/services/createFallbackableHttpClient/createFallbackableHttpClient"; import { CustomSnackbarProvider } from "../../../../packages/ui/context/CustomSnackbarProvider"; import { setupQuery } from "../../tests/unit/query-client"; import { useDepositParams, useSaveSettings } from "./useSaveSettings"; @@ -73,7 +74,9 @@ describe("Settings management", () => { describe(useDepositParams.name, () => { it("should fetch deposit params successfully", async () => { - const chainApiHttpClient = mock(); + const chainApiHttpClient = mock({ + isFallbackEnabled: false + } as FallbackableHttpClient); const depositParams = { denom: "uakt", minDeposit: "1000000" @@ -100,7 +103,9 @@ describe("Settings management", () => { }); it("handles error when fetching deposit params", async () => { - const chainApiHttpClient = mock(); + const chainApiHttpClient = mock({ + isFallbackEnabled: false + } as FallbackableHttpClient); chainApiHttpClient.get.mockRejectedValue(new Error("Failed to fetch deposit params")); const { result } = setupQuery(() => useDepositParams(), { diff --git a/apps/deploy-web/src/queries/useSaveSettings.ts b/apps/deploy-web/src/queries/useSaveSettings.ts index 9c923984c..160cf1b10 100644 --- a/apps/deploy-web/src/queries/useSaveSettings.ts +++ b/apps/deploy-web/src/queries/useSaveSettings.ts @@ -1,3 +1,4 @@ +import type { UseQueryOptions } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query"; import type { AxiosInstance, AxiosResponse } from "axios"; import { useSnackbar } from "notistack"; @@ -33,11 +34,12 @@ async function getDepositParams(chainApiHttpClient: AxiosInstance) { return JSON.parse(depositParams.param.value) as DepositParams[]; } -export function useDepositParams(options = {}) { +export function useDepositParams(options?: Omit, "queryKey" | "queryFn">) { const { chainApiHttpClient } = useServices(); return useQuery({ queryKey: QueryKeys.getDepositParamsKey(), queryFn: () => getDepositParams(chainApiHttpClient), - ...options + ...options, + enabled: options?.enabled !== false && !chainApiHttpClient.isFallbackEnabled }); } diff --git a/apps/deploy-web/src/services/app-di-container/browser-di-container.ts b/apps/deploy-web/src/services/app-di-container/browser-di-container.ts index 43aa0a9de..047604820 100644 --- a/apps/deploy-web/src/services/app-di-container/browser-di-container.ts +++ b/apps/deploy-web/src/services/app-di-container/browser-di-container.ts @@ -40,6 +40,15 @@ export const services = createChildContainer(rootContainer, { }), /** TODO: https://github.com/akash-network/console/issues/1720 */ publicConsoleApiHttpClient: () => services.applyAxiosInterceptors(services.createAxios()), + fallbackChainApiHttpClient: () => + services.applyAxiosInterceptors(services.createAxios(), { + request: [ + config => { + config.baseURL = services.apiUrlService.getBaseApiUrlFor(services.networkStore.selectedNetworkId); + return config; + } + ] + }), networkStore: () => networkStore, appConfig: () => browserEnvConfig, authService: () => new AuthService(services.urlService, services.internalApiHttpClient) diff --git a/apps/deploy-web/src/services/createFallbackableHttpClient/createFallbackableHttpClient.spec.ts b/apps/deploy-web/src/services/createFallbackableHttpClient/createFallbackableHttpClient.spec.ts index c12646183..fea5064e5 100644 --- a/apps/deploy-web/src/services/createFallbackableHttpClient/createFallbackableHttpClient.spec.ts +++ b/apps/deploy-web/src/services/createFallbackableHttpClient/createFallbackableHttpClient.spec.ts @@ -14,11 +14,11 @@ describe(createFallbackableHttpClient.name, () => { }); it("uses fallback http client when request fails after 3 retries and should", async () => { - const onFailure = jest.fn(); + const onUnavailableError = jest.fn(); const options: ChainApiHttpClientOptions = { baseURL: "https://api.test.com", shouldFallback: () => false, - onFailure + onUnavailableError }; const fetch = jest.fn(async () => new Response("error", { status: 500 })); @@ -33,7 +33,7 @@ describe(createFallbackableHttpClient.name, () => { url: "/test" }) ); - expect(onFailure).toHaveBeenCalledTimes(1); + expect(onUnavailableError).toHaveBeenCalledTimes(1); }); it("calls onSuccess callback when request succeeds", async () => { @@ -53,11 +53,11 @@ describe(createFallbackableHttpClient.name, () => { }); it("falls back to fallback http client if shouldFallback returns true (circuit breaker is open)", async () => { - const onFailure = jest.fn(); + const onUnavailableError = jest.fn(); const options: ChainApiHttpClientOptions = { baseURL: "https://api.test.com", shouldFallback: () => true, - onFailure + onUnavailableError }; const fetch = jest.fn(async () => new Response("test", { status: 500 })); @@ -66,7 +66,7 @@ describe(createFallbackableHttpClient.name, () => { await Promise.all([chainApiHttpClient.get("/test"), jest.runAllTimersAsync()]); await chainApiHttpClient.get("/test"); // fails fast becaues circuit breaker is open - expect(onFailure).toHaveBeenCalledWith(expect.any(BrokenCircuitError)); + expect(onUnavailableError).toHaveBeenCalledWith(expect.any(BrokenCircuitError)); expect(fallbackHttpClient.request).toHaveBeenCalledTimes(2); // once after failed retries, another one when circuit breaker is open }); diff --git a/apps/deploy-web/src/services/createFallbackableHttpClient/createFallbackableHttpClient.ts b/apps/deploy-web/src/services/createFallbackableHttpClient/createFallbackableHttpClient.ts index 14530d961..4e8a1e0c8 100644 --- a/apps/deploy-web/src/services/createFallbackableHttpClient/createFallbackableHttpClient.ts +++ b/apps/deploy-web/src/services/createFallbackableHttpClient/createFallbackableHttpClient.ts @@ -1,15 +1,15 @@ import type { createHttpClient as createDefaultHttpClient, HttpClient } from "@akashnetwork/http-sdk"; -import { isHttpError } from "@akashnetwork/http-sdk"; import { ExponentialBackoff, isBrokenCircuitError } from "cockatiel"; -import { createFetchAdapter } from "../createFetchAdapter/createFetchAdapter"; +import { createFetchAdapter, isNetworkOrIdempotentRequestError } from "../createFetchAdapter/createFetchAdapter"; +export type FallbackableHttpClient = HttpClient & { isFallbackEnabled: boolean }; export function createFallbackableHttpClient( createHttpClient: typeof createDefaultHttpClient, fallbackHttpClient: HttpClient, options: ChainApiHttpClientOptions -) { - return createHttpClient({ +): FallbackableHttpClient { + const httpClient = createHttpClient({ baseURL: options.baseURL, adapter: createFetchAdapter({ circuitBreaker: { @@ -23,9 +23,8 @@ export function createFallbackableHttpClient( } }, onFailure: (error, requestConfig) => { - options.onFailure?.(error); - - if (isHttpError(error) || isBrokenCircuitError(error)) { + if (isNetworkOrIdempotentRequestError(error) || isBrokenCircuitError(error)) { + options.onUnavailableError?.(error); const { adapter, ...restOfRequestConfig } = requestConfig; return fallbackHttpClient.request(restOfRequestConfig); } @@ -36,11 +35,17 @@ export function createFallbackableHttpClient( retries: 0 } }); + + Object.defineProperty(httpClient, "isFallbackEnabled", { + get: options.shouldFallback + }); + + return httpClient as FallbackableHttpClient; } export interface ChainApiHttpClientOptions { baseURL: string; - onFailure?: (error: unknown) => void; + onUnavailableError?: (error: unknown) => void; onSuccess?: () => void; shouldFallback: () => boolean; } diff --git a/apps/deploy-web/src/services/createFetchAdapter/createFetchAdapter.ts b/apps/deploy-web/src/services/createFetchAdapter/createFetchAdapter.ts index 1bd22a4cc..e2fc184f5 100644 --- a/apps/deploy-web/src/services/createFetchAdapter/createFetchAdapter.ts +++ b/apps/deploy-web/src/services/createFetchAdapter/createFetchAdapter.ts @@ -60,7 +60,7 @@ export function createFetchAdapter(options: FetchAdapterOptions = {}): AxiosAdap }; } -function isNetworkOrIdempotentRequestError(error: unknown): boolean { +export function isNetworkOrIdempotentRequestError(error: unknown): boolean { const isNetworkError = error && !axios.isAxiosError(error) && error instanceof Error && "code" in error && error.code; if (isNetworkError) return isRetriableError(error); return axios.isAxiosError(error) && isIdempotentRequestError(error); diff --git a/apps/deploy-web/src/store/walletStore.ts b/apps/deploy-web/src/store/walletStore.ts index 3c29006a2..c008e63ed 100644 --- a/apps/deploy-web/src/store/walletStore.ts +++ b/apps/deploy-web/src/store/walletStore.ts @@ -1,14 +1,18 @@ import { atom } from "jotai"; import { atomWithStorage } from "jotai/utils"; +import type { WalletBalance } from "@src/hooks/useWalletBalance"; + const isSignedInWithTrial = atomWithStorage("isSignedInWithTrial", false); const selectedWalletType = atomWithStorage<"managed" | "custodial">("selectedWalletType", "custodial"); const isWalletModalOpen = atom(false); +const balance = atom(null); const walletStore = { isSignedInWithTrial, selectedWalletType, - isWalletModalOpen + isWalletModalOpen, + balance }; export default walletStore; diff --git a/apps/deploy-web/src/utils/deploymentData/helpers.ts b/apps/deploy-web/src/utils/deploymentData/helpers.ts index 55974bf95..fbf56f8be 100644 --- a/apps/deploy-web/src/utils/deploymentData/helpers.ts +++ b/apps/deploy-web/src/utils/deploymentData/helpers.ts @@ -1,7 +1,6 @@ import { SDL } from "@akashnetwork/akashjs/build/sdl"; import type { v2Sdl } from "@akashnetwork/akashjs/build/sdl/types"; import type { NetworkId } from "@akashnetwork/akashjs/build/types/network"; -import axios from "axios"; export class CustomValidationError extends Error { constructor(message: string) { @@ -31,14 +30,6 @@ const specSuffixes = { Eb: 1000 * 1000 * 1000 * 1000 * 1000 * 1000 }; -export async function getCurrentHeight(apiEndpoint: string) { - const response = await axios.get(`${apiEndpoint}/blocks/latest`); - const data = response.data; - - const height = parseInt(data.block.header.height); - return height; -} - export function parseSizeStr(str: string) { try { const suffix = Object.keys(specSuffixes).find(s => str.toLowerCase().endsWith(s.toLowerCase())); diff --git a/apps/deploy-web/src/utils/deploymentData/v1beta2.ts b/apps/deploy-web/src/utils/deploymentData/v1beta2.ts index 64f25c4ab..069b1915f 100644 --- a/apps/deploy-web/src/utils/deploymentData/v1beta2.ts +++ b/apps/deploy-web/src/utils/deploymentData/v1beta2.ts @@ -1,13 +1,14 @@ import type { SDL } from "@akashnetwork/akashjs/build/sdl"; import type { Attribute, v2Sdl } from "@akashnetwork/akashjs/build/sdl/types"; import type { NetworkId } from "@akashnetwork/akashjs/build/types/network"; +import type { HttpClient } from "@akashnetwork/http-sdk"; import yaml from "js-yaml"; import path from "path"; import { browserEnvConfig } from "@src/config/browser-env.config"; import networkStore from "@src/store/networkStore"; import { stringToBoolean } from "../stringUtils"; -import { CustomValidationError, DeploymentGroups, getCurrentHeight, getSdl, Manifest, ManifestVersion, parseSizeStr } from "./helpers"; +import { CustomValidationError, DeploymentGroups, getSdl, Manifest, ManifestVersion, parseSizeStr } from "./helpers"; function validate(yamlStr: string, yamlJson: v2Sdl, networkId: NetworkId) { let sdl: SDL; @@ -181,7 +182,7 @@ export async function getManifestVersion(yamlJson: string | v2Sdl, asString = fa } export async function NewDeploymentData( - apiEndpoint: string, + chainApiHttpClient: HttpClient, yamlStr: string, dseq: string, fromAddress: string, @@ -207,7 +208,8 @@ export async function NewDeploymentData( }; if (!id.dseq) { - id.dseq = (await getCurrentHeight(apiEndpoint)).toString(); + const response = await chainApiHttpClient.get("/blocks/latest"); + id.dseq = response.data.block.header.height; } return { diff --git a/apps/deploy-web/src/utils/deploymentData/v1beta3.ts b/apps/deploy-web/src/utils/deploymentData/v1beta3.ts index 6dcf41e21..7b8f2caff 100644 --- a/apps/deploy-web/src/utils/deploymentData/v1beta3.ts +++ b/apps/deploy-web/src/utils/deploymentData/v1beta3.ts @@ -1,10 +1,11 @@ import type { Attribute } from "@akashnetwork/akash-api/akash/base/v1beta3"; +import type { HttpClient } from "@akashnetwork/http-sdk"; import yaml from "js-yaml"; import { browserEnvConfig } from "@src/config/browser-env.config"; import networkStore from "@src/store/networkStore"; import type { DepositParams } from "@src/types/deployment"; -import { CustomValidationError, getCurrentHeight, getSdl, Manifest, ManifestVersion } from "./helpers"; +import { CustomValidationError, getSdl, Manifest, ManifestVersion } from "./helpers"; export const ENDPOINT_NAME_VALIDATION_REGEX = /^[a-z]+[-_\da-z]+$/; export const TRIAL_ATTRIBUTE = "console/trials"; @@ -81,7 +82,7 @@ function mapProviderAttributes(attributes: Attribute[]) { } export async function NewDeploymentData( - apiEndpoint: string, + chainApiHttpClient: HttpClient, yamlStr: string, dseq: string | null, fromAddress: string, @@ -97,13 +98,19 @@ export async function NewDeploymentData( const version = await sdl.manifestVersion(); const _deposit = (Array.isArray(deposit) && deposit.find(d => d.denom === denom)) || { denom, amount: deposit.toString() }; + let finalDseq: string = dseq || ""; + if (!finalDseq) { + const response = await chainApiHttpClient.get("/blocks/latest"); + finalDseq = response.data.block.header.height; + } + return { sdl: sdl.data, manifest: mani, groups: groups, deploymentId: { owner: fromAddress, - dseq: dseq || (await getCurrentHeight(apiEndpoint)).toString() + dseq: finalDseq }, orderId: [], leaseId: [],