diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinAddressService.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinAddressService.kt index eeb869ee3..18e4de1d2 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinAddressService.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinAddressService.kt @@ -3,6 +3,7 @@ package com.tangem.blockchain.blockchains.bitcoin import com.tangem.blockchain.blockchains.clore.CloreMainNetParams import com.tangem.blockchain.blockchains.dash.DashMainNetParams import com.tangem.blockchain.blockchains.ducatus.DucatusMainNetParams +import com.tangem.blockchain.blockchains.factorn.Fact0rnMainNetParams import com.tangem.blockchain.blockchains.radiant.RadiantMainNetParams import com.tangem.blockchain.blockchains.ravencoin.RavencoinMainNetParams import com.tangem.blockchain.blockchains.ravencoin.RavencoinTestNetParams @@ -40,6 +41,7 @@ open class BitcoinAddressService( Blockchain.Ravencoin -> RavencoinMainNetParams() Blockchain.RavencoinTestnet -> RavencoinTestNetParams() Blockchain.Radiant -> RadiantMainNetParams() + Blockchain.Fact0rn -> Fact0rnMainNetParams() Blockchain.Clore -> CloreMainNetParams() else -> error( "${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}", @@ -70,13 +72,13 @@ open class BitcoinAddressService( return Address(address, AddressType.Legacy) } - private fun makeSegwitAddress(walletPublicKey: ByteArray): Address { + internal fun makeSegwitAddress(walletPublicKey: ByteArray): Address { val compressedPublicKey = ECKey.fromPublicOnly(walletPublicKey.toCompressedPublicKey()) val address = SegwitAddress.fromKey(networkParameters, compressedPublicKey).toBech32() return Address(address, AddressType.Default) } - private fun validateSegwitAddress(address: String): Boolean { + internal fun validateSegwitAddress(address: String): Boolean { return try { if (blockchain == Blockchain.Ducatus) return false SegwitAddress.fromBech32(networkParameters, address) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinTransactionBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinTransactionBuilder.kt index bf13ad290..2b5953c73 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinTransactionBuilder.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinTransactionBuilder.kt @@ -3,6 +3,7 @@ package com.tangem.blockchain.blockchains.bitcoin import com.tangem.blockchain.blockchains.clore.CloreMainNetParams import com.tangem.blockchain.blockchains.dash.DashMainNetParams import com.tangem.blockchain.blockchains.ducatus.DucatusMainNetParams +import com.tangem.blockchain.blockchains.factorn.Fact0rnMainNetParams import com.tangem.blockchain.blockchains.ravencoin.RavencoinMainNetParams import com.tangem.blockchain.blockchains.ravencoin.RavencoinTestNetParams import com.tangem.blockchain.common.Blockchain @@ -45,6 +46,7 @@ open class BitcoinTransactionBuilder( Blockchain.Dash -> DashMainNetParams() Blockchain.Ravencoin -> RavencoinMainNetParams() Blockchain.RavencoinTestnet -> RavencoinTestNetParams() + Blockchain.Fact0rn -> Fact0rnMainNetParams() Blockchain.Clore -> CloreMainNetParams() else -> error("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}") } diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/factorn/Fact0rnAddressService.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/factorn/Fact0rnAddressService.kt index 820169070..8c8033ef2 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/factorn/Fact0rnAddressService.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/factorn/Fact0rnAddressService.kt @@ -1,15 +1,36 @@ package com.tangem.blockchain.blockchains.factorn +import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressService +import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.address.AddressService import com.tangem.common.card.EllipticCurve +import com.tangem.common.extensions.toHexString +import org.bitcoinj.core.SegwitAddress +import org.bitcoinj.core.Sha256Hash +import org.bitcoinj.script.Script +import org.bitcoinj.script.ScriptBuilder internal class Fact0rnAddressService : AddressService() { + private val bitcoinAddressService = BitcoinAddressService(Blockchain.Fact0rn) + override fun makeAddress(walletPublicKey: ByteArray, curve: EllipticCurve?): String { - TODO("Not yet implemented") + return bitcoinAddressService.makeSegwitAddress(walletPublicKey).value } override fun validate(address: String): Boolean { - TODO("Not yet implemented") + return bitcoinAddressService.validateSegwitAddress(address) + } + + companion object { + + internal fun addressToScript(address: String): Script = + ScriptBuilder.createOutputScript(SegwitAddress.fromBech32(Fact0rnMainNetParams(), address)) + + internal fun addressToScriptHash(address: String): String { + val p2pkhScript = addressToScript(address) + val sha256Hash = Sha256Hash.hash(p2pkhScript.program) + return sha256Hash.reversedArray().toHexString() + } } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/factorn/Fact0rnMainNetParams.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/factorn/Fact0rnMainNetParams.kt new file mode 100644 index 000000000..43bc2eaf7 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/factorn/Fact0rnMainNetParams.kt @@ -0,0 +1,10 @@ +package com.tangem.blockchain.blockchains.factorn + +import org.bitcoinj.params.MainNetParams + +internal class Fact0rnMainNetParams : MainNetParams() { + + init { + segwitAddressHrp = "fact" + } +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/factorn/Fact0rnProvidersBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/factorn/Fact0rnProvidersBuilder.kt index 570dab38d..a2c283c56 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/factorn/Fact0rnProvidersBuilder.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/factorn/Fact0rnProvidersBuilder.kt @@ -1,8 +1,10 @@ package com.tangem.blockchain.blockchains.factorn +import com.tangem.blockchain.blockchains.factorn.network.Fact0rnNetworkService import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.network.providers.OnlyPublicProvidersBuilder import com.tangem.blockchain.common.network.providers.ProviderType +import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.blockchain.network.electrum.ElectrumNetworkProvider import com.tangem.blockchain.network.electrum.ElectrumNetworkProviderFactory @@ -14,6 +16,8 @@ internal class Fact0rnProvidersBuilder( return ElectrumNetworkProviderFactory.create( wssUrl = url, blockchain = blockchain, + okHttpClient = BlockchainSdkRetrofitBuilder.createOkhttpClientForFact0rn(), + supportedProtocolVersion = Fact0rnNetworkService.SUPPORTED_SERVER_VERSION, ) } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/factorn/Fact0rnWalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/factorn/Fact0rnWalletManager.kt deleted file mode 100644 index 14abc9ce2..000000000 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/factorn/Fact0rnWalletManager.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.blockchain.blockchains.factorn - -import com.tangem.blockchain.blockchains.factorn.network.Fact0rnNetworkService -import com.tangem.blockchain.common.* -import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.blockchain.common.transaction.TransactionSendResult -import com.tangem.blockchain.extensions.Result -import com.tangem.blockchain.network.electrum.ElectrumNetworkProvider - -internal class Fact0rnWalletManager( - wallet: Wallet, - networkProviders: List, -) : WalletManager(wallet) { - - private val networkService = Fact0rnNetworkService(networkProviders) - - override val currentHost: String get() = networkService.baseUrl - - override suspend fun updateInternal() { - TODO("Not yet implemented") - } - - override suspend fun send( - transactionData: TransactionData, - signer: TransactionSigner, - ): Result { - TODO("Not yet implemented") - } - - override suspend fun getFee(amount: Amount, destination: String): Result { - TODO("Not yet implemented") - } -} diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/factorn/network/Fact0rnNetworkService.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/factorn/network/Fact0rnNetworkService.kt index a49c07251..65a36c606 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/factorn/network/Fact0rnNetworkService.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/factorn/network/Fact0rnNetworkService.kt @@ -1,13 +1,187 @@ package com.tangem.blockchain.blockchains.factorn.network -import com.tangem.blockchain.common.NetworkProvider +import com.tangem.blockchain.blockchains.bitcoin.BitcoinUnspentOutput +import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinAddressInfo +import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinFee +import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinNetworkProvider +import com.tangem.blockchain.blockchains.factorn.Fact0rnAddressService.Companion.addressToScript +import com.tangem.blockchain.blockchains.factorn.Fact0rnAddressService.Companion.addressToScriptHash +import com.tangem.blockchain.common.BasicTransactionData +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.extensions.* import com.tangem.blockchain.network.MultiNetworkProvider import com.tangem.blockchain.network.electrum.ElectrumNetworkProvider +import com.tangem.blockchain.network.electrum.ElectrumUnspentUTXORecord +import com.tangem.blockchain.network.electrum.api.ElectrumResponse +import com.tangem.common.extensions.hexToBytes +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import java.math.BigDecimal +import java.util.Calendar -internal class Fact0rnNetworkService(providers: List) : NetworkProvider { +internal class Fact0rnNetworkService( + private val blockchain: Blockchain, + providers: List, +) : BitcoinNetworkProvider { override val baseUrl: String get() = multiProvider.currentProvider.baseUrl private val multiProvider = MultiNetworkProvider(providers) + + override suspend fun getInfo(address: String): Result = coroutineScope { + val scriptHash = addressToScriptHash(address) + val balanceDeferred = + async { multiProvider.performRequest(ElectrumNetworkProvider::getAccountBalance, scriptHash) } + val unspentsDeferred = + async { multiProvider.performRequest(ElectrumNetworkProvider::getUnspentUTXOs, scriptHash) } + val balance = balanceDeferred.await().successOr { return@coroutineScope it } + val unspentsUTXOs = unspentsDeferred.await().successOr { return@coroutineScope it } + + val info = BitcoinAddressInfo( + balance = balance.confirmedAmount, + unspentOutputs = createUnspentOutputs( + getUtxoResponseItems = unspentsUTXOs, + address = address, + ), + recentTransactions = createRecentTransactions( + utxoResponseItems = unspentsUTXOs, + address = address, + ), + hasUnconfirmed = balance.unconfirmedAmount != BigDecimal.ZERO, + ) + Result.Success(info) + } + + override suspend fun getFee(): Result = coroutineScope { + val minimalFeeDeferred = async { requestFee(MINIMAL_FEE_BLOCK_AMOUNT) } + val normalFeeDeferred = async { requestFee(NORMAL_FEE_BLOCK_AMOUNT) } + val priorityFeeDeferred = async { requestFee(PRIORITY_FEE_BLOCK_AMOUNT) } + val minimalFee = minimalFeeDeferred.await() + .map { it ?: BigDecimal.ZERO } + .successOr { return@coroutineScope it } + val normalFee = normalFeeDeferred.await() + .map { it ?: minimalFee.multiply(NORMAL_FEE_MULTIPLIER) } + .successOr { minimalFee.multiply(NORMAL_FEE_MULTIPLIER) } + val priorityFee = priorityFeeDeferred.await() + .map { it ?: minimalFee.multiply(PRIORITY_FEE_MULTIPLIER) } + .successOr { minimalFee.multiply(PRIORITY_FEE_MULTIPLIER) } + Result.Success( + BitcoinFee( + minimalPerKb = minimalFee, + normalPerKb = normalFee, + priorityPerKb = priorityFee, + ), + ) + } + + private suspend fun requestFee(blockAmount: Int) = multiProvider + .performRequest { getEstimateFee(blockAmount) } + .map { feeResponse -> + feeResponse.feeInCoinsPer1000Bytes + ?.divide(BigDecimal(BYTES_IN_KB)) + ?.movePointLeft(blockchain.decimals()) + } + + override suspend fun sendTransaction(transaction: String): SimpleResult { + return multiProvider.performRequest(ElectrumNetworkProvider::broadcastTransaction, transaction.hexToBytes()) + .map { SimpleResult.Success } + .successOr { it.toSimpleFailure() } + } + + override suspend fun getSignatureCount(address: String): Result { + return multiProvider.performRequest( + ElectrumNetworkProvider::getTransactionHistory, + addressToScriptHash(address), + ) + .map { Result.Success(it.count()) } + .successOr { it } + } + + private fun createUnspentOutputs( + getUtxoResponseItems: List, + address: String, + ): List = getUtxoResponseItems.map { + val amount = it.value + BitcoinUnspentOutput( + amount = amount, + outputIndex = it.txPos, + transactionHash = it.txHash.hexToBytes(), + outputScript = addressToScript(address).program, + ) + } + + private suspend fun createRecentTransactions( + utxoResponseItems: List, + address: String, + ): List = coroutineScope { + utxoResponseItems + .filter { !it.isConfirmed } + .map { utxo -> async { multiProvider.performRequest { getTransactionInfo(utxo.txHash) } } } + .awaitAll() + .filterIsInstance>() + .map { result -> + val transaction: ElectrumResponse.Transaction = result.data + val vin = transaction.vin ?: listOf() + val vout = transaction.vout ?: listOf() + val isIncoming = vin.any { it.addresses?.contains(address) == false } + var source = "unknown" + var destination = "unknown" + val amount = if (isIncoming) { + destination = address + vin.firstOrNull() + ?.addresses + ?.firstOrNull() + ?.let { source = it } + val outputs = vout + .find { it.scriptPublicKey?.addresses?.contains(address) == true } + ?.value?.toBigDecimal() ?: BigDecimal.ZERO + val inputs = vin + .find { it.addresses?.contains(address) == true } + ?.value?.toBigDecimal() ?: BigDecimal.ZERO + outputs - inputs + } else { + source = address + vout.firstOrNull() + ?.scriptPublicKey + ?.addresses + ?.firstOrNull() + ?.let { destination = it } + val outputs = vout + .asSequence() + .filter { it.scriptPublicKey?.addresses?.contains(address) == false } + .map { it.value.toBigDecimal() } + .sumOf { it } + val fee = transaction.fee?.toBigDecimal() ?: BigDecimal.ZERO + val feeSatoshi = transaction.feeSatoshi?.toBigDecimal() ?: BigDecimal.ZERO + outputs + fee + feeSatoshi + }.movePointLeft(blockchain.decimals()) + + BasicTransactionData( + balanceDif = if (isIncoming) amount else amount.negate(), + hash = transaction.txid, + date = Calendar.getInstance().apply { + timeInMillis = transaction.blockTime + }, + isConfirmed = false, + destination = destination, + source = source, + ) + } + } + + companion object { + const val SUPPORTED_SERVER_VERSION = "1.4" + private const val MINIMAL_FEE_BLOCK_AMOUNT = 8 + private const val NORMAL_FEE_BLOCK_AMOUNT = 4 + private const val PRIORITY_FEE_BLOCK_AMOUNT = 1 + private val NORMAL_FEE_MULTIPLIER = 2.toBigDecimal() + private val PRIORITY_FEE_MULTIPLIER = 8.toBigDecimal() + + /** + * We use 1000, because Electrum node return fee for per 1000 bytes. + */ + const val BYTES_IN_KB = 1000 + } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/address/EstimationFeeAddressFactory.kt b/blockchain/src/main/java/com/tangem/blockchain/common/address/EstimationFeeAddressFactory.kt index cceba2630..09de9a2ba 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/address/EstimationFeeAddressFactory.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/address/EstimationFeeAddressFactory.kt @@ -51,7 +51,7 @@ class EstimationFeeAddressFactory { -> "RT5qKgXdmh9pqtz71cgfL834VfeXFVH1sG" Blockchain.Solana -> "9wuDg6Y4H4j86Kg5aUGrUeaBa3sAUzjMs37KbeGFnRuM" Blockchain.Radiant -> "1K8jBuCKzuwvFCjL7Qpqq69k1hnVXJ31Nc" - Blockchain.Fact0rn -> TODO("Fact0rn") + Blockchain.Fact0rn -> "fact1q69h3nzh7rl2uv09zp5pw26vw58wdcl2j4lyag0" // EVM-like Blockchain.EthereumClassic, Blockchain.EthereumClassicTestnet -> "0xc49722a6f4Fe5A1347710dEAAa1fafF4c275689b" diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/assembly/impl/Fact0rnWalletManagerAssembly.kt b/blockchain/src/main/java/com/tangem/blockchain/common/assembly/impl/Fact0rnWalletManagerAssembly.kt index 1bc62ccb8..2b5e2d9b4 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/assembly/impl/Fact0rnWalletManagerAssembly.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/assembly/impl/Fact0rnWalletManagerAssembly.kt @@ -1,17 +1,31 @@ package com.tangem.blockchain.common.assembly.impl +import com.tangem.blockchain.blockchains.bitcoin.BitcoinFeesCalculator +import com.tangem.blockchain.blockchains.bitcoin.BitcoinTransactionBuilder +import com.tangem.blockchain.blockchains.bitcoin.BitcoinWalletManager import com.tangem.blockchain.blockchains.factorn.Fact0rnProvidersBuilder -import com.tangem.blockchain.blockchains.factorn.Fact0rnWalletManager +import com.tangem.blockchain.blockchains.factorn.network.Fact0rnNetworkService import com.tangem.blockchain.common.assembly.WalletManagerAssembly import com.tangem.blockchain.common.assembly.WalletManagerAssemblyInput +import com.tangem.blockchain.transactionhistory.TransactionHistoryProviderFactory -internal object Fact0rnWalletManagerAssembly : WalletManagerAssembly() { +internal object Fact0rnWalletManagerAssembly : WalletManagerAssembly() { - override fun make(input: WalletManagerAssemblyInput): Fact0rnWalletManager { + override fun make(input: WalletManagerAssemblyInput): BitcoinWalletManager { return with(input.wallet) { - Fact0rnWalletManager( + BitcoinWalletManager( wallet = this, - networkProviders = Fact0rnProvidersBuilder(input.providerTypes).build(blockchain), + transactionBuilder = BitcoinTransactionBuilder( + walletPublicKey = publicKey.blockchainKey, + blockchain = blockchain, + walletAddresses = addresses, + ), + networkProvider = Fact0rnNetworkService( + providers = Fact0rnProvidersBuilder(input.providerTypes).build(blockchain), + blockchain = blockchain, + ), + transactionHistoryProvider = TransactionHistoryProviderFactory.makeProvider(blockchain, input.config), + feesCalculator = BitcoinFeesCalculator(blockchain), ) } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/RetrofitBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/network/RetrofitBuilder.kt index fcf144017..e5d14f915 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/network/RetrofitBuilder.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/RetrofitBuilder.kt @@ -1,5 +1,6 @@ package com.tangem.blockchain.network +import android.annotation.SuppressLint import com.squareup.moshi.* import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.tangem.blockchain.blockchains.aptos.network.response.AptosResource @@ -21,6 +22,11 @@ import retrofit2.converter.moshi.MoshiConverterFactory import java.math.BigDecimal import java.math.BigInteger import java.util.concurrent.TimeUnit +import java.security.SecureRandom +import java.security.cert.X509Certificate +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManager +import javax.net.ssl.X509TrustManager fun createRetrofitInstance(baseUrl: String, headerInterceptors: List = emptyList()): Retrofit = Retrofit.Builder() @@ -49,6 +55,31 @@ object BlockchainSdkRetrofitBuilder { return builder.build() } + + // FIXME: Remove before releasing Fact0rn + internal fun createOkhttpClientForFact0rn(): OkHttpClient { + val builder = build().newBuilder() + + @SuppressLint("CustomX509TrustManager") + val trustAllCerts = arrayOf( + object : X509TrustManager { + @SuppressLint("TrustAllX509TrustManager") + override fun checkClientTrusted(chain: Array, authType: String) {} + + @SuppressLint("TrustAllX509TrustManager") + override fun checkServerTrusted(chain: Array, authType: String) {} + override fun getAcceptedIssuers(): Array { + return arrayOf() + } + }, + ) + val sslContext = SSLContext.getInstance("SSL") + sslContext.init(null, trustAllCerts, SecureRandom()) + builder.sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager) + builder.hostnameVerifier { _, _ -> true } + + return builder.build() + } } data class TimeoutConfig( diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/electrum/DefaultElectrumNetworkProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/network/electrum/DefaultElectrumNetworkProvider.kt index 6a329a4b7..4e03c1b31 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/network/electrum/DefaultElectrumNetworkProvider.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/electrum/DefaultElectrumNetworkProvider.kt @@ -4,22 +4,24 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.BlockchainError import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.extensions.Result +import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.extensions.map import com.tangem.blockchain.network.electrum.api.ElectrumApiService import com.tangem.blockchain.network.electrum.api.ElectrumResponse -import com.tangem.blockchain.network.electrum.api.WebSocketElectrumApiService import com.tangem.common.extensions.toHexString import kotlinx.coroutines.delay -import java.util.concurrent.atomic.AtomicBoolean +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first internal class DefaultElectrumNetworkProvider( override val baseUrl: String, private val blockchain: Blockchain, - private val service: WebSocketElectrumApiService, + private val service: ElectrumApiService, private val supportedProtocolVersion: String, ) : ElectrumNetworkProvider { - private val serverVersionRequested = AtomicBoolean(false) + private val serverVersionRequested = MutableStateFlow(VersionRequestState.NotRequested) override suspend fun getAccountBalance(addressScriptHash: String): Result { firstCheckServer()?.apply { return Result.Failure(this) } @@ -79,6 +81,16 @@ internal class DefaultElectrumNetworkProvider( } } + override suspend fun getTransactionHistory( + addressScriptHash: String, + ): Result> { + firstCheckServer()?.apply { return Result.Failure(this) } + + return retryCall { + service.getTransactionHistory(addressScriptHash = addressScriptHash) + } + } + override suspend fun getTransactionInfo(txHash: String): Result { firstCheckServer()?.apply { return Result.Failure(this) } @@ -96,14 +108,25 @@ internal class DefaultElectrumNetworkProvider( } private suspend fun firstCheckServer(): BlockchainError? { - if (serverVersionRequested.get()) { - return null + return when (val state = serverVersionRequested.value) { + VersionRequestState.NotRequested -> getServerVersion() + is VersionRequestState.Requested -> if (state.result is SimpleResult.Success) null else getServerVersion() + VersionRequestState.Requesting -> { + val requestedState = serverVersionRequested + .filterIsInstance() + .first() + if (requestedState.result is SimpleResult.Success) null else getServerVersion() + } } + } + + private suspend fun getServerVersion(): BlockchainError? { + serverVersionRequested.value = VersionRequestState.Requesting + val serverInfo = service.getServerVersion(supportedProtocolVersion = supportedProtocolVersion) - return when (val serverInfo = service.getServerVersion(supportedProtocolVersion = supportedProtocolVersion)) { + val error: BlockchainError? = when (serverInfo) { is Result.Success -> { if (serverInfo.data.versionNumber == supportedProtocolVersion) { - serverVersionRequested.set(true) null } else { // node doesn't support requested electrum protocol version BlockchainSdkError.UnsupportedOperation( @@ -118,6 +141,9 @@ internal class DefaultElectrumNetworkProvider( serverInfo.error } } + val requestedState = if (error == null) SimpleResult.Success else SimpleResult.Failure(error) + serverVersionRequested.value = VersionRequestState.Requested(requestedState) + return error } private suspend fun retryCall( @@ -140,4 +166,10 @@ internal class DefaultElectrumNetworkProvider( } return block() // last attempt } + + private sealed interface VersionRequestState { + data object NotRequested : VersionRequestState + data object Requesting : VersionRequestState + data class Requested(val result: SimpleResult) : VersionRequestState + } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/electrum/ElectrumNetworkProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/network/electrum/ElectrumNetworkProvider.kt index 3b92640bc..c983ababe 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/network/electrum/ElectrumNetworkProvider.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/electrum/ElectrumNetworkProvider.kt @@ -15,4 +15,6 @@ internal interface ElectrumNetworkProvider : NetworkProvider { suspend fun getTransactionInfo(txHash: String): Result suspend fun broadcastTransaction(rawTx: ByteArray): Result + + suspend fun getTransactionHistory(addressScriptHash: String): Result> } diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/electrum/ElectrumNetworkProviderFactory.kt b/blockchain/src/main/java/com/tangem/blockchain/network/electrum/ElectrumNetworkProviderFactory.kt index 5d504d2ba..a60ea5256 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/network/electrum/ElectrumNetworkProviderFactory.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/electrum/ElectrumNetworkProviderFactory.kt @@ -1,10 +1,14 @@ package com.tangem.blockchain.network.electrum import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.JsonRPCRequest import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder +import com.tangem.blockchain.network.electrum.api.DefaultElectrumApiService import com.tangem.blockchain.network.electrum.api.ElectrumApiService -import com.tangem.blockchain.network.electrum.api.WebSocketElectrumApiService +import com.tangem.blockchain.network.jsonrpc.DefaultJsonRPCService +import com.tangem.blockchain.network.jsonrpc.DefaultJsonRPCWebsocketService import okhttp3.OkHttpClient +import org.jetbrains.annotations.ApiStatus.Experimental internal object ElectrumNetworkProviderFactory { @@ -16,7 +20,37 @@ internal object ElectrumNetworkProviderFactory { ): ElectrumNetworkProvider = DefaultElectrumNetworkProvider( baseUrl = wssUrl, blockchain = blockchain, - service = WebSocketElectrumApiService(wssUrl = wssUrl, okHttpClient = okHttpClient), + service = DefaultElectrumApiService( + rpcService = DefaultJsonRPCWebsocketService( + wssUrl = wssUrl, + pingPongRequestFactory = { + JsonRPCRequest( + method = "server.ping", + id = "keepAlive", + params = emptyList(), + ) + }, + okHttpClient = okHttpClient, + ), + ), + supportedProtocolVersion = supportedProtocolVersion, + ) + + @Experimental + fun createHttpsVersion( + httpsUrl: String, + blockchain: Blockchain, + supportedProtocolVersion: String = ElectrumApiService.SUPPORTED_PROTOCOL_VERSION, + okHttpClient: OkHttpClient = BlockchainSdkRetrofitBuilder.build(), + ): ElectrumNetworkProvider = DefaultElectrumNetworkProvider( + baseUrl = httpsUrl, + blockchain = blockchain, + service = DefaultElectrumApiService( + rpcService = DefaultJsonRPCService( + url = httpsUrl, + okHttpClient = okHttpClient, + ), + ), supportedProtocolVersion = supportedProtocolVersion, ) } diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/electrum/ElectrumNetworkService.kt b/blockchain/src/main/java/com/tangem/blockchain/network/electrum/ElectrumNetworkService.kt index 10e06d5f8..6303e68fc 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/network/electrum/ElectrumNetworkService.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/electrum/ElectrumNetworkService.kt @@ -24,4 +24,9 @@ internal class ElectrumNetworkService(providers: List) override suspend fun broadcastTransaction(rawTx: ByteArray): Result = multiProvider.performRequest(ElectrumNetworkProvider::broadcastTransaction, rawTx) + + override suspend fun getTransactionHistory( + addressScriptHash: String, + ): Result> = + multiProvider.performRequest(ElectrumNetworkProvider::getTransactionHistory, addressScriptHash) } diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/electrum/api/WebSocketElectrumApiService.kt b/blockchain/src/main/java/com/tangem/blockchain/network/electrum/api/DefaultElectrumApiService.kt similarity index 91% rename from blockchain/src/main/java/com/tangem/blockchain/network/electrum/api/WebSocketElectrumApiService.kt rename to blockchain/src/main/java/com/tangem/blockchain/network/electrum/api/DefaultElectrumApiService.kt index 8fbd702db..62ed1c536 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/network/electrum/api/WebSocketElectrumApiService.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/electrum/api/DefaultElectrumApiService.kt @@ -7,28 +7,14 @@ import com.tangem.blockchain.common.JsonRPCRequest import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.fold import com.tangem.blockchain.extensions.map -import com.tangem.blockchain.network.jsonrpc.DefaultJsonRPCWebsocketService +import com.tangem.blockchain.network.jsonrpc.JsonRPCService import com.tangem.blockchain.network.moshi -import okhttp3.OkHttpClient import java.math.BigDecimal -internal class WebSocketElectrumApiService( - wssUrl: String, - okHttpClient: OkHttpClient, +internal class DefaultElectrumApiService( + val rpcService: JsonRPCService, ) : ElectrumApiService { - private val service = DefaultJsonRPCWebsocketService( - wssUrl = wssUrl, - pingPongRequestFactory = { - JsonRPCRequest( - method = "server.ping", - id = "keepAlive", - params = emptyList(), - ) - }, - okHttpClient = okHttpClient, - ) - private val blockTipAdapter: JsonAdapter by lazy { moshi.adapter(ElectrumResponse.BlockTip::class.java) } @@ -143,7 +129,7 @@ internal class WebSocketElectrumApiService( params: List = emptyList(), adapter: JsonAdapter = moshi.adapter(), ): Result { - return service.call( + return rpcService.call( JsonRPCRequest( method = method, params = params, diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/electrum/api/ElectrumResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/network/electrum/api/ElectrumResponse.kt index 234890c41..b153ef217 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/network/electrum/api/ElectrumResponse.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/electrum/api/ElectrumResponse.kt @@ -11,6 +11,7 @@ import java.math.BigDecimal * Original: https://bitcoincash.network/electrum/protocol-methods.html * Rostrum (Nexa): https://bitcoinunlimited.gitlab.io/rostrum/ * Radiant: https://electrumx.readthedocs.io/en/latest/ + * Fact0rn: https://electrumx-spesmilo.readthedocs.io/en/latest// */ internal object ElectrumResponse { diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/jsonrpc/DefaultJsonRPCService.kt b/blockchain/src/main/java/com/tangem/blockchain/network/jsonrpc/DefaultJsonRPCService.kt new file mode 100644 index 000000000..a1d31f3bf --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/network/jsonrpc/DefaultJsonRPCService.kt @@ -0,0 +1,22 @@ +package com.tangem.blockchain.network.jsonrpc + +import com.tangem.blockchain.common.JsonRPCRequest +import com.tangem.blockchain.common.JsonRPCResponse +import com.tangem.blockchain.network.createRetrofitInstance +import okhttp3.OkHttpClient + +internal class DefaultJsonRPCService( + url: String, + okHttpClient: OkHttpClient, +) : JsonRPCService { + + private val service = createRetrofitInstance(url) + .newBuilder() + .client(okHttpClient) + .build() + .create(JsonRPCServiceApi::class.java) + + override suspend fun call(jsonRPCRequest: JsonRPCRequest): Result { + return runCatching { service.call(jsonRPCRequest) } + } +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/jsonrpc/JsonRPCService.kt b/blockchain/src/main/java/com/tangem/blockchain/network/jsonrpc/JsonRPCService.kt new file mode 100644 index 000000000..013b13147 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/network/jsonrpc/JsonRPCService.kt @@ -0,0 +1,15 @@ +package com.tangem.blockchain.network.jsonrpc + +import com.tangem.blockchain.common.JsonRPCRequest +import com.tangem.blockchain.common.JsonRPCResponse + +internal interface JsonRPCService { + + /** + * Synchronized exception free JsonRPC method call. + * + * @param jsonRPCRequest JsonRPC request + * @return the result of the request or websocket connection error + */ + suspend fun call(jsonRPCRequest: JsonRPCRequest): Result +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/jsonrpc/JsonRPCServiceApi.kt b/blockchain/src/main/java/com/tangem/blockchain/network/jsonrpc/JsonRPCServiceApi.kt new file mode 100644 index 000000000..bdae28b5d --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/network/jsonrpc/JsonRPCServiceApi.kt @@ -0,0 +1,14 @@ +package com.tangem.blockchain.network.jsonrpc + +import com.tangem.blockchain.common.JsonRPCRequest +import com.tangem.blockchain.common.JsonRPCResponse +import retrofit2.http.Body +import retrofit2.http.Headers +import retrofit2.http.POST + +internal interface JsonRPCServiceApi { + + @Headers("Content-Type: application/json") + @POST("/") + suspend fun call(@Body jsonRPCRequest: JsonRPCRequest): JsonRPCResponse +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/jsonrpc/JsonRPCWebsocketService.kt b/blockchain/src/main/java/com/tangem/blockchain/network/jsonrpc/JsonRPCWebsocketService.kt index 3ab3ee286..6ecf0d1d4 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/network/jsonrpc/JsonRPCWebsocketService.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/jsonrpc/JsonRPCWebsocketService.kt @@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.StateFlow * When connecting with keepAlive == false, the connection will be held until the timer expires * Otherwise, the connection will be active until the disconnect method is called or any connection error occurs. */ -internal interface JsonRPCWebsocketService { +internal interface JsonRPCWebsocketService : JsonRPCService { /** * Represents current connection state @@ -47,5 +47,5 @@ internal interface JsonRPCWebsocketService { * @param jsonRPCRequest JsonRPC request * @return the result of the request or websocket connection error */ - suspend fun call(jsonRPCRequest: JsonRPCRequest): Result + override suspend fun call(jsonRPCRequest: JsonRPCRequest): Result } diff --git a/blockchain/src/test/java/com/tangem/blockchain/blockchains/factorn/Fact0rnAddressTest.kt b/blockchain/src/test/java/com/tangem/blockchain/blockchains/factorn/Fact0rnAddressTest.kt new file mode 100644 index 000000000..d27ba80bf --- /dev/null +++ b/blockchain/src/test/java/com/tangem/blockchain/blockchains/factorn/Fact0rnAddressTest.kt @@ -0,0 +1,39 @@ +package com.tangem.blockchain.blockchains.factorn + +import com.google.common.truth.Truth +import com.tangem.blockchain.common.address.AddressType +import com.tangem.common.extensions.hexToBytes +import org.bitcoinj.core.SegwitAddress +import org.bitcoinj.script.ScriptBuilder +import org.junit.Test + +class Fact0rnAddressTest { + + private val addressService = Fact0rnAddressService() + private val expectedAddress = "fact1qg2qvzvrgukkp5gct2n8dvuxz99ddxwecmx9sey" + + @Test + fun makeAddressFromCorrectPublicKey() { + val walletPublicKey = "03B6D7E1FB0977A5881A3B1F64F9778B4F56CB2B9EFD6E0D03E60051EAFEBF5831".hexToBytes() + val expectedSize = 1 + + val addresses = addressService.makeAddresses(walletPublicKey) + val address = requireNotNull(addresses.find { it.type == AddressType.Default }) + + Truth.assertThat(addresses.size).isEqualTo(expectedSize) + Truth.assertThat(address.value).isEqualTo(expectedAddress) + } + + @Test + fun makeScriptHashFromAddress() { + val expectedScriptHash = "808171256649754B402099695833B95E4507019B3E494A7DBC6F62058F09050E" + Truth.assertThat(Fact0rnAddressService.addressToScriptHash(expectedAddress)).isEqualTo(expectedScriptHash) + } + + @Test + fun makeScriptFromAddress() { + val expectedScript = ScriptBuilder + .createOutputScript(SegwitAddress.fromBech32(Fact0rnMainNetParams(), expectedAddress)) + Truth.assertThat(Fact0rnAddressService.addressToScript(expectedAddress)).isEqualTo(expectedScript) + } +}