Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/browser-extension-wallet/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<EnvironmentTypes, string>;
TEMPO_VOTE_URLS: Record<EnvironmentTypes, string>;
SESSION_TIMEOUT: Milliseconds;
Expand Down Expand Up @@ -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}`,
Expand Down
31 changes: 27 additions & 4 deletions apps/browser-extension-wallet/src/hooks/useCustomSubmitApi.ts
Original file line number Diff line number Diff line change
@@ -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 => {
Expand All @@ -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 };
};
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const getProviders = async (chainName: Wallet.ChainName): Promise<Wallet.
const baseCardanoServicesUrl = getBaseUrlForChain(chainName);
const baseKoraLabsServicesUrl = getBaseKoraLabsUrlForChain(chainName);
const magic = getMagicForChain(chainName);
const { customSubmitTxUrl, featureFlags } = await getBackgroundStorage();
const { customSubmitTxUrl, featureFlags, customBlockfrostUrl } = await getBackgroundStorage();

const isExperimentEnabled = (experimentName: ExperimentName) => !!(featureFlags?.[magic]?.[experimentName] ?? false);

Expand All @@ -57,6 +57,7 @@ export const getProviders = async (chainName: Wallet.ChainName): Promise<Wallet.
baseCardanoServicesUrl,
baseKoraLabsServicesUrl,
customSubmitTxUrl,
customBlockfrostUrl,
blockfrostConfig: {
...BLOCKFROST_CONFIGS[chainName],
rateLimiter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface BackgroundStorage {
initialPosthogFeatureFlags?: FeatureFlags;
initialPosthogFeatureFlagPayloads?: RawFeatureFlagPayloads;
customSubmitTxUrl?: string;
customBlockfrostUrl?: string;
namiMigration?: {
completed: boolean;
mode: 'lace' | 'nami';
Expand Down
5 changes: 3 additions & 2 deletions apps/browser-extension-wallet/src/types/local-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface UnconfirmedTransaction {
date: string;
}

export interface CustomSubmitApiConfig {
export interface CustomBackendApiConfig {
status: boolean;
url: string;
}
Expand All @@ -64,7 +64,8 @@ export interface ILocalStorage {
showPinExtension?: boolean;
showMultiAddressModal?: boolean;
userAvatar?: Record<`${EnvironmentTypes}${string}`, string>;
isCustomSubmitApiEnabled?: Record<EnvironmentTypes, CustomSubmitApiConfig>;
isCustomSubmitApiEnabled?: Record<EnvironmentTypes, CustomBackendApiConfig>;
isCustomBackendApiEnabled?: Record<EnvironmentTypes, CustomBackendApiConfig>;
isReceiveInAdvancedMode?: boolean;
hasUserAcknowledgedPrivacyPolicyUpdate?: boolean;
}
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -24,32 +25,53 @@ 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,
onClose,
popupView = false
}: CustomSubmitApiDrawerProps): ReactElement => {
const { t } = useTranslation();
const { enableCustomNode } = useWalletManager();
const { enableCustomNode, reloadWallet } = useWalletManager();
const { environmentName } = useWalletStore();
const analytics = useAnalyticsContext();

const [customSubmitTxUrl, setCustomSubmitTxUrl] = useState<string>(DEFAULT_SUBMIT_API);
const [customBlockfrostUrl, setCustomBlockfrostUrl] = useState<string>(DEFAULT_BLOCKFROST_API);
const [isValidationError, setIsValidationError] = useState<boolean>(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);
Expand Down Expand Up @@ -102,28 +124,51 @@ export const CustomSubmitApiDrawer = ({
{t('browserView.settings.wallet.customSubmitApi.descriptionLink')}
</a>
</Text>
<Text className={styles.drawerText} data-testid="custom-submit-api-default-address">
{t('browserView.settings.wallet.customSubmitApi.defaultAddress', { url: DEFAULT_SUBMIT_API })}
</Text>
<div className={styles.customApiContainer}>
<TextBox
label={t('browserView.settings.wallet.customSubmitApi.inputLabel')}
w="$fill"
value={customSubmitTxUrl}
onChange={(event) => setCustomSubmitTxUrl(event.target.value)}
disabled={isCustomApiEnabledForCurrentNetwork}
data-testid="custom-submit-api-url"
/>
<Button.Primary
label={
isCustomApiEnabledForCurrentNetwork
? t('browserView.settings.wallet.customSubmitApi.disable')
: t('browserView.settings.wallet.customSubmitApi.enable')
}
icon={isCustomApiEnabledForCurrentNetwork ? <PauseIcon /> : <PlayIcon />}
onClick={() => handleCustomTxSubmitEndpoint(!isCustomApiEnabledForCurrentNetwork)}
data-testid={`custom-submit-button-${isCustomApiEnabledForCurrentNetwork ? 'disable' : 'enable'}`}
/>
<div className={styles.drawerDescription}>
<Text>Custom Submit API</Text>
<div className={styles.customApiContainer}>
<TextBox
label={t('browserView.settings.wallet.customSubmitApi.inputLabel')}
w="$fill"
value={customSubmitTxUrl}
onChange={(event) => setCustomSubmitTxUrl(event.target.value)}
disabled={isCustomApiEnabledForCurrentNetwork}
data-testid="custom-submit-api-url"
/>
<Button.Primary
label={
isCustomApiEnabledForCurrentNetwork
? t('browserView.settings.wallet.customSubmitApi.disable')
: t('browserView.settings.wallet.customSubmitApi.enable')
}
icon={isCustomApiEnabledForCurrentNetwork ? <PauseIcon /> : <PlayIcon />}
onClick={() => handleCustomTxSubmitEndpoint(!isCustomApiEnabledForCurrentNetwork)}
data-testid={`custom-submit-button-${isCustomApiEnabledForCurrentNetwork ? 'disable' : 'enable'}`}
/>
</div>
</div>
<div className={styles.drawerDescription}>
<Text>Blockfrost API</Text>
<div className={styles.customApiContainer}>
<TextBox
label={t('browserView.settings.wallet.customSubmitApi.inputLabel')}
w="$fill"
value={customBlockfrostUrl}
onChange={(event) => setCustomBlockfrostUrl(event.target.value)}
disabled={isCustomBackendApiEnabledForCurrentNetwork}
data-testid="custom-blockfrost-api-url"
/>
<Button.Primary
label={
isCustomBackendApiEnabledForCurrentNetwork
? t('browserView.settings.wallet.customSubmitApi.disable')
: t('browserView.settings.wallet.customSubmitApi.enable')
}
icon={isCustomBackendApiEnabledForCurrentNetwork ? <PauseIcon /> : <PlayIcon />}
onClick={() => handleCustomBlockfrostApiEndpoint(!isCustomBackendApiEnabledForCurrentNetwork)}
data-testid={`custom-submit-button-${isCustomBackendApiEnabledForCurrentNetwork ? 'disable' : 'enable'}`}
/>
</div>
</div>
{isValidationError && (
<Text className={styles.validationError} data-testid="custom-submit-api-validation-error">
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <T>(endpoint: string, requestInit?: RequestInit): Promise<T> => {
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<T>(endpoint, requestInit);
}
};

return primary;
};
15 changes: 13 additions & 2 deletions packages/cardano/src/wallet/lib/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
TxSubmitProvider,
UtxoProvider
} from '@cardano-sdk/core';
import { createFallbackBlockfrostClient } from './blockfrost-client-with-fallback';

import {
CardanoWsClient,
Expand Down Expand Up @@ -83,6 +84,7 @@ interface ProvidersConfig {
baseCardanoServicesUrl: string;
baseKoraLabsServicesUrl: string;
customSubmitTxUrl?: string;
customBlockfrostUrl?: string;
blockfrostConfig: BlockfrostClientConfig & { rateLimiter: RateLimiter };
};
logger: Logger;
Expand Down Expand Up @@ -135,16 +137,25 @@ const cacheAssignment: Record<CacheName, { count: number; size: number }> = {

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<Provider> = { 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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading