Merge branch 'fix-unauth' into 'main'
[ProtonMail-WebClient.git] / packages / wallet / hooks / useWalletAutoCreate.ts
blobc3e8c616ec8c811bcb9e4dcbcc928df1b85d4576
1 import { useEffect } from 'react';
3 import { useGetAddresses } from '@proton/account/addresses/hooks';
4 import { useGetOrganization } from '@proton/account/organization/hooks';
5 import { useUser } from '@proton/account/user/hooks';
6 import { useGetUserKeys } from '@proton/account/userKeys/hooks';
7 import type andromedaModule from '@proton/andromeda';
8 import type {
9     WasmApiWallet,
10     WasmApiWalletAccount,
11     WasmFiatCurrencySymbol,
12     WasmNetwork,
13     WasmProtonWalletApiClient,
14     WasmUserSettings,
15     WasmWallet,
16 } from '@proton/andromeda';
17 import useAuthentication from '@proton/components/hooks/useAuthentication';
18 import useConfig from '@proton/components/hooks/useConfig';
19 import { getClientID } from '@proton/shared/lib/apps/helper';
20 import { getAppVersionStr } from '@proton/shared/lib/fetch/headers';
21 import { getDecryptedAddressKeysHelper } from '@proton/shared/lib/keys';
23 import { WalletType } from '../types/api';
24 import { encryptWalletData } from '../utils/crypto';
25 import { getDefaultWalletName } from '../utils/wallet';
26 import { isWasmSupported, loadWasmModule } from '../utils/wasm';
28 const DEFAULT_ACCOUNT_LABEL = 'Primary Account';
30 const FIRST_INDEX = 0;
32 // Flag to tell the API the wallet was autocreated
33 const WALLET_AUTOCREATE_FLAG = true;
35 let userWalletSettings: WasmUserSettings;
36 let wasm: typeof andromedaModule;
38 /**
39  * Utility hook creating a wallet if user don't have any
40  *
41  * Requiremenents
42  * - this hook needs to be called inside ExtendedApiContext (see @proton/wallet/contexts/ExtendedApiContext)
43  * - this hook need to be called inside a Redux context which walletReducers (see @proton/wallet/store/slices/index.ts)
44  *
45  * Note:
46  * - For now we create a new wallet for any user without one. Later a field will be introduced by the API so that we can filter only user that never had a wallet
47  */
48 export const useWalletAutoCreate = ({ higherLevelPilot = true }: { higherLevelPilot?: boolean }) => {
49     const getUserKeys = useGetUserKeys();
50     const getOrganization = useGetOrganization();
51     const getAddresses = useGetAddresses();
52     const config = useConfig();
54     const [user] = useUser();
55     const authentication = useAuthentication();
57     const getWalletApi = async () => {
58         const appVersion = getAppVersionStr(getClientID(config.APP_NAME), config.APP_VERSION);
59         return new wasm.WasmProtonWalletApiClient(
60             appVersion,
61             navigator.userAgent,
62             authentication.UID,
63             window.location.origin,
64             config.API_URL
65         ).clients();
66     };
68     const defaultScriptType = () => wasm.WasmScriptType.NativeSegwit;
70     const enableBitcoinViaEmail = async ({
71         wallet,
72         walletAccount,
73         wasmWallet,
74         walletApi,
75         derivationPathParts,
76     }: {
77         wallet: WasmApiWallet;
78         walletAccount: WasmApiWalletAccount;
79         wasmWallet: WasmWallet;
80         walletApi: ReturnType<WasmProtonWalletApiClient['clients']>;
81         derivationPathParts: readonly [number, WasmNetwork, 0];
82     }) => {
83         const addresses = await getAddresses();
84         const userKeys = await getUserKeys();
86         const wasmAccount = new wasm.WasmAccount(
87             wasmWallet,
88             defaultScriptType(),
89             wasm.WasmDerivationPath.fromParts(...derivationPathParts)
90         );
92         const [primaryAddress] = addresses;
93         const [primaryAddressKey] = await getDecryptedAddressKeysHelper(
94             primaryAddress.Keys,
95             user,
96             userKeys,
97             authentication.getPassword()
98         );
100         await walletApi.wallet.addEmailAddress(wallet.ID, walletAccount.ID, primaryAddress.ID);
102         const account = await import('../utils/account');
104         // Fill bitcoin address pool
105         const addressesPoolPayload = await account.generateBitcoinAddressesPayloadToFillPool({
106             addressesToCreate: walletAccount.PoolSize,
107             wasmAccount,
108             walletAccountAddressKey: primaryAddressKey,
109         });
111         if (addressesPoolPayload?.[0]?.length) {
112             await walletApi.bitcoin_address.addBitcoinAddresses(wallet.ID, walletAccount.ID, addressesPoolPayload);
113         }
114     };
116     const setupWalletAccount = async ({
117         wallet,
118         label,
119         derivationPathParts,
120         walletApi,
121         fiatCurrency,
122     }: {
123         wallet: WasmApiWallet;
124         label: string;
125         derivationPathParts: readonly [number, WasmNetwork, 0];
126         walletApi: ReturnType<WasmProtonWalletApiClient['clients']>;
127         fiatCurrency: WasmFiatCurrencySymbol;
128     }) => {
129         const account = await walletApi.wallet.createWalletAccount(
130             wallet.ID,
131             wasm.WasmDerivationPath.fromParts(...derivationPathParts),
132             label,
133             defaultScriptType()
134         );
136         await walletApi.wallet.updateWalletAccountFiatCurrency(wallet.ID, account.Data.ID, fiatCurrency);
138         return account;
139     };
141     const autoCreateWallet = async () => {
142         try {
143             const walletApi = await getWalletApi();
145             const userKeys = await getUserKeys();
146             const network = await walletApi.network.getNetwork();
148             const [primaryUserKey] = userKeys;
150             const mnemonic = new wasm.WasmMnemonic(wasm.WasmWordCount.Words12).asString();
151             const hasPassphrase = false;
153             const compelledWalletName = getDefaultWalletName(false, []);
155             // Encrypt wallet data
156             const [
157                 [encryptedName, encryptedMnemonic, encryptedFirstAccountLabel],
158                 [walletKey, walletKeySignature /** decryptedWalletKey */, , userKeyId],
159             ] = await encryptWalletData([compelledWalletName, mnemonic, DEFAULT_ACCOUNT_LABEL], primaryUserKey);
161             const wasmWallet = new wasm.WasmWallet(network, mnemonic, '');
162             const fingerprint = wasmWallet.getFingerprint();
164             const derivationPathParts = [defaultScriptType(), network, FIRST_INDEX] as const;
166             const { Wallet } = await walletApi.wallet.createWallet(
167                 encryptedName,
168                 false,
169                 WalletType.OnChain,
170                 hasPassphrase,
171                 userKeyId,
172                 walletKey,
173                 walletKeySignature,
174                 encryptedMnemonic,
175                 fingerprint,
176                 undefined,
177                 WALLET_AUTOCREATE_FLAG
178             );
180             const account = await setupWalletAccount({
181                 wallet: Wallet,
182                 label: encryptedFirstAccountLabel,
183                 derivationPathParts,
184                 walletApi,
185                 fiatCurrency: userWalletSettings.FiatCurrency,
186             });
188             await enableBitcoinViaEmail({
189                 wallet: Wallet,
190                 walletAccount: account.Data,
191                 wasmWallet,
192                 derivationPathParts,
193                 walletApi,
194             });
195         } catch (e) {
196             console.error('Could not autocreate wallet from Mail', e);
197         }
198     };
200     const shouldCreateWallet = async () => {
201         try {
202             if (!higherLevelPilot || !isWasmSupported()) {
203                 return false;
204             }
206             let isUserCompatible = user.isFree;
208             if (!isUserCompatible) {
209                 const organization = await getOrganization();
210                 isUserCompatible = organization?.MaxMembers === 1;
211             }
213             if (!isUserCompatible) {
214                 return false;
215             }
217             // lazy load wasm here
218             wasm = await loadWasmModule();
220             const walletApi = await getWalletApi();
221             userWalletSettings = (await walletApi.settings.getUserSettings())[0];
223             return !userWalletSettings.WalletCreated;
224         } catch (e) {
225             console.error('Could not check whether or not wallet autocreation is needed', e);
226             return false;
227         }
228     };
230     useEffect(() => {
231         const run = async () => {
232             if (await shouldCreateWallet()) {
233                 await autoCreateWallet();
234             }
235         };
237         void run();
238     }, []);