diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinTransactionHistoryProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinTransactionHistoryProvider.kt index 259e9d1e8..cfe4452ac 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinTransactionHistoryProvider.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinTransactionHistoryProvider.kt @@ -6,7 +6,6 @@ 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 @@ -25,11 +24,21 @@ internal class BitcoinTransactionHistoryProvider( private val blockBookApi: BlockBookApi, ) : TransactionHistoryProvider { - override suspend fun getTransactionHistoryState(address: String): TransactionHistoryState { + override suspend fun getTransactionHistoryState( + address: String, + filterType: TransactionHistoryRequest.FilterType, + ): TransactionHistoryState { return try { - val addressResponse = withContext(Dispatchers.IO) { blockBookApi.getAddress(address) } - if (addressResponse.txs > 0) { - TransactionHistoryState.Success.HasTransactions(addressResponse.txs) + val response = withContext(Dispatchers.IO) { + blockBookApi.getTransactions( + address = address, + page = 1, + pageSize = 1, // We don't need to know all transactions to define state + filterType = filterType, + ) + } + if (!response.transactions.isNullOrEmpty()) { + TransactionHistoryState.Success.HasTransactions(response.txs) } else { TransactionHistoryState.Success.Empty } @@ -55,9 +64,9 @@ internal class BitcoinTransactionHistoryProvider( ?: emptyList() Result.Success( PaginationWrapper( - page = response.page, - totalPages = response.totalPages, - itemsOnPage = response.itemsOnPage, + page = response.page ?: request.page.number, + totalPages = response.totalPages ?: 0, + itemsOnPage = response.itemsOnPage ?: 0, items = txs ) ) @@ -67,19 +76,17 @@ internal class BitcoinTransactionHistoryProvider( } private fun GetAddressResponse.Transaction.toTransactionHistoryItem(walletAddress: String): TransactionHistoryItem { - val isOutgoing = vin.any { it.addresses.contains(walletAddress) } + val isOutgoing = vin.any { it.addresses?.contains(walletAddress) == true } return TransactionHistoryItem( txHash = txid, timestamp = TimeUnit.SECONDS.toMillis(blockTime.toLong()), - direction = extractTransactionDirection( - isIncoming = !isOutgoing, - tx = this, - walletAddress = walletAddress - ), + isOutgoing = isOutgoing, + destinationType = extractDestinationType(tx = this, walletAddress = walletAddress), + sourceType = sourceType(tx = this, walletAddress = walletAddress), status = if (confirmations > 0) TransactionStatus.Confirmed else TransactionStatus.Unconfirmed, type = TransactionType.Transfer, amount = extractAmount( - isIncoming = !isOutgoing, + isOutgoing = isOutgoing, tx = this, walletAddress = walletAddress, blockchain = blockchain, @@ -87,61 +94,68 @@ internal class BitcoinTransactionHistoryProvider( ) } - private fun extractTransactionDirection( - isIncoming: Boolean, + private fun extractDestinationType( tx: GetAddressResponse.Transaction, walletAddress: String, - ): TransactionDirection { - val address: TransactionHistoryItem.Address = if (isIncoming) { - val inputsWithOtherAddresses = tx.vin - .filter { !it.addresses.contains(walletAddress) } - .flatMap { it.addresses } - .toSet() - when { - inputsWithOtherAddresses.isEmpty() -> TransactionHistoryItem.Address.Single(rawAddress = walletAddress) - inputsWithOtherAddresses.size == 1 -> TransactionHistoryItem.Address.Single( - rawAddress = inputsWithOtherAddresses.first() - ) - else -> TransactionHistoryItem.Address.Multiple - } - } else { - val outputsWithOtherAddresses = tx.vout - .filter { !it.addresses.contains(walletAddress) } - .flatMap { it.addresses } - .toSet() - when { - outputsWithOtherAddresses.isEmpty() -> TransactionHistoryItem.Address.Single(rawAddress = walletAddress) - outputsWithOtherAddresses.size == 1 -> TransactionHistoryItem.Address.Single( - rawAddress = outputsWithOtherAddresses.first() - ) - else -> TransactionHistoryItem.Address.Multiple - } + ): TransactionHistoryItem.DestinationType { + val outputsWithOtherAddresses = tx.vout + .filter { it.addresses?.contains(walletAddress) == false } + .mapNotNull { it.addresses } + .flatten() + .toSet() + return when { + outputsWithOtherAddresses.isEmpty() -> TransactionHistoryItem.DestinationType.Single( + TransactionHistoryItem.AddressType.User(walletAddress) + ) + + outputsWithOtherAddresses.size == 1 -> TransactionHistoryItem.DestinationType.Single( + TransactionHistoryItem.AddressType.User(outputsWithOtherAddresses.first()) + ) + + else -> TransactionHistoryItem.DestinationType.Multiple( + outputsWithOtherAddresses.map { TransactionHistoryItem.AddressType.User(it) } + ) + } + } + + private fun sourceType( + tx: GetAddressResponse.Transaction, + walletAddress: String, + ): TransactionHistoryItem.SourceType { + val inputsWithOtherAddresses = tx.vin + .filter { it.addresses?.contains(walletAddress) == false } + .mapNotNull { it.addresses } + .flatten() + .toSet() + return when { + inputsWithOtherAddresses.isEmpty() -> TransactionHistoryItem.SourceType.Single(walletAddress) + inputsWithOtherAddresses.size == 1 -> TransactionHistoryItem.SourceType.Single(inputsWithOtherAddresses.first()) + else -> TransactionHistoryItem.SourceType.Multiple(inputsWithOtherAddresses.toList()) } - return if (isIncoming) TransactionDirection.Incoming(address) else TransactionDirection.Outgoing(address) } private fun extractAmount( - isIncoming: Boolean, + isOutgoing: Boolean, tx: GetAddressResponse.Transaction, walletAddress: String, blockchain: Blockchain, ): Amount { return try { - val amount = if (isIncoming) { - val outputs = tx.vout - .find { it.addresses.contains(walletAddress) } - ?.value.toBigDecimalOrDefault() - val inputs = tx.vin - .find { it.addresses.contains(walletAddress) } - ?.value.toBigDecimalOrDefault() - outputs - inputs - } else { + val amount = if (isOutgoing) { val outputs = tx.vout - .filter { !it.addresses.contains(walletAddress) } + .filter { it.addresses?.contains(walletAddress) == false } .mapNotNull { it.value?.toBigDecimalOrNull() } .sumOf { it } val fee = tx.fees.toBigDecimalOrDefault() outputs + fee + } else { + val outputs = tx.vout + .find { it.addresses?.contains(walletAddress) == true} + ?.value.toBigDecimalOrDefault() + val inputs = tx.vin + .find { it.addresses?.contains(walletAddress) == true } + ?.value.toBigDecimalOrDefault() + outputs - inputs } Amount(value = amount.movePointLeft(blockchain.decimals()), blockchain = blockchain) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinWalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinWalletManager.kt index 56d0239b1..a39771b5a 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinWalletManager.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinWalletManager.kt @@ -92,7 +92,9 @@ open class BitcoinWalletManager( balanceDif = it.value.sumOf { transaction -> transaction.balanceDif }, hash = it.value[0].hash, date = it.value[0].date, - isConfirmed = it.value[0].isConfirmed + isConfirmed = it.value[0].isConfirmed, + destination = it.value[0].destination, + source = it.value[0].source, ) } return BitcoinAddressInfo(balance, unspentOutputs, finalTransactions, hasUnconfirmed) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/blockchaininfo/BlockchainInfoNetworkProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/blockchaininfo/BlockchainInfoNetworkProvider.kt index 3ea8620c6..1c8dc64a7 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/blockchaininfo/BlockchainInfoNetworkProvider.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/blockchaininfo/BlockchainInfoNetworkProvider.kt @@ -38,15 +38,6 @@ class BlockchainInfoNetworkProvider() : BitcoinNetworkProvider { val addressData = addressDeferred.await() val unspents = unspentsDeferred.await() - val transactions = addressData.transactions!!.map { - BasicTransactionData( - balanceDif = it.balanceDif!!.toBigDecimal().movePointLeft(decimals), - hash = it.hash!!, - isConfirmed = it.blockHeight != 0L, - date = Calendar.getInstance().apply { this.timeInMillis = it.time!! * 1000 } - ) - } - val unspentOutputs = unspents.unspentOutputs!!.map { BitcoinUnspentOutput( amount = it.amount!!.toBigDecimal().movePointLeft(decimals), @@ -60,7 +51,7 @@ class BlockchainInfoNetworkProvider() : BitcoinNetworkProvider { balance = addressData.finalBalance?.toBigDecimal()?.movePointLeft(decimals) ?: 0.toBigDecimal(), unspentOutputs = unspentOutputs, - recentTransactions = transactions + recentTransactions = addressData.transactions.toRecentTransactions(walletAddress = address), )) } } catch (exception: Exception) { @@ -123,6 +114,42 @@ class BlockchainInfoNetworkProvider() : BitcoinNetworkProvider { } } + private fun List?.toRecentTransactions(walletAddress: String): List { + return this?.map { it.toBasicTransactionData(walletAddress) } ?: emptyList() + } + + private fun BlockchainInfoTransaction.toBasicTransactionData(walletAddress: String): BasicTransactionData { + val balanceDiff = balanceDif ?: 0 + val isIncoming = balanceDiff > 0 + val date = Calendar.getInstance().also { calendar -> + if (time != null) calendar.timeInMillis = time * 100 + } + var source = "unknown" + var destination = "unknown" + if (isIncoming) { + inputs + .firstOrNull { it.previousOutput?.address != walletAddress } + ?.previousOutput + ?.address + ?.let { source = it } + destination = walletAddress + } else { + source = walletAddress + outputs + .firstOrNull { it.address != walletAddress } + ?.address + ?.let { destination = it } + } + return BasicTransactionData( + balanceDif = balanceDiff.toBigDecimal().movePointLeft(decimals), + hash = hash.orEmpty(), + date = date, + isConfirmed = blockHeight != 0L, + destination = destination, + source = source, + ) + } + private suspend fun getRemainingTransactions( address: String, transactionsTotal: Int, @@ -154,4 +181,4 @@ class BlockchainInfoNetworkProvider() : BitcoinNetworkProvider { Result.Failure(exception.toBlockchainSdkError()) } } -} \ No newline at end of file +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/blockchaininfo/BlockchainInfoResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/blockchaininfo/BlockchainInfoResponse.kt index 7ea456059..b10de4813 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/blockchaininfo/BlockchainInfoResponse.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/blockchaininfo/BlockchainInfoResponse.kt @@ -11,8 +11,11 @@ data class BlockchainInfoAddress( @Json(name = "n_tx") val transactionCount: Int? = null, + @Json(name = "address") + val address: String? = null, + @Json(name = "txs") - val transactions: List? = null + val transactions: List? = null, ) @JsonClass(generateAdapter = true) @@ -28,7 +31,13 @@ data class BlockchainInfoTransaction( @Json(name = "vin_sz") val inputCount: Int? = null, - val time: Long? = null + val time: Long? = null, + + @Json(name = "inputs") + val inputs: List = emptyList(), + + @Json(name = "outputs") + val outputs: List = emptyList(), ) @JsonClass(generateAdapter = true) @@ -49,7 +58,7 @@ data class BlockchainInfoUtxo( val amount: Long? = null, @Json(name = "script") - val outputScript: String? = null + val outputScript: String? = null, ) @JsonClass(generateAdapter = true) @@ -58,5 +67,47 @@ data class BlockchainInfoFees( val regularFeePerByte: Int? = null, @Json(name = "priority") - val priorityFeePerByte: Int? = null -) \ No newline at end of file + val priorityFeePerByte: Int? = null, +) + +@JsonClass(generateAdapter = true) +data class BlockchainInfoInput( + @Json(name = "sequence") + val sequence: Int?, + + @Json(name = "witness") + val witness: String?, + + @Json(name = "script") + val script: String?, + + @Json(name = "n") + val index: Int?, + + @Json(name = "prev_out") + val previousOutput: BlockchainInfoOutput?, +) + +@JsonClass(generateAdapter = true) +data class BlockchainInfoOutput( + @Json(name = "type") + val type: Int?, + + @Json(name = "spent") + val spent: Boolean?, + + @Json(name = "value") + val value: Long?, + + @Json(name = "script") + val script: String?, + + @Json(name = "addr") + val address: String?, + + @Json(name = "n") + val index: Int?, + + @Json(name = "tx_index") + val txIndex: Long?, +) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/EthereumJsonRpcProvidersExt.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/EthereumJsonRpcProvidersExt.kt index b41dfa5ef..46ccc79b6 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/EthereumJsonRpcProvidersExt.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/EthereumJsonRpcProvidersExt.kt @@ -13,6 +13,7 @@ internal fun Blockchain.getEthereumJsonRpcProviders( val providers = when (this) { Blockchain.Arbitrum -> listOfNotNull( EthereumJsonRpcProvider(baseUrl = "https://arb1.arbitrum.io/rpc/"), + getNowNodesProvider(baseUrl = "https://arbitrum.nownodes.io/", config = config), getInfuraProvider(baseUrl = "https://arbitrum-mainnet.infura.io/v3/", config = config) ) Blockchain.ArbitrumTestnet -> listOf( @@ -46,18 +47,18 @@ internal fun Blockchain.getEthereumJsonRpcProviders( ) ) Blockchain.Ethereum -> listOfNotNull( - getInfuraProvider(baseUrl = "https://mainnet.infura.io/v3/", config = config), getNowNodesProvider(baseUrl = "https://eth.nownodes.io/", config = config), - getGetBlockProvider(baseUrl = "https://eth.getblock.io/mainnet/", config = config) + getGetBlockProvider(baseUrl = "https://eth.getblock.io/mainnet/", config = config), + getInfuraProvider(baseUrl = "https://mainnet.infura.io/v3/", config = config), ) Blockchain.EthereumTestnet -> listOfNotNull( getNowNodesProvider(baseUrl = "https://eth-goerli.nownodes.io/", config = config), getInfuraProvider(baseUrl = "https://goerli.infura.io/v3/", config = config) ) Blockchain.EthereumClassic -> listOfNotNull( + EthereumJsonRpcProvider(baseUrl = "https://etc.etcdesktop.com/"), getGetBlockProvider(baseUrl = "https://etc.getblock.io/mainnet/", config = config), EthereumJsonRpcProvider(baseUrl = "https://www.ethercluster.com/etc/"), - EthereumJsonRpcProvider(baseUrl = "https://etc.etcdesktop.com/"), EthereumJsonRpcProvider(baseUrl = "https://blockscout.com/etc/mainnet/api/eth-rpc/"), EthereumJsonRpcProvider(baseUrl = "https://etc.mytokenpocket.vip/"), EthereumJsonRpcProvider(baseUrl = "https://besu-de.etc-network.info/"), diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/EthereumTransactionHistoryProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/EthereumTransactionHistoryProvider.kt index 4105bfbc0..2f56b3374 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/EthereumTransactionHistoryProvider.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/EthereumTransactionHistoryProvider.kt @@ -22,11 +22,21 @@ internal class EthereumTransactionHistoryProvider( private val blockBookApi: BlockBookApi, ) : TransactionHistoryProvider { - override suspend fun getTransactionHistoryState(address: String): TransactionHistoryState { + override suspend fun getTransactionHistoryState( + address: String, + filterType: TransactionHistoryRequest.FilterType, + ): TransactionHistoryState { return try { - val addressResponse = withContext(Dispatchers.IO) { blockBookApi.getAddress(address) } - if (!addressResponse.transactions.isNullOrEmpty()) { - TransactionHistoryState.Success.HasTransactions(addressResponse.transactions.size) + val response = withContext(Dispatchers.IO) { + blockBookApi.getTransactions( + address = address, + page = 1, + pageSize = 1, // We don't need to know all transactions to define state + filterType = filterType, + ) + } + if (!response.transactions.isNullOrEmpty()) { + TransactionHistoryState.Success.HasTransactions(response.transactions.size) } else { TransactionHistoryState.Success.Empty } @@ -56,9 +66,9 @@ internal class EthereumTransactionHistoryProvider( ?: emptyList() Result.Success( PaginationWrapper( - page = response.page, - totalPages = response.totalPages, - itemsOnPage = response.itemsOnPage, + page = response.page ?: request.page.number, + totalPages = response.totalPages ?: 0, + itemsOnPage = response.itemsOnPage ?: 0, items = txs ) ) @@ -71,7 +81,14 @@ internal class EthereumTransactionHistoryProvider( walletAddress: String, filterType: TransactionHistoryRequest.FilterType, ): TransactionHistoryItem? { - val isIncoming = checkIsIncoming(walletAddress, this, filterType) + val destinationType = extractDestinationType(walletAddress, this, filterType).guard { + Log.info { "Transaction $this doesn't contain a required value" } + return null + } + val sourceType = extractSourceType(this).guard { + Log.info { "Transaction $this doesn't contain a required value" } + return null + } val amount = extractAmount(tx = this, filterType = filterType).guard { Log.info { "Transaction $this doesn't contain a required value" } return null @@ -80,35 +97,15 @@ internal class EthereumTransactionHistoryProvider( return TransactionHistoryItem( txHash = txid, timestamp = TimeUnit.SECONDS.toMillis(blockTime.toLong()), - direction = extractTransactionDirection( - isIncoming = isIncoming, - tx = this, - ), + isOutgoing = isOutgoing(walletAddress, this, filterType), + destinationType = destinationType, + sourceType = sourceType, 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 @@ -129,19 +126,74 @@ internal class EthereumTransactionHistoryProvider( // 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) + return TransactionHistoryItem.TransactionType.ContractMethod(id = methodId) + } + + private fun isOutgoing( + walletAddress: String, + transaction: GetAddressResponse.Transaction, + filterType: TransactionHistoryRequest.FilterType, + ): Boolean { + return when (filterType) { + TransactionHistoryRequest.FilterType.Coin -> transaction.vin + .firstOrNull() + ?.addresses + ?.firstOrNull() + .equals(walletAddress, ignoreCase = true) + + is TransactionHistoryRequest.FilterType.Contract -> transaction.tokenTransfers + .firstOrNull { filterType.address.equals(it.contract, true) } + ?.from.equals(walletAddress, ignoreCase = true) } } + private fun extractDestinationType( + walletAddress: String, + tx: GetAddressResponse.Transaction, + filterType: TransactionHistoryRequest.FilterType, + ): TransactionHistoryItem.DestinationType? { + val address = tx.vout + .firstOrNull() + ?.addresses + ?.firstOrNull() + .guard { return null } + + return when (filterType) { + TransactionHistoryRequest.FilterType.Coin -> { + TransactionHistoryItem.DestinationType.Single( + addressType = if (tx.tokenTransfers.isEmpty()) { + TransactionHistoryItem.AddressType.User(address) + } else { + TransactionHistoryItem.AddressType.Contract(address) + } + ) + } + + is TransactionHistoryRequest.FilterType.Contract -> { + val transfer = tx.tokenTransfers + .firstOrNull { filterType.address.equals(it.contract, ignoreCase = true) } + .guard { return null } + val isOutgoing = transfer.from == walletAddress + TransactionHistoryItem.DestinationType.Single( + addressType = if (isOutgoing) { + TransactionHistoryItem.AddressType.User(transfer.to) + } else { + TransactionHistoryItem.AddressType.User(transfer.from) + }, + ) + } + } + } + + private fun extractSourceType(tx: GetAddressResponse.Transaction): TransactionHistoryItem.SourceType? { + val address = tx.vin + .firstOrNull() + ?.addresses + ?.firstOrNull() + .guard { return null } + return TransactionHistoryItem.SourceType.Single(address = address) + } + private fun extractAmount( tx: GetAddressResponse.Transaction, filterType: TransactionHistoryRequest.FilterType, @@ -156,9 +208,7 @@ internal class EthereumTransactionHistoryProvider( is TransactionHistoryRequest.FilterType.Contract -> { val transfer = tx.tokenTransfers .firstOrNull { filterType.address.equals(it.contract, ignoreCase = true) } - .guard { - return null - } + .guard { return null } val transferValue = transfer.value ?: "0" val token = Token( name = transfer.name.orEmpty(), @@ -170,19 +220,4 @@ internal class EthereumTransactionHistoryProvider( } } } - - 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()) - ) - } - } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/EthereumWalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/EthereumWalletManager.kt index 6d572120f..a50b03b39 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/EthereumWalletManager.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/EthereumWalletManager.kt @@ -11,7 +11,6 @@ import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.extensions.successOr import com.tangem.common.CompletionResult -import com.tangem.common.extensions.toHexString import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import org.kethereum.extensions.toHexString @@ -83,7 +82,7 @@ open class EthereumWalletManager( transactionToSign = signResponse.data.second ) val sendResult = networkProvider - .sendTransaction("0x" + transactionToSend.toHexString()) + .sendTransaction(transactionToSend.toHexString()) if (sendResult is SimpleResult.Success) { transactionData.hash = transactionToSend.keccak().toHexString() @@ -118,7 +117,7 @@ open class EthereumWalletManager( return when (val signerResponse = signer.sign(transactionToSign.hash, wallet.publicKey)) { is CompletionResult.Success -> { val transactionToSend = transactionBuilder.buildToSend(signerResponse.data, transactionToSign) - val sendResult = networkProvider.sendTransaction("0x" + transactionToSend.toHexString()) + val sendResult = networkProvider.sendTransaction(transactionToSend.toHexString()) sendResult } @@ -249,7 +248,7 @@ open class EthereumWalletManager( is AmountType.Token -> { if (finalData == null) { to = amount.type.token.contractAddress - finalData = "0x" + EthereumUtils.createErc20TransferData(destination, amount).toHexString() + finalData = EthereumUtils.createErc20TransferData(destination, amount).toHexString() } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/ActionCalculation.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/ActionCalculation.kt index 0eacbec6b..59298513b 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/ActionCalculation.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/ActionCalculation.kt @@ -9,28 +9,30 @@ import com.tangem.blockchain.blockchains.near.network.api.ProtocolConfigResult * @see Docs * @author Anton Zhilenkov on 17.08.2023. */ -internal fun ProtocolConfigResult.calculateSendFundsFee(gasPrice: NearGasPrice): Yocto { - val transferConfig = runtimeConfig.transactionCosts.actionCreationConfig.transferCost - val receiptConfig = runtimeConfig.transactionCosts.actionReceiptCreationConfig +internal fun ProtocolConfigResult.calculateSendFundsFee(gasPrice: NearGasPrice, isImplicitAccount: Boolean): Yocto { + with(runtimeConfig.transactionCosts) { + val receiptCreationCost = actionReceiptCreationConfig.cost - val actionCost = (transferConfig.sendNotSir + receiptConfig.sendNotSir).toBigInteger() - .times(gasPrice.yoctoGasPrice.value) + val transferCost = actionCreationConfig.transferCost.cost - val executionCost = (transferConfig.execution + receiptConfig.execution).toBigInteger() - .times(gasPrice.yoctoGasPrice.value) + val additionalCosts = if (isImplicitAccount) { + actionCreationConfig.createAccountCost.cost + actionCreationConfig.addKeyCost.fullAccessCost.cost + } else { + 0 + } - return Yocto(actionCost + executionCost) + val gas = (receiptCreationCost + transferCost + additionalCosts).toBigInteger() + val gasPriceValue = gasPrice.yoctoGasPrice.value + + return Yocto(gas * gasPriceValue) + } } internal fun ProtocolConfigResult.calculateCreateAccountFee(gasPrice: NearGasPrice): Yocto { - val createAccount = runtimeConfig.transactionCosts.actionCreationConfig.createAccountCost - val receiptConfig = runtimeConfig.transactionCosts.actionReceiptCreationConfig - - val actionCost = (createAccount.sendNotSir + receiptConfig.sendNotSir).toBigInteger() - .times(gasPrice.yoctoGasPrice.value) + val createAccount = runtimeConfig.transactionCosts.actionCreationConfig.createAccountCost.cost - val executionCost = (createAccount.execution + receiptConfig.execution).toBigInteger() - .times(gasPrice.yoctoGasPrice.value) - - return Yocto(actionCost + executionCost) + return Yocto(createAccount.toBigInteger() * gasPrice.yoctoGasPrice.value) } + +private val ProtocolConfigResult.CostConfig.cost: Long + get() = execution + sendNotSir diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/NearTransactionBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/NearTransactionBuilder.kt index f5f78ee9f..5a586b9b4 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/NearTransactionBuilder.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/NearTransactionBuilder.kt @@ -9,10 +9,7 @@ import com.tangem.common.KeyPair import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.guard import com.tangem.crypto.CryptoUtils -import wallet.core.jni.Base58 -import wallet.core.jni.CoinType -import wallet.core.jni.DataVector -import wallet.core.jni.TransactionCompiler +import wallet.core.jni.* import wallet.core.jni.proto.Common import wallet.core.jni.proto.NEAR import wallet.core.jni.proto.TransactionCompiler.PreSigningOutput @@ -30,11 +27,10 @@ class NearTransactionBuilder( // https://github.com/trustwallet/wallet-core/blob/master/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/near/TestNEARSigner.kt fun buildForSign( transaction: TransactionData, - withAccountCreation: Boolean, nonce: Long, blockHash: String, ): ByteArray { - val input = createSigningInput(transaction, withAccountCreation, nonce, blockHash) + val input = createSigningInput(transaction, nonce, blockHash) val txInputData = input.toByteArray() val preImageHashes = TransactionCompiler.preImageHashes(coinType, txInputData) @@ -50,11 +46,10 @@ class NearTransactionBuilder( fun buildForSend( transaction: TransactionData, signature: ByteArray, - withAccountCreation: Boolean, nonce: Long, blockHash: String, ): ByteArray { - val input = createSigningInput(transaction, withAccountCreation, nonce, blockHash) + val input = createSigningInput(transaction, nonce, blockHash) val txInputData = input.toByteArray() val signatures = DataVector() @@ -77,7 +72,6 @@ class NearTransactionBuilder( private fun createSigningInput( transaction: TransactionData, - withAccountCreation: Boolean, nonce: Long, blockHash: String, ): NEAR.SigningInput { @@ -85,15 +79,12 @@ class NearTransactionBuilder( throw BlockchainSdkError.FailedToBuildTx } val transfer = NEAR.Transfer.newBuilder() - .setDeposit(NearAmount(sendAmountValue).toByteString()) + .setDeposit(ByteString.copyFrom(NearAmount(sendAmountValue).toLittleEndian())) .build() - val action = NEAR.Action.newBuilder() + val actionBuilder = NEAR.Action.newBuilder() .setTransfer(transfer) - if (withAccountCreation) { - action.setCreateAccount(NEAR.CreateAccount.newBuilder().build()) - } - return createSigningInputWithAction(transaction, nonce, blockHash, action.build()) + return createSigningInputWithAction(transaction, nonce, blockHash, actionBuilder.build()) .build() } @@ -109,7 +100,7 @@ class NearTransactionBuilder( .setReceiverId(transaction.destinationAddress) .addActions(action) .setBlockHash(ByteString.copyFrom(Base58.decodeNoCheck(blockHash))) - .setPrivateKey(ByteString.copyFrom(keyPair.privateKey)) // ?? + .setPublicKey(ByteString.copyFrom(publicKey.blockchainKey)) } private fun generateKeyPair(): KeyPair { diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/NearWalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/NearWalletManager.kt index e95f619b3..b04916a1a 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/NearWalletManager.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/NearWalletManager.kt @@ -9,6 +9,7 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.* import com.tangem.common.CompletionResult +import com.tangem.crypto.encodeToBase58String import java.math.BigDecimal /** @@ -24,28 +25,48 @@ class NearWalletManager( get() = networkService.host override suspend fun updateInternal() { - when (val walletInfoResult = networkService.getAccount(wallet.address)) { + val walletInfoResult = networkService.getAccount(wallet.address) + val protocolConfigResult = networkService.getProtocolConfig().successOr { return } + when (walletInfoResult) { is Result.Success -> { when (val account = walletInfoResult.data) { - is NearAccount.Full -> updateWallet(account.near.value) - NearAccount.Empty -> updateWallet(BigDecimal.ZERO) + is NearAccount.Full -> updateWallet( + amountValue = account.near.value, + depositValue = account.storageUsage.value * protocolConfigResult.runtimeConfig.storageAmountPerByte + ) + + NearAccount.NotInitialized -> { + updateError(BlockchainSdkError.AccountNotFound) + return + } } } + is Result.Failure -> updateError(walletInfoResult.error) } + + updateTransactions() } - private fun updateWallet(amountValue: BigDecimal) { - if (amountValue >= NearAmount.DEPOSIT_VALUE) { - val realAmount = amountValue - NearAmount.DEPOSIT_VALUE + private fun updateWallet(amountValue: BigDecimal, depositValue: BigDecimal) { + if (amountValue >= depositValue) { + val realAmount = amountValue - depositValue wallet.setAmount(Amount(realAmount, wallet.blockchain)) wallet.setReserveValue(NearAmount.DEPOSIT_VALUE) } else { - // should we attach the reserve in that situation ? wallet.setReserveValue(amountValue) } } + private suspend fun updateTransactions() { + wallet.recentTransactions.firstOrNull()?.let { + val status = networkService.getStatus(requireNotNull(it.hash), it.sourceAddress).successOr { return } + if (status.isSuccessful) { + it.status = TransactionStatus.Confirmed + } + } + } + private fun updateError(error: BlockchainError) { Log.e(this::class.java.simpleName, error.customMessage) if (error is BlockchainSdkError) throw error @@ -56,14 +77,17 @@ class NearWalletManager( val protocolConfig = networkService.getProtocolConfig().successOr { return it } val gasPrice = networkService.getGas(blockHash = null).successOr { return it } + val isImplicitAccount = destination.length == IMPLICIT_ACCOUNT_ADDRESS_LENGTH + return when (destinationAccount) { is NearAccount.Full -> { - val feeYocto = protocolConfig.calculateSendFundsFee(gasPrice) + val feeYocto = protocolConfig.calculateSendFundsFee(gasPrice, isImplicitAccount) val feeAmount = Amount(NearAmount(feeYocto).value, wallet.blockchain) Result.Success(TransactionFee.Single(Fee.Common(feeAmount))) } - NearAccount.Empty -> { - val feeYocto = protocolConfig.calculateSendFundsFee(gasPrice) + + + NearAccount.NotInitialized -> { + val feeYocto = protocolConfig.calculateSendFundsFee(gasPrice, isImplicitAccount) + protocolConfig.calculateCreateAccountFee(gasPrice) val feeAmount = Amount(NearAmount(feeYocto).value, wallet.blockchain) Result.Success(TransactionFee.Single(Fee.Common(feeAmount))) @@ -72,15 +96,12 @@ class NearWalletManager( } override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult { - val accessKey = networkService.getAccessKey(wallet.address) - .successOr { return it.toSimpleFailure() } - val destinationAccount = networkService.getAccount(transactionData.destinationAddress) - .successOr { return it.toSimpleFailure() } - val buildWithAccountCreation = destinationAccount is NearAccount.Empty + val accessKey = + networkService.getAccessKey(wallet.address, wallet.publicKey.blockchainKey.encodeToBase58String()) + .successOr { return it.toSimpleFailure() } val txToSign = txBuilder.buildForSign( transaction = transactionData, - withAccountCreation = buildWithAccountCreation, nonce = accessKey.nextNonce, blockHash = accessKey.blockHash, ) @@ -90,24 +111,34 @@ class NearWalletManager( val txToSend = txBuilder.buildForSend( transaction = transactionData, signature = signatureResult.data, - withAccountCreation = buildWithAccountCreation, nonce = accessKey.nextNonce, blockHash = accessKey.blockHash, ) - when (val sendResult = networkService.sendTransaction(txToSend.encodeBase64NoWrap())) { + when (val sendResultHash = networkService.sendTransaction(txToSend.encodeBase64NoWrap())) { is Result.Success -> { - transactionData.hash = sendResult.data.hash - wallet.addOutgoingTransaction(transactionData) + transactionData.hash = sendResultHash.data + wallet.addOutgoingTransaction(transactionData = transactionData, hashToLowercase = false) + SimpleResult.Success } + is Result.Failure -> { - sendResult.toSimpleFailure() + sendResultHash.toSimpleFailure() } } } + is CompletionResult.Failure -> { SimpleResult.Failure(signatureResult.error.toBlockchainSdkError()) } } } + + suspend fun getAccount(address: String): Result { + return networkService.getAccount(address) + } + + companion object { + private const val IMPLICIT_ACCOUNT_ADDRESS_LENGTH = 64 + } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/NearJsonRpcNetworkProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/NearJsonRpcNetworkProvider.kt index e8564511c..a739a83ea 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/NearJsonRpcNetworkProvider.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/NearJsonRpcNetworkProvider.kt @@ -14,6 +14,7 @@ import com.tangem.blockchain.network.moshi class NearJsonRpcNetworkProvider( override val baseUrl: String, private val api: NearApi, + private val urlPostfix: String = "", ) : NearNetworkProvider { private val protocolConfigAdapter = moshi.adapter>( @@ -61,7 +62,7 @@ class NearJsonRpcNetworkProvider( private val txStatusAdapter = moshi.adapter>( Types.newParameterizedType( NearResponse::class.java, - SendTransactionAsyncResult::class.java, + TransactionStatusResult::class.java, ) ) @@ -81,9 +82,9 @@ class NearJsonRpcNetworkProvider( } } - override suspend fun getAccessKey(accountId: String): Result { + override suspend fun getAccessKey(params: NearGetAccessKeyParams): Result { return try { - postMethod(NearMethod.AccessKey.View(accountId), accessKeyResultAdapter).toResult() + postMethod(NearMethod.AccessKey.View(params.address, params.publicKeyEncodedToBase58), accessKeyResultAdapter).toResult() } catch (ex: Exception) { Result.Failure(ex.toBlockchainSdkError()) } @@ -105,12 +106,9 @@ class NearJsonRpcNetworkProvider( } } - override suspend fun getTransactionStatus( - txHash: String, - senderAccountId: String, - ): Result { + override suspend fun getTransactionStatus(params: NearGetTxParams): Result { return try { - postMethod(NearMethod.Transaction.Status(txHash, senderAccountId), txStatusAdapter).toResult() + postMethod(NearMethod.Transaction.Status(params.txHash, params.senderId), txStatusAdapter).toResult() } catch (ex: Exception) { Result.Failure(ex.toBlockchainSdkError()) } @@ -126,7 +124,7 @@ class NearJsonRpcNetworkProvider( @Throws(IllegalArgumentException::class) private suspend fun postMethod(method: NearMethod, adapter: JsonAdapter): T { - val responseBody = api.sendJsonRpc(method.asRequestBody()) + val responseBody = api.sendJsonRpc(method.asRequestBody(), urlPostfix) return requireNotNull( value = adapter.fromJson(responseBody.string()) as T, lazyMessage = { "Can not parse response" }, diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/NearModels.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/NearModels.kt index 385dc96ef..171194795 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/NearModels.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/NearModels.kt @@ -2,6 +2,7 @@ package com.tangem.blockchain.blockchains.near.network import com.google.protobuf.ByteString import com.tangem.blockchain.blockchains.near.network.Yocto.Companion.YOCTO_DECIMALS +import com.tangem.common.extensions.hexToBytes import java.math.BigDecimal import java.math.BigInteger @@ -46,8 +47,6 @@ data class NearAmount(val yocto: Yocto) { val value: BigDecimal by lazy { yocto.value.toBigDecimal().movePointLeft(YOCTO_DECIMALS) } - fun toByteString(): ByteString = yocto.toByteString() - operator fun plus(near: NearAmount): NearAmount { return NearAmount(yocto.plus(near.yocto)) } @@ -60,6 +59,18 @@ data class NearAmount(val yocto: Yocto) { return NearAmount(yocto.times(near.yocto)) } + /** + * @return Yocto value in hex little endian format + */ + fun toLittleEndian(): ByteArray { + var hexString = yocto.value.toString(16) + + val leadingZeroesCount = 32 - hexString.length + hexString = "0".repeat(leadingZeroesCount) + hexString + + return hexString.chunked(2).reversed().joinToString("").hexToBytes() + } + companion object { /** * Accounts must have enough tokens to cover its storage which currently costs 0.0001 NEAR per byte. @@ -77,12 +88,16 @@ sealed class NearAccount { /** * An object corresponding to an existing account with its amount */ - data class Full(val near: NearAmount, val blockHash: String) : NearAccount() + data class Full( + val near: NearAmount, + val blockHash: String, + val storageUsage: NearAmount + ) : NearAccount() /** * An object corresponding to a non-existent account */ - object Empty : NearAccount() + object NotInitialized : NearAccount() } data class NearNetworkStatus( @@ -115,4 +130,15 @@ data class NearGasPrice( data class NearSentTransaction( val hash: String, + val isSuccessful: Boolean, ) + +class NearGetAccessKeyParams( + val address: String, + val publicKeyEncodedToBase58: String, +) + +class NearGetTxParams( + val txHash: String, + val senderId: String, +) \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/NearNetworkProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/NearNetworkProvider.kt index 1c3634cc7..c628e3f2c 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/NearNetworkProvider.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/NearNetworkProvider.kt @@ -13,13 +13,13 @@ interface NearNetworkProvider : NetworkProvider { suspend fun getNetworkStatus(): Result - suspend fun getAccessKey(accountId: String): Result + suspend fun getAccessKey(params: NearGetAccessKeyParams): Result suspend fun getAccount(address: String): Result suspend fun getGas(blockHash: String): Result - suspend fun getTransactionStatus(txHash: String, senderAccountId: String): Result + suspend fun getTransactionStatus(params: NearGetTxParams): Result suspend fun sendTransaction(signedTxBase64: String): Result } \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/NearNetworkService.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/NearNetworkService.kt index c7549d57f..d40262b95 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/NearNetworkService.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/NearNetworkService.kt @@ -11,7 +11,7 @@ import com.tangem.blockchain.network.MultiNetworkProvider * @author Anton Zhilenkov on 01.08.2023. */ class NearNetworkService( - private val blockchain: Blockchain, + blockchain: Blockchain, private val multiJsonRpcProvider: MultiNetworkProvider, ) { @@ -24,14 +24,13 @@ class NearNetworkService( val host: String get() = multiJsonRpcProvider.currentProvider.baseUrl suspend fun getAccount(address: String): Result { - val result = multiJsonRpcProvider.performRequest(NearNetworkProvider::getAccount, address) - - return when (result) { + return when (val result = multiJsonRpcProvider.performRequest(NearNetworkProvider::getAccount, address)) { is Result.Success -> { Result.Success( NearAccount.Full( near = NearAmount(Yocto(result.data.amount)), blockHash = result.data.blockHash, + storageUsage = NearAmount(Yocto(result.data.storageUsage.toBigInteger())) ) ) } @@ -40,7 +39,7 @@ class NearNetworkService( val nearError = result.mapToNearError() ?: return result return if (nearError is NearError.UnknownAccount) { - Result.Success(NearAccount.Empty) + Result.Success(NearAccount.NotInitialized) } else { result } @@ -48,9 +47,12 @@ class NearNetworkService( } } - suspend fun getAccessKey(address: String): Result { - val accessKeyResult = multiJsonRpcProvider.performRequest(NearNetworkProvider::getAccessKey, address) - .successOr { return it } + suspend fun getAccessKey(address: String, publicKeyEncodedToBase58: String): Result { + val accessKeyResult = + multiJsonRpcProvider.performRequest( + request = NearNetworkProvider::getAccessKey, + data = NearGetAccessKeyParams(address, publicKeyEncodedToBase58) + ).successOr { return it } val accessKey = AccessKey(accessKeyResult.nonce, accessKeyResult.blockHeight, accessKeyResult.blockHash) return Result.Success(accessKey) @@ -70,12 +72,22 @@ class NearNetworkService( return Result.Success(gasPrice) } - suspend fun sendTransaction(signedTxBase64: String): Result { - val sendTxResult = multiJsonRpcProvider.performRequest(NearNetworkProvider::sendTransaction, signedTxBase64) + suspend fun sendTransaction(signedTxBase64: String): Result { + val sendTxHash = multiJsonRpcProvider.performRequest(NearNetworkProvider::sendTransaction, signedTxBase64) .successOr { return it } + return Result.Success(sendTxHash) + } + + suspend fun getStatus(txHash: String, senderId: String): Result { + val sendTxResult = multiJsonRpcProvider.performRequest( + request = NearNetworkProvider::getTransactionStatus, + data = NearGetTxParams(txHash, senderId) + ).successOr { return it } + val nearWalletInfo = NearSentTransaction( - hash = sendTxResult + hash = sendTxResult.transaction.hash, + isSuccessful = sendTxResult.status.successValue != null ) return Result.Success(nearWalletInfo) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/api/NearApi.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/api/NearApi.kt index 618e5bfbf..1971acc1b 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/api/NearApi.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/api/NearApi.kt @@ -1,12 +1,11 @@ package com.tangem.blockchain.blockchains.near.network.api import com.tangem.blockchain.common.JsonRPCRequest -import com.tangem.blockchain.extensions.encodeBase58 -import com.tangem.common.extensions.hexToBytes import okhttp3.ResponseBody import retrofit2.http.Body import retrofit2.http.Headers import retrofit2.http.POST +import retrofit2.http.Url /** * NearApi provides access to posting requests to jsonRPC endpoints. @@ -17,8 +16,11 @@ import retrofit2.http.POST interface NearApi { @Headers("Content-Type: application/json", "Accept: application/json") - @POST("./") - suspend fun sendJsonRpc(@Body body: JsonRPCRequest): ResponseBody + @POST + suspend fun sendJsonRpc( + @Body body: JsonRPCRequest, + @Url urlPostfix: String + ): ResponseBody } internal sealed interface NearMethod { @@ -43,22 +45,24 @@ internal sealed interface NearMethod { sealed class AccessKey : NearMethod { - data class View(val accountId: String) : AccessKey() { - override fun asRequestBody(): JsonRPCRequest = JsonRPCRequest( - method = "query", - params = mapOf( - "request_type" to "view_access_key", - "finality" to "final", - "account_id" to accountId, - "public_key" to "ed25519:${accountId.hexToBytes().encodeBase58()}" - ), - ) + class View(private val accountId: String, private val publicKeyEncodedToBase58: String) : AccessKey() { + override fun asRequestBody(): JsonRPCRequest{ + return JsonRPCRequest( + method = "query", + params = mapOf( + "request_type" to "view_access_key", + "finality" to "final", + "account_id" to accountId, + "public_key" to "ed25519:${publicKeyEncodedToBase58}" + ), + ) + } } } sealed class Account : NearMethod { - data class View(val accountId: String) : Account() { + class View(private val accountId: String) : Account() { override fun asRequestBody(): JsonRPCRequest = JsonRPCRequest( method = "query", params = mapOf( @@ -72,14 +76,14 @@ internal sealed interface NearMethod { sealed class GasPrice : NearMethod { - data class BlockHeight(val blockHeight: Long) : GasPrice() { + class BlockHeight(private val blockHeight: Long) : GasPrice() { override fun asRequestBody(): JsonRPCRequest = JsonRPCRequest( method = "gas_price", params = arrayOf(blockHeight), ) } - data class BlockHash(val blockHash: String) : GasPrice() { + class BlockHash(private val blockHash: String) : GasPrice() { override fun asRequestBody(): JsonRPCRequest = JsonRPCRequest( method = "gas_price", params = arrayOf(blockHash), @@ -96,14 +100,14 @@ internal sealed interface NearMethod { sealed class Transaction : NearMethod { - data class SendTxAsync(val signedTxBase64: String) : Transaction() { + class SendTxAsync(private val signedTxBase64: String) : Transaction() { override fun asRequestBody(): JsonRPCRequest = JsonRPCRequest( method = "broadcast_tx_async", params = arrayOf(signedTxBase64), ) } - data class Status(val txHash: String, val senderAccountId: String) : Transaction() { + class Status(private val txHash: String, private val senderAccountId: String) : Transaction() { override fun asRequestBody(): JsonRPCRequest = JsonRPCRequest( method = "tx", params = arrayOf(txHash, senderAccountId), diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/api/NearResponseResult.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/api/NearResponseResult.kt index b24093fa4..8dd7a7594 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/api/NearResponseResult.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/near/network/api/NearResponseResult.kt @@ -2,6 +2,8 @@ package com.tangem.blockchain.blockchains.near.network.api import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import java.math.BigDecimal +import java.math.BigInteger /** * @author Anton Zhilenkov on 08.08.2023. @@ -11,12 +13,13 @@ data class ProtocolConfigResult( @Json(name = "chain_id") val chainId: String, @Json(name = "protocol_version") val protocolVersion: String, @Json(name = "genesis_height") val genesisHeight: Long, - @Json(name = "max_gas_price") val maxGasPrice: Long, - @Json(name = "min_gas_price") val minGasPrice: Long, + @Json(name = "max_gas_price") val maxGasPrice: BigDecimal, + @Json(name = "min_gas_price") val minGasPrice: BigDecimal, @Json(name = "runtime_config") val runtimeConfig: RuntimeConfig, ) { data class RuntimeConfig( @Json(name = "transaction_costs") val transactionCosts: TransactionCost, + @Json(name = "storage_amount_per_byte") val storageAmountPerByte: BigDecimal, ) data class TransactionCost( @@ -106,29 +109,26 @@ typealias SendTransactionAsyncResult = String data class TransactionStatusResult( @Json(name = "status") val status: Status, @Json(name = "transaction") val transaction: Transaction, - @Json(name = "transaction_outcome") val transactionOutcome: Outcome, - @Json(name = "receipts_outcome") val receiptsOutcome: Outcome, ) { @JsonClass(generateAdapter = true) data class Status( - @Json(name = "SuccessValue") val successValue: String, + @Json(name = "SuccessValue") val successValue: String?, ) @JsonClass(generateAdapter = true) data class Transaction( @Json(name = "signer_id") val signerId: String, @Json(name = "public_key") val publicKey: String, - @Json(name = "nonce") val nonce: Int, + @Json(name = "nonce") val nonce: Long, @Json(name = "receiver_id") val receiverId: String, - @Json(name = "actions") val actions: List, @Json(name = "signature") val signature: String, @Json(name = "hash") val hash: String, ) @JsonClass(generateAdapter = true) data class Outcome( - @Json(name = "proof") val proof: List, + // @Json(name = "proof") val proof: List, @Json(name = "block_hash") val blockHash: String, @Json(name = "id") val id: String, @Json(name = "outcome") val outcome: OutcomeData, @@ -142,11 +142,9 @@ data class TransactionStatusResult( @JsonClass(generateAdapter = true) data class OutcomeData( - @Json(name = "logs") val logs: List, @Json(name = "receipt_ids") val receiptIds: List, @Json(name = "gas_burnt") val gasBurnt: Double, @Json(name = "tokens_burnt") val tokensBurnt: String, - @Json(name = "executor_id") val executorId: String, @Json(name = "status") val status: Any, ) } \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt b/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt index e280f4292..a5db120a1 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt @@ -56,7 +56,7 @@ enum class Blockchain( FantomTestnet("FTM/test", "FTM", "Fantom Testnet"), Litecoin("LTC", "LTC", "Litecoin"), Near("NEAR", "NEAR", "NEAR Protocol"), - NearTestnet("NEAR", "NEAR", "NEAR Protocol Testnet"), + NearTestnet("NEAR/test", "NEAR", "NEAR Protocol Testnet"), Polkadot("Polkadot", "DOT", "Polkadot"), PolkadotTestnet("Polkadot", "WND", "Polkadot Westend Testnet"), Kava("KAVA", "KAVA", "Kava EVM"), diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/WalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/common/WalletManager.kt index be9a2295e..04129889d 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/WalletManager.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/WalletManager.kt @@ -151,12 +151,11 @@ abstract class WalletManager( } private fun BasicTransactionData.toTransactionData(): TransactionData { - val isIncoming = this.balanceDif.signum() > 0 return TransactionData( amount = Amount(wallet.amounts[AmountType.Coin]!!, this.balanceDif.abs()), fee = null, - sourceAddress = if (isIncoming) "unknown" else wallet.address, - destinationAddress = if (isIncoming) wallet.address else "unknown", + sourceAddress = source, + destinationAddress = destination, hash = this.hash, date = this.date, status = if (this.isConfirmed) { diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/assembly/impl/NearWalletManagerAssembly.kt b/blockchain/src/main/java/com/tangem/blockchain/common/assembly/impl/NearWalletManagerAssembly.kt index b4607719e..b11c89195 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/assembly/impl/NearWalletManagerAssembly.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/assembly/impl/NearWalletManagerAssembly.kt @@ -21,6 +21,7 @@ internal object NearWalletManagerAssembly : WalletManagerAssembly) : DestinationType() + } + + sealed class SourceType { - data class Incoming(override val address: Address) : TransactionDirection - data class Outgoing(override val address: Address) : TransactionDirection + data class Single(val address: String) : SourceType() + data class Multiple(val addresses: List) : SourceType() + } + + sealed class AddressType { + abstract val address: String + + data class User(override val address: String) : AddressType() + data class Contract(override val address: String) : AddressType() } 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 + data class ContractMethod(val id: String) : TransactionType } sealed class TransactionStatus { @@ -35,9 +41,4 @@ data class TransactionHistoryItem( object Unconfirmed : TransactionStatus() object Confirmed : TransactionStatus() } - - sealed class Address { - data class Single(val rawAddress: String) : Address() - object Multiple : Address() - } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/txhistory/TransactionHistoryProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/common/txhistory/TransactionHistoryProvider.kt index f2b15a3d2..3e83e32a1 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/txhistory/TransactionHistoryProvider.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/txhistory/TransactionHistoryProvider.kt @@ -5,7 +5,7 @@ import com.tangem.blockchain.extensions.Result interface TransactionHistoryProvider { - suspend fun getTransactionHistoryState(address: String): TransactionHistoryState + suspend fun getTransactionHistoryState(address: String, filterType: TransactionHistoryRequest.FilterType): TransactionHistoryState suspend fun getTransactionsHistory(request: TransactionHistoryRequest): Result> } diff --git a/blockchain/src/main/java/com/tangem/blockchain/extensions/DebouncedInvoke.kt b/blockchain/src/main/java/com/tangem/blockchain/extensions/DebouncedInvoke.kt index 5877cb390..59bf376ad 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/extensions/DebouncedInvoke.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/extensions/DebouncedInvoke.kt @@ -1,5 +1,7 @@ package com.tangem.blockchain.extensions +import android.os.SystemClock + /** * Class wrapping method invocation to be debounced within specified interval [invokeIntervalMillis]. * @@ -9,18 +11,31 @@ internal class DebouncedInvoke( private val invokeIntervalMillis: Long = 10000L, ) { private var lastInvokeTime = 0L + private var maybeException: Throwable? = null /** Call executable [block] with specified interval or force execution with [forceUpdate] */ suspend fun invokeOnExpire(forceUpdate: Boolean = false, block: suspend () -> Unit) { if (forceUpdate) { - block() + callWithException(block) } else if (isExpired()) { - lastInvokeTime = System.currentTimeMillis() + maybeException = null + lastInvokeTime = SystemClock.elapsedRealtime() + callWithException(block) + } else if (maybeException != null) { + throw maybeException as Throwable + } + } + + private suspend fun callWithException(block: suspend () -> Unit) { + try { block() + } catch (e: Throwable) { + maybeException = e + throw e } } private fun isExpired(): Boolean { - return System.currentTimeMillis() - lastInvokeTime >= invokeIntervalMillis + return SystemClock.elapsedRealtime() - lastInvokeTime >= invokeIntervalMillis } } \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/providers/BitcoinExternalLinkProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/providers/BitcoinExternalLinkProvider.kt index 813225b94..84ad9bff6 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/providers/BitcoinExternalLinkProvider.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/providers/BitcoinExternalLinkProvider.kt @@ -5,7 +5,7 @@ import com.tangem.blockchain.externallinkprovider.ExternalLinkProvider internal class BitcoinExternalLinkProvider(isTestnet: Boolean) : ExternalLinkProvider { override val explorerBaseUrl: String = - if (isTestnet) "https://www.blockchair.com/bitcoin/" else "https://www.blockchair.com/bitcoin/testnet/" + if (isTestnet) "https://www.blockchair.com/bitcoin/testnet/" else "https://www.blockchair.com/bitcoin/" override val testNetTopUpUrl: String? = if (isTestnet) "https://coinfaucet.eu/en/btc-testnet/" else null diff --git a/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/providers/CosmosExternalLinkProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/providers/CosmosExternalLinkProvider.kt index bcec6f776..e44399deb 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/providers/CosmosExternalLinkProvider.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/providers/CosmosExternalLinkProvider.kt @@ -11,11 +11,12 @@ internal class CosmosExternalLinkProvider(private val isTestnet: Boolean) : Exte if (isTestnet) "https://discord.com/channels/669268347736686612/953697793476821092" else null override fun explorerUrl(walletAddress: String, contractAddress: String?): String { - val path = if (isTestnet) "accounts/$walletAddress" else "account/$walletAddress" + val path = if (isTestnet) "accounts/$walletAddress" else "address/$walletAddress" return explorerBaseUrl + path } override fun explorerTransactionUrl(transactionHash: String): String { - return explorerBaseUrl + "transactions/$transactionHash" + val path = if (isTestnet) "transactions/$transactionHash" else "tx/$transactionHash" + return explorerBaseUrl + path } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/providers/EthereumClassicExternalLinkProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/providers/EthereumClassicExternalLinkProvider.kt index 081bee872..21e898b09 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/providers/EthereumClassicExternalLinkProvider.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/providers/EthereumClassicExternalLinkProvider.kt @@ -10,10 +10,10 @@ internal class EthereumClassicExternalLinkProvider(isTestnet: Boolean) : Externa override val testNetTopUpUrl: String? = if (isTestnet) "https://kottifaucet.me" else null override fun explorerUrl(walletAddress: String, contractAddress: String?): String { - return explorerBaseUrl + "address/$walletAddress/transactions" + return explorerBaseUrl + "address/$walletAddress" } override fun explorerTransactionUrl(transactionHash: String): String { - return explorerBaseUrl + "tx/$transactionHash/transactions" + return explorerBaseUrl + "tx/$transactionHash" } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/BlockBookNetworkProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/BlockBookNetworkProvider.kt index 1b594a6db..a48d9a2c4 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/BlockBookNetworkProvider.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/BlockBookNetworkProvider.kt @@ -95,7 +95,7 @@ class BlockBookNetworkProvider( address: String, ): List { val outputScript = transactions.firstNotNullOfOrNull { transaction -> - transaction.vout.firstOrNull { it.addresses.contains(address) }?.hex + transaction.vout.firstOrNull { it.addresses?.contains(address) == true }?.hex } ?: return emptyList() return getUtxoResponseItems.mapNotNull { @@ -118,19 +118,33 @@ class BlockBookNetworkProvider( return transactions .filter { it.confirmations == 0 } .map { transaction -> - val isIncoming = transaction.vin.any { !it.addresses.contains(address) } + val isIncoming = transaction.vin.any { it.addresses?.contains(address) == false } + var source = "unknown" + var destination = "unknown" val amount = if (isIncoming) { + destination = address + transaction.vin + .firstOrNull() + ?.addresses + ?.firstOrNull() + ?.let { source = it } val outputs = transaction.vout - .find { it.addresses.contains(address) } + .find { it.addresses?.contains(address) == true } ?.value.toBigDecimalOrDefault() val inputs = transaction.vin - .find { it.addresses.contains(address) } + .find { it.addresses?.contains(address) == true } ?.value.toBigDecimalOrDefault() outputs - inputs } else { + source = address + transaction.vout + .firstOrNull() + ?.addresses + ?.firstOrNull() + ?.let { destination = it } val outputs = transaction.vout .asSequence() - .filter { !it.addresses.contains(address) } + .filter { it.addresses?.contains(address) == false } .mapNotNull { it.value?.toBigDecimalOrNull() } .sumOf { it } val fee = transaction.fees.toBigDecimalOrDefault() @@ -144,6 +158,8 @@ class BlockBookNetworkProvider( timeInMillis = transaction.blockTime.toLong() }, isConfirmed = false, + destination = destination, + source = source, ) } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/network/responses/GetAddressResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/network/responses/GetAddressResponse.kt index 7471b9cf5..c2d7cf78a 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/network/responses/GetAddressResponse.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/network/responses/GetAddressResponse.kt @@ -9,9 +9,9 @@ data class GetAddressResponse( @Json(name = "unconfirmedTxs") val unconfirmedTxs: Int, @Json(name = "txs") val txs: Int, @Json(name = "transactions") val transactions: List?, - @Json(name = "page") val page: Int, - @Json(name = "totalPages") val totalPages: Int, - @Json(name = "itemsOnPage") val itemsOnPage: Int, + @Json(name = "page") val page: Int?, + @Json(name = "totalPages") val totalPages: Int?, + @Json(name = "itemsOnPage") val itemsOnPage: Int?, ) { @JsonClass(generateAdapter = true) @@ -29,13 +29,13 @@ data class GetAddressResponse( @JsonClass(generateAdapter = true) data class Vin( - @Json(name = "addresses") val addresses: List, + @Json(name = "addresses") val addresses: List?, @Json(name = "value") val value: String?, ) @JsonClass(generateAdapter = true) data class Vout( - @Json(name = "addresses") val addresses: List, + @Json(name = "addresses") val addresses: List?, @Json(name = "hex") val hex: String?, @Json(name = "value") val value: String?, )