Updated on 2026-08-14
This commit is contained in:
commit
1186c3c3f1
3 changed files with 209 additions and 0 deletions
|
|
@ -0,0 +1,71 @@
|
|||
package com.tangem.blockchain.bitcoin
|
||||
|
||||
import com.tangem.blockchain.extensions.calculateRipemd160
|
||||
import com.tangem.common.extensions.calculateSha256
|
||||
import org.bitcoinj.core.AddressFormatException
|
||||
import org.bitcoinj.core.Base58
|
||||
import org.bitcoinj.core.SegwitAddress
|
||||
import org.bitcoinj.params.MainNetParams
|
||||
import org.bitcoinj.params.TestNet3Params
|
||||
import java.security.MessageDigest
|
||||
|
||||
class BitcoinAddressFactory {
|
||||
companion object {
|
||||
fun makeAddress(cardPublicKey: ByteArray, testNet: Boolean = false): String {
|
||||
val netSelectionByte = if (testNet) 0x6f.toByte() else 0x00.toByte()
|
||||
val hash1 = cardPublicKey.calculateSha256().calculateRipemd160()
|
||||
val hash2 = byteArrayOf(netSelectionByte).plus(hash1).calculateSha256().calculateSha256()
|
||||
val result = byteArrayOf(netSelectionByte) + hash1 + hash2[0] + hash2[1] + hash2[2] + hash2[3]
|
||||
return Base58.encode(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BitcoinAddressValidator {
|
||||
companion object {
|
||||
|
||||
private const val firstLetters = "123nm"
|
||||
private const val firstLettersNonTestNet = "13"
|
||||
|
||||
fun validate(address: String, testNet: Boolean = false): Boolean {
|
||||
if (firstLetters.contains(address.first())) {
|
||||
if (testNet && firstLettersNonTestNet.contains(address.first())) return false
|
||||
if (address.length !in 26..35) return false
|
||||
val decoded = address.decodeBase58() ?: return false
|
||||
val hash = sha256(decoded, 0, 21, 2)
|
||||
return hash.sliceArray(0..3).contentEquals(decoded.sliceArray(21..24))
|
||||
} else {
|
||||
return validateSegwitAddress(address, testNet)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sha256(data: ByteArray, start: Int, len: Int, recursion: Int): ByteArray {
|
||||
if (recursion == 0) return data
|
||||
val md = MessageDigest.getInstance("SHA-256")
|
||||
md.update(data.sliceArray(start until start + len))
|
||||
return sha256(md.digest(), 0, 32, recursion - 1)
|
||||
}
|
||||
|
||||
private fun String.decodeBase58(): ByteArray? {
|
||||
return try {
|
||||
Base58.decode(this)
|
||||
} catch (exception: AddressFormatException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateSegwitAddress(address: String, testNet: Boolean): Boolean {
|
||||
return try {
|
||||
if (testNet) {
|
||||
SegwitAddress.fromBech32(TestNet3Params(), address)
|
||||
true
|
||||
} else {
|
||||
SegwitAddress.fromBech32(MainNetParams(), address)
|
||||
true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package com.tangem.blockchain.bitcoin
|
||||
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.extensions.toCanonicalised
|
||||
import org.bitcoinj.core.*
|
||||
import org.bitcoinj.crypto.TransactionSignature
|
||||
import org.bitcoinj.script.Script
|
||||
import org.bitcoinj.script.ScriptBuilder
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.math.BigInteger
|
||||
|
||||
class BitcoinTransactionBuilder(private val testNet: Boolean) {
|
||||
|
||||
private lateinit var transaction: Transaction
|
||||
private var networkParameters: NetworkParameters? = null
|
||||
var unspentOutputs: List<UnspentTransaction> = listOf()
|
||||
|
||||
fun calculateChange(transactionData: TransactionData) : Long {
|
||||
val fullAmount = unspentOutputs.map { it.amount }.sum()
|
||||
return fullAmount - (transactionData.amount.value!!.toLong() + (transactionData.fee?.value!!.toLong()))
|
||||
}
|
||||
|
||||
fun buildToSign(
|
||||
transactionData: TransactionData): List<ByteArray> {
|
||||
|
||||
val change: Long = calculateChange(transactionData)
|
||||
|
||||
networkParameters = if (testNet) {
|
||||
NetworkParameters.fromID(NetworkParameters.ID_TESTNET)
|
||||
} else {
|
||||
NetworkParameters.fromID(NetworkParameters.ID_MAINNET)
|
||||
}
|
||||
transaction = transactionData.toBitcoinJTransaction(networkParameters, unspentOutputs, change)
|
||||
|
||||
val hashesForSign: MutableList<ByteArray> = mutableListOf()
|
||||
for (input in transaction.inputs) {
|
||||
val index = input.index
|
||||
hashesForSign[index] = transaction.hashForSignature(index, input.scriptBytes, Transaction.SigHash.ALL, false).bytes
|
||||
}
|
||||
return hashesForSign
|
||||
}
|
||||
|
||||
fun buildToSend(signedTransaction: ByteArray, publicKey: ByteArray): ByteArray {
|
||||
for (index in transaction.inputs.indices) {
|
||||
transaction.inputs[index].scriptSig = createScript(index, signedTransaction, publicKey)
|
||||
}
|
||||
val serializer = BitcoinSerializer(networkParameters, false)
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
serializer.serialize(transaction, outputStream)
|
||||
return outputStream.toByteArray()
|
||||
}
|
||||
|
||||
private fun createScript(index: Int, signedTransaction: ByteArray, publicKey: ByteArray): Script {
|
||||
val r = BigInteger(1, signedTransaction.copyOfRange(index * 64, 32 + index * 64));
|
||||
val s = BigInteger(1, signedTransaction.copyOfRange(32 + index * 64, 64 + index * 64));
|
||||
val signature = TransactionSignature(r, s.toCanonicalised())
|
||||
return ScriptBuilder.createInputScript(signature, ECKey.fromPublicOnly(publicKey))
|
||||
}
|
||||
}
|
||||
|
||||
internal fun TransactionData.toBitcoinJTransaction(networkParameters: NetworkParameters?,
|
||||
unspentOutputs: List<UnspentTransaction>,
|
||||
change: Long): Transaction {
|
||||
val transaction = Transaction(networkParameters)
|
||||
for (utxo in unspentOutputs) {
|
||||
transaction.addInput(Sha256Hash.wrap(utxo.hash), utxo.outputIndex, Script(utxo.outputScript))
|
||||
}
|
||||
transaction.addOutput(
|
||||
Coin.parseCoin(this.amount.value!!.toPlainString()),
|
||||
Address.fromString(networkParameters, this.destinationAddress))
|
||||
if (change != 0L) {
|
||||
transaction.addOutput(
|
||||
Coin.parseCoin(change.toString()),
|
||||
Address.fromString(networkParameters,
|
||||
this.sourceAddress))
|
||||
}
|
||||
return transaction
|
||||
}
|
||||
|
||||
class UnspentTransaction(
|
||||
val amount: Long,
|
||||
val outputIndex: Long,
|
||||
val hash: ByteArray,
|
||||
val outputScript: ByteArray
|
||||
)
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.tangem.blockchain.bitcoin
|
||||
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.wallets.CurrencyWallet
|
||||
import com.tangem.tasks.TaskEvent
|
||||
import org.bitcoinj.core.NetworkParameters
|
||||
import org.bitcoinj.core.Transaction
|
||||
|
||||
class BitcoinWalletManager(
|
||||
private val cardId: String,
|
||||
private val walletPublicKey: ByteArray,
|
||||
walletConfig: WalletConfig,
|
||||
isTestNet: Boolean = false
|
||||
) : WalletManager,
|
||||
TransactionEstimator,
|
||||
TransactionSender,
|
||||
FeeProvider {
|
||||
|
||||
override val blockchain = if (isTestNet) Blockchain.BitcoinTestnet else Blockchain.Bitcoin
|
||||
private val address = blockchain.makeAddress(walletPublicKey)
|
||||
override var wallet: Wallet = CurrencyWallet(walletConfig, address)
|
||||
private val transactionBuilder = BitcoinTransactionBuilder(isTestNet)
|
||||
|
||||
|
||||
override fun update() {
|
||||
transactionBuilder.unspentOutputs = listOf()
|
||||
}
|
||||
|
||||
|
||||
override fun getEstimateSize(transactionData: TransactionData): Int {
|
||||
val transaction: Transaction = transactionData.toBitcoinJTransaction(
|
||||
NetworkParameters.fromID(NetworkParameters.ID_MAINNET),
|
||||
transactionBuilder.unspentOutputs,
|
||||
transactionBuilder.calculateChange(transactionData)
|
||||
)
|
||||
var size: Int = transaction.unsafeBitcoinSerialize().size
|
||||
size += transaction.inputs.sumBy { 130 }
|
||||
return size
|
||||
}
|
||||
|
||||
override fun send(transactionData: TransactionData, signer: TransactionSigner) {
|
||||
val hashes = transactionBuilder.buildToSign(transactionData)
|
||||
signer.sign(hashes.toTypedArray(), cardId) {
|
||||
when (it) {
|
||||
is TaskEvent.Event -> transactionBuilder.buildToSend(it.data.signature, walletPublicKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFee(amount: Amount, source: String, destination: String): List<Amount> {
|
||||
return BitcoinServer.getFee()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue