Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AND-9157 Fact0rn support #844

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
@@ -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()
}
}
}
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
@@ -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<ProviderType>,
) : OnlyPublicProvidersBuilder<ElectrumNetworkProvider>(providerTypes) {

override fun createProvider(url: String, blockchain: Blockchain): ElectrumNetworkProvider {
return ElectrumNetworkProviderFactory.create(
wssUrl = url,
blockchain = blockchain,
okHttpClient = BlockchainSdkRetrofitBuilder.createOkhttpClientForFact0rn(),
supportedProtocolVersion = Fact0rnNetworkService.SUPPORTED_SERVER_VERSION,
)
}
}
Original file line number Diff line number Diff line change
@@ -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<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) }
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<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

/**
* 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,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
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -216,6 +218,7 @@ enum class Blockchain(
Aptos, AptosTestnet,
Hedera, HederaTestnet,
Radiant,
Fact0rn,
Koinos, KoinosTestnet,
InternetComputer,
Clore,
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -540,6 +544,7 @@ enum class Blockchain(
Manta, MantaTestnet,
PolygonZkEVM, PolygonZkEVMTestnet,
Radiant,
Fact0rn,
Base, BaseTestnet,
Moonriver, MoonriverTestnet,
Mantle, MantleTestnet,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class EstimationFeeAddressFactory {
-> "RT5qKgXdmh9pqtz71cgfL834VfeXFVH1sG"
Blockchain.Solana -> "9wuDg6Y4H4j86Kg5aUGrUeaBa3sAUzjMs37KbeGFnRuM"
Blockchain.Radiant -> "1K8jBuCKzuwvFCjL7Qpqq69k1hnVXJ31Nc"
Blockchain.Fact0rn -> "fact1q69h3nzh7rl2uv09zp5pw26vw58wdcl2j4lyag0"
// EVM-like
Blockchain.EthereumClassic, Blockchain.EthereumClassicTestnet ->
"0xc49722a6f4Fe5A1347710dEAAa1fafF4c275689b"
Expand Down
Original file line number Diff line number Diff line change
@@ -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<BitcoinWalletManager>() {

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),
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
Loading
Loading