Skip to content

Commit a2315b9

Browse files
Feat/lw 11872 injection namespace less confusing (#1545)
- dapp compatibility mode setting - inject cip30 based on dapp compat mode
1 parent 57ccfa0 commit a2315b9

File tree

12 files changed

+296
-29
lines changed

12 files changed

+296
-29
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/* eslint-disable no-magic-numbers */
2+
/* eslint-disable @typescript-eslint/no-explicit-any */
3+
import { initializeInjectedScript } from '../inject';
4+
import { consumeRemoteAuthenticatorApi, consumeRemoteWalletApi } from '../api-consumers';
5+
import { getWalletMode } from '../injectUtil';
6+
import { injectGlobal } from '@cardano-sdk/dapp-connector';
7+
import { WalletMode } from '../../types';
8+
9+
jest.mock('../api-consumers');
10+
jest.mock('../injectUtil', () => ({
11+
...jest.requireActual<any>('../injectUtil'),
12+
getWalletMode: jest.fn()
13+
}));
14+
15+
jest.mock('@cardano-sdk/dapp-connector', () => ({
16+
...jest.requireActual<any>('@cardano-sdk/dapp-connector'),
17+
injectGlobal: jest.fn()
18+
}));
19+
20+
describe('initializeInjectedScript', () => {
21+
const logger = console;
22+
const walletApi = {};
23+
const authenticator = {};
24+
25+
beforeEach(() => {
26+
jest.clearAllMocks();
27+
(consumeRemoteAuthenticatorApi as jest.Mock).mockReturnValue(authenticator);
28+
(consumeRemoteWalletApi as jest.Mock).mockReturnValue(walletApi);
29+
});
30+
31+
describe('wallet mode not cached eagerly injects in lace with extensions', () => {
32+
it('ignores dappInjectCompatibilityMode when in lace mode', async () => {
33+
const latestWalletMode = Promise.resolve({ mode: 'lace', dappInjectCompatibilityMode: true });
34+
(getWalletMode as jest.Mock).mockReturnValue({ latestWalletMode });
35+
36+
await initializeInjectedScript({ logger });
37+
38+
expect(injectGlobal).toHaveBeenCalledTimes(1);
39+
expect(injectGlobal).toHaveBeenCalledWith(
40+
expect.anything(),
41+
expect.objectContaining({ supportedExtensions: [{ cip: 95 }] }),
42+
expect.anything()
43+
);
44+
});
45+
46+
it('waits for dappInjectCompatibilityMode; does not inject in nami', async () => {
47+
const latestWalletMode = Promise.resolve({ mode: 'nami', dappInjectCompatibilityMode: false });
48+
(getWalletMode as jest.Mock).mockReturnValue({ latestWalletMode });
49+
50+
await initializeInjectedScript({ logger });
51+
52+
expect(injectGlobal).toHaveBeenCalledTimes(1);
53+
expect(injectGlobal).toHaveBeenCalledWith(
54+
expect.anything(),
55+
expect.objectContaining({ supportedExtensions: [{ cip: 95 }] }),
56+
expect.anything()
57+
);
58+
});
59+
60+
it('waits for dappInjectCompatibilityMode and lazy injects in nami too', async () => {
61+
const latestWalletMode = Promise.resolve({ mode: 'nami', dappInjectCompatibilityMode: true });
62+
(getWalletMode as jest.Mock).mockReturnValue({ latestWalletMode });
63+
64+
await initializeInjectedScript({ logger });
65+
66+
expect(injectGlobal).toHaveBeenCalledTimes(2);
67+
expect(injectGlobal).toHaveBeenCalledWith(
68+
expect.anything(),
69+
expect.objectContaining({ supportedExtensions: [{ cip: 95 }] }),
70+
expect.anything()
71+
);
72+
73+
expect(injectGlobal).toHaveBeenCalledWith(
74+
expect.anything(),
75+
expect.objectContaining({ supportedExtensions: [] }),
76+
expect.anything(),
77+
'nami'
78+
);
79+
});
80+
});
81+
82+
describe('wallet mode cached', () => {
83+
describe('lace mode injects lace with extensions', () => {
84+
it('eagerly injects in lace and ignores dappInjectCompatibilityMode', async () => {
85+
const cachedWalletMode: WalletMode = { mode: 'lace', dappInjectCompatibilityMode: true };
86+
const latestWalletMode = Promise.resolve({ mode: 'nami', dappInjectCompatibilityMode: false });
87+
(getWalletMode as jest.Mock).mockReturnValue({ cachedWalletMode, latestWalletMode });
88+
89+
await initializeInjectedScript({ logger });
90+
91+
expect(injectGlobal).toHaveBeenCalledTimes(1);
92+
expect(injectGlobal).toHaveBeenCalledWith(
93+
expect.anything(),
94+
expect.objectContaining({ supportedExtensions: [{ cip: 95 }] }),
95+
expect.anything()
96+
);
97+
});
98+
});
99+
100+
describe('nami mode injects lace with extensions', () => {
101+
it('eagerly injects in nami and does not await latest because dappInjectCompatibilityMode is enabled', async () => {
102+
const cachedWalletMode: WalletMode = { mode: 'nami', dappInjectCompatibilityMode: true };
103+
const latestWalletMode = Promise.resolve({ mode: 'nami', dappInjectCompatibilityMode: false });
104+
(getWalletMode as jest.Mock).mockReturnValue({ cachedWalletMode, latestWalletMode });
105+
106+
await initializeInjectedScript({ logger });
107+
108+
expect(injectGlobal).toHaveBeenCalledWith(
109+
expect.anything(),
110+
expect.objectContaining({ supportedExtensions: [{ cip: 95 }] }),
111+
expect.anything()
112+
);
113+
expect(injectGlobal).toHaveBeenCalledWith(
114+
expect.anything(),
115+
expect.objectContaining({ supportedExtensions: [] }),
116+
expect.anything(),
117+
'nami'
118+
);
119+
});
120+
121+
it('awaits latest value and lazily injects in nami when dappInjectCompatibilityMode is initially disabled', async () => {
122+
const cachedWalletMode: WalletMode = { mode: 'nami', dappInjectCompatibilityMode: false };
123+
const latestWalletMode = Promise.resolve({ mode: 'nami', dappInjectCompatibilityMode: true });
124+
(getWalletMode as jest.Mock).mockReturnValue({ cachedWalletMode, latestWalletMode });
125+
126+
await initializeInjectedScript({ logger });
127+
128+
expect(injectGlobal).toHaveBeenCalledWith(
129+
expect.anything(),
130+
expect.objectContaining({ supportedExtensions: [{ cip: 95 }] }),
131+
expect.anything()
132+
);
133+
expect(injectGlobal).toHaveBeenCalledWith(
134+
expect.anything(),
135+
expect.objectContaining({ supportedExtensions: [] }),
136+
expect.anything(),
137+
'nami'
138+
);
139+
});
140+
});
141+
});
142+
});

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
runContentScriptMessageProxy
77
} from '@cardano-sdk/web-extension';
88
import { consumeRemoteAuthenticatorApi, consumeRemoteWalletApi } from './api-consumers';
9-
import { modeApiProperties, WALLET_MODE_CHANNEL } from './injectUtil';
9+
import { laceFeaturesApiProperties, LACE_FEATURES_CHANNEL } from './injectUtil';
1010

1111
// Disable logging in production for performance & security measures
1212
if (process.env.USE_DAPP_CONNECTOR === 'true') {
@@ -21,8 +21,8 @@ if (process.env.USE_DAPP_CONNECTOR === 'true') {
2121
consumeRemoteWalletApi({ walletName }, dependencies),
2222
consumeRemoteApi(
2323
{
24-
baseChannel: WALLET_MODE_CHANNEL,
25-
properties: modeApiProperties
24+
baseChannel: LACE_FEATURES_CHANNEL,
25+
properties: laceFeaturesApiProperties
2626
},
2727
dependencies
2828
)

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

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
import { Cip30Wallet, injectGlobal, WalletProperties } from '@cardano-sdk/dapp-connector';
33
import { cip30, injectedRuntime, MessengerDependencies } from '@cardano-sdk/web-extension';
44
import { consumeRemoteAuthenticatorApi, consumeRemoteWalletApi } from './api-consumers';
5-
import { getMode } from './injectUtil';
5+
import { getWalletMode, isCachedWalletModeResult } from './injectUtil';
6+
import { WalletMode } from '../types';
67

78
const cip30WalletProperties: WalletProperties = {
89
// eslint-disable-next-line max-len
@@ -19,7 +20,38 @@ const cip30WalletNamiProperties: WalletProperties = {
1920
supportedExtensions: []
2021
};
2122

22-
const initializeInjectedScript = async ({ logger }: cip30.InitializeInjectedDependencies) => {
23+
/** Injects wallet in `lace` namespace with all extensions enabled */
24+
const injectInLaceNs = (
25+
walletApi: ReturnType<typeof consumeRemoteWalletApi>,
26+
authenticator: ReturnType<typeof consumeRemoteAuthenticatorApi>,
27+
logger: cip30.InitializeInjectedDependencies['logger']
28+
): void => {
29+
const wallet = new Cip30Wallet(cip30WalletProperties, { api: walletApi, authenticator, logger });
30+
31+
// Always inject in lace namespace with all extensions
32+
injectGlobal(window, wallet, logger);
33+
};
34+
35+
/**
36+
* Inject in nami namespace for compatibility with dapps supporting only nami, when in nami mode and compatibility mode enabled.
37+
*
38+
* @returns true if injected in nami namespace, false otherwise
39+
*/
40+
const injectInNamiNs = (
41+
{ mode, dappInjectCompatibilityMode }: WalletMode,
42+
walletApi: ReturnType<typeof consumeRemoteWalletApi>,
43+
authenticator: ReturnType<typeof consumeRemoteAuthenticatorApi>,
44+
logger: cip30.InitializeInjectedDependencies['logger']
45+
): boolean => {
46+
if (mode === 'nami' && dappInjectCompatibilityMode) {
47+
const namiWallet = new Cip30Wallet(cip30WalletNamiProperties, { api: walletApi, authenticator, logger });
48+
injectGlobal(window, namiWallet, logger, 'nami');
49+
return true;
50+
}
51+
return false;
52+
};
53+
54+
export const initializeInjectedScript = async ({ logger }: cip30.InitializeInjectedDependencies): Promise<void> => {
2355
const dependencies: MessengerDependencies = {
2456
logger,
2557
runtime: injectedRuntime
@@ -28,14 +60,28 @@ const initializeInjectedScript = async ({ logger }: cip30.InitializeInjectedDepe
2860
const authenticator = consumeRemoteAuthenticatorApi(cip30WalletProperties, dependencies);
2961
const walletApi = consumeRemoteWalletApi(cip30WalletProperties, dependencies);
3062

31-
const mode = await getMode(injectedRuntime);
63+
// Always eagerly inject in lace namespace with all extensions enabled to speed up injection
64+
injectInLaceNs(walletApi, authenticator, logger);
65+
66+
const result = getWalletMode(injectedRuntime);
67+
68+
if (isCachedWalletModeResult(result)) {
69+
// Cache was primed. Use it to speed up injection
70+
const { cachedWalletMode, latestWalletMode } = result;
3271

33-
const wallet =
34-
mode === 'lace'
35-
? new Cip30Wallet(cip30WalletProperties, { api: walletApi, authenticator, logger })
36-
: new Cip30Wallet(cip30WalletNamiProperties, { api: walletApi, authenticator, logger });
72+
const namiInjected = injectInNamiNs(cachedWalletMode, walletApi, authenticator, logger);
3773

38-
injectGlobal(window, wallet, logger, mode === 'nami' ? 'nami' : wallet.name);
74+
if (!namiInjected) {
75+
// Cached values indicated that we should NOT inject in nami namespace.
76+
// No harm in waiting for the latest value and lazy inject it if was updated.
77+
const latestMode = await latestWalletMode;
78+
injectInNamiNs(latestMode, walletApi, authenticator, logger);
79+
}
80+
} else {
81+
// Cache was not primed. Wait for the latest value and inject in nami namespace if needed
82+
const { mode, dappInjectCompatibilityMode } = await result.latestWalletMode;
83+
injectInNamiNs({ mode, dappInjectCompatibilityMode }, walletApi, authenticator, logger);
84+
}
3985
};
4086

4187
if (process.env.USE_DAPP_CONNECTOR === 'true') {
Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ModeApi } from '../types';
1+
import type { LaceFeaturesApi, WalletMode } from '../types';
22

33
import {
44
consumeRemoteApi,
@@ -7,18 +7,22 @@ import {
77
RemoteApiPropertyType
88
} from '@cardano-sdk/web-extension';
99

10+
const WALLET_MODE_STORAGE_KEY = 'lace-wallet-mode';
1011
const logger = console;
1112

12-
export const WALLET_MODE_CHANNEL = 'wallet-mode';
13-
export const modeApiProperties: RemoteApiProperties<ModeApi> = {
13+
export const LACE_FEATURES_CHANNEL = 'lace-features';
14+
export const laceFeaturesApiProperties: RemoteApiProperties<LaceFeaturesApi> = {
1415
getMode: RemoteApiPropertyType.MethodReturningPromise
1516
};
1617

17-
const withModeApi = async <T>(invoke: (api: ModeApi) => Promise<T>, runtime: MinimalRuntime): Promise<T> => {
18+
const withLaceFeaturesApi = async <T>(
19+
invoke: (api: LaceFeaturesApi) => Promise<T>,
20+
runtime: MinimalRuntime
21+
): Promise<T> => {
1822
const modeApi = consumeRemoteApi(
1923
{
20-
baseChannel: WALLET_MODE_CHANNEL,
21-
properties: modeApiProperties
24+
baseChannel: LACE_FEATURES_CHANNEL,
25+
properties: laceFeaturesApiProperties
2226
},
2327
{
2428
runtime,
@@ -32,5 +36,25 @@ const withModeApi = async <T>(invoke: (api: ModeApi) => Promise<T>, runtime: Min
3236
}
3337
};
3438

35-
export const getMode = async (runtime: MinimalRuntime): Promise<'lace' | 'nami'> =>
36-
withModeApi(async (modeApi) => await modeApi.getMode(), runtime);
39+
type WalletModeResult =
40+
| { cachedWalletMode: WalletMode; latestWalletMode: Promise<WalletMode> }
41+
| { latestWalletMode: Promise<WalletMode> };
42+
43+
export const isCachedWalletModeResult = (
44+
result: WalletModeResult
45+
): result is { cachedWalletMode: WalletMode; latestWalletMode: Promise<WalletMode> } => 'cachedWalletMode' in result;
46+
47+
const loadAndStoreWalletMode = async (runtime: MinimalRuntime) =>
48+
withLaceFeaturesApi(async (laceFeaturesApi) => {
49+
const laceMode = await laceFeaturesApi.getMode();
50+
localStorage.setItem(WALLET_MODE_STORAGE_KEY, JSON.stringify(laceMode));
51+
return laceMode;
52+
}, runtime);
53+
54+
export const getWalletMode = (runtime: MinimalRuntime): WalletModeResult => {
55+
const cachedWalletMode = localStorage.getItem(WALLET_MODE_STORAGE_KEY);
56+
return {
57+
...(cachedWalletMode && { cachedWalletMode: JSON.parse(cachedWalletMode) }),
58+
latestWalletMode: loadAndStoreWalletMode(runtime)
59+
};
60+
};

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
TokenPrices,
1313
CoinPrices,
1414
ChangeModeData,
15-
ModeApi
15+
LaceFeaturesApi
1616
} from '../../types';
1717
import { Subject, of, BehaviorSubject } from 'rxjs';
1818
import { walletRoutePaths } from '@routes/wallet-paths';
@@ -23,7 +23,7 @@ import { config } from '@src/config';
2323
import { getADAPriceFromBackgroundStorage, closeAllLaceWindows } from '../util';
2424
import { currencies as currenciesMap, currencyCode } from '@providers/currency/constants';
2525
import { clearBackgroundStorage, getBackgroundStorage, setBackgroundStorage } from '../storage';
26-
import { modeApiProperties, WALLET_MODE_CHANNEL } from '../injectUtil';
26+
import { laceFeaturesApiProperties, LACE_FEATURES_CHANNEL } from '../injectUtil';
2727

2828
export const requestMessage$ = new Subject<Message>();
2929
export const backendFailures$ = new BehaviorSubject(0);
@@ -190,16 +190,16 @@ if (process.env.USE_TOKEN_PRICING === 'true') {
190190
setInterval(fetchTokenPrices, TOKEN_PRICE_CHECK_INTERVAL);
191191
}
192192

193-
exposeApi<ModeApi>(
193+
exposeApi<LaceFeaturesApi>(
194194
{
195195
api$: of({
196196
getMode: async () => {
197-
const { namiMigration } = await getBackgroundStorage();
198-
return namiMigration?.mode || 'lace';
197+
const { namiMigration, dappInjectCompatibilityMode } = await getBackgroundStorage();
198+
return { mode: namiMigration?.mode || 'lace', dappInjectCompatibilityMode: !!dappInjectCompatibilityMode };
199199
}
200200
}),
201-
baseChannel: WALLET_MODE_CHANNEL,
202-
properties: modeApiProperties
201+
baseChannel: LACE_FEATURES_CHANNEL,
202+
properties: laceFeaturesApiProperties
203203
},
204204
{ logger: console, runtime }
205205
);

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,11 @@ export type BackgroundService = {
105105
backendFailures$: BehaviorSubject<number>;
106106
};
107107

108-
export type WalletMode = BackgroundStorage['namiMigration']['mode'];
108+
export type WalletMode = {
109+
mode: BackgroundStorage['namiMigration']['mode'];
110+
dappInjectCompatibilityMode: BackgroundStorage['dappInjectCompatibilityMode'];
111+
};
109112

110-
export type ModeApi = {
113+
export type LaceFeaturesApi = {
111114
getMode: () => Promise<WalletMode>;
112115
};

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface BackgroundStorage {
3535
completed: boolean;
3636
mode: 'lace' | 'nami';
3737
};
38+
dappInjectCompatibilityMode?: boolean;
3839
optedInBeta?: boolean;
3940
}
4041

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,17 @@ export const NamiView = withDappContext((): React.ReactElement => {
9393
txBuilder: collateralTxBuilder
9494
} = useCollateral();
9595

96+
const [isCompatibilityMode, setIsCompatibilityMode] = useState<boolean>();
97+
useEffect(() => {
98+
getBackgroundStorage()
99+
.then(({ dappInjectCompatibilityMode }) => setIsCompatibilityMode(dappInjectCompatibilityMode))
100+
.catch(console.error);
101+
}, []);
102+
const handleCompatibilityModeChoice = async (newCompatMode: boolean) => {
103+
await setBackgroundStorage({ dappInjectCompatibilityMode: newCompatMode });
104+
setIsCompatibilityMode(newCompatMode);
105+
};
106+
96107
const handleResolver = useHandleResolver();
97108

98109
const cardanoPrice = priceResult.cardano.price;
@@ -195,7 +206,9 @@ export const NamiView = withDappContext((): React.ReactElement => {
195206
removeDapp,
196207
connectedDapps,
197208
isAnalyticsOptIn,
209+
isCompatibilityMode,
198210
handleAnalyticsChoice,
211+
handleCompatibilityModeChoice,
199212
createWallet: createWalletFromPrivateKey,
200213
getMnemonic,
201214
deleteWallet,

0 commit comments

Comments
 (0)