Use source loader for email sprite icons
[ProtonMail-WebClient.git] / packages / wallet / store / slices / apiWalletTransactionData.ts
blob899ea2264f5148471d0c6650457726b312f93c16
1 import { createAction, createSlice } from '@reduxjs/toolkit';
2 import pick from 'lodash/pick';
3 import set from 'lodash/set';
5 import type { AddressKeysState, ModelState, UserKeysState } from '@proton/account';
6 import { dispatchGetAllAddressesKeys, getInitialModelState, userKeysThunk } from '@proton/account';
7 import { createAsyncModelThunk, handleAsyncModel } from '@proton/redux-utilities';
8 import { toMap } from '@proton/shared/lib/helpers/object';
9 import { type SimpleMap } from '@proton/shared/lib/interfaces';
10 import isTruthy from '@proton/utils/isTruthy';
11 import {
12     type AccountIdByDerivationPathAndWalletId,
13     type DecryptedTransactionData,
14     type NetworkTransactionByHashedTxId,
15     createApiTransactions,
16     fetchApiTransactions,
17     hashApiTransactions,
18 } from '@proton/wallet';
20 import type { WalletThunkArguments } from '../thunk';
22 export const apiWalletTransactionDataSliceName = 'api_wallet_transaction_data' as const;
24 export type WalletTransactionByHashedTxId = SimpleMap<DecryptedTransactionData>;
26 export interface ApiWalletTransactionDataState extends UserKeysState, AddressKeysState {
27     [apiWalletTransactionDataSliceName]: ModelState<WalletTransactionByHashedTxId>;
30 type SliceState = ApiWalletTransactionDataState[typeof apiWalletTransactionDataSliceName];
31 type Model = NonNullable<SliceState['value']>;
33 export const selectApiWalletTransactionData = (state: ApiWalletTransactionDataState) =>
34     state[apiWalletTransactionDataSliceName];
36 export interface WalletTransactionsThunkArg {
37     walletId: string;
38     walletKey: CryptoKey | undefined;
39     walletHmacKey: CryptoKey | undefined;
40     networkTransactionByHashedTxId: NetworkTransactionByHashedTxId;
41     accountIDByDerivationPathByWalletID: AccountIdByDerivationPathAndWalletId;
44 const modelThunk = createAsyncModelThunk<Model, ApiWalletTransactionDataState, WalletThunkArguments, [WalletTransactionsThunkArg]>(
45     `${apiWalletTransactionDataSliceName}/fetch`,
46     {
47         miss: async ({ extraArgument, options, getState, dispatch }) => {
48             if (!options?.thunkArg) {
49                 return Promise.resolve({});
50             }
52             const userKeys = await dispatch(userKeysThunk());
53             const addressKeys = await dispatchGetAllAddressesKeys(dispatch);
55             const [
56                 {
57                     walletId,
58                     walletHmacKey,
59                     walletKey,
60                     networkTransactionByHashedTxId,
61                     accountIDByDerivationPathByWalletID,
62                 },
63             ] = options.thunkArg;
65             const hashedTxIds = Object.keys(networkTransactionByHashedTxId);
66             const state = getState()[apiWalletTransactionDataSliceName];
68             if (!walletHmacKey || !walletKey || !hashedTxIds.length) {
69                 return state;
70             }
72             let updatedState = { ...state.value };
73             let notFoundHashedTxIds = hashedTxIds?.filter((hashedTxId) => !updatedState[hashedTxId]);
75             const fetchedTransactions = await fetchApiTransactions({
76                 api: extraArgument.walletApi.clients(),
77                 hashedTxids: notFoundHashedTxIds,
78                 walletId,
79                 walletKey,
80                 userPrivateKeys: userKeys.map((k) => k.privateKey),
81                 addressesPrivateKeys: addressKeys.map((k) => k.privateKey),
82             });
84             updatedState = {
85                 ...updatedState,
86                 ...toMap(fetchedTransactions, 'HashedTransactionID'),
87             };
89             notFoundHashedTxIds = notFoundHashedTxIds?.filter((hashedTxId) => !updatedState[hashedTxId]);
90             if (!!notFoundHashedTxIds?.length) {
91                 const hashedTransactions = await hashApiTransactions({
92                     api: extraArgument.walletApi.clients(),
93                     walletId,
94                     walletKey,
95                     hmacKey: walletHmacKey,
96                     userPrivateKeys: userKeys.map((k) => k.privateKey),
97                     addressesPrivateKeys: addressKeys.map((k) => k.privateKey),
98                     checkShouldAbort: () => false, // TODO check
99                 });
101                 updatedState = {
102                     ...updatedState,
103                     ...toMap(hashedTransactions, 'HashedTransactionID'),
104                 };
105             }
107             notFoundHashedTxIds = notFoundHashedTxIds?.filter((hashedTxId) => !updatedState[hashedTxId]);
108             const transactionsWithoutApiData = notFoundHashedTxIds
109                 .map((hashedTxId) => networkTransactionByHashedTxId[hashedTxId])
110                 .filter(isTruthy);
112             const createdTransactions = await createApiTransactions({
113                 api: extraArgument.walletApi.clients(),
114                 walletId,
115                 walletKey,
116                 userKeys,
117                 checkShouldAbort: () => false, // TODO check
118                 accountIDByDerivationPathByWalletID,
119                 transactionsWithoutApiData,
120             });
122             updatedState = {
123                 ...updatedState,
124                 ...toMap(createdTransactions, 'HashedTransactionID'),
125             };
127             notFoundHashedTxIds = notFoundHashedTxIds?.filter((hashedTxId) => !updatedState[hashedTxId]);
129             if (!!notFoundHashedTxIds?.length) {
130                 console.warn("Some transactions weren't find", notFoundHashedTxIds);
131             }
133             // We return the whole update state, then requested network transaction needs to be picked from it
134             return updatedState;
135         },
136         previous: ({ options, getState }) => {
137             const state = getState()[apiWalletTransactionDataSliceName];
139             if (!options?.thunkArg) {
140                 return undefined;
141             }
143             const [{ networkTransactionByHashedTxId }] = options.thunkArg;
144             const hashedTxIds = Object.keys(networkTransactionByHashedTxId);
146             const stateValue = state.value;
147             const hashedTxIdToFetch = hashedTxIds.filter((hashedTxId) => !stateValue?.[hashedTxId]);
149             // If some hashedTxId are not in the store yet, we return undefined, else we pick the provided hashedTxIds from the store
150             return hashedTxIdToFetch.length ? undefined : { ...state, value: pick(stateValue, hashedTxIds) };
151         },
152     }
155 const initialState = getInitialModelState<Model>();
157 export const updateWalletTransaction = createAction(
158     'wallet-transaction/update',
159     (payload: { hashedTransactionId: string; update: Partial<DecryptedTransactionData> }) => ({ payload })
162 const slice = createSlice({
163     name: apiWalletTransactionDataSliceName,
164     initialState,
165     reducers: {},
166     extraReducers: (builder) => {
167         handleAsyncModel(builder, modelThunk);
168         builder.addCase(updateWalletTransaction, (state, action) => {
169             const transaction = state.value?.[action.payload.hashedTransactionId];
171             if (state.value && transaction) {
172                 set(state.value, action.payload.hashedTransactionId, {
173                     ...transaction,
174                     ...action.payload.update,
175                 });
176             }
177         });
178     },
181 export const apiWalletTransactionDataReducer = { [apiWalletTransactionDataSliceName]: slice.reducer };
182 export const apiWalletTransactionDataThunk = modelThunk.thunk;