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 2, 2024
1 parent 1131055 commit 96a6e7f
Show file tree
Hide file tree
Showing 20 changed files with 366 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.tangem.blockchain.blockchains.bitcoin

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 @@ -39,13 +40,16 @@ open class BitcoinAddressService(
Blockchain.Ravencoin -> RavencoinMainNetParams()
Blockchain.RavencoinTestnet -> RavencoinTestNetParams()
Blockchain.Radiant -> RadiantMainNetParams()
Blockchain.Fact0rn -> Fact0rnMainNetParams()
else -> error(
"${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}",
)
}

override fun makeAddress(walletPublicKey: ByteArray, curve: EllipticCurve?) =
makeLegacyAddress(walletPublicKey).value
override fun makeAddress(walletPublicKey: ByteArray, curve: EllipticCurve?) = when (blockchain) {
Blockchain.Fact0rn -> makeSegwitAddress(walletPublicKey).value
else -> makeLegacyAddress(walletPublicKey).value
}

override fun validate(address: String): Boolean {
return validateLegacyAddress(address) || validateSegwitAddress(address)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.tangem.blockchain.blockchains.bitcoin

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 @@ -44,6 +45,7 @@ open class BitcoinTransactionBuilder(
Blockchain.Dash -> DashMainNetParams()
Blockchain.Ravencoin -> RavencoinMainNetParams()
Blockchain.RavencoinTestnet -> RavencoinTestNetParams()
Blockchain.Fact0rn -> Fact0rnMainNetParams()
else -> error("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}")
}
var unspentOutputs: List<BitcoinUnspentOutput>? = null
Expand Down

This file was deleted.

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
Expand Up @@ -3,6 +3,7 @@ package com.tangem.blockchain.blockchains.factorn
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 @@ -11,9 +12,10 @@ internal class Fact0rnProvidersBuilder(
) : OnlyPublicProvidersBuilder<ElectrumNetworkProvider>(providerTypes) {

override fun createProvider(url: String, blockchain: Blockchain): ElectrumNetworkProvider {
return ElectrumNetworkProviderFactory.create(
wssUrl = url,
return ElectrumNetworkProviderFactory.createHttpsVersion(
httpsUrl = url,
blockchain = blockchain,
okHttpClient = BlockchainSdkRetrofitBuilder.createOkhttpClientForFact0rn(),
)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,13 +1,194 @@
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.Fact0rnMainNetParams
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 com.tangem.common.extensions.toHexString
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import org.bitcoinj.core.SegwitAddress
import org.bitcoinj.core.Sha256Hash
import org.bitcoinj.script.Script
import org.bitcoinj.script.ScriptBuilder
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 {
fun feeDeferred(blocks: Int) = async {
multiProvider.performRequest { getEstimateFee(blocks) }
.map { feeResponse ->
feeResponse.feeInCoinsPer1000Bytes
?.divide(BigDecimal(BYTES_IN_KB))
?.movePointLeft(blockchain.decimals())
?: BigDecimal.ZERO
}
}

val minimalFee = feeDeferred(MINIMAL_NUMBER_OF_CONFIRMATION_BLOCKS).await()
.successOr { return@coroutineScope it }
val normalFee = feeDeferred(NORMAL_NUMBER_OF_CONFIRMATION_BLOCKS).await()
.successOr { return@coroutineScope it }
val priorityFee = feeDeferred(PRIORITY_NUMBER_OF_CONFIRMATION_BLOCKS).await()
.successOr { return@coroutineScope it }

Result.Success(
BitcoinFee(
minimalPerKb = minimalFee,
normalPerKb = normalFee,
priorityPerKb = priorityFee,
),
)
}

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,
)
}
}

private fun addressToScript(address: String): Script =
ScriptBuilder.createOutputScript(SegwitAddress.fromBech32(Fact0rnMainNetParams(), address))

private fun addressToScriptHash(address: String): String {
val p2pkhScript = addressToScript(address)
val sha256Hash = Sha256Hash.hash(p2pkhScript.program)
return sha256Hash.reversedArray().toHexString()
}

private companion object {
const val MINIMAL_NUMBER_OF_CONFIRMATION_BLOCKS = 25
const val NORMAL_NUMBER_OF_CONFIRMATION_BLOCKS = 10
const val PRIORITY_NUMBER_OF_CONFIRMATION_BLOCKS = 5

/**
* 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 @@ -10,7 +10,6 @@ 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
Expand Down Expand Up @@ -310,6 +309,7 @@ enum class Blockchain(
Dogecoin,
Ducatus,
Dash,
Fact0rn,
Ravencoin, RavencoinTestnet,
-> BitcoinAddressService(this)

Expand Down Expand Up @@ -391,7 +391,6 @@ enum class Blockchain(
Nexa, NexaTestnet -> NexaAddressService(this.isTestnet())
Koinos, KoinosTestnet -> KoinosAddressService()
Radiant -> RadiantAddressService()
Fact0rn -> Fact0rnAddressService()
Casper, CasperTestnet -> CasperAddressService()
Unknown -> error("unsupported blockchain")
}
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 96a6e7f

Please sign in to comment.