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 new file mode 100644 index 000000000..8c8033ef2 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/factorn/Fact0rnAddressService.kt @@ -0,0 +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 { + return bitcoinAddressService.makeSegwitAddress(walletPublicKey).value + } + + override fun validate(address: String): Boolean { + 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 new file mode 100644 index 000000000..a2c283c56 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/factorn/Fact0rnProvidersBuilder.kt @@ -0,0 +1,23 @@ +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 + +internal class Fact0rnProvidersBuilder( + override val providerTypes: List, +) : OnlyPublicProvidersBuilder(providerTypes) { + + override fun createProvider(url: String, blockchain: Blockchain): ElectrumNetworkProvider { + 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/network/Fact0rnNetworkService.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/factorn/network/Fact0rnNetworkService.kt new file mode 100644 index 000000000..d2d8efbe5 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/factorn/network/Fact0rnNetworkService.kt @@ -0,0 +1,177 @@ +package com.tangem.blockchain.blockchains.factorn.network + +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( + 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) } + Result.Success( + BitcoinFee( + minimalPerKb = minimalFeeDeferred.await().successOr { return@coroutineScope it }, + normalPerKb = normalFeeDeferred.await().successOr { return@coroutineScope it }, + priorityPerKb = priorityFeeDeferred.await().successOr { return@coroutineScope it }, + ), + ) + } + + private suspend fun requestFee(blockAmount: Int) = multiProvider + .performRequest { getEstimateFee(blockAmount) } + .map { feeResponse -> + feeResponse.feeInCoinsPer1000Bytes + ?.divide(BigDecimal(BYTES_IN_KB)) + ?.movePointLeft(blockchain.decimals()) + ?: BigDecimal.ZERO + } + + 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 + + /** + * 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/Blockchain.kt b/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt index 1bbcfb53c..a92ade9aa 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt @@ -10,6 +10,7 @@ import com.tangem.blockchain.blockchains.chia.ChiaAddressService import com.tangem.blockchain.blockchains.decimal.DecimalAddressService import com.tangem.blockchain.blockchains.ethereum.Chain import com.tangem.blockchain.blockchains.ethereum.EthereumAddressService +import com.tangem.blockchain.blockchains.factorn.Fact0rnAddressService import com.tangem.blockchain.blockchains.hedera.HederaAddressService import com.tangem.blockchain.blockchains.kaspa.KaspaAddressService import com.tangem.blockchain.blockchains.koinos.KoinosAddressService @@ -145,6 +146,7 @@ enum class Blockchain( MoonriverTestnet("moonriver/test", "MOVR", "Moonriver Testnet"), Mantle("mantle", "MNT", "Mantle"), MantleTestnet("mantle/test", "MNT", "Mantle Testnet"), + Fact0rn("fact0rn", "FACT", "Fact0rn"), Flare("flare", "FLR", "Flare"), FlareTestnet("flare/test", "FLR", "Flare Testnet"), Taraxa("taraxa", "TARA", "Taraxa"), @@ -216,6 +218,7 @@ enum class Blockchain( Aptos, AptosTestnet, Hedera, HederaTestnet, Radiant, + Fact0rn, Koinos, KoinosTestnet, InternetComputer, Clore, @@ -391,6 +394,7 @@ enum class Blockchain( Nexa, NexaTestnet -> NexaAddressService(this.isTestnet()) Koinos, KoinosTestnet -> KoinosAddressService() Radiant -> RadiantAddressService() + Fact0rn -> Fact0rnAddressService() Casper, CasperTestnet -> CasperAddressService() Unknown -> error("unsupported blockchain") } @@ -540,6 +544,7 @@ enum class Blockchain( Manta, MantaTestnet, PolygonZkEVM, PolygonZkEVMTestnet, Radiant, + Fact0rn, Base, BaseTestnet, Moonriver, MoonriverTestnet, Mantle, MantleTestnet, diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt b/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt index 25ba1cfdc..c479c923a 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt @@ -198,6 +198,7 @@ class WalletManagerFactory( Blockchain.Hedera, Blockchain.HederaTestnet -> HederaWalletManagerAssembly(dataStorage, accountCreator) Blockchain.Nexa, Blockchain.NexaTestnet -> NexaWalletManagerAssembly Blockchain.Radiant -> RadiantWalletManagerAssembly + Blockchain.Fact0rn -> Fact0rnWalletManagerAssembly Blockchain.Koinos, Blockchain.KoinosTestnet -> KoinosWalletManagerAssembly Blockchain.Filecoin -> FilecoinWalletManagerAssembly Blockchain.Sei, Blockchain.SeiTestnet -> SeiWalletManagerAssembly 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 482dd726b..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,6 +51,7 @@ class EstimationFeeAddressFactory { -> "RT5qKgXdmh9pqtz71cgfL834VfeXFVH1sG" Blockchain.Solana -> "9wuDg6Y4H4j86Kg5aUGrUeaBa3sAUzjMs37KbeGFnRuM" Blockchain.Radiant -> "1K8jBuCKzuwvFCjL7Qpqq69k1hnVXJ31Nc" + 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 new file mode 100644 index 000000000..2b5e2d9b4 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/common/assembly/impl/Fact0rnWalletManagerAssembly.kt @@ -0,0 +1,32 @@ +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.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() { + + override fun make(input: WalletManagerAssemblyInput): BitcoinWalletManager { + return with(input.wallet) { + BitcoinWalletManager( + wallet = this, + 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/common/derivation/DerivationConfigV1.kt b/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV1.kt index 1b49ebc38..59aaccbe3 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV1.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV1.kt @@ -159,6 +159,7 @@ object DerivationConfigV1 : DerivationConfig() { Blockchain.NexaTestnet, -> mapOf(AddressType.Default to DerivationPath("m/44'/29223'/0'/0/0")) Blockchain.Radiant -> mapOf(AddressType.Default to DerivationPath("m/44'/512'/0'/0/0")) + Blockchain.Fact0rn -> mapOf(AddressType.Default to DerivationPath("m/44'/42069'/0'/0/0")) Blockchain.Koinos, Blockchain.KoinosTestnet, -> mapOf(AddressType.Default to DerivationPath("m/44'/659'/0'/0/0")) diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV2.kt b/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV2.kt index 4c761d7a0..f61376570 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV2.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV2.kt @@ -159,6 +159,7 @@ object DerivationConfigV2 : DerivationConfig() { Blockchain.NexaTestnet, -> mapOf(AddressType.Default to DerivationPath("m/44'/29223'/0'/0/0")) Blockchain.Radiant -> mapOf(AddressType.Default to DerivationPath("m/44'/512'/0'/0/0")) + Blockchain.Fact0rn -> mapOf(AddressType.Default to DerivationPath("m/44'/42069'/0'/0/0")) Blockchain.Koinos, Blockchain.KoinosTestnet, -> mapOf(AddressType.Default to DerivationPath("m/44'/659'/0'/0/0")) diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV3.kt b/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV3.kt index f9a9c641e..7dd1928b2 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV3.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV3.kt @@ -155,6 +155,7 @@ object DerivationConfigV3 : DerivationConfig() { Blockchain.NexaTestnet, -> mapOf(AddressType.Default to DerivationPath("m/44'/29223'/0'/0/0")) Blockchain.Radiant -> mapOf(AddressType.Default to DerivationPath("m/44'/512'/0'/0/0")) + Blockchain.Fact0rn -> mapOf(AddressType.Default to DerivationPath("m/44'/42069'/0'/0/0")) Blockchain.Koinos, Blockchain.KoinosTestnet, -> mapOf(AddressType.Default to DerivationPath("m/44'/659'/0'/0/0")) diff --git a/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/ExternalLinkProviderFactory.kt b/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/ExternalLinkProviderFactory.kt index 16513b5c8..5ce4fa4af 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/ExternalLinkProviderFactory.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/ExternalLinkProviderFactory.kt @@ -79,6 +79,7 @@ internal object ExternalLinkProviderFactory { Blockchain.Flare, Blockchain.FlareTestnet -> FlareExternalLinkProvider(isTestnet) Blockchain.Taraxa, Blockchain.TaraxaTestnet -> TaraxaExternalLinkProvider(isTestnet) Blockchain.Radiant -> RadiantExternalLinkProvider() + Blockchain.Fact0rn -> Fact0rnExternalLinkProvider() Blockchain.Koinos, Blockchain.KoinosTestnet -> KoinosExternalLinkProvider(isTestnet) Blockchain.Filecoin -> FilecoinExternalLinkProvider() Blockchain.Blast, Blockchain.BlastTestnet -> BlastLinkProvider(isTestnet) diff --git a/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/providers/Fact0rnExternalLinkProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/providers/Fact0rnExternalLinkProvider.kt new file mode 100644 index 000000000..c087fc7d9 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/providers/Fact0rnExternalLinkProvider.kt @@ -0,0 +1,17 @@ +package com.tangem.blockchain.externallinkprovider.providers + +import com.tangem.blockchain.externallinkprovider.ExternalLinkProvider +import com.tangem.blockchain.externallinkprovider.TxExploreState + +internal class Fact0rnExternalLinkProvider : ExternalLinkProvider { + + override val explorerBaseUrl: String = "https://explorer.fact0rn.io/" + + override fun explorerUrl(walletAddress: String, contractAddress: String?): String { + return explorerBaseUrl + "address/$walletAddress" + } + + override fun getExplorerTxUrl(transactionHash: String): TxExploreState { + return TxExploreState.Url(explorerBaseUrl + "tx/$transactionHash") + } +} 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) + } +}