Updated on 2026-08-14

This commit is contained in:
Tangem 2019-12-23 08:33:29 +00:00
commit 1186c3c3f1
3 changed files with 209 additions and 0 deletions

View file

@ -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
}
}
}
}

View file

@ -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
)

View file

@ -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()
}
}