Skip to content

Commit eaa7240

Browse files
vetalcoremirceahasegangreatertomi
authored
fix: [lw-11877]: force Nami-mode users to switch to Lace delegating from dapp popup (#1559)
* fix: [lw-11877]: Nudge Nami-mode users to switch to Lace delegating to dRep * fix: [lw-11877]: handle switch to lace from dapp confirmation popup * fix: [lw-11877]: check for governance fields to show banner * fix: [lw-11877]: resolve sonarcloud issues * fix: [lw-11877]: fix unit tests * fix: [lw-11877]: resolve pr comments * fix: resolve sdet comment * fix: resolve build issue on main --------- Co-authored-by: Mircea Hasegan <mircea.hasegan@iohk.io> Co-authored-by: John Oshalusi <john.oshalusi@iohk.io>
1 parent 33702d2 commit eaa7240

File tree

10 files changed

+268
-71
lines changed

10 files changed

+268
-71
lines changed

apps/browser-extension-wallet/src/dapp-connector.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,20 @@ import { AddressesDiscoveryOverlay } from 'components/AddressesDiscoveryOverlay'
1717
import { useEffect, useState } from 'react';
1818
import { getBackgroundStorage } from '@lib/scripts/background/storage';
1919
import { NamiDappConnector } from './views/nami-mode/indexInternal';
20+
import { storage } from 'webextension-polyfill';
21+
import { TxWitnessRequestProvider } from '@providers/TxWitnessRequestProvider';
2022

2123
const App = (): React.ReactElement => {
2224
const [mode, setMode] = useState<'lace' | 'nami'>();
25+
26+
storage.onChanged.addListener((changes) => {
27+
const oldModeValue = changes.BACKGROUND_STORAGE?.oldValue?.namiMigration;
28+
const newModeValue = changes.BACKGROUND_STORAGE?.newValue?.namiMigration;
29+
if (oldModeValue?.mode !== newModeValue?.mode) {
30+
setMode(newModeValue);
31+
}
32+
});
33+
2334
useEffect(() => {
2435
const getWalletMode = async () => {
2536
const { namiMigration } = await getBackgroundStorage();
@@ -46,7 +57,9 @@ const App = (): React.ReactElement => {
4657
<ExternalLinkOpenerProvider>
4758
<AddressesDiscoveryOverlay>
4859
<UIThemeProvider>
49-
{mode === 'nami' ? <NamiDappConnector /> : <DappConnectorView />}
60+
<TxWitnessRequestProvider>
61+
{mode === 'nami' ? <NamiDappConnector /> : <DappConnectorView />}
62+
</TxWitnessRequestProvider>
5063
</UIThemeProvider>
5164
</AddressesDiscoveryOverlay>
5265
</ExternalLinkOpenerProvider>

apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.tsx

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ import { useDisallowSignTx, useSignWithHardwareWallet, useOnBeforeUnload } from
1010
import { TX_CREATION_TYPE_KEY, TxCreationType } from '@providers/AnalyticsProvider/analyticsTracker';
1111
import { txSubmitted$ } from '@providers/AnalyticsProvider/onChain';
1212
import { useAnalyticsContext } from '@providers';
13-
import { signingCoordinator } from '@lib/wallet-api-ui';
1413
import { senderToDappInfo } from '@src/utils/senderToDappInfo';
1514
import { exposeApi, RemoteApiPropertyType } from '@cardano-sdk/web-extension';
1615
import { UserPromptService } from '@lib/scripts/background/services';
1716
import { DAPP_CHANNELS } from '@src/utils/constants';
18-
import { of, take } from 'rxjs';
17+
import { of } from 'rxjs';
1918
import { runtime } from 'webextension-polyfill';
2019
import { Skeleton } from 'antd';
2120
import { DappTransactionContainer } from './DappTransactionContainer';
21+
import { useTxWitnessRequest } from '@providers/TxWitnessRequestProvider';
2222

2323
export const ConfirmTransaction = (): React.ReactElement => {
2424
const { t } = useTranslation();
@@ -47,30 +47,33 @@ export const ConfirmTransaction = (): React.ReactElement => {
4747
isHardwareWallet ? signWithHardwareWallet() : setNextView();
4848
};
4949

50+
const txWitnessRequest = useTxWitnessRequest();
51+
5052
useEffect(() => {
51-
const subscription = signingCoordinator.transactionWitnessRequest$.pipe(take(1)).subscribe(async (r) => {
52-
setDappInfo(await senderToDappInfo(r.signContext.sender));
53-
setSignTxRequest(r);
54-
});
53+
(async () => {
54+
if (!txWitnessRequest) return (): (() => void) => void 0;
55+
56+
setDappInfo(await senderToDappInfo(txWitnessRequest.signContext.sender));
57+
setSignTxRequest(txWitnessRequest);
5558

56-
const api = exposeApi<Pick<UserPromptService, 'readyToSignTx'>>(
57-
{
58-
api$: of({
59-
async readyToSignTx(): Promise<boolean> {
60-
return Promise.resolve(true);
61-
}
62-
}),
63-
baseChannel: DAPP_CHANNELS.userPrompt,
64-
properties: { readyToSignTx: RemoteApiPropertyType.MethodReturningPromise }
65-
},
66-
{ logger: console, runtime }
67-
);
59+
const api = exposeApi<Pick<UserPromptService, 'readyToSignTx'>>(
60+
{
61+
api$: of({
62+
async readyToSignTx(): Promise<boolean> {
63+
return Promise.resolve(true);
64+
}
65+
}),
66+
baseChannel: DAPP_CHANNELS.userPrompt,
67+
properties: { readyToSignTx: RemoteApiPropertyType.MethodReturningPromise }
68+
},
69+
{ logger: console, runtime }
70+
);
6871

69-
return () => {
70-
subscription.unsubscribe();
71-
api.shutdown();
72-
};
73-
}, [setSignTxRequest, setDappInfo]);
72+
return () => {
73+
api.shutdown();
74+
};
75+
})();
76+
}, [setSignTxRequest, setDappInfo, txWitnessRequest]);
7477

7578
const onCancelTransaction = () => {
7679
analytics.sendEventToPostHog(PostHogAction.SendTransactionSummaryCancelClick, {

apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmTransaction.test.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const mockUseViewsFlowContext = jest.fn();
1212
const mockUseSignWithHardwareWallet = jest.fn();
1313
const mockUseOnBeforeUnload = jest.fn();
1414
const mockUseComputeTxCollateral = jest.fn().mockReturnValue(BigInt(1_000_000));
15+
const mockUseTxWitnessRequest = jest.fn().mockReturnValue({});
1516
const mockCreateTxInspector = jest.fn().mockReturnValue(() => ({ minted: [] as any, burned: [] as any }));
1617
import * as React from 'react';
1718
import { cleanup, render, act, fireEvent } from '@testing-library/react';
@@ -98,6 +99,16 @@ jest.mock('../hooks.ts', () => {
9899
};
99100
});
100101

102+
jest.mock('@src/utils/senderToDappInfo', () => ({
103+
...jest.requireActual<any>('@src/utils/senderToDappInfo'),
104+
senderToDappInfo: jest.fn().mockReturnValue({})
105+
}));
106+
107+
jest.mock('@providers/TxWitnessRequestProvider', () => ({
108+
...jest.requireActual<any>('@providers/TxWitnessRequestProvider'),
109+
useTxWitnessRequest: mockUseTxWitnessRequest
110+
}));
111+
101112
jest.mock('@hooks/useComputeTxCollateral', (): typeof UseComputeTxCollateral => ({
102113
...jest.requireActual<typeof UseComputeTxCollateral>('@hooks/useComputeTxCollateral'),
103114
useComputeTxCollateral: mockUseComputeTxCollateral
@@ -127,13 +138,23 @@ describe('Testing ConfirmTransaction component', () => {
127138
mockUseViewsFlowContext.mockReset();
128139
mockUseViewsFlowContext.mockReturnValue({
129140
utils: {},
141+
setDappInfo: jest.fn(),
130142
signTxRequest: {
131143
request: {
132144
transaction: {
133145
toCore: jest.fn().mockReturnValue({ id: 'test-tx-id' }),
134146
getId: jest.fn().mockReturnValue({ id: 'test-tx-id' })
135147
}
136-
}
148+
},
149+
set: jest.fn()
150+
}
151+
});
152+
mockUseTxWitnessRequest.mockReset();
153+
mockUseTxWitnessRequest.mockReturnValue({
154+
signContext: { sender: { tab: { id: 'tabid', favIconUrl: 'favIconUrl' } } },
155+
transaction: {
156+
toCore: jest.fn().mockReturnValue({ id: 'test-tx-id' }),
157+
getId: jest.fn().mockReturnValue({ id: 'test-tx-id' })
137158
}
138159
});
139160
mockConfirmTransactionContent.mockReset();
@@ -168,13 +189,23 @@ describe('Testing ConfirmTransaction component', () => {
168189
mockUseViewsFlowContext.mockReset();
169190
mockUseViewsFlowContext.mockReturnValue({
170191
utils: { setNextView: setNextViewMock },
192+
setDappInfo: jest.fn(),
171193
signTxRequest: {
172194
request: {
173195
transaction: {
174196
getId: jest.fn().mockReturnValue({ id: 'test-tx-id' }),
175197
toCore: jest.fn().mockReturnValue(signTxData.tx)
176198
}
177-
}
199+
},
200+
set: jest.fn()
201+
}
202+
});
203+
mockUseTxWitnessRequest.mockReset();
204+
mockUseTxWitnessRequest.mockReturnValue({
205+
signContext: { sender: { tab: { id: 'tabid', favIconUrl: 'favIconUrl' } } },
206+
transaction: {
207+
getId: jest.fn().mockReturnValue({ id: 'test-tx-id' }),
208+
toCore: jest.fn().mockReturnValue(signTxData.tx)
178209
}
179210
});
180211

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { useState, useEffect } from 'react';
44
import { getCollateral } from '@cardano-sdk/core';
55
import { ObservableWalletState } from './useWalletState';
66

7-
export const useComputeTxCollateral = (wallet: ObservableWalletState, tx?: Wallet.Cardano.Tx): bigint | undefined => {
7+
export const useComputeTxCollateral = (wallet?: ObservableWalletState, tx?: Wallet.Cardano.Tx): bigint | undefined => {
88
const [txCollateral, setTxCollateral] = useState<bigint>();
99

1010
useEffect(() => {
11-
if (!tx) return;
11+
if (!tx || !wallet) return;
1212

1313
const computeCollateral = async () => {
1414
const inputResolver = createHistoricalOwnInputResolver({
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React, { createContext, FC, useContext, useEffect, useState } from 'react';
2+
import { useLocation } from 'react-router-dom';
3+
import { signingCoordinator } from '@lib/wallet-api-ui';
4+
import { exposeApi, RemoteApiPropertyType, TransactionWitnessRequest } from '@cardano-sdk/web-extension';
5+
import { Wallet } from '@lace/cardano';
6+
import { of, take } from 'rxjs';
7+
import { DAPP_CHANNELS } from '@src/utils/constants';
8+
import { runtime } from 'webextension-polyfill';
9+
import { UserPromptService } from '@lib/scripts/background/services';
10+
import { dAppRoutePaths } from '@routes';
11+
12+
export type TxWitnessRequestContextType = TransactionWitnessRequest<Wallet.WalletMetadata, Wallet.AccountMetadata>;
13+
14+
// eslint-disable-next-line unicorn/no-null
15+
const TxWitnessRequestContext = createContext<TxWitnessRequestContextType | null>(null);
16+
17+
export const useTxWitnessRequest = (): TxWitnessRequestContextType => {
18+
const context = useContext(TxWitnessRequestContext);
19+
if (context === null) throw new Error('TxWitnessRequestContext not defined');
20+
return context;
21+
};
22+
23+
export const TxWitnessRequestProvider: FC = ({ children }) => {
24+
const [request, setRequest] = useState<TxWitnessRequestContextType | undefined>();
25+
const location = useLocation();
26+
27+
useEffect(() => {
28+
if (location.pathname !== dAppRoutePaths.dappSignTx) {
29+
return () => void 0;
30+
}
31+
32+
const subscription = signingCoordinator.transactionWitnessRequest$.pipe(take(1)).subscribe(async (r) => {
33+
setRequest(r);
34+
});
35+
36+
const api = exposeApi<Pick<UserPromptService, 'readyToSignTx'>>(
37+
{
38+
api$: of({
39+
async readyToSignTx(): Promise<boolean> {
40+
return Promise.resolve(true);
41+
}
42+
}),
43+
baseChannel: DAPP_CHANNELS.userPrompt,
44+
properties: { readyToSignTx: RemoteApiPropertyType.MethodReturningPromise }
45+
},
46+
{ logger: console, runtime }
47+
);
48+
49+
return () => {
50+
subscription.unsubscribe();
51+
api.shutdown();
52+
};
53+
}, [location.pathname, setRequest]);
54+
55+
return <TxWitnessRequestContext.Provider value={request}>{children}</TxWitnessRequestContext.Provider>;
56+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './context';

apps/browser-extension-wallet/src/views/nami-mode/NamiDappConnectorView.tsx

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
/* eslint-disable max-statements */
2-
import React, { useCallback, useMemo } from 'react';
2+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
33
import { DappConnector, DApp, DappOutsideHandlesProvider, CommonOutsideHandlesProvider } from '@lace/nami';
44
import { useWalletStore } from '@src/stores';
55
import { useBackgroundServiceAPIContext, useTheme } from '@providers';
66
import { useHandleResolver, useWalletManager } from '@hooks';
77
import { signingCoordinator, walletManager, withSignTxConfirmation } from '@lib/wallet-api-ui';
88
import { withDappContext } from '@src/features/dapp/context';
99
import { CARDANO_COIN_SYMBOL } from './constants';
10-
import { DappDataService } from '@lib/scripts/types';
10+
import { DappDataService, BackgroundStorage } from '@lib/scripts/types';
1111
import { consumeRemoteApi, exposeApi, RemoteApiPropertyType } from '@cardano-sdk/web-extension';
1212
import { DAPP_CHANNELS } from '@src/utils/constants';
1313
import { runtime } from 'webextension-polyfill';
@@ -22,9 +22,12 @@ import { createWalletAssetProvider } from '@cardano-sdk/wallet';
2222
import { tryGetAssetInfos } from './utils';
2323
import { useNetworkError } from '@hooks/useNetworkError';
2424
import { useSecrets } from '@lace/core';
25+
import { getBackgroundStorage, setBackgroundStorage } from '@lib/scripts/background/storage';
26+
import { useTxWitnessRequest } from '@providers/TxWitnessRequestProvider';
27+
import type { TransactionWitnessRequest } from '@cardano-sdk/web-extension';
2528

2629
const DAPP_TOAST_DURATION = 100;
27-
const dappConnector: Omit<DappConnector, 'getAssetInfos'> = {
30+
const dappConnector: Omit<DappConnector, 'getAssetInfos' | 'txWitnessRequest'> = {
2831
getDappInfo: () => {
2932
const dappDataService = consumeRemoteApi<Pick<DappDataService, 'getDappInfo'>>(
3033
{
@@ -61,42 +64,22 @@ const dappConnector: Omit<DappConnector, 'getAssetInfos'> = {
6164
onCleanup();
6265
}, DAPP_TOAST_DURATION);
6366
},
64-
getSignTxRequest: async () => {
65-
const userPromptService = exposeApi<Pick<UserPromptService, 'readyToSignTx'>>(
66-
{
67-
api$: of({
68-
async readyToSignTx(): Promise<boolean> {
69-
return Promise.resolve(true);
70-
}
71-
}),
72-
baseChannel: DAPP_CHANNELS.userPrompt,
73-
properties: { readyToSignTx: RemoteApiPropertyType.MethodReturningPromise }
67+
getSignTxRequest: async (r: TransactionWitnessRequest<Wallet.WalletMetadata, Wallet.AccountMetadata>) => ({
68+
dappInfo: await senderToDappInfo(r.signContext.sender),
69+
request: {
70+
data: { tx: r.transaction.toCbor(), addresses: r.signContext.knownAddresses },
71+
reject: async (onCleanup: () => void) => {
72+
await r.reject('User declined to sign');
73+
setTimeout(() => {
74+
onCleanup();
75+
}, DAPP_TOAST_DURATION);
7476
},
75-
{ logger: console, runtime }
76-
);
77-
78-
return firstValueFrom(
79-
signingCoordinator.transactionWitnessRequest$.pipe(
80-
map(async (r) => ({
81-
dappInfo: await senderToDappInfo(r.signContext.sender),
82-
request: {
83-
data: { tx: r.transaction.toCbor(), addresses: r.signContext.knownAddresses },
84-
reject: async (onCleanup: () => void) => {
85-
await r.reject('User declined to sign');
86-
setTimeout(() => {
87-
onCleanup();
88-
}, DAPP_TOAST_DURATION);
89-
},
90-
sign: async (password: string) => {
91-
const passphrase = Buffer.from(password, 'utf8');
92-
await r.sign(passphrase, { willRetryOnFailure: true }).finally(() => passphrase.fill(0));
93-
}
94-
}
95-
})),
96-
finalize(() => userPromptService.shutdown())
97-
)
98-
);
99-
},
77+
sign: async (password: string) => {
78+
const passphrase = Buffer.from(password, 'utf8');
79+
await r.sign(passphrase, { willRetryOnFailure: true }).finally(() => passphrase.fill(0));
80+
}
81+
}
82+
}),
10083
getSignDataRequest: async () => {
10184
const userPromptService = exposeApi<Pick<UserPromptService, 'readyToSignData'>>(
10285
{
@@ -137,6 +120,7 @@ const dappConnector: Omit<DappConnector, 'getAssetInfos'> = {
137120

138121
export const NamiDappConnectorView = withDappContext((): React.ReactElement => {
139122
const { sendEventToPostHog } = useAnalytics();
123+
const [namiMigration, setNamiMigration] = useState<BackgroundStorage['namiMigration']>();
140124
const backgroundServices = useBackgroundServiceAPIContext();
141125
const { walletRepository } = useWalletManager();
142126
const {
@@ -198,14 +182,37 @@ export const NamiDappConnectorView = withDappContext((): React.ReactElement => {
198182

199183
const handleResolver = useHandleResolver();
200184

185+
useEffect(() => {
186+
getBackgroundStorage()
187+
.then((storage) => setNamiMigration(storage.namiMigration))
188+
.catch(console.error);
189+
}, []);
190+
191+
const switchWalletMode = useCallback(async () => {
192+
const mode = 'lace';
193+
const migration: BackgroundStorage['namiMigration'] = {
194+
...namiMigration,
195+
mode
196+
};
197+
198+
setNamiMigration(migration);
199+
backgroundServices.handleChangeMode({ mode });
200+
await setBackgroundStorage({
201+
namiMigration: migration
202+
});
203+
}, [backgroundServices, namiMigration]);
204+
205+
const txWitnessRequest = useTxWitnessRequest();
206+
201207
return (
202208
<DappOutsideHandlesProvider
203209
{...{
204210
theme: theme.name,
205211
walletManager,
206212
walletRepository,
207213
environmentName,
208-
dappConnector: { ...dappConnector, getAssetInfos },
214+
dappConnector: { ...dappConnector, txWitnessRequest, getAssetInfos },
215+
switchWalletMode,
209216
secretsUtil
210217
}}
211218
>

0 commit comments

Comments
 (0)