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 * Additionally, 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 { data: 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)).data;
58 const parsedSender = Sender && parseSender(Sender);
60 const apiTransactionB = {
62 Label: decryptedLabel,
69 return apiTransactionB;
72 const { data: Body } = apiTransaction.Body
73 ? await decryptTextData(apiTransaction.Body, addressKeys)
75 const SerialisedToList = apiTransaction.ToList && (await decryptTextData(apiTransaction.ToList, addressKeys)).data;
77 const ToList = parsedRecipientList(SerialisedToList);
86 export const buildNetworkTransactionByHashedTxId = async (
87 transactions: WasmTransactionDetails[],
89 ): Promise<NetworkTransactionByHashedTxId> => {
90 return transactions.reduce((prevPromise, transaction) => {
91 return prevPromise.then(async (acc) => {
93 const hashedTxIdBuffer = await hmac(hmacKey, transaction.txid);
94 const key = uint8ArrayToBase64String(new Uint8Array(hashedTxIdBuffer));
99 HashedTransactionID: key,
107 }, Promise.resolve({}));
111 * Fetches transactions from the API, decrypts the transaction data, and returns the decrypted transactions.
114 export const fetchApiTransactions = async ({
120 addressesPrivateKeys,
123 hashedTxids: string[] | undefined;
125 walletKey: CryptoKey;
126 userPrivateKeys: PrivateKeyReference[];
127 addressesPrivateKeys: PrivateKeyReference[];
129 const transactionsApiData = await api.wallet
130 .getWalletTransactions(walletId, undefined, hashedTxids)
131 .then((data) => data[0]);
133 const fetched: DecryptedTransactionData[] = [];
135 // populate txData with api data
136 for (const { Data: transactionApiData } of transactionsApiData) {
137 const { HashedTransactionID } = transactionApiData;
139 if (HashedTransactionID) {
140 const decryptedTransactionData = await decryptTransactionData(
147 fetched.push(decryptedTransactionData);
155 * Encrypts the transaction data, creates new transactions in the API and then returns
156 * the created transactions.
158 export const createApiTransactions = async ({
162 accountIDByDerivationPathByWalletID,
163 transactionsWithoutApiData,
169 walletKey: CryptoKey;
170 accountIDByDerivationPathByWalletID: AccountIdByDerivationPathAndWalletId;
171 transactionsWithoutApiData: (WasmTransactionDetails & { HashedTransactionID: string })[];
172 userKeys: DecryptedKey[];
173 checkShouldAbort: () => boolean;
175 const created: DecryptedTransactionData[] = [];
176 const [primaryUserKeys] = userKeys;
178 for (const transaction of transactionsWithoutApiData) {
179 if (checkShouldAbort()) {
184 const normalisedDerivationPath = removeMasterPrefix(transaction.account_derivation_path);
185 const accountId = accountIDByDerivationPathByWalletID[walletId]?.[normalisedDerivationPath];
191 const txid = await encryptPgp(transaction.txid, [primaryUserKeys.publicKey]);
193 // TODO: this can only occur on encryption error: we need to better handle that
198 const { Data: createdTransaction } = await api.wallet.createWalletTransaction(walletId, accountId, {
200 hashed_txid: transaction.HashedTransactionID,
201 transaction_time: transaction.time?.confirmation_time
202 ? transaction.time?.confirmation_time.toString()
203 : Math.floor(Date.now() / SECOND).toString(),
205 exchange_rate_id: null,
208 const decryptedTransactionData = await decryptTransactionData(
211 userKeys.map((k) => k.privateKey)
214 created.push(decryptedTransactionData);
216 console.error('Could not create missing tx data', error);
223 const getWalletTransactionsToHash = async (api: WasmApiClients, walletId: string) => {
225 const walletTransactionsToHash = await api.wallet.getWalletTransactionsToHash(walletId);
226 return walletTransactionsToHash[0];
233 * This function hashes API transactions by first decrypting the transaction data,
234 * then hashing the transaction ID, and finally updating the transaction with the hashed ID.
236 export const hashApiTransactions = async ({
242 addressesPrivateKeys,
247 walletKey: CryptoKey;
249 userPrivateKeys: PrivateKeyReference[];
250 addressesPrivateKeys: PrivateKeyReference[];
251 checkShouldAbort: () => boolean;
253 const hashed: DecryptedTransactionData[] = [];
255 // TODO: check pagination
256 const walletTransactionsToHash = await getWalletTransactionsToHash(api, walletId);
258 for (const walletTransactionToHash of walletTransactionsToHash) {
259 if (checkShouldAbort()) {
265 const decryptedTransactionData = await decryptTransactionData(
266 walletTransactionToHash.Data,
272 // TODO: this can only occur if decryption fails. We need to better handle that
273 if (!decryptedTransactionData.TransactionID || !walletTransactionToHash.Data.WalletAccountID) {
278 const hashedTxIdBuffer = await hmac(hmacKey, decryptedTransactionData.TransactionID);
279 const hashedTransactionID = uint8ArrayToBase64String(new Uint8Array(hashedTxIdBuffer));
282 .updateWalletTransactionHashedTxId(
284 walletTransactionToHash.Data.WalletAccountID,
285 walletTransactionToHash.Data.ID,
291 ...decryptedTransactionData,
292 HashedTransactionID: hashedTransactionID,
295 // TODO: do something to avoid creating wallet transaction when error occurs here
296 console.error('An error occurred during transaction decryption, we will create a new transaction', e);