Skip to content

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.

License

horizontalsystems/bitcoin-kit-android

Repository files navigation

BitcoinKit

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 in Kotlin. It implements Bitcoin P2P Protocol and can be extended to be a client of other Bitcoin forks like BitcoinCash, Litecoin, etc.
  • bitcoinkit extends bitcoincore, makes it usable with Bitcoin network.
  • bitcoincashkit extends bitcoincore, makes it usable with BitcoinCash(ABC) network.
  • litecoinkit extends bitcoincore, makes it usable with Litecoin network.
  • dashkit extends bitcoincore, makes it usable with Dash network.
  • hodler is a plugin for bitcoincore, 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.

Core Features

  • 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

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:

  • 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.

Additional parameters:

  • networkType: Mainnet or Testnet
  • confirmationsThreshold: Minimum number of confirmations required for an unspent output to be available for use (default: 6)

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)
}
  • fromUid and limit 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

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

  • 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).

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

  • Base58 and Bech32
  • Validation of BCH hard forks
  • ASERT, DAA, EDA validations

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

  • Instant send
  • LLMQ lock, Masternodes validation

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

  • OP_PUSHBYTES_3 070040 OP_CSV OP_DROP part ensures that needed amount of time is passed. Specifically 07 part of 070040 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 of P2PKH output, that ensures the spender is the owner of the private key matching the public key hashed to 853316620ed93e4ade18f8218f9aa15dc36c768e.

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

  • 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

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

  • JDK >= 1.8
  • Android 6 (minSdkVersion 23) or greater

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

  • HDWalletKit - HD Wallet related features, mnemonic phrase

Contributing

Contributing

License

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