diff --git a/blockchain/src/main/java/com/tangem/blockchain/bitcoin/BitcoinAddress.kt b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/BitcoinAddress.kt new file mode 100644 index 0000000000..54fabba6b5 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/BitcoinAddress.kt @@ -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 + } + } + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/bitcoin/BitcoinTransactionBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/BitcoinTransactionBuilder.kt new file mode 100644 index 0000000000..1d99ce8db0 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/BitcoinTransactionBuilder.kt @@ -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 = 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 { + + 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 = 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, + 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 +) \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/bitcoin/BitcoinWalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/BitcoinWalletManager.kt new file mode 100644 index 0000000000..d7da71a6fc --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/BitcoinWalletManager.kt @@ -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 { + return BitcoinServer.getFee() + } +} \ No newline at end of file