horizontalsystems / bitcoin-kit-android

Comprehensive Bitcoin development library for Android, implemented on Kotlin. SPV wallet implementation for Bitcoin, Bitcoin Cash, Litecoin and Dash blockchains. Fully compliant with existing standards and BIPs.
https://unstoppable.money
MIT License
154 stars 68 forks source link
bch bitcoin bitcoin-cash bitcoin-core bitcoin-transaction bitcoin-wallet blockchain-wallet btc btc-wallet dash dash-wallet dashcoin dashcore decentralized hd-wallet kotlin litecoin p2p spv spv-wallet-toolkit

BitcoinKit

bitcoin-kit-android is a Bitcoin wallet toolkit implemented in Kotlin. It consists of following libraries:

Being an SPV client, bitcoincore downloads and validates all the block headers, inclusion of transactions in the blocks, integrity and immutability of transactions as described in the Bitcoin whitepaper or delegates validation to the extensions that implement the forks of Bitcoin.

Core Features

bitcoinkit

Usage

Initialization

First, you need an instance of BitcoinKit class. You can initialize it with Mnemonic seed or BIP32 extended key (private or public). To generate seed from mnemonic seed phrase you can use HdWalletKit to convert a word list to a seed.

val words = listOf("mnemonic", "phrase", "words")
val passphrase: String = ""

val seed = Mnemonic().toSeed(words, passphrase)

Then you can pass a seed to initialize an instance of BitcoinKit

val context = Application()

val bitcoinKit = BitcoinKit(
    context = context,
    seed = seed,
    walletId = "unique_wallet_id",
    syncMode = BitcoinCore.SyncMode.Api(),
    networkType = NetworkType.MainNet,
    confirmationsThreshold = 6,
    purpose = HDWallet.Purpose.BIP84
)

purpose

bitcoinkit supports BIP44, BIP49 and BIP84 wallets. They have different derivation paths, so you need to specify this on kit initialization.

syncMode

bitcoinkit pulls all historical transactions of given account from bitcoin peers according to SPV protocol. This process may take several hours as it needs to download every block header with some transactions to find transactions concerning the accounts addresses. In order to speed up the initial blockchain scan, bitcoincore has some optimization options:

If you set Full() to syncMode, then only decentralized peers are used. Once the initial blockchain scan is completed, the remaining synchronization works with decentralized peers only for all syncModes.

Additional parameters:

Initializing with HD extended key

You can initialize BitcoinKit using BIP32 Extended Private/Public Key as follows:

val extendedKey = HDExtendedKey("xprvA1BgyAq84AiAsrMm6DKqwCXDwxLBXq76dpUfuNXNziGMzDxYLjE9AkuYBAQTpt6aJu4nFYamh6BbrRkys5fJcxGd7qixNrpVpPBxui9oYyF")

val bitcoinKit = BitcoinKit(
    context = context,
    extendedKey = extendedKey,
    walletId = "unique_wallet_id",
    syncMode = BitcoinCore.SyncMode.Api(),
    networkType = NetworkType.MainNet,
    confirmationsThreshold = 6
)

If you restore with a public extended key, then you only will be able to watch the wallet. You won't be able to send any transactions. This is how the watch account feature is implemented.

Starting and Stopping

BitcoinKit requires to be started with start command. It will be in synced state as long as it is possible. You can call stop to stop it

bitcoinKit.start()
bitcoinKit.stop()

Getting wallet data

Balance

Balance is provided in Satoshis:

val balance = bitcoinKit.balance

println(balance.spendable)
println(balance.unspendable)

Unspendable balance is non-zero if you have UTXO that is currently not spendable due to some custom unlock script. These custom scripts can be implemented as a plugin, like Hodler

Last Block Info

val blockInfo = bitcoinKit.lastBlockInfo ?: return

println(blockInfo.headerHash)
println(blockInfo.height)
println(blockInfo.timestamp)

Receive Address

Get an address which you can receive coins to. Receive address is changed each time after you actually get some coins in that address

bitcoinKit.receiveAddress()   // "mgv1KTzGZby57K5EngZVaPdPtphPmEWjiS"

Transactions

You can get your transactions using transactions(fromUid: String? = null, type: TransactionFilterType? = null, limit: Int? = null) method of the BitcoinKit instance. It returns Single<List>. You'll need to subscribe and get transactions asynchronously. See RX Single Observers for more info.

val disposables = CompositeDisposable()

bitcoinKit.transactions().subscribe { transactionInfos ->
    for (transactionInfo in transactionInfos) {
        println("Uid: ${transactionInfo.uid}")
        println("Hash: ${transactionInfo.transactionHash}")
    }
}.let {
    disposables.add(it)
}

TransactionInfo

A sample dump:

// transactionInfo = {TransactionInfo}
//    amount = 13114
//    blockHeight = 740024
//    conflictingTxHash = null
//    fee = null
//    inputs = {ArrayList} size = 1
//      0 = {TransactionInputInfo}
//          address = "16s6q8dAgLbDT3szEc4nvTh81deRCBtEa1"
//          mine = false
//          value = null
//    outputs = {ArrayList}  size = 2
//      0 = {TransactionOutputInfo}
//          address = "bc1qsg9ul383f8pespcvc8u3katl6gnsr7sjyfe3pc"
//          changeOutput = false
//          mine = true
//          pluginData = null
//          pluginDataString = null
//          pluginId = null
//          value = 13114
//      1 = {TransactionOutputInfo}
//          address = "16VCm8mYhHE3EiELi8GiYEqAjnPu1TSgAV"
//          changeOutput = false
//          mine = false
//          pluginData = null
//          pluginDataString = null
//          pluginId = null
//          value = 1422
//    status = {TransactionStatus} RELAYED
//    timestamp = 1654766137
//    transactionHash = "cadf99db1e145dcfadfa2bc3eacb94831eb6c53d376f4f873aa4ac017b8c7f8f"
//    transactionIndex = 2760
//    type = {TransactionType} Incoming
//    uid = "75934663-3c84-4b38-9b6d-810d3433de17"

uid

A local unique ID

type

status

Sending BTC

bitcoinKit.send(address = "36k1UofZ2iP2NYax9znDCsksajfKeKLLMJ", value = 100000000, feeRate = 10, sortType = TransactionDataSortType.Bip69)

This first validates a given address and amount, creates new transaction, then sends it over the peers network. If there's any error with given address/amount or network, it raises an exception.

Validate address

bitcoinKit.validateAddress(address = "mrjQyzbX9SiJxRC2mQhT4LvxFEmt9KEeRY")

Evaluate fee

bitcoinKit.fee(address = "36k1UofZ2iP2NYax9znDCsksajfKeKLLMJ", value = 100000000, feeRate = 10)

Parsing BIP21 URI

You can use parsePaymentAddress method to parse a BIP21 URI:

bitcoinKit.parsePaymentAddress("bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz")

// ▿ BitcoinPaymentData
//   - address : "175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W"
//   - version : null
//   - amount : 50.0
//   - label : "Luke-Jr"
//   - message : "Donation for project xyz"
//   - parameters : null

Subscribing to BitcoinKit data

Balance, transactions, last blocks synced and kit state are available in real-time. BitcoinKit.Listener interface must be implemented and set to BitcoinKit instance to receive that.

class Manager(val bitcoinKit: BitcoinKit) : BitcoinKit.Listener {

    init {
        bitcoinKit.listener = this
    }

    override fun onBalanceUpdate(balance: BalanceInfo) {
    }

    override fun onLastBlockInfoUpdate(blockInfo: BlockInfo) {
    }

    override fun onKitStateUpdate(state: BitcoinCore.KitState) {
    }

    override fun onTransactionsUpdate(inserted: List<TransactionInfo>, updated: List<TransactionInfo>) {
    }

    override fun onTransactionsDelete(hashes: List<String>) {
    }

}

bitcoincashkit

Features

Usage

Because BitcoinCash is a fork of Bitcoin, the usage of this library does not differ much from bitcoinkit. So we only describe some differences between them.

Initialization

All BitcoinCash wallets use default BIP44 derivation path where coinType is 145 according to SLIP44. But since it's a fork of Bitcoin, 0 coinType also can be restored.

val context = Application()
val seed = Mnemonic().toSeed(listOf("mnemonic", "phrase", "words"), "")

val bitcoinCashKit = BitcoinCashKit(
        context = context,
        seed = seed,
        walletId = "unique_wallet_id",
        syncMode = BitcoinCore.SyncMode.Api(),
        networkType = NetworkType.MainNet(MainNetBitcoinCash.CoinType.Type145),
        confirmationsThreshold = 6
)

litecoinkit

Usage identical to bitcoinkit

dashkit

Features

Usage

Initialization

val context = Application()
val seed = Mnemonic().toSeed(listOf("mnemonic", "phrase", "words"), "")

val dashKit = DashKit(
        context = context,
        seed = seed,
        walletId = "unique_wallet_id",
        syncMode = BitcoinCore.SyncMode.Api(),
        networkType = NetworkType.MainNet,
        confirmationsThreshold = 6
)

DashTransactionInfo

Dash has some transactions marked instant. So, instead of TransactionInfo object DashKit works with DashTransactionInfo that has that field and a respective DashKit.Listener listener class.

hodler

hodler is a plugin to bitcoincore, that makes it possible to lock bitcoins until some time in the future. It relies on CHECKSEQUENCEVERIFY and Relative time-locks. It may be used with other forks of Bitcoin that support them. UnstooppableWallet opts in this plugin and enables it for Bitcoin as an experimental feature.

How it works

To lock funds we create P2SH output where redeem script has OP_CSV OpCode that ensures that the input has a proper Sequence Number(nSequence) field and that it enables a relative time-lock.

In this sample transaction the second input unlocks such an output. It has a signature, public key and the following redeem script in its scriptSig:

OP_PUSHBYTES_3 070040 OP_CSV OP_DROP OP_DUP OP_HASH160 OP_PUSHBYTES_20 853316620ed93e4ade18f8218f9aa15dc36c768e OP_EQUALVERIFY OP_CHECKSIG

Detection of incoming time-locked funds

When you have such an P2SH output, you only have an address and a hash of a redeem script in the output. If you are not aware of incoming time-locked funds in advance, there's no way you can detect that a particular output is yours. For this reason, we add an extra OP_RETURN output beside that P2SH output as a hint. That output tells us

For example, this is a hint output for the input above. It has following data:

OP_RETURN OP_PUSHNUM_1 OP_PUSHBYTES_2 0700 OP_PUSHBYTES_20 853316620ed93e4ade18f8218f9aa15dc36c768e

Limitations

Locked time periods

This plugin can lock coins for 1 hour, 1 month, half a year and 1 year. This is a limitation arising from the need of restoring those outputs using Simplified Payment Verification (SPV) Bloom Filters. Since each lock time generates different P2SH addresses, it wouldn't be possible to restore those outputs without knowing the exact lock time period in advance. So we generate 4 different addresses for each public key and use them in the bloom filters.

BTC amount

We allow maximum 0.5 BTC to be locked. We assume that's an acceptable amount to be locked if done unintentionally.

Prerequisites

Installation

Add the JitPack to module build.gradle

repositories {
    maven { url 'https://jitpack.io' }
}

Add the following dependency to your build.gradle file:

dependencies {
    implementation 'com.github.horizontalsystems:bitcoin-kit-android:master-SNAPSHOT'
}

Example App

All features of the library are used in example project. It can be referred as a starting point for usage of the library.

Dependencies

Contributing

Contributing

License

The bitcoin-kit-android is open source and available under the terms of the MIT License