Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: [lw-11877]: force Nami-mode users to switch to Lace delegating from dapp popup #1559

Merged
merged 10 commits into from
Dec 11, 2024
15 changes: 14 additions & 1 deletion apps/browser-extension-wallet/src/dapp-connector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,20 @@ import { AddressesDiscoveryOverlay } from 'components/AddressesDiscoveryOverlay'
import { useEffect, useState } from 'react';
import { getBackgroundStorage } from '@lib/scripts/background/storage';
import { NamiDappConnector } from './views/nami-mode/indexInternal';
import { storage } from 'webextension-polyfill';
import { TxWitnessRequestProvider } from '@providers/TxWitnessRequestProvider';

const App = (): React.ReactElement => {
const [mode, setMode] = useState<'lace' | 'nami'>();

storage.onChanged.addListener((changes) => {
const oldModeValue = changes.BACKGROUND_STORAGE?.oldValue?.namiMigration;
const newModeValue = changes.BACKGROUND_STORAGE?.newValue?.namiMigration;
if (oldModeValue?.mode !== newModeValue?.mode) {
setMode(newModeValue);
}
});

useEffect(() => {
const getWalletMode = async () => {
const { namiMigration } = await getBackgroundStorage();
Expand All @@ -46,7 +57,9 @@ const App = (): React.ReactElement => {
<ExternalLinkOpenerProvider>
<AddressesDiscoveryOverlay>
<UIThemeProvider>
{mode === 'nami' ? <NamiDappConnector /> : <DappConnectorView />}
<TxWitnessRequestProvider>
{mode === 'nami' ? <NamiDappConnector /> : <DappConnectorView />}
</TxWitnessRequestProvider>
</UIThemeProvider>
</AddressesDiscoveryOverlay>
</ExternalLinkOpenerProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import { useDisallowSignTx, useSignWithHardwareWallet, useOnBeforeUnload } from
import { TX_CREATION_TYPE_KEY, TxCreationType } from '@providers/AnalyticsProvider/analyticsTracker';
import { txSubmitted$ } from '@providers/AnalyticsProvider/onChain';
import { useAnalyticsContext } from '@providers';
import { signingCoordinator } from '@lib/wallet-api-ui';
import { senderToDappInfo } from '@src/utils/senderToDappInfo';
import { exposeApi, RemoteApiPropertyType } from '@cardano-sdk/web-extension';
import { UserPromptService } from '@lib/scripts/background/services';
import { DAPP_CHANNELS } from '@src/utils/constants';
import { of, take } from 'rxjs';
import { of } from 'rxjs';
import { runtime } from 'webextension-polyfill';
import { Skeleton } from 'antd';
import { DappTransactionContainer } from './DappTransactionContainer';
import { useTxWitnessRequest } from '@providers/TxWitnessRequestProvider';

export const ConfirmTransaction = (): React.ReactElement => {
const { t } = useTranslation();
Expand Down Expand Up @@ -47,30 +47,33 @@ export const ConfirmTransaction = (): React.ReactElement => {
isHardwareWallet ? signWithHardwareWallet() : setNextView();
};

const txWitnessRequest = useTxWitnessRequest();

useEffect(() => {
const subscription = signingCoordinator.transactionWitnessRequest$.pipe(take(1)).subscribe(async (r) => {
setDappInfo(await senderToDappInfo(r.signContext.sender));
setSignTxRequest(r);
});
(async () => {
if (!txWitnessRequest) return (): (() => void) => void 0;

setDappInfo(await senderToDappInfo(txWitnessRequest.signContext.sender));
setSignTxRequest(txWitnessRequest);

const api = exposeApi<Pick<UserPromptService, 'readyToSignTx'>>(
{
api$: of({
async readyToSignTx(): Promise<boolean> {
return Promise.resolve(true);
}
}),
baseChannel: DAPP_CHANNELS.userPrompt,
properties: { readyToSignTx: RemoteApiPropertyType.MethodReturningPromise }
},
{ logger: console, runtime }
);
const api = exposeApi<Pick<UserPromptService, 'readyToSignTx'>>(
{
api$: of({
async readyToSignTx(): Promise<boolean> {
return Promise.resolve(true);
}
}),
baseChannel: DAPP_CHANNELS.userPrompt,
properties: { readyToSignTx: RemoteApiPropertyType.MethodReturningPromise }
},
{ logger: console, runtime }
);

return () => {
subscription.unsubscribe();
api.shutdown();
};
}, [setSignTxRequest, setDappInfo]);
return () => {
api.shutdown();
};
})();
}, [setSignTxRequest, setDappInfo, txWitnessRequest]);

const onCancelTransaction = () => {
analytics.sendEventToPostHog(PostHogAction.SendTransactionSummaryCancelClick, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const mockUseViewsFlowContext = jest.fn();
const mockUseSignWithHardwareWallet = jest.fn();
const mockUseOnBeforeUnload = jest.fn();
const mockUseComputeTxCollateral = jest.fn().mockReturnValue(BigInt(1_000_000));
const mockUseTxWitnessRequest = jest.fn().mockReturnValue({});
const mockCreateTxInspector = jest.fn().mockReturnValue(() => ({ minted: [] as any, burned: [] as any }));
import * as React from 'react';
import { cleanup, render, act, fireEvent } from '@testing-library/react';
Expand Down Expand Up @@ -98,6 +99,16 @@ jest.mock('../hooks.ts', () => {
};
});

jest.mock('@src/utils/senderToDappInfo', () => ({
...jest.requireActual<any>('@src/utils/senderToDappInfo'),
senderToDappInfo: jest.fn().mockReturnValue({})
}));

jest.mock('@providers/TxWitnessRequestProvider', () => ({
...jest.requireActual<any>('@providers/TxWitnessRequestProvider'),
useTxWitnessRequest: mockUseTxWitnessRequest
}));

jest.mock('@hooks/useComputeTxCollateral', (): typeof UseComputeTxCollateral => ({
...jest.requireActual<typeof UseComputeTxCollateral>('@hooks/useComputeTxCollateral'),
useComputeTxCollateral: mockUseComputeTxCollateral
Expand Down Expand Up @@ -127,13 +138,23 @@ describe('Testing ConfirmTransaction component', () => {
mockUseViewsFlowContext.mockReset();
mockUseViewsFlowContext.mockReturnValue({
utils: {},
setDappInfo: jest.fn(),
signTxRequest: {
request: {
transaction: {
toCore: jest.fn().mockReturnValue({ id: 'test-tx-id' }),
getId: jest.fn().mockReturnValue({ id: 'test-tx-id' })
}
}
},
set: jest.fn()
}
});
mockUseTxWitnessRequest.mockReset();
mockUseTxWitnessRequest.mockReturnValue({
signContext: { sender: { tab: { id: 'tabid', favIconUrl: 'favIconUrl' } } },
transaction: {
toCore: jest.fn().mockReturnValue({ id: 'test-tx-id' }),
getId: jest.fn().mockReturnValue({ id: 'test-tx-id' })
}
});
mockConfirmTransactionContent.mockReset();
Expand Down Expand Up @@ -168,13 +189,23 @@ describe('Testing ConfirmTransaction component', () => {
mockUseViewsFlowContext.mockReset();
mockUseViewsFlowContext.mockReturnValue({
utils: { setNextView: setNextViewMock },
setDappInfo: jest.fn(),
signTxRequest: {
request: {
transaction: {
getId: jest.fn().mockReturnValue({ id: 'test-tx-id' }),
toCore: jest.fn().mockReturnValue(signTxData.tx)
}
}
},
set: jest.fn()
}
});
mockUseTxWitnessRequest.mockReset();
mockUseTxWitnessRequest.mockReturnValue({
signContext: { sender: { tab: { id: 'tabid', favIconUrl: 'favIconUrl' } } },
transaction: {
getId: jest.fn().mockReturnValue({ id: 'test-tx-id' }),
toCore: jest.fn().mockReturnValue(signTxData.tx)
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { useState, useEffect } from 'react';
import { getCollateral } from '@cardano-sdk/core';
import { ObservableWalletState } from './useWalletState';

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

useEffect(() => {
if (!tx) return;
if (!tx || !wallet) return;

const computeCollateral = async () => {
const inputResolver = createHistoricalOwnInputResolver({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React, { createContext, FC, useContext, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { signingCoordinator } from '@lib/wallet-api-ui';
import { exposeApi, RemoteApiPropertyType, TransactionWitnessRequest } from '@cardano-sdk/web-extension';
import { Wallet } from '@lace/cardano';
import { of, take } from 'rxjs';
import { DAPP_CHANNELS } from '@src/utils/constants';
import { runtime } from 'webextension-polyfill';
import { UserPromptService } from '@lib/scripts/background/services';
import { dAppRoutePaths } from '@routes';

export type TxWitnessRequestContextType = TransactionWitnessRequest<Wallet.WalletMetadata, Wallet.AccountMetadata>;

// eslint-disable-next-line unicorn/no-null
const TxWitnessRequestContext = createContext<TxWitnessRequestContextType | null>(null);

export const useTxWitnessRequest = (): TxWitnessRequestContextType => {
const context = useContext(TxWitnessRequestContext);
if (context === null) throw new Error('TxWitnessRequestContext not defined');
return context;
};

export const TxWitnessRequestProvider: FC = ({ children }) => {
const [request, setRequest] = useState<TxWitnessRequestContextType | undefined>();
const location = useLocation();

useEffect(() => {
if (location.pathname !== dAppRoutePaths.dappSignTx) {
return () => void 0;
}

const subscription = signingCoordinator.transactionWitnessRequest$.pipe(take(1)).subscribe(async (r) => {
setRequest(r);
});

const api = exposeApi<Pick<UserPromptService, 'readyToSignTx'>>(
{
api$: of({
async readyToSignTx(): Promise<boolean> {
return Promise.resolve(true);
}
}),
baseChannel: DAPP_CHANNELS.userPrompt,
properties: { readyToSignTx: RemoteApiPropertyType.MethodReturningPromise }
},
{ logger: console, runtime }
);

return () => {
subscription.unsubscribe();
api.shutdown();
};
}, [location.pathname, setRequest]);

return <TxWitnessRequestContext.Provider value={request}>{children}</TxWitnessRequestContext.Provider>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './context';
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/* eslint-disable max-statements */
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { DappConnector, DApp, DappOutsideHandlesProvider, CommonOutsideHandlesProvider } from '@lace/nami';
import { useWalletStore } from '@src/stores';
import { useBackgroundServiceAPIContext, useTheme } from '@providers';
import { useHandleResolver, useWalletManager } from '@hooks';
import { signingCoordinator, walletManager, withSignTxConfirmation } from '@lib/wallet-api-ui';
import { withDappContext } from '@src/features/dapp/context';
import { CARDANO_COIN_SYMBOL } from './constants';
import { DappDataService } from '@lib/scripts/types';
import { DappDataService, BackgroundStorage } from '@lib/scripts/types';
import { consumeRemoteApi, exposeApi, RemoteApiPropertyType } from '@cardano-sdk/web-extension';
import { DAPP_CHANNELS } from '@src/utils/constants';
import { runtime } from 'webextension-polyfill';
Expand All @@ -22,9 +22,12 @@ import { createWalletAssetProvider } from '@cardano-sdk/wallet';
import { tryGetAssetInfos } from './utils';
import { useNetworkError } from '@hooks/useNetworkError';
import { useSecrets } from '@lace/core';
import { getBackgroundStorage, setBackgroundStorage } from '@lib/scripts/background/storage';
import { useTxWitnessRequest } from '@providers/TxWitnessRequestProvider';
import type { TransactionWitnessRequest } from '@cardano-sdk/web-extension';

const DAPP_TOAST_DURATION = 100;
const dappConnector: Omit<DappConnector, 'getAssetInfos'> = {
const dappConnector: Omit<DappConnector, 'getAssetInfos' | 'txWitnessRequest'> = {
getDappInfo: () => {
const dappDataService = consumeRemoteApi<Pick<DappDataService, 'getDappInfo'>>(
{
Expand Down Expand Up @@ -61,42 +64,22 @@ const dappConnector: Omit<DappConnector, 'getAssetInfos'> = {
onCleanup();
}, DAPP_TOAST_DURATION);
},
getSignTxRequest: async () => {
const userPromptService = exposeApi<Pick<UserPromptService, 'readyToSignTx'>>(
{
api$: of({
async readyToSignTx(): Promise<boolean> {
return Promise.resolve(true);
}
}),
baseChannel: DAPP_CHANNELS.userPrompt,
properties: { readyToSignTx: RemoteApiPropertyType.MethodReturningPromise }
getSignTxRequest: async (r: TransactionWitnessRequest<Wallet.WalletMetadata, Wallet.AccountMetadata>) => ({
dappInfo: await senderToDappInfo(r.signContext.sender),
request: {
data: { tx: r.transaction.toCbor(), addresses: r.signContext.knownAddresses },
reject: async (onCleanup: () => void) => {
await r.reject('User declined to sign');
setTimeout(() => {
onCleanup();
}, DAPP_TOAST_DURATION);
},
{ logger: console, runtime }
);

return firstValueFrom(
signingCoordinator.transactionWitnessRequest$.pipe(
map(async (r) => ({
dappInfo: await senderToDappInfo(r.signContext.sender),
request: {
data: { tx: r.transaction.toCbor(), addresses: r.signContext.knownAddresses },
reject: async (onCleanup: () => void) => {
await r.reject('User declined to sign');
setTimeout(() => {
onCleanup();
}, DAPP_TOAST_DURATION);
},
sign: async (password: string) => {
const passphrase = Buffer.from(password, 'utf8');
await r.sign(passphrase, { willRetryOnFailure: true }).finally(() => passphrase.fill(0));
}
}
})),
finalize(() => userPromptService.shutdown())
)
);
},
sign: async (password: string) => {
const passphrase = Buffer.from(password, 'utf8');
await r.sign(passphrase, { willRetryOnFailure: true }).finally(() => passphrase.fill(0));
}
}
}),
getSignDataRequest: async () => {
const userPromptService = exposeApi<Pick<UserPromptService, 'readyToSignData'>>(
{
Expand Down Expand Up @@ -137,6 +120,7 @@ const dappConnector: Omit<DappConnector, 'getAssetInfos'> = {

export const NamiDappConnectorView = withDappContext((): React.ReactElement => {
const { sendEventToPostHog } = useAnalytics();
const [namiMigration, setNamiMigration] = useState<BackgroundStorage['namiMigration']>();
const backgroundServices = useBackgroundServiceAPIContext();
const { walletRepository } = useWalletManager();
const {
Expand Down Expand Up @@ -198,14 +182,37 @@ export const NamiDappConnectorView = withDappContext((): React.ReactElement => {

const handleResolver = useHandleResolver();

useEffect(() => {
getBackgroundStorage()
.then((storage) => setNamiMigration(storage.namiMigration))
.catch(console.error);
}, []);

const switchWalletMode = useCallback(async () => {
const mode = 'lace';
const migration: BackgroundStorage['namiMigration'] = {
...namiMigration,
mode
};

setNamiMigration(migration);
backgroundServices.handleChangeMode({ mode });
await setBackgroundStorage({
namiMigration: migration
});
}, [backgroundServices, namiMigration]);

const txWitnessRequest = useTxWitnessRequest();

return (
<DappOutsideHandlesProvider
{...{
theme: theme.name,
walletManager,
walletRepository,
environmentName,
dappConnector: { ...dappConnector, getAssetInfos },
dappConnector: { ...dappConnector, txWitnessRequest, getAssetInfos },
switchWalletMode,
secretsUtil
}}
>
Expand Down
Loading
Loading