BitcoinKit.Swift
is a package that extends BitcoinCore.Swift and makes it usable with Bitcoin
Mainnet and Testnet networks.
First, you nead an instance of BitcoinKit.Kit 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.Swift to convert a word list to a seed.
import HdWalletKit
let words = ["mnemonic", "phrase", "words"]
let passphrase: String = ""
let seed = Mnemonic.seed(mnemonic: words, passphrase: passphrase)!
Then you can pass a seed to initialize an instance of BitcoinKit.Kit
let bitcoinKit = try BitcoinKit.Kit(
seed: seed,
purpose: Purpose.bip84,
walletId: "unique_wallet_id",
syncMode: BitcoinCore.SyncMode.full,
networkType: BitcoinKit.Kit.NetworkType.mainNet,
confirmationsThreshold: 3,
logger: nil
)
BitcoinKit.Swift supports BIP44
, BIP49
and BIP84
wallets. They have different derivation paths, so you need to specify this on kit initialization.
BitcoinKit.Swift 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, BitcoinKit.Swift 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)logger
: (optional) an instance of Logger class
You can initialize BitcoinKit.Kit
using BIP32 Extended Private/Public Key as follows:
let extendedKey = try! HDExtendedKey(extendedKey: "xprvA1BgyAq84AiAsrMm6DKqwCXDwxLBXq76dpUfuNXNziGMzDxYLjE9AkuYBAQTpt6aJu4nFYamh6BbrRkys5fJcxGd7qixNrpVpPBxui9oYyF")
let bitcoinKit = try BitcoinKit.Kit(
extendedKey: extendedKey,
walletId: "unique_wallet_id",
syncMode: BitcoinCore.SyncMode.full,
networkType: BitcoinKit.Kit.NetworkType.mainNet,
confirmationsThreshold: 3,
logger: nil
)
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.Kit require 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
:
let balance = bitcoinKit.balance
print(balance.spendable)
print(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
let blockInfo = bitcoinKit.lastBlockInfo!
print(blockInfo.headerHash)
print(blockInfo.height)
print(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? = nil, type: TransactionFilterType?, limit: Int? = nil)
method of the BitcoinKit.Kit instance. It returns Single<[TransactionInfo]>. You'll need to subscribe and get transactions asynchronously. See RX Single Observers for more info.
let disposeBag = DisposeBag() // This must be retained
bitcoinKit.transactions(type: nil)
.subscribe(
onSuccess: { transactionInfos in
for transactionInfo in transactionInfos {
print("Hash: \(transactionInfo.uid)")
print("Hash: \(transactionInfo.transactionHash)")
}
}
)
.disposed(by: disposeBag)
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 transations
A sample dump:
// (BitcoinCore.TransactionInfo) {
// uid = "CD2BCD61-49E1-419C-AFF2-E4FF5D28E375"
// transactionHash = "e1ef748cf68a73a59cddad4dde1251d043eed7e3543907be6a635fba4522bc97"
// transactionIndex = 1
// inputs = 2 values {
// [0] = (TransactionInputInfo) {
// mine = true
// address = "36k1UofZ2iP2NYax9znDCsksajfKeKLLMJ"
// value = 69988
// }
// [1] = (TransactionInputInfo) {
// mine = true
// address = "3QYxvoQHKipha2H3U8yeNh5cfutZK8qBPb"
// value = 5976
// }
// }
// outputs = 2 values {
// [0] = (TransactionOutputInfo) {
// mine = true
// changeOutput = true
// value = 69217
// address = "38Ckn9tueUqTB8oy7UBWe1Gzy6uJpLZNep"
// pluginId = nil
// pluginData = nil
// pluginDataString = nil
// }
// [1] = (TransactionOutputInfo) {
// mine = true
// changeOutput = false
// value = 5976
// address = "3N5r5te5617JcBftWt34nTC9sJ7ofL3rmS"
// pluginId = nil
// pluginData = nil
// pluginDataString = nil
// }
// }
// amount = 5976
// type = sentToSelf
// fee = 771
// blockHeight = 770158
// timestamp = 1672742140
// status = relayed
// conflictingHash = nil
// }
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).
try! bitcoinKit.send(to: "36k1UofZ2iP2NYax9znDCsksajfKeKLLMJ", value: 100000000, feeRate: 10, sortType: .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.
try bitcoinKit.validate(address: "mrjQyzbX9SiJxRC2mQhT4LvxFEmt9KEeRY")
try bitcoinKit.fee(to: "36k1UofZ2iP2NYax9znDCsksajfKeKLLMJ", value: 100000000, feeRate: 10, sortType: .bip69)
You can use parse
method to parse a BIP21 URI:
bitcoinKit.parse(paymentAddress: "bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz")
// ▿ BitcoinPaymentData
// - address : "175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W"
// - version : nil
// ▿ amount : Optional<Double>
// - some : 50.0
// ▿ label : Optional<String>
// - some : "Luke-Jr"
// ▿ message : Optional<String>
// - some : "Donation for project xyz"
// - parameters : nil
Balance, transactions, last blocks synced and kit state are available in real-time. BitcoinCoreDelegate
protocol must be implemented and set to BitcoinKit.Kit instance to receive that.
class Manager {
let bitcoinKit: BitcoinKit.Kit
init(kit: BitcoinKit.Kit) {
bitcoinKit = kit
bitcoinKit.delegate = self
}
}
extension Manager: BitcoinCoreDelegate {
func transactionsUpdated(inserted: [TransactionInfo], updated: [TransactionInfo]) {
}
func transactionsDeleted(hashes: [String]) {
}
private func balanceUpdated(balance: Int) {
}
func lastBlockInfoUpdated(lastBlockInfo: BlockInfo) {
}
public func kitStateUpdated(state: BitcoinCore.KitState) {
// BitcoinCore.KitState can be one of 3 following states:
//
// synced
// apiSyncing(transactions: Int)
// syncing(progress: Double)
// notSynced(error: Error)
//
// These states can be used to implement progress bar, etc
}
}
- Xcode 10.0+
- Swift 5+
- iOS 13+
dependencies: [
.package(url: "https://github.com/horizontalsystems/BitcoinKit.Swift.git", .upToNextMajor(from: "1.0.0"))
]
All features of the library are used in example project. It can be referred as a starting point for usage of the library.
The BitcoinKit
toolkit is open source and available under the terms of the MIT License.