1 import type { WasmApiClients, WasmApiWalletTransaction, WasmTransactionDetails } from '@proton/andromeda';
2 import type { PrivateKeyReference } from '@proton/crypto';
3 import { SECOND } from '@proton/shared/lib/constants';
4 import { uint8ArrayToBase64String } from '@proton/shared/lib/helpers/encoding';
5 import type { DecryptedKey, SimpleMap } from '@proton/shared/lib/interfaces';
6 import noop from '@proton/utils/noop';
9 AccountIdByDerivationPathAndWalletId,
10 DecryptedTransactionData,
11 NetworkTransactionByHashedTxId,
14 import { removeMasterPrefix } from './account';
15 import { decryptTextData, decryptWalletData, encryptPgp, hmac } from './crypto';
17 const parsedRecipientList = (toList: string | null): SimpleMap<string> => {
19 const parsed = toList ? JSON.parse(toList) : {};
21 // TODO: check with API why some toList are arrays
22 return Array.isArray(parsed) ? parsed[0] : parsed;
29 * BitcoinViaEmail API sets Sender as string, but in ExternalSend / ExternalReceive, sender is an object
31 const parseSender = (sender: string): string | SenderObject => {
33 const parsed: SenderObject = JSON.parse(sender);
41 * Decrypt transaction data. If addressKeys is not provided, we won't try to decrypt Body, Sender and ToList.
43 * Additionnally, TransactionID decryption might fail if Tx was created by a third party (using address keys)
45 export const decryptTransactionData = async (
46 apiTransaction: WasmApiWalletTransaction,
48 userPrivateKeys?: PrivateKeyReference[],
49 addressKeys?: PrivateKeyReference[]
50 ): Promise<DecryptedTransactionData> => {
51 const keys = [...(userPrivateKeys ? userPrivateKeys : []), ...(addressKeys ?? [])];
53 const [decryptedLabel = ''] = await decryptWalletData([apiTransaction.Label], walletKey).catch(() => []);
55 const TransactionID = await decryptTextData(apiTransaction.TransactionID, keys);
56 // Sender is encrypted with addressKey in BitcoinViaEmail but with userKey when manually set (unknown sender)
57 const Sender = apiTransaction.Sender && (await decryptTextData(apiTransaction.Sender, keys));
58 const parsedSender = Sender && parseSender(Sender);
60 const apiTransactionB = {
62 Label: decryptedLabel,
69 return apiTransactionB;
72 const Body = apiTransaction.Body && (await decryptTextData(apiTransaction.Body, addressKeys));
73 const SerialisedToList = apiTransaction.ToList && (await decryptTextData(apiTransaction.ToList, addressKeys));
75 const ToList = parsedRecipientList(SerialisedToList);
84 export const buildNetworkTransactionByHashedTxId = async (
85 transactions: WasmTransactionDetails[],
87 ): Promise<NetworkTransactionByHashedTxId> => {
88 return transactions.reduce((prevPromise, transaction) => {
89 return prevPromise.then(async (acc) => {
91 const hashedTxIdBuffer = await hmac(hmacKey, transaction.txid);
92 const key = uint8ArrayToBase64String(new Uint8Array(hashedTxIdBuffer));
97 HashedTransactionID: key,
105 }, Promise.resolve({}));
109 * Fetches transactions from the API, decrypts the transaction data, and returns the decrypted transactions.
112 export const fetchApiTransactions = async ({
118 addressesPrivateKeys,
121 hashedTxids: string[] | undefined;
123 walletKey: CryptoKey;
124 userPrivateKeys: PrivateKeyReference[];
125 addressesPrivateKeys: PrivateKeyReference[];
127 const transactionsApiData = await api.wallet
128 .getWalletTransactions(walletId, undefined, hashedTxids)
129 .then((data) => data[0]);
131 const fetched: DecryptedTransactionData[] = [];
133 // populate txData with api data
134 for (const { Data: transactionApiData } of transactionsApiData) {
135 const { HashedTransactionID } = transactionApiData;
137 if (HashedTransactionID) {
138 const decryptedTransactionData = await decryptTransactionData(
145 fetched.push(decryptedTransactionData);
153 * Encrypts the transaction data, creates new transactions in the API and then returns
154 * the created transactions.
156 export const createApiTransactions = async ({
160 accountIDByDerivationPathByWalletID,
161 transactionsWithoutApiData,
167 walletKey: CryptoKey;
168 accountIDByDerivationPathByWalletID: AccountIdByDerivationPathAndWalletId;
169 transactionsWithoutApiData: (WasmTransactionDetails & { HashedTransactionID: string })[];
170 userKeys: DecryptedKey[];
171 checkShouldAbort: () => boolean;
173 const created: DecryptedTransactionData[] = [];
174 const [primaryUserKeys] = userKeys;
176 for (const transaction of transactionsWithoutApiData) {
177 if (checkShouldAbort()) {
182 const normalisedDerivationPath = removeMasterPrefix(transaction.account_derivation_path);
183 const accountId = accountIDByDerivationPathByWalletID[walletId]?.[normalisedDerivationPath];
189 const txid = await encryptPgp(transaction.txid, [primaryUserKeys.publicKey]);
191 // TODO: this can only occur on encryption error: we need to better handle that
196 const { Data: createdTransaction } = await api.wallet.createWalletTransaction(walletId, accountId, {
198 hashed_txid: transaction.HashedTransactionID,
199 transaction_time: transaction.time?.confirmation_time
200 ? transaction.time?.confirmation_time.toString()
201 : Math.floor(Date.now() / SECOND).toString(),
203 exchange_rate_id: null,
206 const decryptedTransactionData = await decryptTransactionData(
209 userKeys.map((k) => k.privateKey)
212 created.push(decryptedTransactionData);
214 console.error('Could not create missing tx data', error);
221 const getWalletTransactionsToHash = async (api: WasmApiClients, walletId: string) => {
223 const walletTransactionsToHash = await api.wallet.getWalletTransactionsToHash(walletId);
224 return walletTransactionsToHash[0];
231 * This function hashes API transactions by first decrypting the transaction data,
232 * then hashing the transaction ID, and finally updating the transaction with the hashed ID.
234 export const hashApiTransactions = async ({
240 addressesPrivateKeys,
245 walletKey: CryptoKey;
247 userPrivateKeys: PrivateKeyReference[];
248 addressesPrivateKeys: PrivateKeyReference[];
249 checkShouldAbort: () => boolean;
251 const hashed: DecryptedTransactionData[] = [];
253 // TODO: check pagination
254 const walletTransactionsToHash = await getWalletTransactionsToHash(api, walletId);
256 for (const walletTransactionToHash of walletTransactionsToHash) {
257 if (checkShouldAbort()) {
263 const decryptedTransactionData = await decryptTransactionData(
264 walletTransactionToHash.Data,
270 // TODO: this can only occur if decryption fails. We need to better handle that
271 if (!decryptedTransactionData.TransactionID || !walletTransactionToHash.Data.WalletAccountID) {
276 const hashedTxIdBuffer = await hmac(hmacKey, decryptedTransactionData.TransactionID);
277 const hashedTransactionID = uint8ArrayToBase64String(new Uint8Array(hashedTxIdBuffer));
280 .updateWalletTransactionHashedTxId(
282 walletTransactionToHash.Data.WalletAccountID,
283 walletTransactionToHash.Data.ID,
289 ...decryptedTransactionData,
290 HashedTransactionID: hashedTransactionID,
293 // TODO: do something to avoid creating wallet transaction when error occurs here
294 console.error('An error occurred during transaction decryption, we will create a new transaction', e);