Skip to content

Commit

Permalink
AND-9157 Fact0rn based on Bitcoin wallet
Browse files Browse the repository at this point in the history
  • Loading branch information
nemelianov-tangem committed Dec 12, 2024
1 parent ff0b2fa commit bb85717
Show file tree
Hide file tree
Showing 21 changed files with 450 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}",
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.tangem.blockchain.blockchains.factorn

import org.bitcoinj.params.MainNetParams

internal class Fact0rnMainNetParams : MainNetParams() {

init {
segwitAddressHrp = "fact"
}
}
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -14,6 +16,8 @@ internal class Fact0rnProvidersBuilder(
return ElectrumNetworkProviderFactory.create(
wssUrl = url,
blockchain = blockchain,
okHttpClient = BlockchainSdkRetrofitBuilder.createOkhttpClientForFact0rn(),
supportedProtocolVersion = Fact0rnNetworkService.SUPPORTED_SERVER_VERSION,
)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<ElectrumNetworkProvider>) : NetworkProvider {
internal class Fact0rnNetworkService(
private val blockchain: Blockchain,
providers: List<ElectrumNetworkProvider>,
) : BitcoinNetworkProvider {

override val baseUrl: String
get() = multiProvider.currentProvider.baseUrl

private val multiProvider = MultiNetworkProvider(providers)

override suspend fun getInfo(address: String): Result<BitcoinAddressInfo> = 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<BitcoinFee> = 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<Int> {
return multiProvider.performRequest(
ElectrumNetworkProvider::getTransactionHistory,
addressToScriptHash(address),
)
.map { Result.Success(it.count()) }
.successOr { it }
}

private fun createUnspentOutputs(
getUtxoResponseItems: List<ElectrumUnspentUTXORecord>,
address: String,
): List<BitcoinUnspentOutput> = 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<ElectrumUnspentUTXORecord>,
address: String,
): List<BasicTransactionData> = coroutineScope {
utxoResponseItems
.filter { !it.isConfirmed }
.map { utxo -> async { multiProvider.performRequest { getTransactionInfo(utxo.txHash) } } }
.awaitAll()
.filterIsInstance<Result.Success<ElectrumResponse.Transaction>>()
.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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Fact0rnWalletManager>() {
internal object Fact0rnWalletManagerAssembly : WalletManagerAssembly<BitcoinWalletManager>() {

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),
)
}
}
Expand Down
Loading

0 comments on commit bb85717

Please sign in to comment.