bitcoin-kit-android
is a Bitcoin wallet toolkit implemented in Kotlin. It consists of following libraries:
bitcoincore
is a core library that implements a full Simplified Payment Verification (SPV
) client inKotlin
. It implements BitcoinP2P Protocol
and can be extended to be a client of other Bitcoin forks like BitcoinCash, Litecoin, etc.bitcoinkit
extends bitcoincore, makes it usable withBitcoin
network.bitcoincashkit
extends bitcoincore, makes it usable withBitcoinCash(ABC)
network.litecoinkit
extends bitcoincore, makes it usable withLitecoin
network.dashkit
extends bitcoincore, makes it usable withDash
network.hodler
is a plugin forbitcoincore
, that makes it possible to lock certain amount of coins until some time in the future.
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.
- Bitcoin P2P Protocol implementation in Kotlin.
- Full SPV implementation for fast mobile performance with account security and privacy in mind
-
P2PK
,P2PKH
,P2SH-P2WPKH
,P2WPKH
outputs support. - Restoring with mnemonic seed. (Generated from private seed phrase)
- Restoring with BIP32 extended public key. (This becomes a
Watch account
unable to spend funds) - Quick initial restore over node API. (optional)
- Handling transaction (Replacement)/(Double spend)/(Failure by expiration)
- Optimized UTXO selection when spending coins.
- BIP69 or simple shuffle output ordering. (configurable)
- BIP21 URI schemes with payment address, amount, label and other parameters
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
)
bitcoinkit supports BIP44
, BIP49
and BIP84
wallets. They have different derivation paths, so you need to specify this on kit initialization.
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:
-
It doesn't download blocks added before the BIP44 was implemented by wallets, because there were no transactions concerning addresses generated by BIP44 wallets.
-
If you set API() or NewWallet() to syncMode parameter, it first requests from an API(currently Blockchain.com) the hashes of the blocks where there are transactions we need. Then, it downloads those blocks from the bitcoin peers. This reduces the initial synchronization time to several minutes. This also carries some risks that makes it possible for a middle-man attacker to learn about the addresses requested from your IP address. But your funds are totally safe.
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.
networkType
: Mainnet or TestnetconfirmationsThreshold
: Minimum number of confirmations required for an unspent output to be available for use (default: 6)
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.
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()
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
val blockInfo = bitcoinKit.lastBlockInfo ?: return
println(blockInfo.headerHash)
println(blockInfo.height)
println(blockInfo.timestamp)
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"
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)
}
fromUid
andlimit
parameters can be used for pagination.type
parameter enables to filter transactions by coins flow. You can pass incoming OR outgoing to get filtered transactions
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
- Incoming
- Outgoing
- SentToSelf
status
- NEW -> transaction is in mempool
- RELAYED -> transaction is in block
- INVALID -> transaction is not included in block due to an error OR replaced by another one (RBF).
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.
bitcoinKit.validateAddress(address = "mrjQyzbX9SiJxRC2mQhT4LvxFEmt9KEeRY")
bitcoinKit.fee(address = "36k1UofZ2iP2NYax9znDCsksajfKeKLLMJ", value = 100000000, feeRate = 10)
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
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>) {
}
}
-
Base58
andBech32
- Validation of BCH hard forks
-
ASERT
,DAA
,EDA
validations
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.
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
)
Usage identical to bitcoinkit
- Instant send
- LLMQ lock, Masternodes validation
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
)
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
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.
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
OP_PUSHBYTES_3 070040 OP_CSV OP_DROP
part ensures that needed amount of time is passed. Specifically07
part of070040
bytes says that it's locked for 1 hour. See here and here for how it's evaluated.OP_DUP OP_HASH160 OP_PUSHBYTES_20 853316620ed93e4ade18f8218f9aa15dc36c768e OP_EQUALVERIFY OP_CHECKSIG
part is the same locking script as ofP2PKH
output, that ensures the spender is the owner of the private key matching the public key hashed to853316620ed93e4ade18f8218f9aa15dc36c768e
.
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
- ID of the plugin (1 byte):
bitcoincore
can handle multiple plugins like this one. - Time-lock period (2 bytes)
- Hash of the receiver's public key (20 bytes)
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
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.
We allow maximum 0.5 BTC to be locked. We assume that's an acceptable amount to be locked if done unintentionally.
- JDK >= 1.8
- Android 6 (minSdkVersion 23) or greater
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'
}
All features of the library are used in example project. It can be referred as a starting point for usage of the library.
- HDWalletKit - HD Wallet related features, mnemonic phrase
The bitcoin-kit-android
is open source and available under the terms of the MIT License