Skip to content

Commit 177d837

Browse files
feat: lmp bundle integration [LW-13474] (#1992)
* chore: add option to set webpack publicPath via environment variable LMP-v1 bundle build has a separate webpack build, which sets it's own public path that is separate from public path of individual applications. It loads the scripts a little bit differently (e.g. directly calls 'importScripts' in sw script) which prevents webpack from inferring the path correctly based on current script context. Therefore webpack build needs to be configured to specify it explicitly. * feat: lmp bundle service and minimal UI integration integrate with LMP bundle: - when 'midnight-wallets' FF is enabled, show 'Add Midnight Wallet' - list LMP wallets in profile dropdown - expose wallets in SW via global variable - get LMP wallets via remote api * test: add option to run e2e tests for LMP bundle --------- Co-authored-by: Lukasz Jagiela <[email protected]>
1 parent 17bc875 commit 177d837

File tree

20 files changed

+268
-8
lines changed

20 files changed

+268
-8
lines changed

apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/DropdownMenuOverlay.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ import { getBackgroundStorage, setBackgroundStorage } from '@lib/scripts/backgro
3333
import { useBackgroundServiceAPIContext } from '@providers';
3434
import { WarningModal } from '@src/views/browser-view/components';
3535
import { useTranslation } from 'react-i18next';
36-
import { useCurrentWallet, useWalletManager } from '@hooks';
36+
import { useCurrentWallet, useWalletManager, useLMP } from '@hooks';
3737
import { useCurrentBlockchain } from '@src/multichain';
38+
import { AddNewMidnightWalletLink } from './components/AddNewMidnightWalletLink';
3839

3940
interface Props extends MenuProps {
4041
isPopup?: boolean;
@@ -44,7 +45,7 @@ interface Props extends MenuProps {
4445
open: boolean;
4546
}
4647

47-
// eslint-disable-next-line complexity
48+
// eslint-disable-next-line complexity, max-statements
4849
export const DropdownMenuOverlay: VFC<Props> = ({
4950
isPopup,
5051
lockWalletButton = <LockWallet />,
@@ -59,9 +60,11 @@ export const DropdownMenuOverlay: VFC<Props> = ({
5960
const { walletRepository } = useWalletManager();
6061
const currentWallet = useCurrentWallet();
6162
const wallets = useObservable(walletRepository.wallets$);
63+
const { midnightWallets } = useLMP();
6264

6365
const sharedWalletsEnabled = posthog?.isFeatureFlagEnabled('shared-wallets');
6466
const bitcoinWalletsEnabled = posthog?.isFeatureFlagEnabled('bitcoin-wallets');
67+
const midnightWalletsEnabled = posthog?.isFeatureFlagEnabled('midnight-wallets');
6568
const [currentSection, setCurrentSection] = useState<Sections>(Sections.Main);
6669
const { environmentName, setManageAccountsWallet, walletType, isSharedWallet } = useWalletStore();
6770
const { blockchain } = useCurrentBlockchain();
@@ -133,6 +136,7 @@ export const DropdownMenuOverlay: VFC<Props> = ({
133136
);
134137
const showAddSharedWalletLink = sharedWalletsEnabled && !isSharedWallet && !hasLinkedSharedWallet;
135138
const showAddBitcoinWalletLink = bitcoinWalletsEnabled;
139+
const showAddMidnightWalletLink = midnightWalletsEnabled && midnightWallets && midnightWallets.length === 0;
136140

137141
const handleNamiModeChange = async (activated: boolean) => {
138142
const mode = activated ? 'nami' : 'lace';
@@ -188,6 +192,7 @@ export const DropdownMenuOverlay: VFC<Props> = ({
188192
)}
189193
{!isBitcoinWallet && showAddSharedWalletLink && <AddSharedWalletLink isPopup={isPopup} />}
190194
{showAddBitcoinWalletLink && <AddNewBitcoinWalletLink isPopup={isPopup} />}
195+
{showAddMidnightWalletLink && <AddNewMidnightWalletLink />}
191196
{!isBitcoinWallet && <AddressBookLink />}
192197
<SettingsLink />
193198
<Separator />
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React, { useCallback } from 'react';
2+
import { Menu } from 'antd';
3+
import styles from '../DropdownMenuOverlay.module.scss';
4+
import { PostHogAction } from '@lace/common';
5+
import { useTranslation } from 'react-i18next';
6+
import { useLMP } from '@hooks';
7+
8+
interface Props {
9+
sendAnalyticsEvent?: (event: PostHogAction) => void;
10+
}
11+
12+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
13+
export const AddNewMidnightWalletLink = (_: Props): React.ReactElement => {
14+
const { t } = useTranslation();
15+
const { switchToLMP } = useLMP();
16+
const onClick = useCallback(() => {
17+
switchToLMP();
18+
// TODO: send analytics event
19+
}, [switchToLMP]);
20+
21+
return (
22+
<Menu.Item data-testid="header-menu-add-midnight-wallet" className={styles.menuItem} onClick={onClick}>
23+
{t('browserView.sideMenu.links.addMidnightWallet')}
24+
</Menu.Item>
25+
);
26+
};

apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { CopyToClipboard } from 'react-copy-to-clipboard';
99
import { addEllipsis, toast, useObservable } from '@lace/common';
1010
import { WalletStatusContainer } from '@components/WalletStatus';
1111
import { UserAvatar } from './UserAvatar';
12-
import { useGetHandles, useWalletAvatar, useWalletManager } from '@hooks';
12+
import { useGetHandles, useWalletAvatar, useWalletManager, useLMP } from '@hooks';
1313
import { useAnalyticsContext } from '@providers';
1414
import { PostHogAction, WALLET_TYPE_KEY } from '@providers/AnalyticsProvider/analyticsTracker';
1515
import { ProfileDropdown } from '@input-output-hk/lace-ui-toolkit';
@@ -19,6 +19,7 @@ import { Separator } from './Separator';
1919
import { getUiWalletType } from '@src/utils/get-ui-wallet-type';
2020
import { isScriptWallet } from '@lace/core';
2121
import { useCurrentBlockchain } from '@src/multichain';
22+
import { LmpBundleWallet } from '@src/utils/lmp';
2223

2324
const ADRESS_FIRST_PART_LENGTH = 10;
2425
const ADRESS_LAST_PART_LENGTH = 5;
@@ -59,6 +60,7 @@ export const UserInfo = ({
5960
useWalletManager();
6061
const analytics = useAnalyticsContext();
6162
const wallets = useObservable(walletRepository.wallets$, NO_WALLETS);
63+
const { midnightWallets = [], switchToLMP } = useLMP();
6264
const walletAddress = walletInfo ? walletInfo.addresses[0].address.toString() : '';
6365
const shortenedWalletAddress = addEllipsis(walletAddress, ADRESS_FIRST_PART_LENGTH, ADRESS_LAST_PART_LENGTH);
6466
const [fullWalletName, setFullWalletName] = useState<string>('');
@@ -193,6 +195,27 @@ export const UserInfo = ({
193195
]
194196
);
195197

198+
const renderLmpWallet = useCallback(
199+
({ walletIcon, walletId, walletName }: LmpBundleWallet, isLast: boolean) => (
200+
<div key={walletId}>
201+
<ProfileDropdown.WalletOption
202+
style={{ textAlign: 'left' }}
203+
key={walletId}
204+
title={shortenWalletName(walletName, WALLET_OPTION_NAME_MAX_LENGTH)}
205+
id={`wallet-option-${walletId}`}
206+
onClick={switchToLMP}
207+
type={'hot'}
208+
profile={{
209+
fallbackText: walletName,
210+
imageSrc: walletIcon
211+
}}
212+
/>
213+
{isLast ? undefined : <Separator />}
214+
</div>
215+
),
216+
[switchToLMP]
217+
);
218+
196219
const renderScriptWallet = useCallback(
197220
(wallet: ScriptWallet<Wallet.WalletMetadata>) => renderWalletOption({ wallet }),
198221
[renderWalletOption]
@@ -228,7 +251,14 @@ export const UserInfo = ({
228251
})}
229252
>
230253
{process.env.USE_MULTI_WALLET === 'true' ? (
231-
<div>{wallets.map((wallet, i) => renderWallet(wallet, i === wallets.length - 1))}</div>
254+
<>
255+
<div>
256+
{wallets.map((wallet, i) =>
257+
renderWallet(wallet, i === wallets.length - 1 && midnightWallets.length === 0)
258+
)}
259+
{midnightWallets.map((wallet, i) => renderLmpWallet(wallet, i === midnightWallets.length - 1))}
260+
</div>
261+
</>
232262
) : (
233263
<CopyToClipboard text={handleName || walletAddress}>
234264
<AntdTooltip

apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const mockLedgerGetXpub = jest.fn();
2121
const mockTrezorGetXpub = jest.fn();
2222
const mockInitializeTrezorTransport = jest.fn();
2323
const mockLedgerCreateWithDevice = jest.fn();
24+
const mockUseLMP = jest.fn();
2425
const mockUseAppSettingsContext = jest.fn().mockReturnValue([{}, jest.fn()]);
2526
const mockUseSecrets = {
2627
password: {} as Partial<Password>,
@@ -116,6 +117,11 @@ jest.mock('@providers/AnalyticsProvider/getUserIdService', () => {
116117
};
117118
});
118119

120+
jest.mock('@hooks/useLMP', () => ({
121+
__esModule: true,
122+
useLMP: mockUseLMP
123+
}));
124+
119125
const getWrapper =
120126
({ backgroundService }: { backgroundService?: BackgroundServiceAPIProviderProps['value'] }) =>
121127
({ children }: { children: React.ReactNode }) =>
@@ -153,6 +159,7 @@ const walletDisplayInfoMockData = {
153159
describe('Testing useWalletManager hook', () => {
154160
beforeEach(() => {
155161
jest.resetAllMocks();
162+
mockUseLMP.mockReturnValue({ midnightWallets: [], switchToLMP: jest.fn() });
156163
jest.spyOn(AppSettings, 'useAppSettingsContext').mockReturnValue([{}, jest.fn()]);
157164
jest.spyOn(stores, 'useWalletStore').mockImplementation(() => ({}));
158165
});

apps/browser-extension-wallet/src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ export * from './useActionExecution';
2626
export * from './useCustomSubmitApi';
2727
export * from './useCurrentWallet';
2828
export * from './useHasEnoughCollateral';
29+
export * from './useLMP';
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { consumeRemoteApi } from '@cardano-sdk/web-extension';
2+
import { logger, useObservable } from '@lace/common';
3+
import { APP_MODE, bundleAppApiProps, lmpApiBaseChannel, LmpBundleWallet, lmpModeStorage } from '@src/utils/lmp';
4+
import { runtime } from 'webextension-polyfill';
5+
6+
const lmpApi = consumeRemoteApi(
7+
{
8+
baseChannel: lmpApiBaseChannel,
9+
properties: bundleAppApiProps
10+
},
11+
{ logger, runtime }
12+
);
13+
14+
const switchToLMP = (): void =>
15+
void (async () => {
16+
await lmpModeStorage.set(APP_MODE.LMP);
17+
if (window.location.pathname.startsWith('/popup.html')) {
18+
chrome.tabs.create({ url: '/tab.html' });
19+
} else {
20+
window.location.href = '/tab.html';
21+
}
22+
})();
23+
24+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
25+
export const useLMP = () => {
26+
const midnightWallets = useObservable<LmpBundleWallet[] | undefined>(lmpApi.wallets$);
27+
return { midnightWallets, switchToLMP };
28+
};

apps/browser-extension-wallet/src/hooks/useWalletManager.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { deepEquals, HexBlob } from '@cardano-sdk/util';
4444
import { BackgroundService } from '@lib/scripts/types';
4545
import { getChainName } from '@src/utils/get-chain-name';
4646
import { useCustomSubmitApi } from '@hooks/useCustomSubmitApi';
47+
import { useLMP } from '@hooks/useLMP';
4748
import { setBackgroundStorage } from '@lib/scripts/background/storage';
4849
import * as KeyManagement from '@cardano-sdk/key-management';
4950
import { Buffer } from 'buffer';
@@ -396,6 +397,7 @@ export const useWalletManager = (): UseWalletManager => {
396397
const storedChain = getValueFromLocalStorage('appSettings');
397398
return (storedChain?.chainName && chainIdFromName(storedChain.chainName)) || DEFAULT_CHAIN_ID;
398399
}, [currentChain]);
400+
const { midnightWallets, switchToLMP } = useLMP();
399401

400402
const getWalletInfo = useCallback(async (): Promise<Wallet.WalletDisplayInfo | undefined> => {
401403
const { activeBlockchain } = await backgroundService.getBackgroundStorage();
@@ -1121,6 +1123,10 @@ export const useWalletManager = (): UseWalletManager => {
11211123
for (const network of [BtcWallet.Network.Mainnet, BtcWallet.Network.Testnet]) {
11221124
await bitcoinWalletManager.destroyData(walletToDelete.walletId, network);
11231125
}
1126+
1127+
if (midnightWallets && midnightWallets?.length > 0) {
1128+
switchToLMP();
1129+
}
11241130
},
11251131
[
11261132
resetWalletLock,
@@ -1131,7 +1137,9 @@ export const useWalletManager = (): UseWalletManager => {
11311137
clearAddressBook,
11321138
clearNftsFolders,
11331139
getCurrentChainId,
1134-
activateWallet
1140+
activateWallet,
1141+
midnightWallets,
1142+
switchToLMP
11351143
]
11361144
);
11371145

apps/browser-extension-wallet/src/lib/scripts/background/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import './onError';
33
import './onUpdate';
44
import './services';
55
import './services/expose';
6+
import './services/lmpService';
67
import './expose-background-service';
78
import './keep-alive-sw';
89
import './onUninstall';
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/* eslint-disable @typescript-eslint/no-var-requires */
2+
import { BundleAppApi, LmpBundleWallet, v1ApiGlobalProperty } from '@src/utils/lmp';
3+
import { firstValueFrom, map } from 'rxjs';
4+
import { bitcoinWalletManager, walletManager, walletRepository } from '../wallet';
5+
import { AnyBip32Wallet, AnyWallet, InMemoryWallet, WalletType } from '@cardano-sdk/web-extension';
6+
import { logger } from '@lace/common';
7+
import { Wallet } from '@lace/cardano';
8+
import { setBackgroundStorage } from '../storage';
9+
import { Bitcoin } from '@lace/bitcoin';
10+
11+
const cardanoLogo = require('../../../../assets/icons/browser-view/cardano-logo.svg').default;
12+
const bitcoinLogo = require('../../../../assets/icons/browser-view/bitcoin-logo.svg').default;
13+
14+
const isBitcoinWallet = (
15+
wallet: AnyWallet<Wallet.WalletMetadata, Wallet.AccountMetadata>
16+
): wallet is InMemoryWallet<Wallet.WalletMetadata, Wallet.AccountMetadata> =>
17+
wallet.type === WalletType.InMemory && wallet.blockchainName === 'Bitcoin';
18+
const isBip32Wallet = (
19+
wallet: AnyWallet<Wallet.WalletMetadata, Wallet.AccountMetadata>
20+
): wallet is AnyBip32Wallet<Wallet.WalletMetadata, Wallet.AccountMetadata> => wallet.type !== WalletType.Script;
21+
const isInMemoryWallet = (
22+
wallet: AnyWallet<Wallet.WalletMetadata, Wallet.AccountMetadata>
23+
): wallet is InMemoryWallet<Wallet.WalletMetadata, Wallet.AccountMetadata> => wallet.type === WalletType.InMemory;
24+
25+
const api: BundleAppApi = {
26+
wallets$: walletRepository.wallets$.pipe(
27+
map((wallets) =>
28+
wallets.map(
29+
(wallet): LmpBundleWallet => ({
30+
// TODO: icon should be different based on wallet type (same as in v1 dropdown menu)
31+
walletIcon: isBitcoinWallet(wallet) ? bitcoinLogo : cardanoLogo,
32+
walletId: wallet.walletId,
33+
walletName: wallet.metadata.name,
34+
encryptedRecoveryPhrase: isInMemoryWallet(wallet) ? wallet.encryptedSecrets.keyMaterial : undefined
35+
})
36+
)
37+
)
38+
),
39+
activate: async (walletId): Promise<void> => {
40+
const wallets = await firstValueFrom(walletRepository.wallets$);
41+
const wallet = wallets.find((w) => w.walletId === walletId);
42+
if (!wallet) {
43+
logger.warn('Failed to activate wallet: not found', walletId);
44+
return;
45+
}
46+
47+
if (isBip32Wallet(wallet)) {
48+
const accountIndex = wallet.metadata.lastActiveAccountIndex || 0;
49+
const bitcoinActiveWallet = await firstValueFrom(bitcoinWalletManager.activeWallet$);
50+
const cardanoActiveWallet = await firstValueFrom(walletManager.activeWallet$);
51+
if (isBitcoinWallet(wallet)) {
52+
await setBackgroundStorage({
53+
activeBlockchain: 'bitcoin'
54+
});
55+
await bitcoinWalletManager.activate({
56+
network:
57+
// If Bitcoin wallet is active, use the same chainID
58+
// If Cardano wallet is active, use the same network type
59+
// Otherwise use Mainnet
60+
bitcoinActiveWallet?.props.network ||
61+
cardanoActiveWallet?.props.chainId.networkId === Wallet.Cardano.NetworkId.Testnet
62+
? Bitcoin.Network.Testnet
63+
: Bitcoin.Network.Mainnet,
64+
walletId,
65+
accountIndex
66+
});
67+
} else {
68+
await setBackgroundStorage({
69+
activeBlockchain: 'cardano'
70+
});
71+
await walletManager.activate({
72+
chainId:
73+
// If Cardano wallet is active, use the same chainID
74+
// If Bitcoin wallet is active, use the same network type (preprod if testnet)
75+
// Otherwise use Mainnet
76+
cardanoActiveWallet?.props.chainId || bitcoinActiveWallet?.props.network === Bitcoin.Network.Testnet
77+
? Wallet.Cardano.ChainIds.Preprod
78+
: Wallet.Cardano.ChainIds.Mainnet,
79+
walletId,
80+
accountIndex
81+
});
82+
}
83+
}
84+
}
85+
};
86+
87+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
88+
(globalThis as any)[v1ApiGlobalProperty] = api;

apps/browser-extension-wallet/src/lib/scripts/types/feature-flags.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export enum ExperimentName {
1313
DAPP_EXPLORER = 'dapp-explorer',
1414
SEND_CONSOLE_ERRORS_TO_SENTRY = 'send-console-errors-to-sentry',
1515
BITCOIN_WALLETS = 'bitcoin-wallets',
16+
MIDNIGHT_WALLETS = 'midnight-wallets',
1617
NFTPRINTLAB = 'nftprintlab',
1718
GLACIER_DROP = 'glacier-drop',
1819
MEMPOOLSPACE_FEE_MARKET = 'bitcoin-mempool-space-fee-market',

0 commit comments

Comments
 (0)