diff --git a/apps/browser-extension-wallet/src/config.ts b/apps/browser-extension-wallet/src/config.ts index a9cd804ce6..0cefc4b586 100644 --- a/apps/browser-extension-wallet/src/config.ts +++ b/apps/browser-extension-wallet/src/config.ts @@ -32,6 +32,7 @@ export type Config = { CEXPLORER_URL_PATHS: CExplorerUrlPaths; SAVED_PRICE_DURATION: number; DEFAULT_SUBMIT_API: string; + DEFAULT_BLOCKFROST_API: string; GOV_TOOLS_URLS: Record; TEMPO_VOTE_URLS: Record; SESSION_TIMEOUT: Milliseconds; @@ -142,6 +143,7 @@ export const config = (): Config => { ? Number(process.env.SAVED_PRICE_DURATION_IN_MINUTES) : 720, DEFAULT_SUBMIT_API: 'http://localhost:8090/api/submit/tx', + DEFAULT_BLOCKFROST_API: 'http://127.0.0.1:60536', GOV_TOOLS_URLS: { Mainnet: `${process.env.GOV_TOOLS_URL_MAINNET}`, Preprod: `${process.env.GOV_TOOLS_URL_PREPROD}`, diff --git a/apps/browser-extension-wallet/src/hooks/useCustomSubmitApi.ts b/apps/browser-extension-wallet/src/hooks/useCustomSubmitApi.ts index 0046acc9b0..80ae9808b5 100644 --- a/apps/browser-extension-wallet/src/hooks/useCustomSubmitApi.ts +++ b/apps/browser-extension-wallet/src/hooks/useCustomSubmitApi.ts @@ -1,10 +1,10 @@ import { useLocalStorage } from '@hooks/useLocalStorage'; import { EnvironmentTypes } from '@stores'; -import { CustomSubmitApiConfig } from '@types'; +import { CustomBackendApiConfig } from '@types'; interface useCustomSubmitApiReturn { - getCustomSubmitApiForNetwork: (network: EnvironmentTypes) => CustomSubmitApiConfig; - updateCustomSubmitApi: (network: EnvironmentTypes, data: CustomSubmitApiConfig) => void; + getCustomSubmitApiForNetwork: (network: EnvironmentTypes) => CustomBackendApiConfig; + updateCustomSubmitApi: (network: EnvironmentTypes, data: CustomBackendApiConfig) => void; } export const useCustomSubmitApi = (): useCustomSubmitApiReturn => { @@ -18,9 +18,32 @@ export const useCustomSubmitApi = (): useCustomSubmitApiReturn => { return { status, url }; }; - const updateCustomSubmitApi = (network: EnvironmentTypes, data: CustomSubmitApiConfig) => { + const updateCustomSubmitApi = (network: EnvironmentTypes, data: CustomBackendApiConfig) => { updateCustomSubmitApiEnabled({ ...isCustomSubmitApiEnabled, [network]: data }); }; return { getCustomSubmitApiForNetwork, updateCustomSubmitApi }; }; + +interface useCustomBackendConfigurationApiReturn { + getCustomBackendApiForNetwork: (network: EnvironmentTypes) => CustomBackendApiConfig; + updateCustomBackendApi: (network: EnvironmentTypes, data: CustomBackendApiConfig) => void; +} + +export const useCustomBackendApi = (): useCustomBackendConfigurationApiReturn => { + const [isCustomBackendApiEnabled, { updateLocalStorage: updateCustomBackendApiEnabled }] = + useLocalStorage('isCustomBackendApiEnabled'); + + const getCustomBackendApiForNetwork = (network: EnvironmentTypes) => { + const networkConfig = isCustomBackendApiEnabled?.[network]; + const status = networkConfig?.status || false; + const url = networkConfig?.url || ''; + return { status, url }; + }; + + const updateCustomBackendApi = (network: EnvironmentTypes, data: CustomBackendApiConfig) => { + updateCustomBackendApiEnabled({ ...isCustomBackendApiEnabled, [network]: data }); + }; + + return { getCustomBackendApiForNetwork, updateCustomBackendApi }; +}; diff --git a/apps/browser-extension-wallet/src/lib/scripts/background/config.ts b/apps/browser-extension-wallet/src/lib/scripts/background/config.ts index d2634d12b5..8009d380fe 100644 --- a/apps/browser-extension-wallet/src/lib/scripts/background/config.ts +++ b/apps/browser-extension-wallet/src/lib/scripts/background/config.ts @@ -47,7 +47,7 @@ export const getProviders = async (chainName: Wallet.ChainName): Promise !!(featureFlags?.[magic]?.[experimentName] ?? false); @@ -57,6 +57,7 @@ export const getProviders = async (chainName: Wallet.ChainName): Promise; - isCustomSubmitApiEnabled?: Record; + isCustomSubmitApiEnabled?: Record; + isCustomBackendApiEnabled?: Record; isReceiveInAdvancedMode?: boolean; hasUserAcknowledgedPrivacyPolicyUpdate?: boolean; } diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/settings/components/CustomSubmitApiDrawer.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/settings/components/CustomSubmitApiDrawer.tsx index 65ee7befb4..e1a294858c 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/settings/components/CustomSubmitApiDrawer.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/settings/components/CustomSubmitApiDrawer.tsx @@ -1,11 +1,12 @@ +/* eslint-disable complexity */ import React, { ReactElement, useEffect, useState } from 'react'; import { Drawer, DrawerHeader, DrawerNavigation, logger, PostHogAction, toast } from '@lace/common'; import { Typography } from 'antd'; import styles from './SettingsLayout.module.scss'; import { useTranslation } from 'react-i18next'; import { Button, TextBox } from '@input-output-hk/lace-ui-toolkit'; -import { getBackgroundStorage } from '@lib/scripts/background/storage'; -import { useCustomSubmitApi, useWalletManager } from '@hooks'; +import { getBackgroundStorage, setBackgroundStorage } from '@lib/scripts/background/storage'; +import { useCustomBackendApi, useCustomSubmitApi, useWalletManager } from '@hooks'; import { useWalletStore } from '@stores'; import { isValidURL } from '@utils/is-valid-url'; import { useAnalyticsContext } from '@providers'; @@ -24,7 +25,7 @@ interface CustomSubmitApiDrawerProps { } const LEARN_SUBMIT_API_URL = 'https://github.com/IntersectMBO/cardano-node/tree/master/cardano-submit-api'; -const { DEFAULT_SUBMIT_API } = config(); +const { DEFAULT_SUBMIT_API, DEFAULT_BLOCKFROST_API } = config(); export const CustomSubmitApiDrawer = ({ visible, @@ -32,24 +33,45 @@ export const CustomSubmitApiDrawer = ({ popupView = false }: CustomSubmitApiDrawerProps): ReactElement => { const { t } = useTranslation(); - const { enableCustomNode } = useWalletManager(); + const { enableCustomNode, reloadWallet } = useWalletManager(); const { environmentName } = useWalletStore(); const analytics = useAnalyticsContext(); const [customSubmitTxUrl, setCustomSubmitTxUrl] = useState(DEFAULT_SUBMIT_API); + const [customBlockfrostUrl, setCustomBlockfrostUrl] = useState(DEFAULT_BLOCKFROST_API); const [isValidationError, setIsValidationError] = useState(false); const { getCustomSubmitApiForNetwork } = useCustomSubmitApi(); - + const { getCustomBackendApiForNetwork, updateCustomBackendApi } = useCustomBackendApi(); const isCustomApiEnabledForCurrentNetwork = getCustomSubmitApiForNetwork(environmentName).status; + const isCustomBackendApiEnabledForCurrentNetwork = getCustomBackendApiForNetwork(environmentName).status || false; useEffect(() => { getBackgroundStorage() .then((storage) => { setCustomSubmitTxUrl(storage.customSubmitTxUrl || DEFAULT_SUBMIT_API); + setCustomBlockfrostUrl(storage.customBlockfrostUrl || DEFAULT_BLOCKFROST_API); }) .catch(logger.error); }, []); + const handleCustomBlockfrostApiEndpoint = async (enable: boolean) => { + // TODO: abstract + if (enable && !isValidURL(customBlockfrostUrl)) { + setIsValidationError(true); + } + + setIsValidationError(false); + const value = enable ? customBlockfrostUrl : undefined; + + if (value) + updateCustomBackendApi(environmentName, { + status: !!value, + url: value + }); + await setBackgroundStorage({ customBlockfrostUrl: value }); + await reloadWallet(); + }; + const handleCustomTxSubmitEndpoint = async (enable: boolean) => { if (enable && !isValidURL(customSubmitTxUrl)) { setIsValidationError(true); @@ -102,28 +124,51 @@ export const CustomSubmitApiDrawer = ({ {t('browserView.settings.wallet.customSubmitApi.descriptionLink')} - - {t('browserView.settings.wallet.customSubmitApi.defaultAddress', { url: DEFAULT_SUBMIT_API })} - -
- setCustomSubmitTxUrl(event.target.value)} - disabled={isCustomApiEnabledForCurrentNetwork} - data-testid="custom-submit-api-url" - /> - : } - onClick={() => handleCustomTxSubmitEndpoint(!isCustomApiEnabledForCurrentNetwork)} - data-testid={`custom-submit-button-${isCustomApiEnabledForCurrentNetwork ? 'disable' : 'enable'}`} - /> +
+ Custom Submit API +
+ setCustomSubmitTxUrl(event.target.value)} + disabled={isCustomApiEnabledForCurrentNetwork} + data-testid="custom-submit-api-url" + /> + : } + onClick={() => handleCustomTxSubmitEndpoint(!isCustomApiEnabledForCurrentNetwork)} + data-testid={`custom-submit-button-${isCustomApiEnabledForCurrentNetwork ? 'disable' : 'enable'}`} + /> +
+
+
+ Blockfrost API +
+ setCustomBlockfrostUrl(event.target.value)} + disabled={isCustomBackendApiEnabledForCurrentNetwork} + data-testid="custom-blockfrost-api-url" + /> + : } + onClick={() => handleCustomBlockfrostApiEndpoint(!isCustomBackendApiEnabledForCurrentNetwork)} + data-testid={`custom-submit-button-${isCustomBackendApiEnabledForCurrentNetwork ? 'disable' : 'enable'}`} + /> +
{isValidationError && ( diff --git a/packages/cardano/src/wallet/lib/blockfrost-client-with-fallback.ts b/packages/cardano/src/wallet/lib/blockfrost-client-with-fallback.ts new file mode 100644 index 0000000000..828b1a0ddf --- /dev/null +++ b/packages/cardano/src/wallet/lib/blockfrost-client-with-fallback.ts @@ -0,0 +1,54 @@ +/* eslint-disable unicorn/no-null */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { BlockfrostClientConfig, RateLimiter, BlockfrostClient } from '@cardano-sdk/cardano-services-client'; + +type FallbackClientConfig = { + customBlockfrostUrl?: string; + rateLimiter: RateLimiter; + config: BlockfrostClientConfig; +}; + +export const createFallbackBlockfrostClient = ({ + customBlockfrostUrl, + config, + rateLimiter +}: FallbackClientConfig): BlockfrostClient => { + const primary = new BlockfrostClient( + { + ...config, + baseUrl: customBlockfrostUrl ?? config.baseUrl + }, + { rateLimiter } + ); + + const fallback = customBlockfrostUrl + ? new BlockfrostClient( + { + ...config, + baseUrl: config.baseUrl + }, + { rateLimiter } + ) + : null; + + const originalRequest = primary.request.bind(primary); + + primary.request = async (endpoint: string, requestInit?: RequestInit): Promise => { + try { + return await originalRequest(endpoint, requestInit); + } catch (error: any) { + // eslint-disable-next-line no-magic-numbers + if (!fallback || error?.response?.status === 404) { + throw error; + } + + console.warn( + `[BlockfrostFallback] Primary (${customBlockfrostUrl}) failed: ${error?.message}. Retrying with base (${config.baseUrl})` + ); + + return fallback.request(endpoint, requestInit); + } + }; + + return primary; +}; diff --git a/packages/cardano/src/wallet/lib/providers.ts b/packages/cardano/src/wallet/lib/providers.ts index f9bcb07c6b..d30aa4fa17 100644 --- a/packages/cardano/src/wallet/lib/providers.ts +++ b/packages/cardano/src/wallet/lib/providers.ts @@ -16,6 +16,7 @@ import { TxSubmitProvider, UtxoProvider } from '@cardano-sdk/core'; +import { createFallbackBlockfrostClient } from './blockfrost-client-with-fallback'; import { CardanoWsClient, @@ -83,6 +84,7 @@ interface ProvidersConfig { baseCardanoServicesUrl: string; baseKoraLabsServicesUrl: string; customSubmitTxUrl?: string; + customBlockfrostUrl?: string; blockfrostConfig: BlockfrostClientConfig & { rateLimiter: RateLimiter }; }; logger: Logger; @@ -135,16 +137,25 @@ const cacheAssignment: Record = { export const createProviders = ({ axiosAdapter, - env: { baseCardanoServicesUrl: baseUrl, baseKoraLabsServicesUrl, customSubmitTxUrl, blockfrostConfig }, + env: { + baseCardanoServicesUrl: baseUrl, + baseKoraLabsServicesUrl, + customSubmitTxUrl, + blockfrostConfig, + customBlockfrostUrl + }, logger, experiments: { useWebSocket }, extensionLocalStorage }: ProvidersConfig): WalletProvidersDependencies => { const httpProviderConfig: CreateHttpProviderConfig = { baseUrl, logger, adapter: axiosAdapter }; - const blockfrostClient = new BlockfrostClient(blockfrostConfig, { + const blockfrostClient = createFallbackBlockfrostClient({ + customBlockfrostUrl, + config: blockfrostConfig, rateLimiter: blockfrostConfig.rateLimiter }); + const assetProvider = new BlockfrostAssetProvider(blockfrostClient, logger); const networkInfoProvider = new BlockfrostNetworkInfoProvider(blockfrostClient, logger); const chainHistoryProvider = new BlockfrostChainHistoryProvider({ diff --git a/packages/translation/src/lib/translations/browser-extension-wallet/en.json b/packages/translation/src/lib/translations/browser-extension-wallet/en.json index 6c324b584f..de0845274d 100644 --- a/packages/translation/src/lib/translations/browser-extension-wallet/en.json +++ b/packages/translation/src/lib/translations/browser-extension-wallet/en.json @@ -345,8 +345,8 @@ "browserView.settings.wallet.customSubmitApi.enabled": "Enabled", "browserView.settings.wallet.customSubmitApi.inputLabel": "Insert the URL here", "browserView.settings.wallet.customSubmitApi.settingsLinkDescription": "Send transactions through a local Cardano node and cardano-submit-api", - "browserView.settings.wallet.customSubmitApi.settingsLinkTitle": "Custom Submit API", - "browserView.settings.wallet.customSubmitApi.title": "Custom submit API", + "browserView.settings.wallet.customSubmitApi.settingsLinkTitle": "Backend Configuration", + "browserView.settings.wallet.customSubmitApi.title": "Backend Configuration", "browserView.settings.wallet.customSubmitApi.usingCustomTxSubmitEndpoint": "Your custom submit API is enabled, so Lace will use it to submit transactions.", "browserView.settings.wallet.customSubmitApi.usingStandardTxSubmitEndpoint": "Your custom submit API is disabled, so Lace will use its own infrastructure to send transactions.", "browserView.settings.wallet.customSubmitApi.validationError": "Invalid URL",