Skip to content
This repository has been archived by the owner on Oct 28, 2022. It is now read-only.

Commit

Permalink
Merge branch 'master' into production
Browse files Browse the repository at this point in the history
  • Loading branch information
ealymbaev committed Feb 13, 2019
2 parents fbe5d7b + 8ab8b6c commit 34c8079
Show file tree
Hide file tree
Showing 20 changed files with 609 additions and 89 deletions.
2 changes: 1 addition & 1 deletion HSBitcoinKit.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = 'HSBitcoinKit'
spec.version = '0.2.0'
spec.version = '0.3'
spec.summary = 'Bitcoin wallet library for Swift'
spec.description = <<-DESC
HSBitcoinKit implements Bitcoin protocol in Swift. It is an implementation of the Bitcoin SPV protocol written (almost) entirely in swift.
Expand Down
12 changes: 12 additions & 0 deletions HSBitcoinKit/HSBitcoinKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
D00DA94E213E86BC007F82D6 /* MemPoolMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00DA94D213E86BC007F82D6 /* MemPoolMessage.swift */; };
D026EFF021AFD1FB00B6640D /* ConnectionTimeoutManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D026EFEF21AFD1FB00B6640D /* ConnectionTimeoutManagerTests.swift */; };
D093F45E21414CC000C5D8CD /* PeerHostManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D093F45D21414CC000C5D8CD /* PeerHostManager.swift */; };
D0F63BE421EF349D000CAB79 /* DataProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F63BE321EF349D000CAB79 /* DataProviderTests.swift */; };
D31711EA20F7192400AEF61B /* PeerConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D31711E920F7192300AEF61B /* PeerConnection.swift */; };
D31711FF20F7193000AEF61B /* PingMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D31711EB20F7192F00AEF61B /* PingMessage.swift */; };
D317120020F7193000AEF61B /* AddressMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D31711EC20F7192F00AEF61B /* AddressMessage.swift */; };
Expand Down Expand Up @@ -409,6 +410,7 @@
D00DA94D213E86BC007F82D6 /* MemPoolMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemPoolMessage.swift; sourceTree = "<group>"; };
D026EFEF21AFD1FB00B6640D /* ConnectionTimeoutManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionTimeoutManagerTests.swift; sourceTree = "<group>"; };
D093F45D21414CC000C5D8CD /* PeerHostManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerHostManager.swift; sourceTree = "<group>"; };
D0F63BE321EF349D000CAB79 /* DataProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProviderTests.swift; sourceTree = "<group>"; };
D31711E920F7192300AEF61B /* PeerConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerConnection.swift; sourceTree = "<group>"; };
D31711EB20F7192F00AEF61B /* PingMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PingMessage.swift; sourceTree = "<group>"; };
D31711EC20F7192F00AEF61B /* AddressMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddressMessage.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -927,6 +929,14 @@
path = Serializers;
sourceTree = "<group>";
};
D0F63BE221EF3469000CAB79 /* Core */ = {
isa = PBXGroup;
children = (
D0F63BE321EF349D000CAB79 /* DataProviderTests.swift */,
);
path = Core;
sourceTree = "<group>";
};
D0FCA7092133D250008169E1 /* HDWallet */ = {
isa = PBXGroup;
children = (
Expand All @@ -950,6 +960,7 @@
children = (
11B35F875BE86EBC5D97DECC /* Blocks */,
11B3507A75FAF11E3C461160 /* BlockHeaders */,
D0F63BE221EF3469000CAB79 /* Core */,
D0FCA7092133D250008169E1 /* HDWallet */,
D01B6FAA21230AE80085010F /* Helpers */,
11B352462C0F682238607E64 /* Managers */,
Expand Down Expand Up @@ -1275,6 +1286,7 @@
2FA5D6129186D79E7C7DD57F /* IPeerTaskDelegateTests.swift in Sources */,
2FA5DFD4DDEA0B2F36600346 /* IPeerTaskRequesterTests.swift in Sources */,
58AAAC48ECA38644E789E9C3 /* FeeRateSyncerTests.swift in Sources */,
D0F63BE421EF349D000CAB79 /* DataProviderTests.swift in Sources */,
58AAA5E1FEBF06E6187588FD /* FeeRateManagerTests.swift in Sources */,
58AAA3A0474719B721A0E877 /* PaymentAddressParserTests.swift in Sources */,
58AAA321A8780859892D0C73 /* TransactionOutputExtractorTests.swift in Sources */,
Expand Down
11 changes: 6 additions & 5 deletions HSBitcoinKit/HSBitcoinKit/Core/BitcoinKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import HSHDWalletKit
import RealmSwift
import BigInt
import HSCryptoKit
import RxSwift

public class BitcoinKit {

Expand Down Expand Up @@ -201,10 +202,6 @@ extension BitcoinKit {

extension BitcoinKit {

public var transactions: [TransactionInfo] {
return dataProvider.transactions
}

public var lastBlockInfo: BlockInfo? {
return dataProvider.lastBlockInfo
}
Expand All @@ -213,6 +210,10 @@ extension BitcoinKit {
return dataProvider.balance
}

public func transactions(fromHash: String? = nil, limit: Int? = nil) -> Single<[TransactionInfo]> {
return dataProvider.transactions(fromHash: fromHash, limit: limit)
}

public func send(to address: String, value: Int) throws {
try dataProvider.send(to: address, value: value)
}
Expand All @@ -239,7 +240,7 @@ extension BitcoinKit {

}

extension BitcoinKit: DataProviderDelegate {
extension BitcoinKit: IDataProviderDelegate {

func transactionsUpdated(inserted: [TransactionInfo], updated: [TransactionInfo], deleted: [Int]) {
delegate?.transactionsUpdated(bitcoinKit: self, inserted: inserted, updated: updated, deleted: deleted)
Expand Down
64 changes: 36 additions & 28 deletions HSBitcoinKit/HSBitcoinKit/Core/DataProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ class DataProvider {
private var transactionsNotificationToken: NotificationToken?
private var blocksNotificationToken: NotificationToken?

weak var delegate: DataProviderDelegate?
public var balance: Int = 0
public var lastBlockInfo: BlockInfo? = nil

init(realmFactory: IRealmFactory, addressManager: IAddressManager, addressConverter: IAddressConverter, paymentAddressParser: IPaymentAddressParser, unspentOutputProvider: IUnspentOutputProvider, feeRateManager: IFeeRateManager, transactionCreator: ITransactionCreator, transactionBuilder: ITransactionBuilder, network: INetwork) {
weak var delegate: IDataProviderDelegate?

init(realmFactory: IRealmFactory, addressManager: IAddressManager, addressConverter: IAddressConverter, paymentAddressParser: IPaymentAddressParser, unspentOutputProvider: IUnspentOutputProvider, feeRateManager: IFeeRateManager, transactionCreator: ITransactionCreator, transactionBuilder: ITransactionBuilder, network: INetwork, debounceTime: Double = 0.5) {
self.realmFactory = realmFactory
self.addressManager = addressManager
self.addressConverter = addressConverter
Expand All @@ -35,8 +38,11 @@ class DataProvider {
self.transactionCreator = transactionCreator
self.transactionBuilder = transactionBuilder
self.network = network
self.balance = unspentOutputProvider.balance
self.lastBlockInfo = blockRealmResults.last.map { blockInfo(fromBlock: $0) }

balanceUpdateSubject.debounce(0.5, scheduler: MainScheduler.instance).subscribeAsync(disposeBag: disposeBag, onNext: {
balanceUpdateSubject.debounce(debounceTime, scheduler: MainScheduler.instance).subscribeAsync(disposeBag: disposeBag, onNext: {
self.balance = unspentOutputProvider.balance
self.delegate?.balanceUpdated(balance: self.balance)
})

Expand Down Expand Up @@ -67,7 +73,10 @@ class DataProvider {

private func handleBlocks(changeset: RealmCollectionChange<Results<Block>>) {
if case let .update(collection, deletions, insertions, _) = changeset, let block = collection.last, (!deletions.isEmpty || !insertions.isEmpty) {
delegate?.lastBlockInfoUpdated(lastBlockInfo: blockInfo(fromBlock: block))
let blockInfo = self.blockInfo(fromBlock: block)
lastBlockInfo = blockInfo

delegate?.lastBlockInfoUpdated(lastBlockInfo: blockInfo)
balanceUpdateSubject.onNext(())
}
}
Expand Down Expand Up @@ -121,7 +130,7 @@ class DataProvider {
to: toAddresses,
amount: amount,
blockHeight: transaction.block?.height,
timestamp: transaction.block?.header?.timestamp
timestamp: transaction.timestamp
)
}

Expand All @@ -133,30 +142,35 @@ class DataProvider {
)
}

private func latestFeeRate() -> FeeRate {
return realmFactory.realm.objects(FeeRate.self).last ?? FeeRate.defaultFeeRate
}

}

extension DataProvider: IDataProvider {

var transactions: [TransactionInfo] {
return transactionRealmResults.map { transactionInfo(fromTransaction: $0) }
}

var lastBlockInfo: BlockInfo? {
return blockRealmResults.last.map { blockInfo(fromBlock: $0) }
}
func transactions(fromHash: String?, limit: Int?) -> Single<[TransactionInfo]> {
return Single.create { observer in
let realm = self.realmFactory.realm
var transactions = realm.objects(Transaction.self)
.sorted(by: [SortDescriptor(keyPath: "timestamp", ascending: false), SortDescriptor(keyPath: "order", ascending: false)])

if let fromHash = fromHash, let fromTransaction = realm.objects(Transaction.self).filter("reversedHashHex = %@", fromHash).first {
transactions = transactions.filter(
"timestamp < %@ OR (timestamp = %@ AND order < %@)",
fromTransaction.timestamp,
fromTransaction.timestamp,
fromTransaction.order
)
}

var balance: Int {
var balance = 0
let results: [Transaction]
if let limit = limit {
results = Array(transactions.prefix(limit))
} else {
results = Array(transactions)
}

for output in unspentOutputProvider.allUnspentOutputs {
balance += output.value
observer(.success(results.map() { self.transactionInfo(fromTransaction: $0) }))
return Disposables.create()
}

return balance
}

func send(to address: String, value: Int) throws {
Expand Down Expand Up @@ -212,9 +226,3 @@ extension DataProvider: IDataProvider {
}

}

protocol DataProviderDelegate: class {
func transactionsUpdated(inserted: [TransactionInfo], updated: [TransactionInfo], deleted: [Int])
func balanceUpdated(balance: Int)
func lastBlockInfoUpdated(lastBlockInfo: BlockInfo)
}
11 changes: 9 additions & 2 deletions HSBitcoinKit/HSBitcoinKit/Core/Protocols.swift
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ protocol IUnspentOutputSelector {

protocol IUnspentOutputProvider {
var allUnspentOutputs: [TransactionOutput] { get }
var balance: Int { get }
}

protocol IBlockSyncer: class {
Expand All @@ -349,12 +350,12 @@ protocol IKitStateProviderDelegate: class {
}

protocol IDataProvider {
var delegate: DataProviderDelegate? { get set }
var delegate: IDataProviderDelegate? { get set }

var transactions: [TransactionInfo] { get }
var lastBlockInfo: BlockInfo? { get }
var balance: Int { get }
var receiveAddress: String { get }
func transactions(fromHash: String?, limit: Int?) -> Single<[TransactionInfo]>
func send(to address: String, value: Int) throws
func parse(paymentAddress: String) -> BitcoinPaymentData
func validate(address: String) throws
Expand All @@ -363,6 +364,12 @@ protocol IDataProvider {
var debugInfo: String { get }
}

protocol IDataProviderDelegate: class {
func transactionsUpdated(inserted: [TransactionInfo], updated: [TransactionInfo], deleted: [Int])
func balanceUpdated(balance: Int)
func lastBlockInfoUpdated(lastBlockInfo: BlockInfo)
}

protocol INetwork: class {
var merkleBlockValidator: IMerkleBlockValidator { get }

Expand Down
10 changes: 5 additions & 5 deletions HSBitcoinKit/HSBitcoinKit/Managers/InitialSyncApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ class InitialSyncApi {
let url: String

switch network {
case is BitcoinMainNet: url = "https://btc.horizontalsystems.xyz"
case is BitcoinTestNet: url = "http://btc-testnet.horizontalsystems.xyz"
case is BitcoinCashMainNet: url = "https://bch.horizontalsystems.xyz"
case is BitcoinCashTestNet: url = "http://bch-testnet.horizontalsystems.xyz"
default: url = "http://btc-testnet.horizontalsystems.xyz"
case is BitcoinMainNet: url = "https://btc.horizontalsystems.xyz/apg"
case is BitcoinTestNet: url = "http://btc-testnet.horizontalsystems.xyz/apg"
case is BitcoinCashMainNet: url = "https://bch.horizontalsystems.xyz/apg"
case is BitcoinCashTestNet: url = "http://bch-testnet.horizontalsystems.xyz/apg"
default: url = "http://btc-testnet.horizontalsystems.xyz/apg"
}

apiManager = ApiManager(apiUrl: url, logger: logger)
Expand Down
10 changes: 10 additions & 0 deletions HSBitcoinKit/HSBitcoinKit/Managers/UnspentOutputProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,14 @@ extension UnspentOutputProvider: IUnspentOutputProvider {
}
}

var balance: Int {
var balance = 0

for output in self.allUnspentOutputs {
balance += output.value
}

return balance
}

}
20 changes: 11 additions & 9 deletions HSBitcoinKit/HSBitcoinKit/Managers/UnspentOutputSelector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ struct SelectedUnspentOutputInfo {
let addChangeOutput: Bool // need to add changeOutput. Fee was calculated with change output
}

public enum SelectorError: Error {
case wrongValue
case emptyOutputs
case notEnough(maxFee: Int)
}

class UnspentOutputSelector {
enum SelectorError: Error {
case wrongValue
case emptyOutputs
case notEnough
}

let calculator: ITransactionSizeCalculator

Expand Down Expand Up @@ -53,21 +54,22 @@ extension UnspentOutputSelector: IUnspentOutputSelector {
var lastCalculatedFee = 0

for output in sortedOutputs {
selectedOutputs.append(output)
selectedOutputScriptTypes.append(output.scriptType)
totalValue += output.value

lastCalculatedFee = calculator.transactionSize(inputs: selectedOutputScriptTypes, outputScriptTypes: [outputScriptType]) * feeRate
if senderPay {
fee = lastCalculatedFee
}
if totalValue >= lastCalculatedFee && totalValue >= value + fee {
break
}
selectedOutputs.append(output)
selectedOutputScriptTypes.append(output.scriptType)
totalValue += output.value
}

// if all outputs are selected and total value less than needed throw error
if totalValue < value + fee {
throw SelectorError.notEnough
throw SelectorError.notEnough(maxFee: fee)
}

// if total selected outputs value more than value and fee for transaction with change output + change input -> add fee for change output and mark as need change address
Expand Down
4 changes: 4 additions & 0 deletions HSBitcoinKit/HSBitcoinKit/Models/Transaction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class Transaction: Object {
@objc dynamic var dataHash = Data()
@objc dynamic var version: Int = 0
@objc dynamic var lockTime: Int = 0
@objc dynamic var timestamp: Int = 0
@objc dynamic var order: Int = 0
@objc dynamic var block: Block?

@objc dynamic var isMine: Bool = false
Expand All @@ -33,6 +35,8 @@ class Transaction: Object {
outputs.forEach { self.outputs.append($0) }

self.lockTime = lockTime
self.timestamp = Int(Date().timeIntervalSince1970)

dataHash = CryptoKit.sha256sha256(TransactionSerializer.serialize(transaction: self, withoutWitness: true))
reversedHashHex = dataHash.reversedHex
}
Expand Down
2 changes: 1 addition & 1 deletion HSBitcoinKit/HSBitcoinKit/Models/TransactionInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public struct TransactionInfo {
public let to: [TransactionAddressInfo]
public let amount: Int
public let blockHeight: Int?
public let timestamp: Int?
public let timestamp: Int
}

public struct TransactionAddressInfo {
Expand Down
2 changes: 1 addition & 1 deletion HSBitcoinKit/HSBitcoinKit/Storage/RealmFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class RealmFactory {

configuration = Realm.Configuration(
fileURL: documentsUrl?.appendingPathComponent(realmFileName),
schemaVersion: 1,
schemaVersion: 2,
deleteRealmIfMigrationNeeded: true,
objectTypes: [
Block.self,
Expand Down
Loading

0 comments on commit 34c8079

Please sign in to comment.