mirror of
https://github.com/zhigang1992/wallet.git
synced 2026-01-12 22:53:27 +08:00
refactor: add migration step
This commit is contained in:
@@ -22,7 +22,14 @@ module.exports = {
|
||||
from: { path: '^src/app/*', pathNot: ['^src/app/store/*'] },
|
||||
to: {
|
||||
path: ['^src/app/store/*'],
|
||||
pathNot: [`src/app.*\.hooks\.ts`, `src/app.*\.models\.ts`, `src/app.*\.utils\.ts`],
|
||||
pathNot: [
|
||||
`src/app/store/index.ts`,
|
||||
`src/app.*\.actions\.ts`,
|
||||
`src/app.*\.selectors\.ts`,
|
||||
`src/app.*\.hooks\.ts`,
|
||||
`src/app.*\.models\.ts`,
|
||||
`src/app.*\.utils\.ts`,
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -18,6 +18,7 @@ Object.keys(compilerOptions.paths).forEach(key => {
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
setupFilesAfterEnv: ['./tests/jest-unit.setup.js'],
|
||||
collectCoverage: true,
|
||||
coverageReporters: ['html', 'json-summary'],
|
||||
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"version": "3.2.0",
|
||||
"author": "Hiro Systems PBC",
|
||||
"scripts": {
|
||||
"dev": "concurrently 'node webpack/dev-server.js' 'yarn remotedev --port=8000'",
|
||||
"dev": "concurrently --raw 'node webpack/dev-server.js' 'redux-devtools --hostname=localhost --port=8000'",
|
||||
"dev:test-app": "webpack serve --config test-app/webpack/webpack.config.dev.js",
|
||||
"build": "cross-env NODE_ENV=production EXT_ENV=prod webpack --config webpack/webpack.config.prod.js",
|
||||
"build:analyze": "cross-env ANALYZE=true NODE_ENV=production EXT_ENV=prod webpack --config webpack/webpack.config.prod.js",
|
||||
@@ -165,6 +165,7 @@
|
||||
"@babel/preset-typescript": "7.16.5",
|
||||
"@babel/runtime": "7.16.5",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "0.5.4",
|
||||
"@redux-devtools/cli": "1.0.5",
|
||||
"@schemastore/web-manifest": "0.0.5",
|
||||
"@stacks/auth": "3.0.0",
|
||||
"@stacks/connect-react": "12.0.0",
|
||||
@@ -226,6 +227,7 @@
|
||||
"glob-parent": "6.0.2",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"jest": "26.6.3",
|
||||
"jest-chrome": "0.7.2",
|
||||
"jest-circus": "27.3.1",
|
||||
"jest-dev-server": "6.0.0",
|
||||
"jest-junit": "13.0.0",
|
||||
@@ -239,8 +241,7 @@
|
||||
"react-refresh": "0.11.0",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"redux-devtools-extension": "2.13.9",
|
||||
"remote-redux-devtools": "^0.5.16",
|
||||
"remotedev-server": "^0.3.1",
|
||||
"remote-redux-devtools": "0.5.16",
|
||||
"schema-inspector": "2.0.1",
|
||||
"speed-measure-webpack-plugin": "1.5.0",
|
||||
"standard-version": "9.3.0",
|
||||
@@ -265,6 +266,7 @@
|
||||
"**/**/prismjs": "1.25.0",
|
||||
"**/**/xmldom": "github:xmldom/xmldom#0.7.0",
|
||||
"**/**/@stacks/network": "3.0.0",
|
||||
"@redux-devtools/cli/**/tar": "4.4.18",
|
||||
"bn.js": "5.2.0",
|
||||
"buffer": "6.0.3",
|
||||
"css-what": "5.0.1",
|
||||
|
||||
@@ -18,7 +18,7 @@ import { jotaiWrappedReactQueryQueryClient as queryClient } from '@app/store/com
|
||||
import { theme } from './common/theme';
|
||||
import { GlobalStyles } from './components/global-styles/global-styles';
|
||||
import { AppRoutes } from './routes/app-routes';
|
||||
import { persistor, store } from './store/root-reducer';
|
||||
import { persistor, store } from './store';
|
||||
import { LoadingSpinner } from './components/loading-spinner';
|
||||
|
||||
const reactQueryDevToolsEnabled = false;
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Wallet } from '@stacks/wallet-sdk';
|
||||
|
||||
import { keySlice } from '@app/store/keys/key.slice';
|
||||
import { useAnalytics } from './analytics/use-analytics';
|
||||
import { useAppDispatch } from '@app/store';
|
||||
import { clearSessionLocalData } from '@app/common/store-utils';
|
||||
import { stxChainSlice } from '@app/store/chains/stx-chain.slice';
|
||||
import { createNewAccount } from '@app/store/chains/stx-chain.actions';
|
||||
import { setWalletEncryptionPassword, unlockWalletAction } from '@app/store/keys/keys.actions';
|
||||
|
||||
import { createNewAccount, stxChainActions } from '@app/store/chains/stx-chain.actions';
|
||||
import { keyActions } from '@app/store/keys/key.actions';
|
||||
import { useAnalytics } from './analytics/use-analytics';
|
||||
|
||||
export function useKeyActions() {
|
||||
const analytics = useAnalytics();
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
async setPassword(password: string) {
|
||||
return dispatch(setWalletEncryptionPassword(password));
|
||||
return dispatch(keyActions.setWalletEncryptionPassword(password));
|
||||
},
|
||||
|
||||
generateWalletKey() {
|
||||
return dispatch(keyActions.generateWalletKey());
|
||||
},
|
||||
|
||||
async unlockWallet(password: string) {
|
||||
return dispatch(unlockWalletAction(password));
|
||||
return dispatch(keyActions.unlockWalletAction(password));
|
||||
},
|
||||
|
||||
switchAccount(index: number) {
|
||||
return dispatch(stxChainSlice.actions.switchAccount(index));
|
||||
return dispatch(stxChainActions.switchAccount(index));
|
||||
},
|
||||
|
||||
async createNewAccount(wallet: Wallet) {
|
||||
@@ -33,12 +36,12 @@ export function useKeyActions() {
|
||||
|
||||
async signOut() {
|
||||
void analytics.track('sign_out');
|
||||
dispatch(keySlice.actions.signOut());
|
||||
dispatch(keyActions.signOut());
|
||||
clearSessionLocalData();
|
||||
},
|
||||
|
||||
lockWallet() {
|
||||
return dispatch(keySlice.actions.lockWallet());
|
||||
return dispatch(keyActions.lockWallet());
|
||||
},
|
||||
}),
|
||||
[analytics, dispatch]
|
||||
|
||||
@@ -39,7 +39,7 @@ export function useWallet() {
|
||||
const networks = useNetworkState();
|
||||
const currentNetwork = useCurrentNetworkState();
|
||||
const currentNetworkKey = useCurrentNetworkKey();
|
||||
const vaultMessenger = useKeyActions();
|
||||
const keyActions = useKeyActions();
|
||||
|
||||
const currentAccountDisplayName = currentAccount
|
||||
? getAccountDisplayName(currentAccount)
|
||||
@@ -79,6 +79,6 @@ export function useWallet() {
|
||||
setLatestNonce,
|
||||
setWallet,
|
||||
cancelAuthentication,
|
||||
...vaultMessenger,
|
||||
...keyActions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Box, Flex, Button, Stack } from '@stacks/ui';
|
||||
import { Wallet } from '@stacks/wallet-sdk';
|
||||
|
||||
import { Body } from '@app/components/typography';
|
||||
import { SettingsSelectors } from '@tests/integration/settings.selectors';
|
||||
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
|
||||
import { useKeyActions } from '@app/common/hooks/use-key-actions';
|
||||
import { useGeneratedCurrentWallet } from '@app/store/chains/stx-chain.actions';
|
||||
import { Wallet } from '@stacks/wallet-sdk';
|
||||
import { useGeneratedCurrentWallet } from '@app/store/chains/stx-chain.selectors';
|
||||
|
||||
interface CreateAccountProps {
|
||||
close: () => void;
|
||||
|
||||
@@ -15,6 +15,7 @@ import { HomePageSelectors } from '@tests/page-objects/home-page.selectors';
|
||||
import { useCurrentAccount } from '@app/store/accounts/account.hooks';
|
||||
import { AccountInfoFetcher, BalanceFetcher } from './components/fetchers';
|
||||
import { CENTERED_FULL_PAGE_MAX_WIDTH } from '@app/components/global-styles/full-page-styles';
|
||||
import { HomeTabs } from './components/home-tabs';
|
||||
|
||||
export function Home() {
|
||||
const { decodedAuthRequest } = useOnboardingState();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { validateMnemonic } from 'bip39';
|
||||
|
||||
import {
|
||||
extractPhraseFromPasteEvent,
|
||||
@@ -14,9 +15,8 @@ import {
|
||||
useSeedInputErrorState,
|
||||
} from '@app/store/onboarding/onboarding.hooks';
|
||||
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { keySlice } from '@app/store/keys/key.slice';
|
||||
import { validateMnemonic } from 'bip39';
|
||||
import { useAppDispatch } from '@app/store';
|
||||
import { keyActions } from '@app/store/keys/key.actions';
|
||||
|
||||
export function useSignIn() {
|
||||
const [, setMagicRecoveryCode] = useMagicRecoveryCodeState();
|
||||
@@ -29,7 +29,7 @@ export function useSignIn() {
|
||||
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleSetError = useCallback(
|
||||
(
|
||||
@@ -74,7 +74,7 @@ export function useSignIn() {
|
||||
}
|
||||
|
||||
try {
|
||||
dispatch(keySlice.actions.saveUsersSecretKeyToBeRestored(parsedKeyInput));
|
||||
dispatch(keyActions.saveUsersSecretKeyToBeRestored(parsedKeyInput));
|
||||
void analytics.track('submit_valid_secret_key');
|
||||
navigate(RouteUrls.SetPassword);
|
||||
setIsIdle();
|
||||
|
||||
@@ -7,17 +7,15 @@ import { useOnboardingState } from '@app/common/hooks/auth/use-onboarding-state'
|
||||
import { Header } from '@app/components/header';
|
||||
import { RouteUrls } from '@shared/route-urls';
|
||||
import { useHasAllowedDiagnostics } from '@app/store/onboarding/onboarding.hooks';
|
||||
|
||||
import { WelcomeLayout } from './welcome.layout';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { keySlice } from '@app/store/keys/key.slice';
|
||||
import { useKeyActions } from '@app/common/hooks/use-key-actions';
|
||||
|
||||
export const WelcomePage = memo(() => {
|
||||
const [hasAllowedDiagnostics] = useHasAllowedDiagnostics();
|
||||
const navigate = useNavigate();
|
||||
const { decodedAuthRequest } = useOnboardingState();
|
||||
const analytics = useAnalytics();
|
||||
const dispatch = useDispatch();
|
||||
const keyActions = useKeyActions();
|
||||
|
||||
useRouteHeader(<Header hideActions />);
|
||||
|
||||
@@ -25,9 +23,8 @@ export const WelcomePage = memo(() => {
|
||||
|
||||
const startOnboarding = useCallback(async () => {
|
||||
setIsGeneratingWallet(true);
|
||||
// await makeWallet();
|
||||
|
||||
dispatch(keySlice.actions.generateWalletKey());
|
||||
keyActions.generateWalletKey();
|
||||
|
||||
void analytics.track('generate_new_secret_key');
|
||||
|
||||
@@ -35,7 +32,7 @@ export const WelcomePage = memo(() => {
|
||||
navigate(RouteUrls.SetPassword);
|
||||
}
|
||||
navigate(RouteUrls.BackUpSecretKey);
|
||||
}, [dispatch, analytics, decodedAuthRequest, navigate]);
|
||||
}, [keyActions, analytics, decodedAuthRequest, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasAllowedDiagnostics === undefined) navigate(RouteUrls.RequestDiagnostics);
|
||||
|
||||
51
src/app/pages/save-secret-key/save-secret-key.tsx
Normal file
51
src/app/pages/save-secret-key/save-secret-key.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { memo, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Stack } from '@stacks/ui';
|
||||
|
||||
import { useRouteHeader } from '@app/common/hooks/use-route-header';
|
||||
import { useWallet } from '@app/common/hooks/use-wallet';
|
||||
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
|
||||
import { Header } from '@app/components/header';
|
||||
import { RouteUrls } from '@shared/route-urls';
|
||||
import { useCurrentKey, useGeneratedSecretKey } from '@app/store/keys/key.selectors';
|
||||
|
||||
import { SecretKeyActions } from './components/secret-key-actions';
|
||||
import { SecretKeyMessage } from './components/secret-key-message';
|
||||
import { SecretKeyCard } from './components/secret-key-card';
|
||||
|
||||
export const SaveSecretKey = memo(() => {
|
||||
const { hasSetPassword } = useWallet();
|
||||
const analytics = useAnalytics();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const tmpSecretKey = useGeneratedSecretKey();
|
||||
const currentKey = useCurrentKey();
|
||||
|
||||
const secretKey = currentKey?.secretKey ?? tmpSecretKey;
|
||||
|
||||
useRouteHeader(
|
||||
<Header
|
||||
onClose={
|
||||
hasSetPassword ? () => navigate(RouteUrls.Home) : () => navigate(RouteUrls.Onboarding)
|
||||
}
|
||||
hideActions={!hasSetPassword}
|
||||
title={hasSetPassword ? 'Your Secret Key' : 'Save your Secret Key'}
|
||||
/>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!secretKey) navigate(RouteUrls.Onboarding);
|
||||
}, [navigate, secretKey]);
|
||||
|
||||
useEffect(() => {
|
||||
void analytics.page('view', '/save-your-secret-key');
|
||||
}, [analytics]);
|
||||
|
||||
return (
|
||||
<Stack spacing="loose">
|
||||
<SecretKeyMessage />
|
||||
<SecretKeyCard seedPhrase={secretKey ?? ''} />
|
||||
<SecretKeyActions />
|
||||
</Stack>
|
||||
);
|
||||
});
|
||||
@@ -45,14 +45,14 @@ export function AppRoutes(): JSX.Element | null {
|
||||
}, [analytics, pathname]);
|
||||
|
||||
const hasStateRehydrated = useHasStateRehydrated();
|
||||
const currentWallet = useCurrentKey();
|
||||
const currentKey = useCurrentKey();
|
||||
|
||||
useEffect(() => {
|
||||
// This ensures the route is correct bc the VaultLoader is slow to set wallet state
|
||||
if (pathname === RouteUrls.Home && !currentWallet?.hasSetPassword)
|
||||
navigate(RouteUrls.Onboarding);
|
||||
if (pathname === RouteUrls.Home && !currentKey?.hasSetPassword) navigate(RouteUrls.Onboarding);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentWallet?.hasSetPassword]);
|
||||
}, [currentKey?.hasSetPassword]);
|
||||
|
||||
// check to prevent renders before the state has rehydrated
|
||||
if (!hasStateRehydrated) return <>rehdryating state</>;
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// import { useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
// import { InternalMethods } from '@shared/message-types';
|
||||
import { keyActions } from '@app/store/keys/key.actions';
|
||||
|
||||
export function useOnSignOut(_handler: () => void) {
|
||||
// useEffect(() => {
|
||||
// chrome.runtime.onMessage.addListener(message => {
|
||||
// // if (message.method === InternalMethods.signOut) handler();
|
||||
// });
|
||||
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// }, []);
|
||||
export function useOnSignOut(handler: () => void) {
|
||||
useEffect(() => {
|
||||
chrome.runtime.onMessage.addListener(message => {
|
||||
if (message?.method === keyActions.signOut.type) handler();
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// import { useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
// import { InternalMethods } from '@shared/message-types';
|
||||
import { keyActions } from '@app/store/keys/key.actions';
|
||||
|
||||
export function useOnWalletLock(_handler: () => void) {
|
||||
// useEffect(() => {
|
||||
// chrome.runtime.onMessage.addListener(message => {
|
||||
// if (message.method === InternalMethods.lockWallet) handler();
|
||||
// });
|
||||
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// }, []);
|
||||
export function useOnWalletLock(handler: () => void) {
|
||||
useEffect(() => {
|
||||
chrome.runtime.onMessage.addListener(message => {
|
||||
if (message?.method === keyActions.lockWallet.type) handler();
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { useAsync } from 'react-async-hook';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
createWalletGaiaConfig,
|
||||
generateNewAccount,
|
||||
generateWallet,
|
||||
updateWalletConfig,
|
||||
Wallet,
|
||||
} from '@stacks/wallet-sdk';
|
||||
@@ -11,11 +8,9 @@ import {
|
||||
import { gaiaUrl } from '@shared/constants';
|
||||
import { logger } from '@shared/logger';
|
||||
import { saveWalletConfigLocally } from '@shared/utils/wallet-config-helper';
|
||||
import memoize from 'promise-memoize';
|
||||
|
||||
import { selectCurrentKey, useCurrentKey } from '../keys/key.slice';
|
||||
import { AppThunk } from '../root-reducer';
|
||||
import { selectStxChain, stxChainSlice } from './stx-chain.slice';
|
||||
import { selectCurrentKey } from '@app/store/keys/key.selectors';
|
||||
import { AppThunk } from '@app/store';
|
||||
import { stxChainSlice } from './stx-chain.slice';
|
||||
|
||||
export const createNewAccount = (wallet: Wallet): AppThunk => {
|
||||
return async (dispatch, getState) => {
|
||||
@@ -44,31 +39,4 @@ export const createNewAccount = (wallet: Wallet): AppThunk => {
|
||||
};
|
||||
};
|
||||
|
||||
export const deriveWalletWithAccounts = memoize(
|
||||
async (secretKey: string, highestAccountIndex: number) => {
|
||||
// Here we only want the resulting `Wallet` objects, but the API
|
||||
// requires a password (so it can also return an encrypted key)
|
||||
const walletSdk = await generateWallet({ secretKey, password: '' });
|
||||
// To generate a new account, the wallet-sdk requires the entire `Wallet` to
|
||||
// be supplied so that it can count the `wallet.accounts[]` length, and return
|
||||
// a new `Wallet` object with all the accounts. As we want to generate them
|
||||
// all, we must set the updated value and read it again in the loop
|
||||
let walWithAccounts = walletSdk;
|
||||
for (let i = 0; i < highestAccountIndex; i++) {
|
||||
walWithAccounts = generateNewAccount(walWithAccounts);
|
||||
}
|
||||
return walWithAccounts;
|
||||
}
|
||||
);
|
||||
|
||||
export function useGeneratedCurrentWallet() {
|
||||
const currAccount = useCurrentKey();
|
||||
const stxChainState = useSelector(selectStxChain);
|
||||
return useAsync(async () => {
|
||||
if (!currAccount) return undefined;
|
||||
return deriveWalletWithAccounts(
|
||||
currAccount.secretKey,
|
||||
stxChainState.default.highestAccountIndex
|
||||
);
|
||||
}, [currAccount, stxChainState]).result;
|
||||
}
|
||||
export const stxChainActions = stxChainSlice.actions;
|
||||
|
||||
34
src/app/store/chains/stx-chain.selectors.ts
Normal file
34
src/app/store/chains/stx-chain.selectors.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Wallet } from '@stacks/wallet-sdk';
|
||||
import { useAsync } from 'react-async-hook';
|
||||
import { useSelector } from 'react-redux';
|
||||
import memoize from 'promise-memoize';
|
||||
|
||||
import { RootState } from '@app/store';
|
||||
import { useCurrentKey } from '@app/store/keys/key.selectors';
|
||||
import { RequestDerivedStxAccounts } from '@shared/vault/vault-types';
|
||||
import { InternalMethods } from '@shared/message-types';
|
||||
|
||||
const selectStxChain = (state: RootState) => state.chains.stx;
|
||||
|
||||
export const deriveWalletWithAccounts = memoize(
|
||||
async (secretKey: string, highestAccountIndex: number): Promise<Wallet> => {
|
||||
const message: RequestDerivedStxAccounts = {
|
||||
method: InternalMethods.RequestDerivedStxAccounts,
|
||||
payload: { secretKey, highestAccountIndex },
|
||||
};
|
||||
return new Promise(resolve => chrome.runtime.sendMessage(message, resp => resolve(resp)));
|
||||
}
|
||||
);
|
||||
|
||||
export function useGeneratedCurrentWallet() {
|
||||
const currAccount = useCurrentKey();
|
||||
const stxChainState = useSelector(selectStxChain);
|
||||
return useAsync(async () => {
|
||||
if (!currAccount) return undefined;
|
||||
if (!currAccount.secretKey) return undefined;
|
||||
return deriveWalletWithAccounts(
|
||||
currAccount.secretKey,
|
||||
stxChainState.default.highestAccountIndex
|
||||
);
|
||||
}, [currAccount, stxChainState]).result;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { keySlice } from '../keys/key.slice';
|
||||
import { RootState } from '../root-reducer';
|
||||
|
||||
interface StxChainKeyState {
|
||||
highestAccountIndex: number;
|
||||
@@ -37,5 +36,3 @@ export const stxChainSlice = createSlice({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const selectStxChain = (state: RootState) => state.chains.stx;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { atomWithStore } from 'jotai/redux';
|
||||
import storage from 'redux-persist/lib/storage';
|
||||
import devToolsEnhancer from 'remote-redux-devtools';
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
|
||||
import { keySlice } from './keys/key.slice';
|
||||
import { stxChainSlice } from './chains/stx-chain.slice';
|
||||
import { broadcastActionTypeToOtherFramesMiddleware } from './utils/broadcast-action-types';
|
||||
|
||||
// const storage = new ChromeStorage(chrome.storage.local, chrome.runtime);
|
||||
|
||||
@@ -37,12 +38,14 @@ const persistedReducer = persistReducer(persistConfig, rootReducer);
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: persistedReducer,
|
||||
middleware: getDefaultMiddleware =>
|
||||
getDefaultMiddleware({
|
||||
middleware: getDefaultMiddleware => [
|
||||
...getDefaultMiddleware({
|
||||
serializableCheck: {
|
||||
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
|
||||
},
|
||||
}),
|
||||
broadcastActionTypeToOtherFramesMiddleware,
|
||||
],
|
||||
enhancers: [
|
||||
devToolsEnhancer({
|
||||
hostname: 'localhost',
|
||||
@@ -54,15 +57,17 @@ export const store = configureStore({
|
||||
|
||||
export const persistor = persistStore(store);
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export type RootState = ReturnType<typeof persistedReducer>;
|
||||
|
||||
export type AppThunk<ReturnType = void> = ThunkAction<ReturnType, RootState, unknown, AnyAction>;
|
||||
|
||||
type AppDispatch = typeof store.dispatch;
|
||||
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
|
||||
export const storeAtom = atomWithStore(store);
|
||||
|
||||
export const selectHasRehydrated = (state: RootState) => state._persist.rehydrated;
|
||||
const selectHasRehydrated = (state: RootState) => state._persist.rehydrated;
|
||||
|
||||
export function useHasStateRehydrated() {
|
||||
return useSelector(selectHasRehydrated);
|
||||
@@ -1,11 +1,12 @@
|
||||
import { encryptMnemonic } from '@background/crypto/mnemonic-encryption';
|
||||
import { getDecryptedWalletDetails } from '@background/wallet/unlock-wallet';
|
||||
import { gaiaUrl } from '@shared/constants';
|
||||
import { generateWallet, restoreWalletAccounts } from '@stacks/wallet-sdk';
|
||||
import { stxChainSlice } from '../chains/stx-chain.slice';
|
||||
|
||||
import { AppThunk } from '../root-reducer';
|
||||
import { keySlice, selectCurrentKey, selectGeneratedSecretKey } from './key.slice';
|
||||
import { decryptMnemonic, encryptMnemonic } from '@shared/crypto/mnemonic-encryption';
|
||||
import { gaiaUrl } from '@shared/constants';
|
||||
import { AppThunk } from '@app/store';
|
||||
|
||||
import { stxChainSlice } from '../chains/stx-chain.slice';
|
||||
import { keySlice } from './key.slice';
|
||||
import { selectCurrentKey, selectGeneratedSecretKey } from './key.selectors';
|
||||
|
||||
async function restoredWalletHighestGeneratedAccountIndex(secretKey: string) {
|
||||
try {
|
||||
@@ -23,18 +24,17 @@ async function restoredWalletHighestGeneratedAccountIndex(secretKey: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export const setWalletEncryptionPassword = (password: string): AppThunk => {
|
||||
const setWalletEncryptionPassword = (password: string): AppThunk => {
|
||||
return async (dispatch, getState) => {
|
||||
const secretKey = selectGeneratedSecretKey(getState());
|
||||
if (!secretKey) throw new Error('Cannot generate wallet without first having generated a key');
|
||||
const { encryptedSecretKey, salt } = await encryptMnemonic({ secretKey, password });
|
||||
const highestAccountIndex = await restoredWalletHighestGeneratedAccountIndex(secretKey);
|
||||
dispatch(
|
||||
keySlice.actions.addNewKey({
|
||||
keySlice.actions.createNewWalletComplete({
|
||||
type: 'software',
|
||||
id: 'default',
|
||||
salt,
|
||||
hasSetPassword: true,
|
||||
secretKey,
|
||||
encryptedSecretKey,
|
||||
})
|
||||
@@ -44,12 +44,18 @@ export const setWalletEncryptionPassword = (password: string): AppThunk => {
|
||||
};
|
||||
};
|
||||
|
||||
export const unlockWalletAction = (password: string): AppThunk => {
|
||||
const unlockWalletAction = (password: string): AppThunk => {
|
||||
return async (dispatch, getState) => {
|
||||
const currentKey = selectCurrentKey(getState());
|
||||
if (!currentKey) return;
|
||||
const { encryptedSecretKey, salt } = currentKey;
|
||||
const vault = await getDecryptedWalletDetails(encryptedSecretKey, password, salt);
|
||||
dispatch(keySlice.actions.updateCurrentWallet({ id: 'default', changes: { ...vault } }));
|
||||
const decryptedData = await decryptMnemonic({ encryptedSecretKey, password, salt });
|
||||
dispatch(keySlice.actions.unlockWalletComplete(decryptedData));
|
||||
};
|
||||
};
|
||||
|
||||
export const keyActions = {
|
||||
...keySlice.actions,
|
||||
setWalletEncryptionPassword,
|
||||
unlockWalletAction,
|
||||
};
|
||||
24
src/app/store/keys/key.selectors.ts
Normal file
24
src/app/store/keys/key.selectors.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useMemo } from 'react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { RootState } from '@app/store';
|
||||
|
||||
const selectWalletSlice = (state: RootState) => state.keys;
|
||||
|
||||
export const selectGeneratedSecretKey = createSelector(selectWalletSlice, state => state.secretKey);
|
||||
|
||||
export const selectCurrentKey = createSelector(selectWalletSlice, state => state.entities.default);
|
||||
|
||||
export function withDerivedKeyInformation(key: ReturnType<typeof selectCurrentKey>) {
|
||||
return { ...key, hasSetPassword: !!key?.encryptedSecretKey };
|
||||
}
|
||||
|
||||
export function useCurrentKey() {
|
||||
const currentKey = useSelector(selectCurrentKey);
|
||||
return useMemo(() => withDerivedKeyInformation(currentKey), [currentKey]);
|
||||
}
|
||||
|
||||
export function useGeneratedSecretKey() {
|
||||
return useSelector(selectGeneratedSecretKey);
|
||||
}
|
||||
@@ -1,19 +1,13 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createEntityAdapter, createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createEntityAdapter, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { generateSecretKey } from '@stacks/wallet-sdk';
|
||||
|
||||
import { RootState } from '../root-reducer';
|
||||
import { migrateVaultReducerStoreToNewStateStructure } from '../utils/vault-reducer-migration';
|
||||
|
||||
interface KeyConfigSoftware {
|
||||
type: 'software';
|
||||
id: string;
|
||||
encryptedSecretKey: string;
|
||||
salt: string;
|
||||
// Legacy property from pre-vault reducer refactor. A wallet never arrives to
|
||||
// the state prior to there being a password set for the key's encryption.
|
||||
// `hasSetPassword` can later be inferred on the presence of an encryptedKey
|
||||
// property.
|
||||
hasSetPassword: boolean;
|
||||
secretKey: string;
|
||||
}
|
||||
|
||||
@@ -21,13 +15,15 @@ const keyAdapter = createEntityAdapter<KeyConfigSoftware>();
|
||||
|
||||
// Only used during onboarding, pre-wallet creation
|
||||
// Could well be persisted elsewhere
|
||||
interface TempKeyState {
|
||||
interface ExtraKeyState {
|
||||
secretKey?: null | string;
|
||||
}
|
||||
|
||||
export const initialKeysState = keyAdapter.getInitialState<ExtraKeyState>({ secretKey: null });
|
||||
|
||||
export const keySlice = createSlice({
|
||||
name: 'keys',
|
||||
initialState: keyAdapter.getInitialState<TempKeyState>({ secretKey: null }),
|
||||
initialState: migrateVaultReducerStoreToNewStateStructure(initialKeysState),
|
||||
reducers: {
|
||||
generateWalletKey(state) {
|
||||
state.secretKey = generateSecretKey(256);
|
||||
@@ -41,12 +37,14 @@ export const keySlice = createSlice({
|
||||
state.secretKey = null;
|
||||
},
|
||||
|
||||
addNewKey(state, action: PayloadAction<KeyConfigSoftware>) {
|
||||
createNewWalletComplete(state, action: PayloadAction<KeyConfigSoftware>) {
|
||||
state.secretKey = null;
|
||||
keyAdapter.addOne(state, action.payload);
|
||||
},
|
||||
|
||||
updateCurrentWallet: keyAdapter.updateOne,
|
||||
unlockWalletComplete(state, action: PayloadAction<Partial<KeyConfigSoftware>>) {
|
||||
keyAdapter.updateOne(state, { id: 'default', changes: action.payload });
|
||||
},
|
||||
|
||||
lockWallet(state) {
|
||||
keyAdapter.updateOne(state, { id: 'default', changes: { secretKey: null } as any });
|
||||
@@ -57,13 +55,3 @@ export const keySlice = createSlice({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const selectWalletSlice = (state: RootState) => state.keys;
|
||||
|
||||
export const selectGeneratedSecretKey = createSelector(selectWalletSlice, state => state.secretKey);
|
||||
|
||||
export const selectCurrentKey = createSelector(selectWalletSlice, state => state.entities.default);
|
||||
|
||||
export function useCurrentKey() {
|
||||
return useSelector(selectCurrentKey);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
magicRecoveryCodeState,
|
||||
secretKeyState,
|
||||
seedInputErrorState,
|
||||
seedInputState,
|
||||
} from './onboarding';
|
||||
|
||||
export function useAuthRequest() {
|
||||
@@ -18,10 +17,6 @@ export function useUpdateAuthRequest() {
|
||||
return useUpdateAtom(authRequestState);
|
||||
}
|
||||
|
||||
export function useSeedInputState() {
|
||||
return useAtom(seedInputState);
|
||||
}
|
||||
|
||||
export function useSeedInputErrorState() {
|
||||
return useAtom(seedInputErrorState);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ interface AuthRequestState {
|
||||
}
|
||||
|
||||
export const magicRecoveryCodePasswordState = atom('');
|
||||
export const seedInputState = atom('');
|
||||
export const seedInputErrorState = atom<string | undefined>(undefined);
|
||||
export const secretKeyState = atom(null);
|
||||
export const magicRecoveryCodeState = atom<null | string>(null);
|
||||
@@ -29,10 +28,3 @@ export const hasAllowedDiagnosticsState = atomWithStorage<boolean | undefined>(
|
||||
userHasAllowedDiagnosticsKey,
|
||||
undefined
|
||||
);
|
||||
|
||||
magicRecoveryCodePasswordState.debugLabel = 'magicRecoveryCodePasswordState';
|
||||
seedInputState.debugLabel = 'seedInputState';
|
||||
seedInputErrorState.debugLabel = 'seedInputErrorState';
|
||||
secretKeyState.debugLabel = 'secretKeyState';
|
||||
magicRecoveryCodeState.debugLabel = 'magicRecoveryCodeState';
|
||||
authRequestState.debugLabel = 'authRequestState';
|
||||
|
||||
7
src/app/store/utils/broadcast-action-types.ts
Normal file
7
src/app/store/utils/broadcast-action-types.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { AnyAction, Middleware } from '@reduxjs/toolkit';
|
||||
|
||||
export const broadcastActionTypeToOtherFramesMiddleware: Middleware =
|
||||
_store => next => (action: AnyAction) => {
|
||||
chrome.runtime.sendMessage({ method: action.type });
|
||||
return next(action);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
const driverMap = new WeakMap();
|
||||
const runtimeMap = new WeakMap();
|
||||
|
||||
// ts-unused-exports:disable-next-line
|
||||
export class ChromeStorage {
|
||||
constructor(driver: any, runtime: any) {
|
||||
driverMap.set(this, driver);
|
||||
52
src/app/store/utils/vault-reducer-migration.spec.ts
Normal file
52
src/app/store/utils/vault-reducer-migration.spec.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { LocalStorageMock } from '@tests/mocks/localStorage-mock';
|
||||
import { migrateVaultReducerStoreToNewStateStructure } from './vault-reducer-migration';
|
||||
|
||||
(global as any).localStorage = new LocalStorageMock();
|
||||
|
||||
describe(migrateVaultReducerStoreToNewStateStructure.name, () => {
|
||||
describe('migration scenario', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.setItem('stacks-wallet-salt', 'test-salt');
|
||||
localStorage.setItem('stacks-wallet-encrypted-key', 'test-encrypted-key');
|
||||
});
|
||||
|
||||
test('that it reads localstorage wallet values', () => {
|
||||
jest.spyOn(global.localStorage.__proto__, 'getItem');
|
||||
|
||||
migrateVaultReducerStoreToNewStateStructure({} as any);
|
||||
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith('stacks-wallet-salt');
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith('stacks-wallet-encrypted-key');
|
||||
});
|
||||
|
||||
test('that it returns a migrated state object when wallet values are detected', () => {
|
||||
const returnedValue = migrateVaultReducerStoreToNewStateStructure({} as any);
|
||||
expect(returnedValue).toEqual({
|
||||
ids: ['default'],
|
||||
entities: {
|
||||
default: {
|
||||
type: 'software',
|
||||
id: 'default',
|
||||
encryptedSecretKey: 'test-encrypted-key',
|
||||
salt: 'test-salt',
|
||||
hasSetPassword: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('it removes the existing existing localStorage values', () => {
|
||||
jest.spyOn(global.localStorage.__proto__, 'removeItem');
|
||||
migrateVaultReducerStoreToNewStateStructure({} as any);
|
||||
expect(localStorage.removeItem).toHaveBeenCalledWith('stacks-wallet-salt');
|
||||
expect(localStorage.removeItem).toHaveBeenCalledWith('stacks-wallet-encrypted-key');
|
||||
});
|
||||
});
|
||||
|
||||
describe('no migration needed scenario', () => {
|
||||
test('nothing happens when no localStorage values are detected', () => {
|
||||
const returnedValue = migrateVaultReducerStoreToNewStateStructure({} as any);
|
||||
expect(returnedValue).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
30
src/app/store/utils/vault-reducer-migration.ts
Normal file
30
src/app/store/utils/vault-reducer-migration.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import deepMerge from 'deepmerge';
|
||||
|
||||
import { logger } from '@shared/logger';
|
||||
import type { initialKeysState } from '../keys/key.slice';
|
||||
|
||||
export function migrateVaultReducerStoreToNewStateStructure(initialState: typeof initialKeysState) {
|
||||
const salt = localStorage.getItem('stacks-wallet-salt');
|
||||
const encryptedSecretKey = localStorage.getItem('stacks-wallet-encrypted-key');
|
||||
if (salt && encryptedSecretKey) {
|
||||
logger.debug(
|
||||
'VaultReducer generated Hiro Wallet detected. Running migrating to keys store structure'
|
||||
);
|
||||
const migratedState = {
|
||||
ids: ['default'],
|
||||
entities: {
|
||||
default: {
|
||||
type: 'software',
|
||||
id: 'default',
|
||||
encryptedSecretKey,
|
||||
salt,
|
||||
hasSetPassword: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
localStorage.removeItem('stacks-wallet-salt');
|
||||
localStorage.removeItem('stacks-wallet-encrypted-key');
|
||||
return deepMerge(initialState, migratedState);
|
||||
}
|
||||
return initialState;
|
||||
}
|
||||
@@ -1,29 +1,27 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useAtomCallback, useAtomValue } from 'jotai/utils';
|
||||
import type { BackgroundActions, InMemorySoftwareWalletVault } from '@shared/vault/vault-types';
|
||||
import { gaiaUrl } from '@shared/constants';
|
||||
import { useOnboardingState } from '@app/common/hooks/auth/use-onboarding-state';
|
||||
|
||||
import {
|
||||
createWalletGaiaConfig,
|
||||
getOrCreateWalletConfig,
|
||||
makeAuthResponse,
|
||||
updateWalletConfigWithApp,
|
||||
} from '@stacks/wallet-sdk';
|
||||
|
||||
import { gaiaUrl } from '@shared/constants';
|
||||
import { useOnboardingState } from '@app/common/hooks/auth/use-onboarding-state';
|
||||
import { currentAccountStxAddressState } from '@app/store/accounts';
|
||||
import { localNonceState } from '@app/store/accounts/nonce';
|
||||
import { currentNetworkState } from '@app/store/network/networks';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { finalizeAuthResponse } from '@app/common/actions/finalize-auth-response';
|
||||
import { logger } from '@shared/logger';
|
||||
import {
|
||||
encryptedSecretKeyState,
|
||||
hasSetPasswordState,
|
||||
secretKeyState,
|
||||
walletState,
|
||||
} from './wallet';
|
||||
import { finalizeAuthResponse } from '@app/common/actions/finalize-auth-response';
|
||||
import { logger } from '@shared/logger';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { stxChainSlice } from '../chains/stx-chain.slice';
|
||||
import { useKeyActions } from '@app/common/hooks/use-key-actions';
|
||||
|
||||
export function useWalletState() {
|
||||
return useAtom(walletState);
|
||||
@@ -57,7 +55,7 @@ export function useSetLatestNonceCallback() {
|
||||
|
||||
export function useFinishSignInCallback() {
|
||||
const { decodedAuthRequest, authRequest, appName, appIcon } = useOnboardingState();
|
||||
const dispatch = useDispatch();
|
||||
const keyActions = useKeyActions();
|
||||
return useAtomCallback<void, number>(
|
||||
useCallback(
|
||||
async (get, _set, accountIndex) => {
|
||||
@@ -94,18 +92,10 @@ export function useFinishSignInCallback() {
|
||||
scopes: decodedAuthRequest.scopes,
|
||||
account,
|
||||
});
|
||||
dispatch(stxChainSlice.actions.switchAccount(accountIndex));
|
||||
keyActions.switchAccount(accountIndex);
|
||||
finalizeAuthResponse({ decodedAuthRequest, authRequest, authResponse });
|
||||
},
|
||||
[decodedAuthRequest, authRequest, appIcon, appName, dispatch]
|
||||
[decodedAuthRequest, authRequest, appIcon, appName, keyActions]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function sendBackgroundMessage(message: BackgroundActions) {
|
||||
return new Promise(resolve =>
|
||||
chrome.runtime.sendMessage(message, (vaultResponse: InMemorySoftwareWalletVault) => {
|
||||
resolve(vaultResponse);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ import { atom } from 'jotai';
|
||||
import { fetchWalletConfig, createWalletGaiaConfig } from '@stacks/wallet-sdk';
|
||||
import { gaiaUrl } from '@shared/constants';
|
||||
import { textToBytes } from '@app/common/store-utils';
|
||||
import { storeAtom } from '../root-reducer';
|
||||
import { deriveWalletWithAccounts } from '../chains/stx-chain.actions';
|
||||
import { storeAtom } from '..';
|
||||
import { deriveWalletWithAccounts } from '../chains/stx-chain.selectors';
|
||||
import { withDerivedKeyInformation } from '../keys/key.selectors';
|
||||
|
||||
export const walletState = atom(async get => {
|
||||
const store = get(storeAtom);
|
||||
@@ -24,7 +25,8 @@ export const walletConfigState = atom(async get => {
|
||||
|
||||
export const hasSetPasswordState = atom(get => {
|
||||
const store = get(storeAtom);
|
||||
return !!store.keys.entities.default?.hasSetPassword;
|
||||
if (!store.keys.entities.default) return;
|
||||
return withDerivedKeyInformation(store.keys.entities.default).hasSetPassword;
|
||||
});
|
||||
|
||||
export const encryptedSecretKeyState = atom(get => {
|
||||
|
||||
@@ -13,14 +13,12 @@ import { initSentry } from '@shared/utils/sentry-init';
|
||||
import {
|
||||
CONTENT_SCRIPT_PORT,
|
||||
ExternalMethods,
|
||||
InternalMethods,
|
||||
MessageFromContentScript,
|
||||
} from '@shared/message-types';
|
||||
|
||||
import { popupCenter } from '@background/popup-center';
|
||||
import type { BackgroundActions } from '@shared/vault/vault-types';
|
||||
import { initContextMenuActions } from '@background/init-context-menus';
|
||||
import { logger } from '@shared/logger';
|
||||
import { backgroundMessageHandler } from './message-handler';
|
||||
|
||||
const IS_TEST_ENV = process.env.TEST_ENV === 'true';
|
||||
|
||||
@@ -28,6 +26,7 @@ initSentry();
|
||||
|
||||
initContextMenuActions();
|
||||
|
||||
//
|
||||
// Playwright does not currently support Chrome extension popup testing:
|
||||
// https://github.com/microsoft/playwright/issues/5593
|
||||
async function openRequestInFullPage(path: string, urlParams: URLSearchParams) {
|
||||
@@ -36,7 +35,6 @@ async function openRequestInFullPage(path: string, urlParams: URLSearchParams) {
|
||||
});
|
||||
}
|
||||
|
||||
// Listen for install event
|
||||
chrome.runtime.onInstalled.addListener(details => {
|
||||
Sentry.wrap(async () => {
|
||||
if (details.reason === 'install' && !IS_TEST_ENV) {
|
||||
@@ -95,25 +93,12 @@ chrome.runtime.onConnect.addListener(port =>
|
||||
})
|
||||
);
|
||||
|
||||
function validateMessagesAreFromExtension(sender: chrome.runtime.MessageSender) {
|
||||
// Only respond to internal messages from our UI, not content scripts in other applications
|
||||
return sender.url?.startsWith(chrome.runtime.getURL(''));
|
||||
}
|
||||
|
||||
// Listen for events triggered by the background memory vault
|
||||
chrome.runtime.onMessage.addListener((message: BackgroundActions, sender, sendResponse) =>
|
||||
//
|
||||
// Events from the popup script
|
||||
// Listener fn must return `true` to indicate the response will be async
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) =>
|
||||
Sentry.wrap(() => {
|
||||
if (!validateMessagesAreFromExtension(sender)) {
|
||||
logger.error('Received background script msg from ' + sender.url);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (message.method) {
|
||||
case InternalMethods.TestAction: {
|
||||
sendResponse();
|
||||
}
|
||||
}
|
||||
|
||||
void backgroundMessageHandler(message, sender, sendResponse);
|
||||
return true;
|
||||
})
|
||||
);
|
||||
|
||||
43
src/background/message-handler.ts
Normal file
43
src/background/message-handler.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { logger } from '@shared/logger';
|
||||
import { InternalMethods } from '@shared/message-types';
|
||||
import { BackgroundActions } from '@shared/vault/vault-types';
|
||||
import { generateNewAccount, generateWallet } from '@stacks/wallet-sdk';
|
||||
import memoize from 'promise-memoize';
|
||||
|
||||
function validateMessagesAreFromExtension(sender: chrome.runtime.MessageSender) {
|
||||
// Only respond to internal messages from our UI, not content scripts in other applications
|
||||
return sender.url?.startsWith(chrome.runtime.getURL(''));
|
||||
}
|
||||
|
||||
const deriveWalletWithAccounts = memoize(async (secretKey: string, highestAccountIndex: number) => {
|
||||
// Here we only want the resulting `Wallet` objects, but the API
|
||||
// requires a password (so it can also return an encrypted key)
|
||||
const walletSdk = await generateWallet({ secretKey, password: '' });
|
||||
// To generate a new account, the wallet-sdk requires the entire `Wallet` to
|
||||
// be supplied so that it can count the `wallet.accounts[]` length, and return
|
||||
// a new `Wallet` object with all the accounts. As we want to generate them
|
||||
// all, we must set the updated value and read it again in the loop
|
||||
let walWithAccounts = walletSdk;
|
||||
for (let i = 0; i < highestAccountIndex; i++) {
|
||||
walWithAccounts = generateNewAccount(walWithAccounts);
|
||||
}
|
||||
return walWithAccounts;
|
||||
});
|
||||
|
||||
export async function backgroundMessageHandler(
|
||||
message: BackgroundActions,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
sendResponse: (response?: any) => void
|
||||
) {
|
||||
if (!validateMessagesAreFromExtension(sender)) {
|
||||
logger.error('Error: Received background script msg from ' + sender.url);
|
||||
return;
|
||||
}
|
||||
switch (message.method) {
|
||||
case InternalMethods.RequestDerivedStxAccounts: {
|
||||
const { secretKey, highestAccountIndex } = message.payload;
|
||||
const walletsWithAccounts = await deriveWalletWithAccounts(secretKey, highestAccountIndex);
|
||||
sendResponse(walletsWithAccounts);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { decryptMnemonic } from '@background/crypto/mnemonic-encryption';
|
||||
import { getWallet } from './wallet-utils';
|
||||
|
||||
export async function getDecryptedWalletDetails(
|
||||
encryptedSecretKey: string,
|
||||
password: string,
|
||||
salt: string | undefined
|
||||
) {
|
||||
const hasSetPassword = password !== undefined;
|
||||
|
||||
const decryptedData = await decryptMnemonic({
|
||||
encryptedSecretKey,
|
||||
password,
|
||||
salt,
|
||||
});
|
||||
|
||||
const keyInfo = {
|
||||
secretKey: decryptedData.secretKey,
|
||||
encryptedSecretKey: encryptedSecretKey,
|
||||
currentAccountIndex: 0,
|
||||
hasSetPassword,
|
||||
};
|
||||
|
||||
const wallet = await getWallet({
|
||||
secretKey: decryptedData.secretKey,
|
||||
salt: decryptedData.salt,
|
||||
password,
|
||||
});
|
||||
|
||||
console.log({ wallet });
|
||||
|
||||
// if (!wallet) return;
|
||||
|
||||
const result = {
|
||||
...keyInfo,
|
||||
wallet,
|
||||
salt: decryptedData.salt,
|
||||
encryptedSecretKey: decryptedData.encryptedSecretKey,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export enum ExternalMethods {
|
||||
}
|
||||
|
||||
export enum InternalMethods {
|
||||
TestAction = 'TestAction',
|
||||
RequestDerivedStxAccounts = 'RequestDerivedStxAccounts',
|
||||
}
|
||||
|
||||
export type ExtensionMethods = ExternalMethods | InternalMethods;
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
import { Wallet } from '@stacks/wallet-sdk';
|
||||
|
||||
import { ExtensionMethods, InternalMethods, Message } from '@shared/message-types';
|
||||
|
||||
// In-memory (background) wallet instance
|
||||
export interface InMemorySoftwareWalletVault {
|
||||
encryptedSecretKey?: string;
|
||||
salt?: string;
|
||||
secretKey?: string;
|
||||
wallet?: Wallet;
|
||||
currentAccountIndex?: number;
|
||||
hasSetPassword: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vault <-> Background Script
|
||||
*/
|
||||
@@ -20,6 +8,9 @@ type BackgroundMessage<Msg extends ExtensionMethods, Payload = undefined> = Omit
|
||||
'source'
|
||||
>;
|
||||
|
||||
export type TestMessage = BackgroundMessage<InternalMethods.TestAction, string>;
|
||||
export type RequestDerivedStxAccounts = BackgroundMessage<
|
||||
InternalMethods.RequestDerivedStxAccounts,
|
||||
{ secretKey: string; highestAccountIndex: number }
|
||||
>;
|
||||
|
||||
export type BackgroundActions = TestMessage;
|
||||
export type BackgroundActions = RequestDerivedStxAccounts;
|
||||
|
||||
1
tests/jest-unit.setup.js
Normal file
1
tests/jest-unit.setup.js
Normal file
@@ -0,0 +1 @@
|
||||
Object.assign(global, require('jest-chrome'));
|
||||
22
tests/mocks/localStorage-mock.ts
Normal file
22
tests/mocks/localStorage-mock.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export class LocalStorageMock {
|
||||
store: any;
|
||||
constructor() {
|
||||
this.store = {};
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.store = {};
|
||||
}
|
||||
|
||||
getItem(key: string) {
|
||||
return this.store[key] || null;
|
||||
}
|
||||
|
||||
setItem(key: string, value: string) {
|
||||
this.store[key] = String(value);
|
||||
}
|
||||
|
||||
removeItem(key: string) {
|
||||
delete this.store[key];
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ const config = {
|
||||
inpage: path.join(SRC_ROOT_PATH, 'inpage', 'inpage.ts'),
|
||||
'content-script': path.join(SRC_ROOT_PATH, 'content-scripts', 'content-script.ts'),
|
||||
index: path.join(SRC_ROOT_PATH, 'app', 'index.tsx'),
|
||||
'decryption-worker': path.join(SRC_ROOT_PATH, 'background/workers/decryption-worker.ts'),
|
||||
'decryption-worker': path.join(SRC_ROOT_PATH, 'shared/workers/decryption-worker.ts'),
|
||||
},
|
||||
output: {
|
||||
path: DIST_ROOT_PATH,
|
||||
|
||||
Reference in New Issue
Block a user