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';
12 type AccountIdByDerivationPathAndWalletId,
13 type DecryptedTransactionData,
14 type NetworkTransactionByHashedTxId,
15 createApiTransactions,
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 {
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`,
47 miss: async ({ extraArgument, options, getState, dispatch }) => {
48 if (!options?.thunkArg) {
49 return Promise.resolve({});
52 const userKeys = await dispatch(userKeysThunk());
53 const addressKeys = await dispatchGetAllAddressesKeys(dispatch);
60 networkTransactionByHashedTxId,
61 accountIDByDerivationPathByWalletID,
65 const hashedTxIds = Object.keys(networkTransactionByHashedTxId);
66 const state = getState()[apiWalletTransactionDataSliceName];
68 if (!walletHmacKey || !walletKey || !hashedTxIds.length) {
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,
80 userPrivateKeys: userKeys.map((k) => k.privateKey),
81 addressesPrivateKeys: addressKeys.map((k) => k.privateKey),
86 ...toMap(fetchedTransactions, 'HashedTransactionID'),
89 notFoundHashedTxIds = notFoundHashedTxIds?.filter((hashedTxId) => !updatedState[hashedTxId]);
90 if (!!notFoundHashedTxIds?.length) {
91 const hashedTransactions = await hashApiTransactions({
92 api: extraArgument.walletApi.clients(),
95 hmacKey: walletHmacKey,
96 userPrivateKeys: userKeys.map((k) => k.privateKey),
97 addressesPrivateKeys: addressKeys.map((k) => k.privateKey),
98 checkShouldAbort: () => false, // TODO check
103 ...toMap(hashedTransactions, 'HashedTransactionID'),
107 notFoundHashedTxIds = notFoundHashedTxIds?.filter((hashedTxId) => !updatedState[hashedTxId]);
108 const transactionsWithoutApiData = notFoundHashedTxIds
109 .map((hashedTxId) => networkTransactionByHashedTxId[hashedTxId])
112 const createdTransactions = await createApiTransactions({
113 api: extraArgument.walletApi.clients(),
117 checkShouldAbort: () => false, // TODO check
118 accountIDByDerivationPathByWalletID,
119 transactionsWithoutApiData,
124 ...toMap(createdTransactions, 'HashedTransactionID'),
127 notFoundHashedTxIds = notFoundHashedTxIds?.filter((hashedTxId) => !updatedState[hashedTxId]);
129 if (!!notFoundHashedTxIds?.length) {
130 console.warn("Some transactions weren't find", notFoundHashedTxIds);
133 // We return the whole update state, then requested network transaction needs to be picked from it
136 previous: ({ options, getState }) => {
137 const state = getState()[apiWalletTransactionDataSliceName];
139 if (!options?.thunkArg) {
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) };
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,
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, {
174 ...action.payload.update,
181 export const apiWalletTransactionDataReducer = { [apiWalletTransactionDataSliceName]: slice.reducer };
182 export const apiWalletTransactionDataThunk = modelThunk.thunk;