From cf6a44540d15fd0d26eaa8dbe7126955baf62493 Mon Sep 17 00:00:00 2001 From: Kirill Ageychenko Date: Wed, 6 Nov 2024 19:26:34 +0700 Subject: [PATCH] feat(HW-697): add Tron support for transaction list (#2189) * feat(HW-697): add Tron support for transaction list * fix(HW-932): Tron Transaction issues * feat(HW-934): add fee check to transaction flow screens * fix: transaction list --- src/components/transaction-detail.tsx | 2 +- .../transaction-list/transaction-list.tsx | 49 +- src/event-actions/on-push-notification.ts | 6 +- src/helpers/address-utils.ts | 16 + src/helpers/indexer-transaction-utils.ts | 447 ++++++++++++++---- src/hooks/use-transaction-list.ts | 2 +- src/hooks/use-transaction.ts | 7 +- src/models/provider/provider.model.ts | 2 +- src/models/transaction.ts | 43 +- src/route-types.ts | 7 +- .../HomeStack/HomeFeedStack/account-info.tsx | 1 + .../HomeFeedStack/transaction-detail.tsx | 2 +- .../TransactionStack/transaction-sum.tsx | 84 +++- src/screens/total-value-info.tsx | 1 + src/services/indexer/indexer.ts | 15 +- src/services/tron-network/tron-network.ts | 3 +- src/types.ts | 46 +- src/widgets/transactions-widget/index.tsx | 1 + .../transactions-widget.tsx | 2 +- 19 files changed, 589 insertions(+), 147 deletions(-) diff --git a/src/components/transaction-detail.tsx b/src/components/transaction-detail.tsx index 5d2c4c609..e089ab4fd 100644 --- a/src/components/transaction-detail.tsx +++ b/src/components/transaction-detail.tsx @@ -178,7 +178,7 @@ export const TransactionDetail = observer( short /> - {balance.isPositive() && ( + {balance?.isPositive() && ( , @@ -95,10 +95,15 @@ export const TransactionList = observer( const sections = useMemo( () => - hideContent + hideContent || (!transactions?.length && isTransactionsLoading) ? [] : prepareDataForSectionList(transactions, txTimestampHeadersEnabled), - [transactions, hideContent, txTimestampHeadersEnabled], + [ + transactions, + hideContent, + txTimestampHeadersEnabled, + isTransactionsLoading, + ], ); const listStyle = useMemo( @@ -125,7 +130,7 @@ export const TransactionList = observer( await Transaction.fetchNextTransactions(addresses); }, [isTransactionsLoading]); const keyExtractor = useCallback( - (item: Transaction) => `${item.id}:${item.hash}`, + (item: Transaction) => `${item.id}:${item.hash}:${item.msg.type}`, [], ); @@ -153,20 +158,38 @@ export const TransactionList = observer( }, [addresses, onTransactionPress], ); - const renderListEmptyComponent = useCallback( + const renderListEmptyComponentDefault = useCallback( () => , [], ); + + const renderListEmptyComponent = useMemo(() => { + if (sectionListProps.ListEmptyComponent) { + // if active tab is transactions and transactions are loading + if (!hideContent && isTransactionsLoading) { + return ; + } + return sectionListProps.ListEmptyComponent; + } + return renderListEmptyComponentDefault; + }, [hideContent, renderListEmptyComponentDefault, isTransactionsLoading]); + const renderListFooterComponent = useCallback( () => ( <> - - + {!hideContent && !!sections.length && ( + <> + + + + )} ), [isTransactionsLoading], @@ -178,11 +201,11 @@ export const TransactionList = observer( overScrollMode="never" bounces={false} scrollEnabled={scrollEnabled} - ListEmptyComponent={renderListEmptyComponent} ListFooterComponent={renderListFooterComponent} contentContainerStyle={styles.grow} {...sectionListProps} /* CAN'NOT OVERRIDE */ + ListEmptyComponent={renderListEmptyComponent} sections={sections} renderItem={renderItem} keyExtractor={keyExtractor} diff --git a/src/event-actions/on-push-notification.ts b/src/event-actions/on-push-notification.ts index b643f3cec..4e0594c9b 100644 --- a/src/event-actions/on-push-notification.ts +++ b/src/event-actions/on-push-notification.ts @@ -102,12 +102,16 @@ export async function onPushNotification(message: RemoteMessage) { hash, ); if (transaction) { - Transaction.create(transaction); + Transaction.create( + transaction, + Indexer.instance.getProvidersHeader(Wallet.addressList()), + ); navigator.navigate(HomeStackRoutes.TransactionDetail, { // FIXME: For some reason navigator doesn't understand routing types correctly // @ts-ignore txId: transaction.id, addresses: Wallet.addressList(), + txType: transaction.msg.type, }); } break; diff --git a/src/helpers/address-utils.ts b/src/helpers/address-utils.ts index c9a376504..bcee886c7 100644 --- a/src/helpers/address-utils.ts +++ b/src/helpers/address-utils.ts @@ -15,6 +15,22 @@ import {splitAddress} from '@app/utils'; export const HAQQ_VALIDATOR_PREFIX = 'haqqvaloper'; export class AddressUtils { + static bufferToTron(base64buffer: string): AddressTron { + if (!base64buffer) { + return '' as AddressTron; + } + + if (AddressUtils.isTronAddress(base64buffer)) { + return base64buffer as AddressTron; + } + + return AddressUtils.hexToTron(AddressUtils.fromBuffer(base64buffer)); + } + + static fromBuffer(base64buffer: string) { + return `0x${Buffer.from(base64buffer, 'base64').toString('hex')}`; + } + static hexToTron(address?: string): AddressTron { if (!address) { return '' as AddressTron; diff --git a/src/helpers/indexer-transaction-utils.ts b/src/helpers/indexer-transaction-utils.ts index 91df7151d..1bb24478d 100644 --- a/src/helpers/indexer-transaction-utils.ts +++ b/src/helpers/indexer-transaction-utils.ts @@ -5,8 +5,11 @@ import {Token} from '@app/models/tokens'; import {ParsedTransactionData, Transaction} from '@app/models/transaction'; import {Balance} from '@app/services/balance'; import { + ChainId, IToken, + IndexerProtoMsgTxType, IndexerTransaction, + IndexerTransactionParticipantRole, IndexerTransactionWithType, IndexerTxMsgEthereumTx, IndexerTxMsgType, @@ -35,42 +38,58 @@ const getNativeToken = ( export function parseTransaction( tx: IndexerTransaction, - addresses: string[], + addressesMap: Record, ): Transaction { + let addresses = addressesMap[tx.chain_id]; + + if (!addresses) { + addresses = Object.values(addressesMap).flat(); + } + const parse = () => { - switch (tx.msg.type) { - case IndexerTxMsgType.msgEthereumRaffleTx: - return parseMsgEthereumRaffleTx(tx as any, addresses); - case IndexerTxMsgType.msgWithdrawDelegatorReward: - return parseMsgWithdrawDelegatorReward(tx as any, addresses); - case IndexerTxMsgType.msgDelegate: - return parseMsgDelegate(tx as any, addresses); - case IndexerTxMsgType.msgUndelegate: - return parseMsgUndelegate(tx as any, addresses); - case IndexerTxMsgType.msgEthereumTx: - return parseMsgEthereumTx(tx as any, addresses); - case IndexerTxMsgType.msgEthereumErc20TransferTx: - return parseMsgEthereumErc20TransferTx(tx as any, addresses); - case IndexerTxMsgType.msgSend: - return parseMsgSend(tx as any, addresses); - case IndexerTxMsgType.msgBeginRedelegate: - return parseMsgBeginRedelegate(tx as any, addresses); - case IndexerTxMsgType.msgEthereumApprovalTx: - return parseMsgEthereumApprovalTx(tx as any, addresses); - case IndexerTxMsgType.msgProtoTx: - return parseTransferContractTx(tx as any, addresses); - // TODO: implement other tx types - case IndexerTxMsgType.unknown: - case IndexerTxMsgType.msgVote: - case IndexerTxMsgType.msgWithdrawValidatorCommission: - case IndexerTxMsgType.msgEthereumNftTransferTx: - case IndexerTxMsgType.msgEthereumNftMintTx: - case IndexerTxMsgType.msgConvertIntoVestingAccount: - case IndexerTxMsgType.msgUnjail: - case IndexerTxMsgType.msgCreateValidator: - case IndexerTxMsgType.msgEditValidator: - default: - return undefined; + try { + switch (tx.msg.type) { + case IndexerTxMsgType.msgEthereumRaffleTx: + return parseMsgEthereumRaffleTx(tx as any, addresses); + case IndexerTxMsgType.msgWithdrawDelegatorReward: + return parseMsgWithdrawDelegatorReward(tx as any, addresses); + case IndexerTxMsgType.msgDelegate: + return parseMsgDelegate(tx as any, addresses); + case IndexerTxMsgType.msgUndelegate: + return parseMsgUndelegate(tx as any, addresses); + case IndexerTxMsgType.msgEthereumTx: + return parseMsgEthereumTx(tx as any, addresses); + case IndexerTxMsgType.msgEthereumErc20TransferTx: + return parseMsgEthereumErc20TransferTx(tx as any, addresses); + case IndexerTxMsgType.msgSend: + return parseMsgSend(tx as any, addresses); + case IndexerTxMsgType.msgBeginRedelegate: + return parseMsgBeginRedelegate(tx as any, addresses); + case IndexerTxMsgType.msgEthereumApprovalTx: + return parseMsgEthereumApprovalTx(tx as any, addresses); + case IndexerTxMsgType.msgProtoTx: + return parseMsgProtoTx(tx as any, addresses); + case IndexerTxMsgType.msgEventTx: + return parseMsgEventTx(tx as any, addresses); + // TODO: implement other tx types + case IndexerTxMsgType.unknown: + case IndexerTxMsgType.msgVote: + case IndexerTxMsgType.msgWithdrawValidatorCommission: + case IndexerTxMsgType.msgEthereumNftTransferTx: + case IndexerTxMsgType.msgEthereumNftMintTx: + case IndexerTxMsgType.msgConvertIntoVestingAccount: + case IndexerTxMsgType.msgUnjail: + case IndexerTxMsgType.msgCreateValidator: + case IndexerTxMsgType.msgEditValidator: + default: + return undefined; + } + } catch (err) { + Logger.captureException(err, 'parseTransaction', { + tx, + addressesMap, + }); + return undefined; } }; @@ -136,17 +155,307 @@ function parseMsgEthereumApprovalTx( }; } +function parseMsgEventTx( + tx: IndexerTransactionWithType, + addresses: string[], +): ParsedTransactionData | undefined { + switch (tx.msg.messageType) { + case 'transfer': + return parseTransferEventTx(tx as any, addresses); // send TRC20 token + default: + return undefined; + } +} + +/** + * Example transaction: + { + "block": 49017942, + "chain_id": 2494104990, + "code": 1, + "confirmations": 0, + "fee": "0", + "gas_limit": "13045", + "hash": "0x5f57e6021d9a4f1697351a23b50cd43a21ceceac0bf5582c1a2791d287f0f8a6", + "id": "0x5f57e6021d9a4f1697351a23b50cd43a21ceceac0bf5582c1a2791d287f0f8a6", + "input": "a9059cbb000000000000000000000000f066ec5164b6e38cdcab8daa957c21fa9759215f00000000000000000000000000000000000000000000000000000000006acfc0", + "msg": { + "blockId": "49017942", + "contractAddress": "tsNLx+A6DVJvQ4XupCQNOze0B28=", + "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABqz8A=", + "message": { + "transfer": { + "from": "HXFYEQKwQWNOYkegpTJgRW07eFw=", + "to": "8GbsUWS244zcq42qlXwh+pdZIV8=", + "value": "NzAwMDAwMA==" + } + }, + "messageType": "transfer", + "topic0": "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=", + "topic1": "AAAAAAAAAAAAAAAAHXFYEQKwQWNOYkegpTJgRW07eFw=", + "topic2": "AAAAAAAAAAAAAAAA8GbsUWS244zcq42qlXwh+pdZIV8=", + "txId": "X1fmAh2aTxaXNRojtQzUOiHOzqwL9VgsGieR0ofw+KY=", + "type": "msgEventTx" + }, + "msg_type": "TriggerSmartContract", + "participants": [ + { + "address": "HXFYEQKwQWNOYkegpTJgRW07eFw=", + "blockId": "49017942", + "role": "sender", + "txId": "X1fmAh2aTxaXNRojtQzUOiHOzqwL9VgsGieR0ofw+KY=" + }, + { + "address": "8GbsUWS244zcq42qlXwh+pdZIV8=", + "blockId": "49017942", + "role": "receiver", + "txId": "X1fmAh2aTxaXNRojtQzUOiHOzqwL9VgsGieR0ofw+KY=" + } + ], + "senders": [], + "ts": "2024-11-04T13:07:09Z" + } + */ +function parseTransferEventTx( + tx: IndexerTransactionWithType, + addresses: string[], +): ParsedTransactionData | undefined { + if (!tx.msg.message.transfer) { + return undefined; + } + const contractAddress = AddressUtils.tronToHex( + AddressUtils.bufferToTron(tx.msg.contractAddress), + ); + const token = Token.data[contractAddress] || Token.UNKNOWN_TOKEN; + const from = AddressUtils.bufferToTron(tx.msg.message.transfer.from); + const to = AddressUtils.bufferToTron(tx.msg.message.transfer.to); + + let amountString = ''; + + // is base64 encoded + if (tx?.msg?.message?.transfer?.value?.endsWith('=')) { + amountString = AddressUtils.fromBuffer(tx.msg.message.transfer.value); + } else { + amountString = tx.msg.message.transfer.value; + } + + const isIncoming = addresses.includes(to) && !addresses.includes(from); + + const title = isIncoming + ? getText(I18N.transactionReceiveTitle) + : getText(I18N.transactionSendTitle); + + const subtitle = isIncoming + ? formatAddressForSubtitle(from, 'hexToTron', true) + : formatAddressForSubtitle(to, 'hexToTron', false); + + const amount = [new Balance(amountString, token.decimals!, token.symbol!)]; + + return { + from, + to, + amount, + isContractInteraction: false, + isIncoming, + isOutcoming: !isIncoming, + tokens: [ + { + name: token.name!, + symbol: token.symbol!, + icon: token.image!, + decimals: token.decimals!, + contract_address: contractAddress, + }, + ], + isCosmosTx: false, + isEthereumTx: true, + icon: isIncoming ? IconsName.arrow_receive : IconsName.arrow_send, + title, + subtitle, + }; +} + +function parseMsgProtoTx( + tx: IndexerTransactionWithType, + addresses: string[], +): ParsedTransactionData | undefined { + switch (tx.msg_type) { + case IndexerProtoMsgTxType.transferContract: + return parseTransferContractTx(tx as any, addresses); // send TRX + case IndexerProtoMsgTxType.triggerSmartContract: + return parseTriggerSmartContractTx(tx as any, addresses); // call contract (contract interaction) + default: + return undefined; + } +} + +/** + * Example transaction: + * { + * "block": 49017942, + * "chain_id": 2494104990, + * "code": -1, + * "confirmations": 0, + * "fee": "0", + * "gas_limit": "13045", + * "hash": "0x5f57e6021d9a4f1697351a23b50cd43a21ceceac0bf5582c1a2791d287f0f8a6", + * "id": "0x5f57e6021d9a4f1697351a23b50cd43a21ceceac0bf5582c1a2791d287f0f8a6", + * "input": "a9059cbb000000000000000000000000f066ec5164b6e38cdcab8daa957c21fa9759215f00000000000000000000000000000000000000000000000000000000006acfc0", + * "msg": { + * "triggerSmartContract": { + * "contractAddress": "tsNLx+A6DVJvQ4XupCQNOze0B28=", + * "data": "qQWcuwAAAAAAAAAAAAAAAPBm7FFktuOM3KuNqpV8IfqXWSFfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABqz8A=", + * "ownerAddress": "HXFYEQKwQWNOYkegpTJgRW07eFw=" + * }, + * "type": "msgProtoTx" + * }, + * "msg_type": "TriggerSmartContract", + * "participants": [ + * { + * "address": "HXFYEQKwQWNOYkegpTJgRW07eFw=", + * "blockId": "49017942", + * "role": "sender", + * "txId": "X1fmAh2aTxaXNRojtQzUOiHOzqwL9VgsGieR0ofw+KY=" + * }, + * { + * "address": "tsNLx+A6DVJvQ4XupCQNOze0B28=", + * "blockId": "49017942", + * "role": "receiver", + * "txId": "X1fmAh2aTxaXNRojtQzUOiHOzqwL9VgsGieR0ofw+KY=" + * } + * ], + * "senders": [], + * "ts": "2024-11-04T13:07:09Z" + * } + */ +function parseTriggerSmartContractTx( + tx: IndexerTransactionWithType, + _addresses: string[], +): ParsedTransactionData | undefined { + if (!tx.msg.triggerSmartContract) { + return undefined; + } + + const contractAddress = AddressUtils.tronToHex( + AddressUtils.bufferToTron(tx.msg.triggerSmartContract.contractAddress), + ); + + const senderParticipant = tx.participants.find( + p => p.role === IndexerTransactionParticipantRole.sender, + ); + const receiverParticipant = tx.participants.find( + p => p.role === IndexerTransactionParticipantRole.receiver, + ); + + const from = senderParticipant + ? AddressUtils.bufferToTron(senderParticipant.address) + : ''; + const to = receiverParticipant + ? AddressUtils.bufferToTron(receiverParticipant.address) + : ''; + + const provider = Provider.getByEthChainId(tx.chain_id); + const token = Token.data[contractAddress] || Token.UNKNOWN_TOKEN; + + // Обработка данных контракта + const parsedToken: IndexerTxParsedTokenInfo = token + ? { + icon: token.image!, + decimals: token.decimals!, + name: token.name!, + symbol: token.symbol!, + contract_address: contractAddress, + } + : getNativeToken(provider); + + return { + from, + to, + amount: [Balance.Empty], + isContractInteraction: true, + isIncoming: false, + isOutcoming: true, + tokens: [parsedToken], + isCosmosTx: false, + isEthereumTx: true, + icon: IconsName.contract, + title: getText(I18N.transactionContractTitle), + subtitle: + token.name || + getText(I18N.transactionContractDefaultName).replace( + 'HAQQ Network', + provider?.name!, + ), + }; +} + +/** + * Example transaction: + * { + * "block": 49017933, + * "chain_id": 2494104990, + * "code": -1, + * "confirmations": 0, + * "fee": "0", + * "gas_limit": "0", + * "hash": "0xf6371d0a49d088e524fea7444763eecf414ff2a0c225d677f5be0c005d9318d4", + * "id": "0xf6371d0a49d088e524fea7444763eecf414ff2a0c225d677f5be0c005d9318d4", + * "input": "", + * "msg": { + * "transferContract": { + * "amount": "5000000", + * "ownerAddress": "HXFYEQKwQWNOYkegpTJgRW07eFw=", + * "toAddress": "8GbsUWS244zcq42qlXwh+pdZIV8=" + * }, + * "type": "msgProtoTx" + * }, + * "msg_type": "TransferContract", + * "participants": [ + * { + * "address": "HXFYEQKwQWNOYkegpTJgRW07eFw=", + * "blockId": "49017933", + * "role": "sender", + * "txId": "9jcdCknQiOUk/qdER2Puz0FP8qDCJdZ39b4MAF2TGNQ=" + * }, + * { + * "address": "8GbsUWS244zcq42qlXwh+pdZIV8=", + * "blockId": "49017933", + * "role": "receiver", + * "txId": "9jcdCknQiOUk/qdER2Puz0FP8qDCJdZ39b4MAF2TGNQ=" + * } + * ], + * "senders": [], + * "ts": "2024-11-04T13:06:42Z" + * } + */ function parseTransferContractTx( tx: IndexerTransactionWithType, addresses: string[], -): ParsedTransactionData { - const transaction = parseTronTransaction(tx); - const isIncoming = isIncomingTx(transaction, addresses); - const {from, to} = getFromAndTo(transaction, isIncoming); - const provider = Provider.getByEthChainId(transaction.chain_id); +): ParsedTransactionData | undefined { + if (!tx.msg.transferContract) { + return undefined; + } + + const senderParticipant = tx.participants.find( + p => p.role === IndexerTransactionParticipantRole.sender, + ); + const receiverParticipant = tx.participants.find( + p => p.role === IndexerTransactionParticipantRole.receiver, + ); + + const from = senderParticipant + ? AddressUtils.bufferToTron(senderParticipant.address) + : ''; + const to = receiverParticipant + ? AddressUtils.bufferToTron(receiverParticipant.address) + : ''; + + const isIncoming = addresses.includes(to) && !addresses.includes(from); + + const provider = Provider.getByEthChainId(tx.chain_id); const amount = [ new Balance( - transaction.msg.transferContract.amount, + tx.msg.transferContract.amount, provider?.decimals, provider?.denom, ), @@ -157,27 +466,24 @@ function parseTransferContractTx( : getText(I18N.transactionSendTitle); const subtitle = isIncoming - ? formatAddressForSubtitle(from, 'toTron', true) - : formatAddressForSubtitle(to, 'toTron', false); - const icon = isIncoming ? IconsName.arrow_receive : IconsName.arrow_send; + ? formatAddressForSubtitle(from, 'hexToTron', true) + : formatAddressForSubtitle(to, 'hexToTron', false); - const isContractInteraction = isContractInteractionTx(transaction); + const icon = isIncoming ? IconsName.arrow_receive : IconsName.arrow_send; return { from, to, amount, - isContractInteraction, + isContractInteraction: false, isIncoming, isOutcoming: !isIncoming, tokens: [getNativeToken(provider)], isCosmosTx: false, isEthereumTx: true, - icon: isContractInteraction ? IconsName.contract : icon, - title: isContractInteraction - ? getText(I18N.transactionContractTitle) - : title, - subtitle: isContractInteraction ? getContractName(transaction) : subtitle, + icon: icon, + title, + subtitle: subtitle, }; } @@ -428,7 +734,7 @@ function parseMsgSend( const formatAddressForSubtitle = ( address: string, - format: 'toEth' | 'toHaqq' | 'toTron' = 'toEth', + format: 'toEth' | 'toHaqq' | 'toTron' | 'hexToTron' = 'toEth', from = false, ) => `${from ? 'from' : 'to'} ${shortAddress(AddressUtils[format](address), '•')}`; @@ -450,6 +756,9 @@ function isIncomingTx(tx: IndexerTransaction, addresses: string[]): boolean { if (msg.transferContract?.toAddress) { return tronAddresses.includes(msg.transferContract.toAddress); } + if (msg.triggerSmartContract?.ownerAddress) { + return tronAddresses.includes(msg.triggerSmartContract.ownerAddress); + } return false; } @@ -582,38 +891,18 @@ function getTokensInfo(tx: IndexerTransaction): IndexerTxParsedTokenInfo[] { return [Token.UNKNOWN_TOKEN]; } -function parseTronTransaction( - tx: IndexerTransactionWithType, -): IndexerTransactionWithType { - return { - ...tx, - msg: { - ...tx.msg, - transferContract: { - ...tx.msg.transferContract, - ownerAddress: AddressUtils.hexToTron( - `0x${Buffer.from( - tx.msg.transferContract.ownerAddress, - 'base64', - ).toString('hex')}`, - ), - toAddress: AddressUtils.hexToTron( - `0x${Buffer.from( - tx.msg.transferContract.ownerAddress, - 'base64', - ).toString('hex')}`, - ), - }, - }, - }; -} - function getFromAndTo(tx: IndexerTransaction, isIncoming: boolean) { if (isIncoming) { if (tx?.msg?.type === IndexerTxMsgType.msgProtoTx) { + const from = tx.participants.find( + p => p.role === IndexerTransactionParticipantRole.sender, + )?.address; + const to = tx.participants.find( + p => p.role === IndexerTransactionParticipantRole.receiver, + )?.address; return { - from: tx.msg.transferContract.ownerAddress, - to: tx.msg.transferContract.toAddress, + from: from ? AddressUtils.bufferToTron(from) : '', + to: to ? AddressUtils.bufferToTron(to) : '', }; } diff --git a/src/hooks/use-transaction-list.ts b/src/hooks/use-transaction-list.ts index e963d9bbe..f82a2930e 100644 --- a/src/hooks/use-transaction-list.ts +++ b/src/hooks/use-transaction-list.ts @@ -14,7 +14,7 @@ export function useTransactionList(addressList: string[]) { useEffect(() => { Transaction.fetchLatestTransactions(addressList, true); - }, [addressList[0]]); + }, [addressList]); return {transactions, isTransactionsLoading}; } diff --git a/src/hooks/use-transaction.ts b/src/hooks/use-transaction.ts index 6e73a1f18..5cee23bbe 100644 --- a/src/hooks/use-transaction.ts +++ b/src/hooks/use-transaction.ts @@ -3,11 +3,12 @@ import {useMemo} from 'react'; import {computed} from 'mobx'; import {Transaction} from '@app/models/transaction'; +import {IndexerTxMsgType} from '@app/types'; -export const useTransaction = (txId: string) => { +export const useTransaction = (txId: string, txType: IndexerTxMsgType) => { const tx = useMemo(() => { - return computed(() => Transaction.getById(txId)); - }, [txId]).get(); + return computed(() => Transaction.getById(txId, txType)); + }, [txId, txType]).get(); return tx!; }; diff --git a/src/models/provider/provider.model.ts b/src/models/provider/provider.model.ts index 8b5295e34..96d0d4a39 100644 --- a/src/models/provider/provider.model.ts +++ b/src/models/provider/provider.model.ts @@ -140,7 +140,7 @@ export class ProviderModel { if (this.isTron) { return this.config.explorerTxUrl.replace( EXPLORER_URL_TEMPLATES.TX, - txHash, + txHash.replace(/^0x/, ''), ); } diff --git a/src/models/transaction.ts b/src/models/transaction.ts index 47e4c7a1b..248fa780e 100644 --- a/src/models/transaction.ts +++ b/src/models/transaction.ts @@ -7,7 +7,12 @@ import {Socket} from '@app/models/socket'; import {Wallet} from '@app/models/wallet'; import {Balance} from '@app/services/balance'; import {Indexer} from '@app/services/indexer'; -import {IndexerTransaction, IndexerTxParsedTokenInfo} from '@app/types'; +import { + ChainId, + IndexerTransaction, + IndexerTxMsgType, + IndexerTxParsedTokenInfo, +} from '@app/types'; import {RPCMessage, RPCObserver} from '@app/types/rpc'; import {Token} from './tokens'; @@ -59,7 +64,7 @@ class TransactionStore implements RPCObserver { return this._isLoading; } - create(transaction: IndexerTransaction) { + create(transaction: IndexerTransaction, accounts: Record) { const existingTransaction = this.getById(transaction.id); const newTransaction: Transaction = parseTransaction( @@ -67,7 +72,7 @@ class TransactionStore implements RPCObserver { ...existingTransaction, ...transaction, }, - Wallet.addressList(), + accounts, ); if (existingTransaction) { @@ -92,12 +97,18 @@ class TransactionStore implements RPCObserver { } } - getById(id: string) { + getById(id: string, txType?: IndexerTxMsgType) { const transactionLowerCaseId = id.toLowerCase(); return ( - this.getAll().find( - transaction => transaction.id.toLowerCase() === transactionLowerCaseId, - ) || null + this.getAll().find(transaction => { + if (txType) { + return ( + transaction.id.toLowerCase() === transactionLowerCaseId && + transaction.msg.type === txType + ); + } + return transaction.id.toLowerCase() === transactionLowerCaseId; + }) || null ); } @@ -131,7 +142,9 @@ class TransactionStore implements RPCObserver { return; } - const nextTxList = await this._fetch(accounts); + const nextTxList = await this._fetch( + Indexer.instance.getProvidersHeader(accounts), + ); runInAction(() => { this._transactions = [...this._transactions, ...nextTxList].filter( @@ -149,7 +162,10 @@ class TransactionStore implements RPCObserver { return; } - const newTxs = await this._fetch(accounts, 'latest'); + const newTxs = await this._fetch( + Indexer.instance.getProvidersHeader(accounts), + 'latest', + ); runInAction(() => { this._transactions = newTxs; @@ -158,8 +174,11 @@ class TransactionStore implements RPCObserver { return newTxs; }; - private _fetch = async (accounts: string[], ts?: string) => { + private _fetch = async (accounts: Record, ts?: string) => { try { + runInAction(() => { + this._isLoading = true; + }); const result = await Indexer.instance.getTransactions( accounts, ts ?? this._lastSyncedTransactionTs, @@ -192,12 +211,12 @@ class TransactionStore implements RPCObserver { } const result = message.data.txs || message.data.transactions || []; - const accounts = Wallet.addressList(); + const accounts = Indexer.instance.getProvidersHeader(Wallet.addressList()); const parsed = result .map(tx => parseTransaction(tx, accounts)) .filter(tx => !!tx.parsed); - parsed.forEach(transaction => this.create(transaction)); + parsed.forEach(transaction => this.create(transaction, accounts)); }; clear() { diff --git a/src/route-types.ts b/src/route-types.ts index 6d663fb64..e762f1512 100644 --- a/src/route-types.ts +++ b/src/route-types.ts @@ -16,6 +16,7 @@ import { Eventable, IStory, IToken, + IndexerTxMsgType, JsonRpcMetadata, LedgerWalletInitialData, MarketingEvents, @@ -417,7 +418,11 @@ export type HomeStackParamList = { address: string; isPopup?: boolean; }; - [HomeStackRoutes.TransactionDetail]: {txId: string; addresses: string[]}; + [HomeStackRoutes.TransactionDetail]: { + txId: string; + addresses: string[]; + txType: IndexerTxMsgType; + }; [HomeStackRoutes.InAppBrowser]: { url: string; title?: string; diff --git a/src/screens/HomeStack/HomeFeedStack/account-info.tsx b/src/screens/HomeStack/HomeFeedStack/account-info.tsx index 7f71ae704..ef5c357a4 100644 --- a/src/screens/HomeStack/HomeFeedStack/account-info.tsx +++ b/src/screens/HomeStack/HomeFeedStack/account-info.tsx @@ -47,6 +47,7 @@ export const AccountInfoScreen = observer(() => { navigation.navigate(HomeStackRoutes.TransactionDetail, { txId: tx.id, addresses: [accountId], + txType: tx.msg.type, }); }, [navigation, accountId], diff --git a/src/screens/HomeStack/HomeFeedStack/transaction-detail.tsx b/src/screens/HomeStack/HomeFeedStack/transaction-detail.tsx index 2fea63ad9..a6363ff38 100644 --- a/src/screens/HomeStack/HomeFeedStack/transaction-detail.tsx +++ b/src/screens/HomeStack/HomeFeedStack/transaction-detail.tsx @@ -23,7 +23,7 @@ export const TransactionDetailScreen = observer(() => { HomeStackParamList, HomeStackRoutes.TransactionDetail >(); - const tx = useTransaction(route.params.txId); + const tx = useTransaction(route.params.txId, route.params.txType); const provider = useMemo( () => Provider.getByEthChainId(tx.chain_id), [tx.chain_id], diff --git a/src/screens/HomeStack/TransactionStack/transaction-sum.tsx b/src/screens/HomeStack/TransactionStack/transaction-sum.tsx index 5da252bd4..20cf812c6 100644 --- a/src/screens/HomeStack/TransactionStack/transaction-sum.tsx +++ b/src/screens/HomeStack/TransactionStack/transaction-sum.tsx @@ -37,8 +37,11 @@ export const TransactionSumScreen = observer(() => { >(); const event = useMemo(() => generateUUID(), []); const [to, setTo] = useState(route.params.to); + const provider = + Provider.getByEthChainId(route.params.token.chain_id) ?? + Provider.selectedProvider; const wallet = Wallet.getById(route.params.from); - const balances = Wallet.getBalancesByAddressList([wallet!]); + const balances = Wallet.getBalancesByAddressList([wallet!], provider); const currentBalance = useMemo( () => balances[AddressUtils.toEth(route.params.from)], [balances, route], @@ -53,7 +56,6 @@ export const TransactionSumScreen = observer(() => { const getFee = useCallback( async (amount: Balance) => { try { - const provider = Provider.getByEthChainId(route.params.token.chain_id); const token = route.params.token; if (token.is_erc20) { const contractAddress = provider?.isTron @@ -85,7 +87,7 @@ export const TransactionSumScreen = observer(() => { return null; } }, - [route.params], + [route.params, provider], ); useEffect(() => { @@ -102,27 +104,7 @@ export const TransactionSumScreen = observer(() => { const onPressPreview = useCallback( async (amount: Balance, repeated = false) => { setLoading(true); - const estimate = await getFee(amount); - - let successCondition = false; - - if (Provider.getByEthChainId(route.params.token.chain_id)?.isTron) { - // fee can be zero for TRON if user has enough bandwidth (freezed TRX) - successCondition = !!estimate?.expectedFee ?? false; - } else { - successCondition = estimate?.expectedFee.isPositive() ?? false; - } - - if (successCondition) { - navigation.navigate(TransactionStackRoutes.TransactionConfirmation, { - // @ts-ignore - calculatedFees: estimate, - from: route.params.from, - to, - amount, - token: route.params.token, - }); - } else { + const showError = () => { showModal(ModalType.error, { title: getText(I18N.feeCalculatingRpcErrorTitle), description: getText(I18N.feeCalculatingRpcErrorDescription), @@ -135,10 +117,60 @@ export const TransactionSumScreen = observer(() => { } }, }); + }; + try { + const estimate = await getFee(amount); + const balance = Wallet.getBalance( + route.params.from, + 'available', + provider, + ); + + let totalAmount = estimate?.expectedFee; + + if (amount.isNativeCoin) { + totalAmount = totalAmount?.operate(amount, 'add'); + } + + if (totalAmount && totalAmount.compare(balance, 'gt')) { + return showModal(ModalType.notEnoughGas, { + gasLimit: estimate?.expectedFee!, + currentAmount: balance, + }); + } + + let successCondition = false; + + if (provider.isTron) { + // fee can be zero for TRON if user has enough bandwidth (freezed TRX) + successCondition = !!estimate?.expectedFee ?? false; + } else { + successCondition = estimate?.expectedFee.isPositive() ?? false; + } + + if (successCondition) { + navigation.navigate(TransactionStackRoutes.TransactionConfirmation, { + // @ts-ignore + calculatedFees: estimate, + from: route.params.from, + to, + amount, + token: route.params.token, + }); + } else { + showError(); + } + } catch (err) { + Logger.captureException(err, 'TransactionSumScreen:onPressPreview', { + amount, + provider: provider.name, + }); + showError(); + } finally { + setLoading(false); } - setLoading(false); }, - [fee, navigation, route.params.from, to], + [fee, navigation, route.params.from, to, provider], ); const onContact = useCallback(() => { diff --git a/src/screens/total-value-info.tsx b/src/screens/total-value-info.tsx index e5b729dab..83ba44a11 100644 --- a/src/screens/total-value-info.tsx +++ b/src/screens/total-value-info.tsx @@ -45,6 +45,7 @@ export const TotalValueInfoScreen = observer(() => { navigation.navigate(HomeStackRoutes.TransactionDetail, { txId: tx.id, addresses: addressList, + txType: tx.msg.type, }); }, [navigation, addressList], diff --git a/src/services/indexer/indexer.ts b/src/services/indexer/indexer.ts index 1bdfc5a9c..3ccacf165 100644 --- a/src/services/indexer/indexer.ts +++ b/src/services/indexer/indexer.ts @@ -54,7 +54,7 @@ export class Indexer { this.init(); } - private getProvidersHeader = ( + public getProvidersHeader = ( accounts: string[], provider = Provider.selectedProvider, ) => { @@ -204,18 +204,25 @@ export class Indexer { getTransactions = createAsyncTask( async ( - accounts: string[], + accounts: string[] | Record, latestBlock: string | null, ): Promise => { try { - if (!accounts.length) { + if (Array.isArray(accounts) && !accounts.length) { + return []; + } + + if ( + typeof accounts === 'object' && + Object.values(accounts).every(addresses => !addresses.length) + ) { return []; } const response = await jsonrpcRequest( RemoteConfig.get('proxy_server')!, 'transactions_by_timestamp', - [this.getProvidersHeader(accounts), latestBlock ?? 'latest'], + [accounts, latestBlock ?? 'latest'], ); return response?.transactions || []; diff --git a/src/services/tron-network/tron-network.ts b/src/services/tron-network/tron-network.ts index 5e8aa60f3..b70e03663 100644 --- a/src/services/tron-network/tron-network.ts +++ b/src/services/tron-network/tron-network.ts @@ -107,6 +107,7 @@ const SUN_PER_TRX = 1_000_000; const BANDWIDTH_PRICE_IN_SUN = 1000; // 1000 SUN per byte export class TronNetwork { + static TOKEN_TRANSFER_SELECTOR = 'a9059cbb'; static async broadcastTransaction( signedTransaction: string, provider: ProviderModel, @@ -148,7 +149,7 @@ export class TronNetwork { const data = paramValue?.data ?? ''; // Assuming standard TRC20 transfer function signature // Function: transfer(address _to, uint256 _value) - if (data.startsWith('a9059cbb')) { + if (data.startsWith(TronNetwork.TOKEN_TRANSFER_SELECTOR)) { const valueHex = '0x' + data.slice(74, 138); value = valueHex; } else { diff --git a/src/types.ts b/src/types.ts index 3bd908004..39f2f499c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1809,15 +1809,44 @@ export type IndexerTxMsgApproval = { spender: AddressCosmosHaqq; }; +export type IndexerTxMsgEventTx = { + type: IndexerTxMsgType.msgEventTx; + blockId: string; + contractAddress: string; + data: string; + message: { + transfer?: { + from: string; + to: string; + value: string; + }; + }; + messageType: 'transfer' | string; + topic0: string; + topic1: string; + topic2: string; + txId: string; +}; + export type IndexerTxMsgProtoTx = { type: IndexerTxMsgType.msgProtoTx; - transferContract: { + transferContract?: { amount: string; ownerAddress: AddressTron; toAddress: AddressTron; }; + triggerSmartContract?: { + contractAddress: AddressTron; + data: string; + ownerAddress: AddressTron; + }; }; +export enum IndexerProtoMsgTxType { + transferContract = 'TransferContract', + triggerSmartContract = 'TriggerSmartContract', +} + export enum IndexerTxMsgType { unknown = 'unknown', msgVote = 'msgVote', @@ -1839,6 +1868,7 @@ export enum IndexerTxMsgType { msgEditValidator = 'msgEditValidator', msgEthereumApprovalTx = 'msgEthereumApprovalTx', msgProtoTx = 'msgProtoTx', + msgEventTx = 'msgEventTx', } export type IndexerTxMsgUnion = @@ -1860,7 +1890,8 @@ export type IndexerTxMsgUnion = | {msg: IndexerTxMsgCreateValidatorTx} | {msg: IndexerTxMsgEditValidatorTx} | {msg: IndexerTxMsgApproval} - | {msg: IndexerTxMsgProtoTx}; + | {msg: IndexerTxMsgProtoTx} + | {msg: IndexerTxMsgEventTx}; export enum IndexerTransactionStatus { inProgress = -1, @@ -1880,8 +1911,19 @@ export type IndexerTransaction = { id: string; confirmations: number; msg_type: string; + participants: { + address: string; + blockId: string; + role: IndexerTransactionParticipantRole; + txId: string; + }[]; } & IndexerTxMsgUnion; +export enum IndexerTransactionParticipantRole { + sender = 'sender', + receiver = 'receiver', +} + export type IndexerTransactionWithType = Extract< IndexerTransaction, {msg: {type: T}} diff --git a/src/widgets/transactions-widget/index.tsx b/src/widgets/transactions-widget/index.tsx index a2cdcf08c..9c912c474 100644 --- a/src/widgets/transactions-widget/index.tsx +++ b/src/widgets/transactions-widget/index.tsx @@ -31,6 +31,7 @@ export const TransactionsWidgetWrapper = observer(() => { navigation.navigate(HomeStackRoutes.TransactionDetail, { txId: tx.id, addresses: addressList, + txType: tx.msg.type, }); }, [navigation, addressList], diff --git a/src/widgets/transactions-widget/transactions-widget.tsx b/src/widgets/transactions-widget/transactions-widget.tsx index cbf747a0c..5dc1336c7 100644 --- a/src/widgets/transactions-widget/transactions-widget.tsx +++ b/src/widgets/transactions-widget/transactions-widget.tsx @@ -34,7 +34,7 @@ export const TransactionsWidget = ({ {lastTransactions.map(item => { return (