Skip to content

Commit

Permalink
AND-3671 Transaction history for Ethereum
Browse files Browse the repository at this point in the history
  • Loading branch information
Yoggam1 committed Sep 21, 2023
1 parent 4732380 commit 425b58e
Show file tree
Hide file tree
Showing 12 changed files with 385 additions and 65 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package com.tangem.blockchain.blockchains.bitcoin

import android.util.Log
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.PaginationWrapper
import com.tangem.blockchain.common.toBlockchainSdkError
import com.tangem.blockchain.common.txhistory.TransactionHistoryItem
import com.tangem.blockchain.common.txhistory.TransactionHistoryItem.TransactionDirection
import com.tangem.blockchain.common.txhistory.TransactionHistoryItem.TransactionStatus
import com.tangem.blockchain.common.txhistory.TransactionHistoryItem.TransactionType
import com.tangem.blockchain.common.txhistory.TransactionHistoryProvider
import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest
import com.tangem.blockchain.common.txhistory.TransactionHistoryState
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.toBigDecimalOrDefault
Expand Down Expand Up @@ -34,15 +39,20 @@ internal class BitcoinTransactionHistoryProvider(
}
}

override suspend fun getTransactionsHistory(
address: String,
page: Int,
pageSize: Int,
): Result<PaginationWrapper<TransactionHistoryItem>> {
override suspend fun getTransactionsHistory(request: TransactionHistoryRequest): Result<PaginationWrapper<TransactionHistoryItem>> {
return try {
val response =
withContext(Dispatchers.IO) { blockBookApi.getTransactions(address, page, pageSize) }
val txs = response.transactions?.map { tx -> tx.toTransactionHistoryItem(address) } ?: emptyList()
withContext(Dispatchers.IO) {
blockBookApi.getTransactions(
address = request.address,
page = request.page.number,
pageSize = request.page.size,
filterType = null,
)
}
val txs = response.transactions
?.map { tx -> tx.toTransactionHistoryItem(request.address) }
?: emptyList()
Result.Success(
PaginationWrapper(
page = response.page,
Expand Down Expand Up @@ -128,7 +138,7 @@ internal class BitcoinTransactionHistoryProvider(
} else {
val outputs = tx.vout
.filter { !it.addresses.contains(walletAddress) }
.mapNotNull { it.value.toBigDecimalOrNull() }
.mapNotNull { it.value?.toBigDecimalOrNull() }
.sumOf { it }
val fee = tx.fees.toBigDecimalOrDefault()
outputs + fee
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package com.tangem.blockchain.blockchains.ethereum

import com.tangem.Log
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.txhistory.TransactionHistoryItem
import com.tangem.blockchain.common.txhistory.TransactionHistoryItem.TransactionStatus
import com.tangem.blockchain.common.txhistory.TransactionHistoryProvider
import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest
import com.tangem.blockchain.common.txhistory.TransactionHistoryState
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.network.blockbook.network.BlockBookApi
import com.tangem.blockchain.network.blockbook.network.responses.GetAddressResponse
import com.tangem.blockchain.network.blockbook.network.responses.GetAddressResponse.Transaction.EthereumSpecific
import com.tangem.common.extensions.guard
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.math.BigDecimal
import java.util.concurrent.TimeUnit

internal class EthereumTransactionHistoryProvider(
private val blockchain: Blockchain,
private val blockBookApi: BlockBookApi,
) : TransactionHistoryProvider {

override suspend fun getTransactionHistoryState(address: String): TransactionHistoryState {
return try {
val addressResponse = withContext(Dispatchers.IO) { blockBookApi.getAddress(address) }
if (!addressResponse.transactions.isNullOrEmpty()) {
TransactionHistoryState.Success.HasTransactions(addressResponse.transactions.size)
} else {
TransactionHistoryState.Success.Empty
}
} catch (e: Exception) {
TransactionHistoryState.Failed.FetchError(e)
}
}

override suspend fun getTransactionsHistory(request: TransactionHistoryRequest): Result<PaginationWrapper<TransactionHistoryItem>> {
return try {
val response =
withContext(Dispatchers.IO) {
blockBookApi.getTransactions(
address = request.address,
page = request.page.number,
pageSize = request.page.size,
filterType = request.filterType,
)
}
val txs = response.transactions
?.mapNotNull { tx ->
tx.toTransactionHistoryItem(
walletAddress = request.address,
filterType = request.filterType
)
}
?: emptyList()
Result.Success(
PaginationWrapper(
page = response.page,
totalPages = response.totalPages,
itemsOnPage = response.itemsOnPage,
items = txs
)
)
} catch (e: Exception) {
Result.Failure(e.toBlockchainSdkError())
}
}

private fun GetAddressResponse.Transaction.toTransactionHistoryItem(
walletAddress: String,
filterType: TransactionHistoryRequest.FilterType,
): TransactionHistoryItem? {
val isIncoming = checkIsIncoming(walletAddress, this, filterType)
val amount = extractAmount(tx = this, filterType = filterType).guard {
Log.info { "Transaction $this doesn't contain a required value" }
return null
}

return TransactionHistoryItem(
txHash = txid,
timestamp = TimeUnit.SECONDS.toMillis(blockTime.toLong()),
direction = extractTransactionDirection(
isIncoming = isIncoming,
tx = this,
),
status = extractStatus(tx = this),
type = extractType(tx = this),
amount = amount,
)
}

private fun checkIsIncoming(
walletAddress: String,
transaction: GetAddressResponse.Transaction,
filterType: TransactionHistoryRequest.FilterType,
): Boolean {
return when (filterType) {
TransactionHistoryRequest.FilterType.Coin -> transaction.vin
.firstOrNull()
?.addresses
?.firstOrNull()
.equals(walletAddress, ignoreCase = true)
.not()
is TransactionHistoryRequest.FilterType.Contract -> {
val transfer = transaction.tokenTransfers.firstOrNull { filterType.address.equals(it.contract, true) }
return !transfer?.from.equals(walletAddress, ignoreCase = true)
}
}
}

private fun extractStatus(tx: GetAddressResponse.Transaction): TransactionStatus {
val status = tx.ethereumSpecific?.status.guard {
return if (tx.confirmations > 0) TransactionStatus.Confirmed else TransactionStatus.Unconfirmed
}

return when (EthereumSpecific.StatusType.fromType(status)) {
EthereumSpecific.StatusType.PENDING -> TransactionStatus.Unconfirmed
EthereumSpecific.StatusType.FAILURE -> TransactionStatus.Failed
EthereumSpecific.StatusType.OK -> TransactionStatus.Confirmed
}
}

private fun extractType(tx: GetAddressResponse.Transaction): TransactionHistoryItem.TransactionType {
val methodId = tx.ethereumSpecific?.parsedData?.methodId.guard {
return TransactionHistoryItem.TransactionType.Transfer
}

// MethodId is empty for the coin transfers
if (methodId.isEmpty()) return TransactionHistoryItem.TransactionType.Transfer

return when (methodId) {
"0xa9059cbb" -> TransactionHistoryItem.TransactionType.Transfer
"0xa1903eab" -> TransactionHistoryItem.TransactionType.Submit
"0x095ea7b3" -> TransactionHistoryItem.TransactionType.Approve
"0x617ba037" -> TransactionHistoryItem.TransactionType.Supply
"0x69328dec" -> TransactionHistoryItem.TransactionType.Withdraw
"0xe8eda9df" -> TransactionHistoryItem.TransactionType.Deposit
"0x12aa3caf" -> TransactionHistoryItem.TransactionType.Swap
"0x0502b1c5", "0x2e95b6c8" -> TransactionHistoryItem.TransactionType.Unoswap
else -> TransactionHistoryItem.TransactionType.Custom(id = methodId)
}
}

private fun extractAmount(
tx: GetAddressResponse.Transaction,
filterType: TransactionHistoryRequest.FilterType,
): Amount? {
return when (filterType) {
TransactionHistoryRequest.FilterType.Coin -> Amount(
value = BigDecimal(tx.value).movePointLeft(blockchain.decimals()),
blockchain = blockchain,
type = AmountType.Coin
)

is TransactionHistoryRequest.FilterType.Contract -> {
val transfer = tx.tokenTransfers
.firstOrNull { filterType.address.equals(it.contract, ignoreCase = true) }
.guard {
return null
}
val transferValue = transfer.value ?: "0"
val token = Token(
name = transfer.name.orEmpty(),
symbol = transfer.symbol.orEmpty(),
contractAddress = transfer.contract,
decimals = transfer.decimals,
)
Amount(value = BigDecimal(transferValue).movePointLeft(transfer.decimals), token = token)
}
}
}

private fun extractTransactionDirection(
isIncoming: Boolean,
tx: GetAddressResponse.Transaction,
): TransactionHistoryItem.TransactionDirection {
return if (isIncoming) {
TransactionHistoryItem.TransactionDirection.Incoming(
address = TransactionHistoryItem.Address.Single(tx.vin.first().addresses.first())
)
} else {
TransactionHistoryItem.TransactionDirection.Outgoing(
address = TransactionHistoryItem.Address.Single(tx.vout.first().addresses.first())
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,10 @@ package com.tangem.blockchain.blockchains.ethereum
import android.util.Log
import com.tangem.blockchain.blockchains.ethereum.network.EthereumInfoResponse
import com.tangem.blockchain.blockchains.ethereum.network.EthereumNetworkProvider
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.BlockchainError
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.SignatureCountValidator
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.TokenFinder
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.TransactionStatus
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.toBlockchainSdkError
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.common.txhistory.DefaultTransactionHistoryProvider
import com.tangem.blockchain.common.txhistory.TransactionHistoryProvider
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.blockchain.extensions.successOr
Expand All @@ -34,7 +23,8 @@ open class EthereumWalletManager(
wallet: Wallet,
val transactionBuilder: EthereumTransactionBuilder,
protected val networkProvider: EthereumNetworkProvider,
) : WalletManager(wallet),
transactionHistoryProvider: TransactionHistoryProvider = DefaultTransactionHistoryProvider,
) : WalletManager(wallet, transactionHistoryProvider = transactionHistoryProvider),
TransactionSender,
SignatureCountValidator,
TokenFinder,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package com.tangem.blockchain.common.assembly.impl

import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionBuilder
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionHistoryProvider
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.blockchains.ethereum.getEthereumJsonRpcProviders
import com.tangem.blockchain.blockchains.ethereum.network.EthereumNetworkService
import com.tangem.blockchain.common.assembly.WalletManagerAssembly
import com.tangem.blockchain.common.assembly.WalletManagerAssemblyInput
import com.tangem.blockchain.common.txhistory.DefaultTransactionHistoryProvider
import com.tangem.blockchain.network.blockbook.config.BlockBookConfig
import com.tangem.blockchain.network.blockbook.network.BlockBookApi
import com.tangem.blockchain.network.blockcypher.BlockcypherNetworkProvider

internal object EthereumWalletManagerAssembly : WalletManagerAssembly<EthereumWalletManager>() {
Expand All @@ -24,8 +28,19 @@ internal object EthereumWalletManagerAssembly : WalletManagerAssembly<EthereumWa
blockchain = blockchain,
tokens = input.config.blockcypherTokens
),
)
),
transactionHistoryProvider = if (input.config.nowNodeCredentials != null && input.config.nowNodeCredentials.apiKey.isNotBlank()) {
EthereumTransactionHistoryProvider(
blockchain = blockchain,
blockBookApi = BlockBookApi(
config = BlockBookConfig.NowNodes(nowNodesCredentials = input.config.nowNodeCredentials),
blockchain = blockchain,
)
)
} else {
DefaultTransactionHistoryProvider
}
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ internal object DefaultTransactionHistoryProvider : TransactionHistoryProvider {
TransactionHistoryState.NotImplemented

override suspend fun getTransactionsHistory(
address: String,
page: Int,
pageSize: Int,
request: TransactionHistoryRequest
): Result<PaginationWrapper<TransactionHistoryItem>> = Result.Success(
PaginationWrapper(
page = 0,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.tangem.blockchain.common.txhistory

import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.TransactionStatus

data class TransactionHistoryItem(
val txHash: String,
Expand All @@ -21,6 +20,20 @@ data class TransactionHistoryItem(

sealed interface TransactionType {
object Transfer : TransactionType
object Submit : TransactionType
object Approve : TransactionType
object Supply : TransactionType
object Withdraw : TransactionType
object Deposit : TransactionType
object Swap : TransactionType
object Unoswap : TransactionType
data class Custom(val id: String) : TransactionType
}

sealed class TransactionStatus {
object Failed : TransactionStatus()
object Unconfirmed : TransactionStatus()
object Confirmed : TransactionStatus()
}

sealed class Address {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,5 @@ interface TransactionHistoryProvider {

suspend fun getTransactionHistoryState(address: String): TransactionHistoryState

suspend fun getTransactionsHistory(
address: String,
page: Int,
pageSize: Int,
): Result<PaginationWrapper<TransactionHistoryItem>>
suspend fun getTransactionsHistory(request: TransactionHistoryRequest): Result<PaginationWrapper<TransactionHistoryItem>>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.tangem.blockchain.common.txhistory

private const val DEFAULT_PAGING_SIZE = 20

data class TransactionHistoryRequest(
val address: String,
val page: Page,
val filterType: FilterType,
) {

data class Page(val number: Int, val size: Int = DEFAULT_PAGING_SIZE)

sealed class FilterType {
object Coin : FilterType()
data class Contract(val address: String) : FilterType()
}
}
Loading

0 comments on commit 425b58e

Please sign in to comment.