diff --git a/suite-common/graph/src/balanceHistoryUtils.ts b/suite-common/graph/src/balanceHistoryUtils.ts index 3b02fc23e17..1435cb5d606 100644 --- a/suite-common/graph/src/balanceHistoryUtils.ts +++ b/suite-common/graph/src/balanceHistoryUtils.ts @@ -305,6 +305,152 @@ export const getAccountHistoryMovementItemETH = ({ }; }; +export const getAccountHistoryMovementItemSOL = ({ + transactions, + from, + to, +}: GetAccountHistoryMovementItemParams): AccountHistoryMovement => { + const summaryMap = new Map(); + const allTokensSummaryMap = new Map(); + + transactions.forEach(tx => { + const { blockTime } = tx; + + if ( + !blockTime || + tx.solanaSpecific === undefined || + tx.type === 'failed' || + tx.solanaSpecific.status !== 'confirmed' + ) { + return; + } + + if ((from && blockTime < from) || (to && blockTime > to)) { + return; + } + + const bh: AccountHistoryMovementItem = { + time: blockTime, + txs: 1, + received: new BigNumber(0), + sent: new BigNumber(0), + sentToSelf: new BigNumber(0), + }; + + let countSentToSelf = false; + const solTxData = tx.solanaSpecific; + + if (tx.details.vout.length > 0) { + const bchainVout = tx.details.vout[0]; + const value = new BigNumber(bchainVout.value || '0'); + + if (bchainVout.addresses && bchainVout.addresses.length > 0) { + const txAddrDesc = bchainVout.addresses[0]; + + if (tx.descriptor === txAddrDesc) { + // Check if address is in selfAddrDesc + bh.received = bh.received.plus(value); + } + + if (tx.descriptor === txAddrDesc) { + countSentToSelf = true; + } + } + } + + for (const bchainVin of tx.details.vin) { + if (bchainVin.addresses && bchainVin.addresses.length > 0) { + const txAddrDesc = bchainVin.addresses[0]; + if (txAddrDesc === tx.descriptor) { + if (solTxData.status === 'confirmed') { + const value = new BigNumber(tx.details.vout[0]?.value || '0'); + bh.sent = bh.sent.plus(value); + + if (countSentToSelf && txAddrDesc === tx.descriptor) { + bh.sentToSelf = bh.sentToSelf.plus(value); + } + } + + bh.sent = bh.sent.plus(tx.fee); + } + } + } + + tx.tokens.forEach(token => { + // skip empty amounts + if (token.amount === '') { + return; + } + + if (token.type === 'sent' || token.type === 'recv' || token.type === 'self') { + const tokenSummary: AccountHistoryMovementItem = { + time: blockTime, + txs: 1, + received: new BigNumber(0), + sent: new BigNumber(0), + sentToSelf: new BigNumber(0), + }; + + const amount = new BigNumber(token.amount).div(10 ** token.decimals); + + if (token.type === 'sent') { + tokenSummary.sent = tokenSummary.sent.plus(amount); + } else if (token.type === 'recv') { + tokenSummary.received = tokenSummary.received.plus(amount); + } else if (token.type === 'self') { + tokenSummary.sentToSelf = tokenSummary.sentToSelf.plus(amount); + } + + const tokenContractId = token.contract as TokenAddress; + + if (!allTokensSummaryMap.has(tokenContractId)) { + allTokensSummaryMap.set(tokenContractId, new Map()); + } + + const tokenSummaryMap = allTokensSummaryMap.get(tokenContractId)!; + const existingBlockSummary = tokenSummaryMap.get(blockTime); + + if (existingBlockSummary) { + existingBlockSummary.txs += 1; + existingBlockSummary.received = existingBlockSummary.received.plus( + tokenSummary.received, + ); + existingBlockSummary.sent = existingBlockSummary.sent.plus(tokenSummary.sent); + existingBlockSummary.sentToSelf = existingBlockSummary.sentToSelf.plus( + tokenSummary.sentToSelf, + ); + } else { + tokenSummaryMap.set(blockTime, tokenSummary); + } + } + }); + + if (summaryMap.has(blockTime)) { + const summary = summaryMap.get(blockTime)!; + summary.txs += 1; + summary.received = summary.received.plus(bh.received); + summary.sent = summary.sent.plus(bh.sent); + summary.sentToSelf = summary.sentToSelf.plus(bh.sentToSelf); + } else { + summaryMap.set(blockTime, bh); + } + }); + + const sortedTokensSummaries: Record = {}; + for (const [contract, tokenSummaryMap] of allTokensSummaryMap.entries()) { + const sortedTokenSummaries = Array.from(tokenSummaryMap.values()).sort( + (a, b) => a.time - b.time, + ); + sortedTokensSummaries[contract] = sortedTokenSummaries; + } + const sortedSummaries = Array.from(summaryMap.values()).sort((a, b) => a.time - b.time); + + return { + main: sortedSummaries, + tokens: sortedTokensSummaries, + }; +}; + export const getAccountHistoryMovementFromTransactions = ({ transactions, coin, @@ -327,10 +473,7 @@ export const getAccountHistoryMovementFromTransactions = ({ case 'bnb': return getAccountHistoryMovementItemETH({ transactions, from, to }); case 'sol': - return { - main: [] as AccountHistoryMovementItem[], - tokens: {}, - } as AccountHistoryMovement; + return getAccountHistoryMovementItemSOL({ transactions, from, to }); default: coin satisfies never; throw new Error(`getAccountHistoryMovementItem: Unsupported network ${coin}`); diff --git a/suite-common/graph/src/graphDataFetching.ts b/suite-common/graph/src/graphDataFetching.ts index 1df08f4074e..3eebe1609eb 100644 --- a/suite-common/graph/src/graphDataFetching.ts +++ b/suite-common/graph/src/graphDataFetching.ts @@ -107,6 +107,7 @@ const getBalanceFromAccountInfo = ({ // On Ripple, if we use availableBalance, we will get higher balance, IDK why. return accountInfo.balance; case 'ethereum': + case 'solana': if (contractId) { const token = accountInfo.tokens?.find(t => t.contract === contractId);